diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b589d56..b86273d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1832297..cdecf77 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,3 @@ - import java.net.InetAddress import java.text.SimpleDateFormat import java.util.Date @@ -22,7 +21,7 @@ android { minSdk = 24 targetSdk = 34 // 版本号为x.y.z则versionCode为x*1000000+y*10000+z*100+debug版本号(开发需要时迭代, 两位数) - versionCode = 1_00_00_006 + versionCode = 1_00_00_010 versionName = "1.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/book/content/ContentScreen.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/book/content/ContentScreen.kt index be9ebc9..a91f7a0 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/book/content/ContentScreen.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/book/content/ContentScreen.kt @@ -491,7 +491,7 @@ fun SettingsBottomSheet( ) { item { SettingsSliderEntry( - description = "阅读器字体大小", + title = "阅读器字体大小", unit = "sp", valueRange = 8f..64f, value = settingState.fontSize, @@ -500,7 +500,7 @@ fun SettingsBottomSheet( } item { SettingsSliderEntry( - description = "阅读器行距大小", + title = "阅读器行距大小", unit = "sp", valueRange = 0f..32f, value = settingState.fontLineHeight, @@ -610,7 +610,7 @@ fun SettingsBottomSheet( if(!settingState.autoPadding) { item { SettingsSliderEntry( - description = "上边距", + title = "上边距", unit = "dp", valueRange = 0f..128f, value = settingState.topPadding, @@ -621,7 +621,7 @@ fun SettingsBottomSheet( if(!settingState.autoPadding) { item { SettingsSliderEntry( - description = "下边距", + title = "下边距", unit = "dp", valueRange = 0f..128f, value = settingState.bottomPadding, @@ -632,7 +632,7 @@ fun SettingsBottomSheet( if(!settingState.autoPadding) { item { SettingsSliderEntry( - description = "左边距", + title = "左边距", unit = "dp", valueRange = 0f..128f, value = settingState.leftPadding, @@ -643,7 +643,7 @@ fun SettingsBottomSheet( if(!settingState.autoPadding) { item { SettingsSliderEntry( - description = "右边距", + title = "右边距", unit = "dp", valueRange = 0f..128f, value = settingState.rightPadding, diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/components/SettingsEntry.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/components/SettingsEntry.kt index dd7e0c0..9db0c6b 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/components/SettingsEntry.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/components/SettingsEntry.kt @@ -2,15 +2,24 @@ package indi.dmzz_yyhyy.lightnovelreader.ui.components import android.content.Intent import android.net.Uri +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults @@ -23,9 +32,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat.startActivity @@ -38,6 +52,7 @@ import kotlin.math.roundToInt @Composable fun SettingsSwitchEntry( modifier: Modifier = Modifier, + iconRes: Int = -1, title: String, description: String, checked: Boolean, @@ -46,6 +61,7 @@ fun SettingsSwitchEntry( ) { SettingsSwitchEntry( modifier = modifier, + iconRes = iconRes, title = title, description = description, checked = checked, @@ -57,46 +73,69 @@ fun SettingsSwitchEntry( @Composable fun SettingsSwitchEntry( modifier: Modifier = Modifier, + iconRes: Int, title: String, description: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit, disabled: Boolean = false ) { - FilledCard( + Row( modifier = modifier - .fillMaxWidth(), - shape = RoundedCornerShape(6.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .wrapContentHeight() + .then(if (!disabled) Modifier.clickable { onCheckedChange(!checked) } else Modifier) + .padding(start = 18.dp, end = 14.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .then(if (disabled) Modifier.clickable {} else Modifier.clickable { onCheckedChange(!checked) }) - .padding(18.dp, 10.dp, 20.dp, 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column( - Modifier - .weight(2f) - .padding(end = 4.dp) + if (iconRes > 0) { + Box( + modifier = Modifier + .fillMaxHeight() + .padding(end = 8.dp), + contentAlignment = Alignment.Center ) { - Text( - text = title, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.W500, - fontSize = 18.sp, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1 - ) - Text( - text = description, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.W500, - fontSize = 13.sp, - lineHeight = 14.sp, - color = MaterialTheme.colorScheme.onSurface + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(iconRes), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + contentDescription = "Icon" ) } + } + + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = 4.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge, + fontSize = 16.sp, + lineHeight = 16.sp + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = description, + color = MaterialTheme.colorScheme.secondary, + fontSize = 14.sp, + lineHeight = 18.sp + ) + } + + Box( + modifier = Modifier + .fillMaxHeight(), + contentAlignment = Alignment.Center + ) { Switch( checked = checked, enabled = !disabled, @@ -108,8 +147,9 @@ fun SettingsSwitchEntry( @Composable fun SettingsSliderEntry( + iconRes: Int = -1, modifier: Modifier = Modifier, - description: String, + title: String, unit: String, value: Float, valueRange: ClosedFloatingPointRange = 0f..1f, @@ -120,8 +160,9 @@ fun SettingsSliderEntry( tempValue = value } SettingsSliderEntry( + iconRes = iconRes, modifier = modifier, - description = description, + title = title, unit = unit, value = tempValue, valueRange = valueRange, @@ -131,35 +172,63 @@ fun SettingsSliderEntry( } @Composable -fun SettingsSliderEntry( +private fun SettingsSliderEntry( modifier: Modifier = Modifier, - description: String, + iconRes: Int, + title: String, unit: String, value: Float, valueRange: ClosedFloatingPointRange = 0f..1f, onSlideChange: (Float) -> Unit, onSliderChangeFinished: () -> Unit ) { - FilledCard( - modifier = modifier, - shape = RoundedCornerShape(6.dp) + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .wrapContentHeight() + .padding(start = 18.dp, end = 14.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Column(Modifier.padding(18.dp, 10.dp, 20.dp, 12.dp)) { + if (iconRes > 0) { + Box( + modifier = Modifier + .fillMaxHeight() + .padding(end = 8.dp), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(iconRes), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + contentDescription = "Icon" + ) + } + } + + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = 4.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { Text( - text = description, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.W500, - fontSize = 18.sp, + text = title, color = MaterialTheme.colorScheme.onSurface, - maxLines = 1 + style = MaterialTheme.typography.titleLarge, + fontSize = 16.sp, + lineHeight = 16.sp ) + Spacer(modifier = Modifier.height(2.dp)) Text( text = "${DecimalFormat("#.#").format(value)}$unit", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.W500, - fontSize = 13.sp, - lineHeight = 14.sp, - color = MaterialTheme.colorScheme.secondary, + color = MaterialTheme.colorScheme.primary, + fontSize = 14.sp, + lineHeight = 18.sp, maxLines = 1 ) Slider( @@ -176,54 +245,84 @@ fun SettingsSliderEntry( } } +@OptIn(ExperimentalComposeUiApi::class) @Composable fun SettingsMenuEntry( + iconRes: Int = -1, title: String, - description: String, + description: String? = null, options: MenuOptions, selectedOptionKey: String, onOptionChange: (String) -> Unit ) { var expanded by remember { mutableStateOf(false) } + var offset by remember { mutableStateOf(Offset.Zero) } - FilledCard( + Row( modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(6.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .wrapContentHeight() + .pointerInteropFilter { + offset = Offset(it.x, it.y); false + } + .clickable { expanded = !expanded } + .padding(start = 18.dp, end = 14.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Box(modifier = Modifier.clickable { expanded = !expanded }) { - Row( + if (iconRes > 0) { + Box( modifier = Modifier - .fillMaxWidth() - .padding(18.dp, 10.dp, 20.dp, 12.dp), - verticalAlignment = Alignment.CenterVertically + .fillMaxHeight() + .padding(end = 8.dp), + contentAlignment = Alignment.Center ) { - Column( - Modifier.weight(2f) - ) { - Text( - text = title, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.W500, - fontSize = 18.sp, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1 - ) - Column { - Text( - text = description, - fontSize = 13.sp, - lineHeight = 14.sp, - color = MaterialTheme.colorScheme.onSurface - ) - AnimatedText( - text = options.get(selectedOptionKey).name, - fontSize = 13.sp, - lineHeight = 14.sp, - color = MaterialTheme.colorScheme.primary - ) - } + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(iconRes), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + contentDescription = "Icon" + ) + } + } + + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = 4.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge, + fontSize = 16.sp, + lineHeight = 16.sp + ) + Spacer(modifier = Modifier.height(2.dp)) + description?.let { + Text( + text = description, + color = MaterialTheme.colorScheme.secondary, + fontSize = 14.sp, + lineHeight = 18.sp + ) + } + AnimatedText( + text = options.get(selectedOptionKey).name, + fontSize = 14.sp, + lineHeight = 18.sp, + color = MaterialTheme.colorScheme.primary + ) + Box( + modifier = Modifier.offset { + IntOffset(offset.x.toInt(), 0) } + ) { DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } @@ -234,23 +333,24 @@ fun SettingsMenuEntry( onOptionChange(option.key) expanded = false }, - text = { Text(option.name) }, + text = { Text(option.name, fontSize = 14.sp) }, ) } } } } - } } @Composable fun SettingsClickableEntry( + iconRes: Int = -1, title: String, description: String, option: String? = null ) { SettingsClickableEntry( + iconRes = iconRes, title = title, description = description, option = option, @@ -260,6 +360,7 @@ fun SettingsClickableEntry( @Composable fun SettingsClickableEntry( + iconRes: Int = -1, title: String, description: String, option: String? = null, @@ -267,6 +368,7 @@ fun SettingsClickableEntry( ) { val context = LocalContext.current SettingsClickableEntry( + iconRes = iconRes, title = title, description = description, option = option, @@ -281,59 +383,61 @@ fun SettingsClickableEntry( @Composable fun SettingsClickableEntry( + iconRes: Int = -1, title: String, description: String, option: String? = null, onClick: () -> Unit ) { - var expanded by remember { mutableStateOf(false) } - - FilledCard( + Row( modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(6.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .wrapContentHeight() + .clickable { onClick.invoke() } + .padding(start = 18.dp, end = 14.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Box( - modifier = Modifier.clickable { - expanded = !expanded - onClick.invoke() - } - ) { - Row( + if (iconRes > 0) { + Box( modifier = Modifier - .fillMaxWidth() - .padding(18.dp, 10.dp, 20.dp, 12.dp), - verticalAlignment = Alignment.CenterVertically + .fillMaxHeight() + .padding(end = 8.dp), + contentAlignment = Alignment.Center ) { - Column( - Modifier.weight(2f) - ) { - Text( - text = title, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.W500, - fontSize = 18.sp, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1 - ) - Column { - Text( - text = description, - fontSize = 13.sp, - lineHeight = 14.sp, - color = MaterialTheme.colorScheme.onSurface - ) - option?.let { - Text( - text = it, - fontSize = 13.sp, - lineHeight = 14.sp, - color = MaterialTheme.colorScheme.primary - ) - } - } - } + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(iconRes), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + contentDescription = "Icon" + ) } } + + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = 4.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge, + fontSize = 16.sp, + lineHeight = 16.sp + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = description, + color = MaterialTheme.colorScheme.secondary, + fontSize = 14.sp, + lineHeight = 18.sp + ) + } } } diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/bookshelf/home/BookshelfHomeScreen.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/bookshelf/home/BookshelfHomeScreen.kt index 10582ba..ca81afd 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/bookshelf/home/BookshelfHomeScreen.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/bookshelf/home/BookshelfHomeScreen.kt @@ -13,14 +13,10 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets @@ -62,21 +58,19 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -90,11 +84,9 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.workDataOf import indi.dmzz_yyhyy.lightnovelreader.R -import indi.dmzz_yyhyy.lightnovelreader.data.book.BookInformation import indi.dmzz_yyhyy.lightnovelreader.data.work.SaveBookshelfWork import indi.dmzz_yyhyy.lightnovelreader.ui.components.AddBookToBookshelfDialog import indi.dmzz_yyhyy.lightnovelreader.ui.components.AnimatedText -import indi.dmzz_yyhyy.lightnovelreader.ui.components.Cover import indi.dmzz_yyhyy.lightnovelreader.ui.components.EmptyPage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -125,6 +117,7 @@ fun BookshelfHomeScreen( ) { val scope = rememberCoroutineScope() val context = LocalContext.current + val haptic = LocalHapticFeedback.current val workManager = WorkManager.getInstance(context) val enterAlwaysScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val animatedBackgroundColor by animateColorAsState( @@ -136,15 +129,16 @@ fun BookshelfHomeScreen( val importBookshelfLauncher = launcher(importBookshelf) val lazyListState = rememberLazyListState() var visibleBookshelfSelectDialog by remember { mutableStateOf(false) } - val dialogSelectedBooksheves = remember { mutableStateListOf() } - var updatedBooksExpended by remember { mutableStateOf(true) } - var pinnedBooksExpended by remember { mutableStateOf(true) } - var allBooksExpended by remember { mutableStateOf(true) } + val dialogSelectedBookshelves = remember { mutableStateListOf() } + var updatedBooksExpanded by remember { mutableStateOf(true) } + var pinnedBooksExpanded by remember { mutableStateOf(true) } + var allBooksExpanded by remember { mutableStateOf(true) } topBar { TopBar( scrollBehavior = enterAlwaysScrollBehavior, backgroundColor = animatedBackgroundColor, selectMode = uiState.selectMode, + uiState = uiState, onClickCreate = onClickCreate, onClickSearch = {}, onClickEdit = { onClickEdit(uiState.selectedBookshelfId) }, @@ -195,7 +189,7 @@ fun BookshelfHomeScreen( ) } LaunchedEffect(visibleBookshelfSelectDialog) { - dialogSelectedBooksheves.clear() + dialogSelectedBookshelves.clear() } dialog { if (visibleBookshelfSelectDialog) @@ -203,14 +197,14 @@ fun BookshelfHomeScreen( onDismissRequest = { visibleBookshelfSelectDialog = false }, onConfirmation = { scope.launch { - markSelectedBooks(dialogSelectedBooksheves) + markSelectedBooks(dialogSelectedBookshelves) visibleBookshelfSelectDialog = false } }, - onSelectBookshelf = dialogSelectedBooksheves::add, - onDeselectBookshelf = dialogSelectedBooksheves::remove, + onSelectBookshelf = dialogSelectedBookshelves::add, + onDeselectBookshelf = dialogSelectedBookshelves::remove, allBookshelf = uiState.bookshelfList, - selectedBookshelfIds = dialogSelectedBooksheves + selectedBookshelfIds = dialogSelectedBookshelves ) } LifecycleEventEffect(Lifecycle.Event.ON_START) { @@ -222,15 +216,10 @@ fun BookshelfHomeScreen( clearToast() } Column( - modifier = Modifier - .fillMaxSize() - .drawBehind { - drawRect(animatedBackgroundColor) - } + modifier = Modifier.fillMaxSize() ) { if (uiState.bookshelfList.size > 4) { ScrollableTabRow( - containerColor = animatedBackgroundColor, selectedTabIndex = uiState.selectedTabIndex, edgePadding = 16.dp, indicator = { tabPositions -> @@ -260,8 +249,7 @@ fun BookshelfHomeScreen( } else { PrimaryTabRow( - selectedTabIndex = uiState.selectedTabIndex, - containerColor = animatedBackgroundColor + selectedTabIndex = uiState.selectedTabIndex ) { uiState.bookshelfList.forEach { bookshelf -> Tab( @@ -284,6 +272,13 @@ fun BookshelfHomeScreen( description = "单击“收藏”按钮,将书本加入此书架" ) } + + val onLongPress: (Int) -> Unit = { bookId -> + onClickEnableSelectMode.invoke() + changeBookSelectState(bookId) + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + LazyColumn( modifier = Modifier .fillMaxWidth() @@ -298,20 +293,25 @@ fun BookshelfHomeScreen( modifier = Modifier.animateItem(), icon = painterResource(R.drawable.keep_24px), title = "已更新 (${uiState.selectedBookshelf.updatedBookIds.size})", - expanded = updatedBooksExpended, - onClickExpand = { updatedBooksExpended = !updatedBooksExpended } + expanded = updatedBooksExpanded, + onClickExpand = { updatedBooksExpanded = !updatedBooksExpanded } ) } - if (updatedBooksExpended && !uiState.selectMode) { + if (updatedBooksExpanded && !uiState.selectMode) { items(uiState.selectedBookshelf.updatedBookIds.reversed()) { updatedBookId -> uiState.bookInformationMap[updatedBookId]?.let { - UpdatedBookRow( - modifier = Modifier.animateItem(), + BookCardItem( bookInformation = it, - lastChapterTitle = uiState.bookLastChapterTitleMap[updatedBookId] ?: "", + haptic = haptic, selected = uiState.selectedBookIds.contains(it.id), - onClick = { onClickBook(it.id) }, - onLongPress = {} + latestChapterTitle = uiState.bookLastChapterTitleMap[updatedBookId], + onClick = { + if (!uiState.selectMode) + onClickBook(it.id) + else changeBookSelectState(it.id) + }, + onLongPress = { onLongPress(it.id) }, + progress = {} ) } } @@ -322,26 +322,24 @@ fun BookshelfHomeScreen( modifier = Modifier.animateItem(), icon = painterResource(R.drawable.keep_24px), title = "已固定 (${uiState.selectedBookshelf.pinnedBookIds.size})", - expanded = pinnedBooksExpended, - onClickExpand = { pinnedBooksExpended = !pinnedBooksExpended } + expanded = pinnedBooksExpanded, + onClickExpand = { pinnedBooksExpanded = !pinnedBooksExpanded } ) } - if (pinnedBooksExpended) { + if (pinnedBooksExpanded) { items(uiState.selectedBookshelf.pinnedBookIds.reversed()) { pinnedBookId -> - uiState.bookInformationMap[pinnedBookId]?.let { bookInformation -> - BookRow( - modifier = Modifier.animateItem(), - bookInformation = bookInformation, - selected = uiState.selectedBookIds.contains(bookInformation.id), + uiState.bookInformationMap[pinnedBookId]?.let { + BookCardItem( + bookInformation = it, + haptic = haptic, + selected = uiState.selectedBookIds.contains(it.id), onClick = { if (!uiState.selectMode) - onClickBook(bookInformation.id) - else changeBookSelectState(bookInformation.id) + onClickBook(it.id) + else changeBookSelectState(it.id) }, - onLongPress = { - onClickEnableSelectMode.invoke() - changeBookSelectState(bookInformation.id) - } + onLongPress = { onLongPress(it.id) }, + progress = {} ) } } @@ -352,28 +350,24 @@ fun BookshelfHomeScreen( modifier = Modifier.animateItem(), icon = painterResource(R.drawable.outline_bookmark_24px), title = "全部 (${uiState.selectedBookshelf.allBookIds.size})", - expanded = allBooksExpended, - onClickExpand = { allBooksExpended = !allBooksExpended } + expanded = allBooksExpanded, + onClickExpand = { allBooksExpanded = !allBooksExpanded } ) } - if (allBooksExpended) { - items( - uiState.selectedBookshelf.allBookIds.reversed(), - ) { bookId -> + if (allBooksExpanded) { + items(uiState.selectedBookshelf.allBookIds.reversed()) { bookId -> uiState.bookInformationMap[bookId]?.let { - BookRow( - modifier = Modifier.animateItem(), + BookCardItem( bookInformation = it, + haptic = haptic, selected = uiState.selectedBookIds.contains(it.id), onClick = { if (!uiState.selectMode) onClickBook(it.id) else changeBookSelectState(it.id) }, - onLongPress = { - onClickEnableSelectMode.invoke() - changeBookSelectState(it.id) - } + onLongPress = { onLongPress(it.id) }, + progress = {} ) } } @@ -422,231 +416,13 @@ fun CollapseGroupTitle( } } -@Composable -fun BookRow( - modifier: Modifier = Modifier, - bookInformation: BookInformation, - selected: Boolean, - onClick: () -> Unit, - onLongPress: () -> Unit -) { - val descriptionTextStyle = MaterialTheme.typography.labelLarge.copy( - fontSize = 13.sp, - lineHeight = 12.5.sp, - fontWeight = FontWeight.W400 - ) - BasicBookRow( - modifier = modifier, - bookInformation = bookInformation, - selected = selected, - onClick = onClick, - onLongPress = onLongPress - ) { - Text( - text = buildAnnotatedString { - withStyle(descriptionTextStyle.toSpanStyle()) { - append(bookInformation.author) - } - withStyle( - style = descriptionTextStyle.copy(fontWeight = FontWeight.W900).toSpanStyle() - ) { - append(" · ") - } - withStyle(descriptionTextStyle.toSpanStyle()) { - append(bookInformation.publishingHouse) - } - }, - style = descriptionTextStyle, - maxLines = 1 - ) - Text( - text = buildAnnotatedString { - withStyle(descriptionTextStyle.toSpanStyle()) { - append("${bookInformation.wordCount / 1000}K 字") - } - withStyle(style = SpanStyle(fontWeight = FontWeight.W900)) { - append(" · ") - } - if (!bookInformation.isComplete) - withStyle(descriptionTextStyle.toSpanStyle()) { - append("更新: ${bookInformation.lastUpdated.year}-${bookInformation.lastUpdated.monthValue}-${bookInformation.lastUpdated.dayOfMonth}") - } - else - withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) { - append("已完结") - } - }, - style = descriptionTextStyle, - maxLines = 1 - ) - Text( - text = bookInformation.tags.joinToString(" "), - style = descriptionTextStyle, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = bookInformation.description.trim(), - style = descriptionTextStyle, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } -} - -@Composable -fun UpdatedBookRow( - modifier: Modifier = Modifier, - bookInformation: BookInformation, - lastChapterTitle: String, - selected: Boolean, - onClick: () -> Unit, - onLongPress: () -> Unit -) { - val descriptionTextStyle = MaterialTheme.typography.labelLarge.copy( - fontSize = 12.sp, - lineHeight = 12.5.sp, - fontWeight = FontWeight.W400 - ) - val primary = MaterialTheme.colorScheme.primary - BasicBookRow( - modifier = modifier, - bookInformation = bookInformation, - selected = selected, - onClick = onClick, - onLongPress = onLongPress - ) { - Text( - text = buildAnnotatedString { - withStyle(descriptionTextStyle.toSpanStyle()) { - append(bookInformation.author) - } - withStyle( - style = descriptionTextStyle.copy(fontWeight = FontWeight.W900).toSpanStyle() - ) { - append(" · ") - } - withStyle(descriptionTextStyle.toSpanStyle()) { - append(bookInformation.publishingHouse) - } - }, - style = descriptionTextStyle, - maxLines = 1 - ) - Text( - text = buildAnnotatedString { - withStyle( - style = descriptionTextStyle.copy(fontWeight = FontWeight.W900).toSpanStyle() - ) { - append("更新至 ") - } - withStyle( - style = descriptionTextStyle.copy(fontWeight = FontWeight.W900, color = primary).toSpanStyle() - ) { - append(lastChapterTitle) - } - }, - style = descriptionTextStyle, - maxLines = 1 - ) - Text( - text = bookInformation.tags.joinToString(" "), - style = descriptionTextStyle, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun BasicBookRow( - modifier: Modifier = Modifier, - bookInformation: BookInformation, - selected: Boolean, - onClick: () -> Unit, - onLongPress: () -> Unit, - description: @Composable ColumnScope.() -> Unit -) { - Row( - modifier = modifier - .height(125.dp) - .combinedClickable( - onClick = onClick, - onLongClick = onLongPress - ) - ) { - Box( - Modifier - .size(82.dp, 125.dp) - .clip(RoundedCornerShape(8.dp))) { - Cover( - width = 82.dp, - height = 125.dp, - url = bookInformation.coverUrl, - rounded = 8.dp - ) - androidx.compose.animation.AnimatedVisibility( - visible = selected, - enter = fadeIn(), - exit = fadeOut() - ) { - Box( - Modifier - .fillMaxSize() - .background( - color = MaterialTheme.colorScheme.surfaceContainerHighest.copy( - alpha = 0.7f - ) - ) - ) { - val color = MaterialTheme.colorScheme.primary - Canvas( - Modifier - .align(Alignment.Center) - .size(36.dp)) { - drawCircle( - color = color, - radius = 18.dp.toPx() - ) - } - Icon( - modifier = Modifier - .align(Alignment.Center) - .size(22.dp), - painter = painterResource(R.drawable.check_24px), - tint = MaterialTheme.colorScheme.onPrimary, - contentDescription = null - ) - } - } - } - Column ( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp, 2.dp, 14.dp, 5.dp) - ) { - Text( - text = bookInformation.title, - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.W800, - fontSize = 16.sp, - lineHeight = 18.sp, - maxLines = 2 - ) - description.invoke(this@Column) - } - } -} - - - @OptIn(ExperimentalMaterial3Api::class) @Composable fun TopBar( scrollBehavior: TopAppBarScrollBehavior, backgroundColor: Color, selectMode: Boolean, + uiState: BookshelfHomeUiState, onClickCreate: () -> Unit, onClickSearch: () -> Unit, onClickEdit: () -> Unit, @@ -666,10 +442,12 @@ fun TopBar( var mainMenuWidth by remember { mutableStateOf(0.dp) } var mainMenuItemHeight by remember { mutableStateOf(0.dp) } var exportImportMenuWidth by remember { mutableStateOf(0.dp) } + Box( Modifier .fillMaxWidth() - .padding(horizontal = 12.dp)) { + .padding(horizontal = 12.dp) + ) { Box(Modifier.align(Alignment.TopEnd)) { DropdownMenu( modifier = Modifier @@ -681,13 +459,13 @@ fun TopBar( }, offset = DpOffset(0.dp, (-1).dp), expanded = mainMenuExpended, - onDismissRequest = { mainMenuExpended = false }) { + onDismissRequest = { mainMenuExpended = false } + ) { DropdownMenuItem( text = { Text( text = "新建书架", style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.W400 ) }, onClick = onClickCreate @@ -697,7 +475,6 @@ fun TopBar( Text( text = "编辑此书架", style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.W400 ) }, onClick = onClickEdit @@ -707,7 +484,6 @@ fun TopBar( Text( text = "分享此书架", style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.W400 ) }, onClick = onClickShareBookshelf @@ -717,7 +493,6 @@ fun TopBar( Text( text = "导入和导出...", style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.W400 ) }, trailingIcon = { @@ -728,24 +503,9 @@ fun TopBar( }, onClick = { exportImportMenuExpended = true } ) - DropdownMenuItem( - text = { - Text( - text = "排序方式...", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.W400 - ) - }, - trailingIcon = { - Icon( - painter = painterResource(R.drawable.arrow_right_24px), - contentDescription = null - ) - }, - onClick = { } - ) } } + Box( modifier = Modifier .align(Alignment.TopEnd) @@ -761,15 +521,10 @@ fun TopBar( }, offset = DpOffset(0.dp, mainMenuItemHeight.times(3.5f)), expanded = exportImportMenuExpended, - onDismissRequest = { exportImportMenuExpended = false }) { + onDismissRequest = { exportImportMenuExpended = false } + ) { DropdownMenuItem( - text = { - Text( - text = "导出为 .lnr", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.W400 - ) - }, + text = { Text("导出为 .lnr", style = MaterialTheme.typography.bodyLarge) }, onClick = { onClickSaveThisBookshelf() exportImportMenuExpended = false @@ -777,13 +532,7 @@ fun TopBar( } ) DropdownMenuItem( - text = { - Text( - text = "导出全部为 .lnr", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.W400 - ) - }, + text = { Text("导出全部为 .lnr", style = MaterialTheme.typography.bodyLarge) }, onClick = { onClickSaveAllBookshelf() exportImportMenuExpended = false @@ -791,13 +540,7 @@ fun TopBar( } ) DropdownMenuItem( - text = { - Text( - text = "从文件导入", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.W400 - ) - }, + text = { Text("从文件导入", style = MaterialTheme.typography.bodyLarge) }, onClick = { onClickImportBookshelf() exportImportMenuExpended = false @@ -810,8 +553,9 @@ fun TopBar( MediumTopAppBar( title = { - Text( - text = stringResource(id = R.string.nav_bookshelf), + AnimatedText( + text = if (selectMode) stringResource(R.string.nav_bookshelf_select_mode, uiState.selectedBookIds.size) + else stringResource(R.string.nav_bookshelf), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.W600, color = MaterialTheme.colorScheme.onSurface, @@ -820,7 +564,7 @@ fun TopBar( ) }, navigationIcon = { - AnimatedVisibility(selectMode) { + AnimatedVisibility(visible = selectMode) { IconButton(onClickDisableSelectMode) { Icon( painter = painterResource(R.drawable.cancel_24px), @@ -830,43 +574,53 @@ fun TopBar( } }, actions = { - IconButton( - if (!selectMode) { - scrollBehavior.state.heightOffset = 0f - onClickCreate + if (!selectMode) { + IconButton(onClickCreate) { + Icon( + painter = painterResource(R.drawable.library_add_24px), + contentDescription = "create" + ) + } + /*IconButton(onClickSearch) { + Icon( + painter = painterResource(R.drawable.search_24px), + contentDescription = "search" + ) + }*/ + IconButton(onClick = { mainMenuExpended = true }) { + Icon( + painter = painterResource(R.drawable.more_vert_24px), + contentDescription = stringResource(R.string.more) + ) + } + } else { + IconButton(onClickSelectAll) { + Icon( + painter = painterResource(R.drawable.select_all_24px), + contentDescription = "select all" + ) + } + IconButton(onClickPin) { + Icon( + painter = painterResource(R.drawable.keep_24px), + contentDescription = "pin" + ) + } + IconButton(onClickRemove) { + Icon( + painter = painterResource(R.drawable.bookmark_remove_24px), + contentDescription = "remove" + ) } - else onClickSelectAll - ) { - Icon( - painter = if (!selectMode) painterResource(R.drawable.library_add_24px) else painterResource(R.drawable.select_all_24px), - contentDescription = if (!selectMode) "create" else "select all" - ) - } - IconButton(if (!selectMode) onClickSearch else onClickPin) { - Icon( - painter = if (!selectMode) painterResource(R.drawable.search_24px) else painterResource(R.drawable.keep_24px), - contentDescription = if (!selectMode) "search" else "pin" - ) - } - IconButton(if (!selectMode) { { mainMenuExpended = true } } else onClickRemove) { - Icon( - painter = if (!selectMode) painterResource(R.drawable.more_vert_24px) else painterResource(R.drawable.bookmark_remove_24px), - contentDescription = if (!selectMode) stringResource(R.string.more) else "remove" - ) - } - androidx.compose.animation.AnimatedVisibility(selectMode) { IconButton(onClickBookmark) { Icon( painter = painterResource(R.drawable.outline_bookmark_24px), - contentDescription = "mark" + contentDescription = "bookmark" ) } } }, - windowInsets = - WindowInsets.safeDrawing.only( - WindowInsetsSides.Horizontal + WindowInsetsSides.Top - ), + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top), scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.mediumTopAppBarColors( containerColor = backgroundColor, diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/bookshelf/home/BookshelfUIComponents.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/bookshelf/home/BookshelfUIComponents.kt new file mode 100644 index 0000000..85c2952 --- /dev/null +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/bookshelf/home/BookshelfUIComponents.kt @@ -0,0 +1,389 @@ +package indi.dmzz_yyhyy.lightnovelreader.ui.home.bookshelf.home + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Badge +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxDefaults +import androidx.compose.material3.SwipeToDismissBoxState +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.SwipeToDismissBoxValue.EndToStart +import androidx.compose.material3.SwipeToDismissBoxValue.Settled +import androidx.compose.material3.SwipeToDismissBoxValue.StartToEnd +import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import indi.dmzz_yyhyy.lightnovelreader.R +import indi.dmzz_yyhyy.lightnovelreader.data.book.BookInformation +import indi.dmzz_yyhyy.lightnovelreader.ui.components.Cover +import indi.dmzz_yyhyy.lightnovelreader.utils.SwipeAction + +@Composable +fun BookCardContent( + selected: Boolean, + modifier: Modifier = Modifier, + bookInformation: BookInformation, + latestChapterTitle: String? = null +) { + Row( + modifier = modifier.height(136.dp).padding(4.dp), + ) { + Box( + modifier = Modifier + .size(90.dp, 136.dp) + .clip(RoundedCornerShape(8.dp)) + ) { + Box( + modifier = Modifier.graphicsLayer(alpha = if (selected) 0.7f else 1f) + ) { + Cover( + width = 90.dp, + height = 136.dp, + url = bookInformation.coverUrl, + rounded = 8.dp + ) + if (latestChapterTitle != null) { + Box( + modifier = Modifier.padding(4.dp) + .align(Alignment.TopEnd) + ) { + Badge( + modifier = Modifier.size(12.dp), + ) + } + } + } + + androidx.compose.animation.AnimatedVisibility( + visible = selected, + enter = fadeIn(), + exit = fadeOut() + ) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + val color = MaterialTheme.colorScheme.primary + Canvas( + modifier = Modifier.size(36.dp) + ) { + drawCircle( + color = color, + radius = 18.dp.toPx(), + ) + } + Icon( + modifier = Modifier + .size(22.dp), + painter = painterResource(R.drawable.check_24px), + tint = MaterialTheme.colorScheme.onPrimary, + contentDescription = null + ) + } + } + } + + + Column( + modifier = Modifier.fillMaxWidth().fillMaxHeight() + .padding(start = 12.dp), + verticalArrangement = Arrangement.SpaceBetween, + ) { + val titleLineHeight = 20.sp + Text( + modifier = Modifier.height( + with(LocalDensity.current) { (titleLineHeight * 2).toDp() } + ).wrapContentHeight(Alignment.CenterVertically), + text = bookInformation.title, + maxLines = 2, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + lineHeight = titleLineHeight, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = bookInformation.author, + maxLines = 1, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + lineHeight = 20.sp, + fontSize = 14.sp, + ) + BookStatusIcon(bookInformation) + } + Row ( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + fontSize = 14.sp, + lineHeight = 14.sp, + text = stringResource( + R.string.book_info_update_date, + bookInformation.lastUpdated.year, + bookInformation.lastUpdated.monthValue, + bookInformation.lastUpdated.dayOfMonth + ) + ) + Text( + fontSize = 14.sp, + lineHeight = 14.sp, + text = stringResource( + R.string.book_info_word_count_kilo, + bookInformation.wordCount / 1000 + ) + ) + } + if (latestChapterTitle == null) { + Text( + text = bookInformation.description.trim(), + maxLines = 2, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSecondaryContainer, + lineHeight = 18.sp, + ) + } else { + Column { + Row { + + Text( + text = "已更新至: ", + fontSize = 14.sp, + lineHeight = 18.sp, + ) + } + Row { + Text( + text = latestChapterTitle, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + fontSize = 14.sp, + lineHeight = 18.sp, + ) + } + } + } + } + } +} + +@Composable +fun BookStatusIcon(bookInformation: BookInformation) { + val modifier = Modifier.height(16.dp).width(16.dp) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (bookInformation.isComplete) { + Icon( + modifier = modifier, + painter = painterResource(R.drawable.done_all_24px), + contentDescription = "Completed", + tint = MaterialTheme.colorScheme.outline + ) + } else { + Icon( + modifier = modifier, + painter = painterResource(R.drawable.hourglass_top_24px), + contentDescription = "In Progress", + tint = MaterialTheme.colorScheme.outline + ) + } + + // 可实现: 已动画化标识 + + /*if (bookInformation.isAnimated) { + Icon( + modifier = modifier, + painter = painterResource(R.drawable.live_tv_24px), + contentDescription = "Animated", + tint = MaterialTheme.colorScheme.outline + ) + }*/ + } +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@Composable +fun BookCardItem( + modifier: Modifier = Modifier, + bookInformation: BookInformation, + selected: Boolean = false, + onClick: () -> Unit, + onLongPress: () -> Unit, + latestChapterTitle: String? = null, + swipeToRightAction: SwipeAction = SwipeAction.None, + swipeToLeftAction: SwipeAction = SwipeAction.None, + progress: (SwipeAction) -> Unit?, + haptic: HapticFeedback +){ + + val dismissState = rememberNoFlingSwipeToDismissBoxState( + positionalThreshold = { it * 0.6f }, + confirmValueChange = { + when (it) { + StartToEnd -> { + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + progress.invoke(swipeToRightAction) + } + EndToStart -> { + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + progress.invoke(swipeToLeftAction) + } + Settled -> { } + } + false + }, + ) + + LaunchedEffect(dismissState.dismissDirection) { + if (dismissState.dismissDirection != Settled) { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + } + + val backgroundColor by animateColorAsState( + targetValue = if (selected) MaterialTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.surface, + animationSpec = tween(durationMillis = 300) + ) + + Card { + SwipeToDismissBox( + state = dismissState, + modifier = modifier, + enableDismissFromEndToStart = swipeToLeftAction != SwipeAction.None, + enableDismissFromStartToEnd = swipeToRightAction != SwipeAction.None, + backgroundContent = { + DismissBackground( + dismissState = dismissState, + swipeToLeftAction = swipeToLeftAction, + swipeToRightAction = swipeToRightAction + ) + }, + content = { + Box( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(backgroundColor) + .combinedClickable( + onClick = onClick, + onLongClick = onLongPress, + ) + ) { + BookCardContent( + selected = selected, + latestChapterTitle = latestChapterTitle, + bookInformation = bookInformation + ) + } + } + ) + } +} + +@Composable +@ExperimentalMaterial3Api +fun rememberNoFlingSwipeToDismissBoxState( + initialValue: SwipeToDismissBoxValue = Settled, + confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true }, + positionalThreshold: (totalDistance: Float) -> Float = + SwipeToDismissBoxDefaults.positionalThreshold, +): SwipeToDismissBoxState { + val density = Density(Float.POSITIVE_INFINITY) + return rememberSaveable( + saver = SwipeToDismissBoxState.Saver( + confirmValueChange = confirmValueChange, + density = density, + positionalThreshold = positionalThreshold + ) + ) { + SwipeToDismissBoxState(initialValue, density, confirmValueChange, positionalThreshold) + } +} + +@Composable +private fun DismissBackground( + dismissState: SwipeToDismissBoxState, + swipeToRightAction: SwipeAction, + swipeToLeftAction: SwipeAction +) { + val color = when (dismissState.dismissDirection) { + StartToEnd -> swipeToRightAction.color + EndToStart -> swipeToLeftAction.color + Settled -> Color.Transparent + } + + Row( + modifier = Modifier + .fillMaxSize() + .background(color) + .clip(RoundedCornerShape(12.dp)) + .padding(28.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (dismissState.dismissDirection == StartToEnd) { + Icon( + painter = painterResource(id = swipeToRightAction.iconRes), + contentDescription = swipeToRightAction.description, + tint = MaterialTheme.colorScheme.surface + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + if (dismissState.dismissDirection == EndToStart) { + Icon( + painter = painterResource(id = swipeToLeftAction.iconRes), + contentDescription = swipeToLeftAction.description, + tint = MaterialTheme.colorScheme.surface + ) + } + } +} diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/settings/SettingsScreen.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/settings/SettingsScreen.kt index 7cab9db..3e6df71 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/home/settings/SettingsScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState @@ -84,7 +83,7 @@ fun SettingsScreen( Modifier.verticalScroll(rememberScrollState()) .nestedScroll(pinnedScrollBehavior.nestedScrollConnection) ) { - SettingsCard( + SettingsCategory( title = stringResource(R.string.app_settings), icon = ImageVector.vectorResource(R.drawable.outline_settings_24px) ) { @@ -93,20 +92,20 @@ fun SettingsScreen( checkUpdate = checkUpdate ) } - SettingsCard( + SettingsCategory( title = stringResource(R.string.display_settings), icon = ImageVector.vectorResource(R.drawable.light_mode_24px) ) { DisplaySettingsList(settingState = settingState) } - /*SettingsCard( + /*SettingsCategory( title = "阅读", icon = ImageVector.vectorResource(R.drawable.outline_bookmark_24px), content = { ReaderSettingsList( state = state, ) } )*/ - SettingsCard( + SettingsCategory( title = "数据", icon = ImageVector.vectorResource(R.drawable.hard_disk_24px) ) { @@ -120,7 +119,7 @@ fun SettingsScreen( webDataSourceId = viewModel.webBookDataSourceId, ) } - SettingsCard( + SettingsCategory( title = stringResource(R.string.about_settings), icon = ImageVector.vectorResource(R.drawable.info_24px) ) { @@ -160,62 +159,53 @@ private fun TopBar( ) } - @Composable -fun SettingsCard( +fun SettingsCategory( title: String, icon: ImageVector, content: @Composable ColumnScope.() -> Unit ) { var expanded by remember { mutableStateOf(true) } - Card( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 8.dp, start = 14.dp, end = 14.dp), + modifier = Modifier.fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 14.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, + containerColor = MaterialTheme.colorScheme.surfaceContainerLow ), shape = RoundedCornerShape(16.dp), - elevation = CardDefaults.elevatedCardElevation(2.dp) ) { Column { Row( - modifier = Modifier - .fillMaxWidth() - .height(68.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() ) { - Row( + Box( modifier = Modifier - .weight(1f) - .padding(start = 18.dp), - verticalAlignment = Alignment.CenterVertically + .padding(start = 14.dp) + .size(40.dp) + .background( + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceContainerHigh + ), + contentAlignment = Alignment.Center ) { - Box( - modifier = Modifier - .size(40.dp) - .background( - color = MaterialTheme.colorScheme.surfaceContainerHighest, - shape = CircleShape - ), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - } - Text( - text = title, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - fontSize = 18.sp, - modifier = Modifier.padding(start = 16.dp) + Icon( + imageVector = icon, + tint = MaterialTheme.colorScheme.primary, + contentDescription = null, ) } + + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + modifier = Modifier + .weight(1f) + .padding(horizontal = 14.dp) + ) + IconButton( onClick = { expanded = !expanded }, modifier = Modifier.padding(16.dp) @@ -232,7 +222,7 @@ fun SettingsCard( ) { Column( modifier = Modifier.clip(RoundedCornerShape(16.dp)), - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), content = content ) } diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/utils/SwipeAction.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/utils/SwipeAction.kt new file mode 100644 index 0000000..603f8d2 --- /dev/null +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/utils/SwipeAction.kt @@ -0,0 +1,64 @@ +package indi.dmzz_yyhyy.lightnovelreader.utils + +import androidx.compose.ui.graphics.Color +import indi.dmzz_yyhyy.lightnovelreader.R + +@Suppress("LeakingThis") +sealed class SwipeAction( + val id: String, + val iconRes: Int, + val color: Color, + val description: String, +) { + + companion object { + private val _all = mutableMapOf() + val all: Map = _all + } + + init { + _all[id] = this + } + + data object AddToBookshelf : SwipeAction( + id = "add_to_bookshelf", + iconRes = R.drawable.bookmark_add_24px, + color = Color(0xff2ECC71), + description = "" + ) + + data object RemoveFromBookshelf : SwipeAction( + id = "remove_from_bookshelf", + iconRes = R.drawable.delete_forever_24px, + color = Color(0xffE74C3C), + description = "" + ) + + data object Pin : SwipeAction( + id = "pin", + iconRes = R.drawable.keep_24px, + color = Color(0xff007AFF), + description = "" + ) + + data object Expand : SwipeAction( + id = "expand", + iconRes = R.drawable.expand_circle_down_24px, + color = Color(0xffF1C40F), + description = "" + ) + + data object Info : SwipeAction( + id = "info", + iconRes = R.drawable.info_24px, + color = Color(0xffE67E22), + description = "" + ) + + data object None : SwipeAction( + id = "none", + iconRes = R.drawable.block_24px, + color = Color.Transparent, + description = "" + ) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/blank_24px.xml b/app/src/main/res/drawable/blank_24px.xml new file mode 100644 index 0000000..a806558 --- /dev/null +++ b/app/src/main/res/drawable/blank_24px.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/drawable/block_24px.xml b/app/src/main/res/drawable/block_24px.xml new file mode 100644 index 0000000..540750b --- /dev/null +++ b/app/src/main/res/drawable/block_24px.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/expand_circle_down_24px.xml b/app/src/main/res/drawable/expand_circle_down_24px.xml new file mode 100644 index 0000000..74c73f6 --- /dev/null +++ b/app/src/main/res/drawable/expand_circle_down_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/hourglass_top_24px.xml b/app/src/main/res/drawable/hourglass_top_24px.xml new file mode 100644 index 0000000..1d119b5 --- /dev/null +++ b/app/src/main/res/drawable/hourglass_top_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/live_tv_24px.xml b/app/src/main/res/drawable/live_tv_24px.xml new file mode 100644 index 0000000..90ab05d --- /dev/null +++ b/app/src/main/res/drawable/live_tv_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9b79e70..010657a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -3,6 +3,7 @@ 阅读中 书架 + 选择 书架 (%d) 探索 首页 分类 @@ -76,5 +77,8 @@ 文库 文库筛选 根据小说文库的筛选。 + 更新: %s-%s-%s + %dK 字 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 01f9edb..6ec23af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ In Reading Bookshelf + Select Bookshelf (%d) Exploration Homepage Categories @@ -78,5 +79,8 @@ Publishing House Filter by publishing house Filter by the novel\'s publishing house. + Updated: %s-%s-%s + %dK words + \ No newline at end of file