[优化] 新的阅读记录日期选择,现在可以方便的查看每天的阅读情况
This commit is contained in:
10 files changed
+698
-74
No files matched your search
@@ -55,23 +55,6 @@ data class BookmarkUiState(
|
|||||||
val collapsedGroups: Set<String> = emptySet()
|
val collapsedGroups: Set<String> = emptySet()
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface BookmarkListItem {
|
|
||||||
val key: String
|
|
||||||
|
|
||||||
data class Header(
|
|
||||||
val header: BookmarkGroupHeader,
|
|
||||||
val collapsed: Boolean
|
|
||||||
) : BookmarkListItem {
|
|
||||||
override val key = "header:${header.bookName}|${header.bookAuthor}"
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Item(
|
|
||||||
val data: BookmarkItemUi
|
|
||||||
) : BookmarkListItem {
|
|
||||||
override val key = "item:${data.id}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AllBookmarkViewModel(
|
class AllBookmarkViewModel(
|
||||||
application: Application,
|
application: Application,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package io.legado.app.ui.book.readRecord
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -16,6 +18,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -43,6 +46,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
@@ -52,9 +56,14 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -64,7 +73,6 @@ import cn.hutool.core.date.DateUtil
|
|||||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||||
import io.legado.app.ui.widget.components.AnimatedTextLine
|
import io.legado.app.ui.widget.components.AnimatedTextLine
|
||||||
import io.legado.app.ui.widget.components.Calendar
|
|
||||||
import io.legado.app.ui.widget.components.Cover
|
import io.legado.app.ui.widget.components.Cover
|
||||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||||
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
||||||
@@ -72,6 +80,16 @@ import io.legado.app.ui.widget.components.SearchBarSection
|
|||||||
import io.legado.app.ui.widget.components.SectionHeader
|
import io.legado.app.ui.widget.components.SectionHeader
|
||||||
import io.legado.app.ui.widget.components.button.AlertButton
|
import io.legado.app.ui.widget.components.button.AlertButton
|
||||||
import io.legado.app.ui.widget.components.button.SmallTopBarButton
|
import io.legado.app.ui.widget.components.button.SmallTopBarButton
|
||||||
|
import io.legado.app.ui.widget.components.heatmap.HeatmapCalendarTopBar
|
||||||
|
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
|
||||||
|
import io.legado.app.ui.widget.components.heatmap.HeatmapWeekColumn
|
||||||
|
import io.legado.app.ui.widget.components.heatmap.NoEarlierDataIndicator
|
||||||
|
import io.legado.app.ui.widget.components.heatmap.WeekdayLabelsColumn
|
||||||
|
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.swipe.SwipeAction
|
import io.legado.app.ui.widget.components.swipe.SwipeAction
|
||||||
import io.legado.app.ui.widget.components.swipe.SwipeActionContainer
|
import io.legado.app.ui.widget.components.swipe.SwipeActionContainer
|
||||||
import io.legado.app.utils.StringUtils.formatFriendlyDate
|
import io.legado.app.utils.StringUtils.formatFriendlyDate
|
||||||
@@ -166,19 +184,6 @@ fun ReadRecordScreen(
|
|||||||
onQueryChange = { viewModel.setSearchKey(it) }
|
onQueryChange = { viewModel.setSearchKey(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AnimatedVisibility(visible = showCalendar) {
|
|
||||||
CalendarSection(
|
|
||||||
selectedDate = state.selectedDate,
|
|
||||||
onDateSelected = { date ->
|
|
||||||
viewModel.setSelectedDate(date)
|
|
||||||
showCalendar = false
|
|
||||||
},
|
|
||||||
onClearDate = {
|
|
||||||
viewModel.setSelectedDate(null)
|
|
||||||
showCalendar = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
@@ -286,6 +291,30 @@ fun ReadRecordScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showCalendar) {
|
||||||
|
val sheetState =
|
||||||
|
androidx.compose.material3.rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
androidx.compose.material3.ModalBottomSheet(
|
||||||
|
onDismissRequest = { showCalendar = false },
|
||||||
|
sheetState = sheetState
|
||||||
|
) {
|
||||||
|
HeatmapCalendarSection(
|
||||||
|
dailyReadCounts = state.dailyReadCounts,
|
||||||
|
dailyReadTimes = state.dailyReadTimes,
|
||||||
|
selectedDate = state.selectedDate,
|
||||||
|
onDateSelected = { date ->
|
||||||
|
viewModel.setSelectedDate(date)
|
||||||
|
showCalendar = false
|
||||||
|
},
|
||||||
|
onClearDate = {
|
||||||
|
viewModel.setSelectedDate(null)
|
||||||
|
showCalendar = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -329,6 +358,139 @@ fun SummarySection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun HeatmapCalendarSection(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
dailyReadCounts: Map<LocalDate, Int>,
|
||||||
|
dailyReadTimes: Map<LocalDate, Long>,
|
||||||
|
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)
|
||||||
|
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LaunchedEffect(weeks) {
|
||||||
|
if (weeks.isNotEmpty()) {
|
||||||
|
listState.scrollToItem(weeks.size - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val showLeftGradient by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val showRightGradient by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
listState.canScrollForward
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val leftAlpha by animateFloatAsState(
|
||||||
|
targetValue = if (showLeftGradient) 1f else 0f,
|
||||||
|
animationSpec = tween(durationMillis = 200),
|
||||||
|
label = "LeftGradientAlpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
val rightAlpha by animateFloatAsState(
|
||||||
|
targetValue = if (showRightGradient) 1f else 0f,
|
||||||
|
animationSpec = tween(durationMillis = 200),
|
||||||
|
label = "RightGradientAlpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.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,
|
||||||
|
cellSpacing = config.cellSpacing
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
state = listState,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(config.cellSpacing),
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
|
||||||
|
val width = size.width
|
||||||
|
val gradientWidthPx = config.gradientWidth.toPx()
|
||||||
|
val leftStop = gradientWidthPx / width
|
||||||
|
val rightStop = 1f - (gradientWidthPx / width)
|
||||||
|
|
||||||
|
val colorStops = arrayOf(
|
||||||
|
0f to Color.Black.copy(alpha = 1f - leftAlpha),
|
||||||
|
leftStop to Color.Black,
|
||||||
|
rightStop to Color.Black,
|
||||||
|
1f to Color.Black.copy(alpha = 1f - rightAlpha)
|
||||||
|
)
|
||||||
|
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
colorStops = colorStops,
|
||||||
|
startX = 0f,
|
||||||
|
endX = width
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.DstIn
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val firstReadDate = listOfNotNull(
|
||||||
|
dailyReadCounts.filterValues { it > 0 }.keys.minOrNull(),
|
||||||
|
dailyReadTimes.filterValues { it > 0L }.keys.minOrNull()
|
||||||
|
).minOrNull()
|
||||||
|
|
||||||
|
if (firstReadDate != null) {
|
||||||
|
item {
|
||||||
|
NoEarlierDataIndicator(cellSize = config.cellSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(weeks.size) { weekIndex ->
|
||||||
|
HeatmapWeekColumn(
|
||||||
|
week = weeks[weekIndex],
|
||||||
|
mode = currentMode,
|
||||||
|
dailyReadCounts = dailyReadCounts,
|
||||||
|
dailyReadTimes = dailyReadTimes,
|
||||||
|
selectedDate = selectedDate,
|
||||||
|
config = config,
|
||||||
|
onDateSelected = onDateSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
HeatmapLegend(
|
||||||
|
mode = currentMode,
|
||||||
|
config = config
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun LazyListScope.renderListByMode(
|
fun LazyListScope.renderListByMode(
|
||||||
displayMode: DisplayMode,
|
displayMode: DisplayMode,
|
||||||
state: ReadRecordUiState,
|
state: ReadRecordUiState,
|
||||||
@@ -615,22 +777,6 @@ fun DateHeader(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CalendarSection(
|
|
||||||
selectedDate: LocalDate?,
|
|
||||||
onDateSelected: (LocalDate) -> Unit,
|
|
||||||
onClearDate: () -> Unit
|
|
||||||
) {
|
|
||||||
val effectiveInitialDate = selectedDate ?: LocalDate.now()
|
|
||||||
Calendar(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
initialDate = effectiveInitialDate,
|
|
||||||
selectedDate = selectedDate,
|
|
||||||
onDateSelected = onDateSelected,
|
|
||||||
onClearDate = onClearDate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReadingSummaryCard(
|
fun ReadingSummaryCard(
|
||||||
title: String,
|
title: String,
|
||||||
|
|||||||
@@ -24,14 +24,13 @@ import java.util.Date
|
|||||||
data class ReadRecordUiState(
|
data class ReadRecordUiState(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val totalReadTime: Long = 0,
|
val totalReadTime: Long = 0,
|
||||||
//每日聚合明细
|
|
||||||
val groupedRecords: Map<String, List<ReadRecordDetail>> = emptyMap(),
|
val groupedRecords: Map<String, List<ReadRecordDetail>> = emptyMap(),
|
||||||
//每日所有阅读会话
|
|
||||||
val timelineRecords: Map<String, List<ReadRecordSession>> = emptyMap(),
|
val timelineRecords: Map<String, List<ReadRecordSession>> = emptyMap(),
|
||||||
//最后阅读列表
|
|
||||||
val latestRecords: List<ReadRecord> = emptyList(),
|
val latestRecords: List<ReadRecord> = emptyList(),
|
||||||
val selectedDate: LocalDate? = null,
|
val selectedDate: LocalDate? = null,
|
||||||
val searchKey: String? = null
|
val searchKey: String? = null,
|
||||||
|
val dailyReadCounts: Map<LocalDate, Int> = emptyMap(),
|
||||||
|
val dailyReadTimes: Map<LocalDate, Long> = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class DisplayMode {
|
enum class DisplayMode {
|
||||||
@@ -71,6 +70,18 @@ class ReadRecordViewModel(
|
|||||||
) { data, selectedDate, searchKey ->
|
) { data, selectedDate, searchKey ->
|
||||||
val dateStr = selectedDate?.format(DateTimeFormatter.ISO_LOCAL_DATE)
|
val dateStr = selectedDate?.format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||||
|
|
||||||
|
val dailyCounts = data.details
|
||||||
|
.groupBy { it.date }
|
||||||
|
.mapKeys { LocalDate.parse(it.key, DateTimeFormatter.ISO_LOCAL_DATE) }
|
||||||
|
.mapValues { it.value.size }
|
||||||
|
|
||||||
|
val dailyTimes = data.sessions
|
||||||
|
.groupBy { DateUtil.format(Date(it.startTime), "yyyy-MM-dd") }
|
||||||
|
.mapKeys { LocalDate.parse(it.key, DateTimeFormatter.ISO_LOCAL_DATE) }
|
||||||
|
.mapValues { (_, sessions) ->
|
||||||
|
sessions.sumOf { (it.endTime - it.startTime).coerceAtLeast(0L) }
|
||||||
|
}
|
||||||
|
|
||||||
val filteredDetails = data.details.filter { detail ->
|
val filteredDetails = data.details.filter { detail ->
|
||||||
dateStr == null || detail.date == dateStr
|
dateStr == null || detail.date == dateStr
|
||||||
}
|
}
|
||||||
@@ -97,7 +108,9 @@ class ReadRecordViewModel(
|
|||||||
timelineRecords = timelineMap,
|
timelineRecords = timelineMap,
|
||||||
latestRecords = data.latestRecords,
|
latestRecords = data.latestRecords,
|
||||||
selectedDate = selectedDate,
|
selectedDate = selectedDate,
|
||||||
searchKey = searchKey
|
searchKey = searchKey,
|
||||||
|
dailyReadCounts = dailyCounts,
|
||||||
|
dailyReadTimes = dailyTimes
|
||||||
)
|
)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
|
|||||||
@@ -160,7 +160,8 @@ fun SearchContentScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = Icons.Default.FindReplace,
|
iconChecked = Icons.Default.FindReplace,
|
||||||
|
iconUnchecked = Icons.Default.FindReplace,
|
||||||
activeText = "替换开启",
|
activeText = "替换开启",
|
||||||
inactiveText = "替换关闭"
|
inactiveText = "替换关闭"
|
||||||
)
|
)
|
||||||
@@ -177,7 +178,8 @@ fun SearchContentScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = Icons.Default.Code,
|
iconChecked = Icons.Default.Code,
|
||||||
|
iconUnchecked = Icons.Default.Code,
|
||||||
activeText = "正则开启",
|
activeText = "正则开启",
|
||||||
inactiveText = "正则关闭"
|
inactiveText = "正则关闭"
|
||||||
)
|
)
|
||||||
|
|||||||
+19
-7
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.ui.widget.components.button
|
package io.legado.app.ui.widget.components.button
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -27,12 +28,15 @@ import kotlinx.coroutines.delay
|
|||||||
fun AnimatedActionButton(
|
fun AnimatedActionButton(
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
icon: ImageVector,
|
iconChecked: ImageVector,
|
||||||
|
iconUnchecked: ImageVector,
|
||||||
activeText: String,
|
activeText: String,
|
||||||
inactiveText: String
|
inactiveText: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var showText by remember { mutableStateOf(false) }
|
var showText by remember { mutableStateOf(false) }
|
||||||
|
var lastCheckedState by remember { mutableStateOf(checked) }
|
||||||
|
|
||||||
LaunchedEffect(showText) {
|
LaunchedEffect(showText) {
|
||||||
if (showText) {
|
if (showText) {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
@@ -44,6 +48,7 @@ fun AnimatedActionButton(
|
|||||||
contentPadding = PaddingValues(4.dp),
|
contentPadding = PaddingValues(4.dp),
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
|
lastCheckedState = it
|
||||||
onCheckedChange(it)
|
onCheckedChange(it)
|
||||||
showText = true
|
showText = true
|
||||||
}
|
}
|
||||||
@@ -51,18 +56,25 @@ fun AnimatedActionButton(
|
|||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
AnimatedContent(
|
||||||
contentDescription = null
|
targetState = checked,
|
||||||
)
|
label = "IconAnimation"
|
||||||
|
) { targetChecked ->
|
||||||
|
Icon(
|
||||||
|
imageVector = if (targetChecked) iconChecked else iconUnchecked,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showText
|
visible = showText
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (checked) activeText else inactiveText,
|
text = if (lastCheckedState) activeText else inactiveText,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|||||||
+307
@@ -0,0 +1,307 @@
|
|||||||
|
package io.legado.app.ui.widget.components.heatmap
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
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.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.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedIconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import io.legado.app.ui.widget.components.button.AnimatedActionButton
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热力图日历顶部操作栏
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun HeatmapCalendarTopBar(
|
||||||
|
currentMode: HeatmapMode,
|
||||||
|
onModeChanged: (HeatmapMode) -> Unit,
|
||||||
|
onClearDate: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("时间线", style = MaterialTheme.typography.titleMedium)
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
AnimatedActionButton(
|
||||||
|
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))
|
||||||
|
|
||||||
|
OutlinedIconButton(
|
||||||
|
onClick = onClearDate,
|
||||||
|
shapes = IconButtonDefaults.shapes(),
|
||||||
|
border = ButtonDefaults.outlinedButtonBorder()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Delete,
|
||||||
|
contentDescription = "清除选择"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 星期标签列(周一到周日)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun WeekdayLabelsColumn(
|
||||||
|
cellSize: Dp,
|
||||||
|
cellSpacing: Dp,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(top = 20.dp, end = 8.dp)
|
||||||
|
) {
|
||||||
|
val labels = listOf("一", "二", "三", "四", "五", "六", "日")
|
||||||
|
|
||||||
|
labels.forEachIndexed { index, label ->
|
||||||
|
if (index % 2 == 0) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.height(cellSize)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.height(cellSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 6) Spacer(modifier = Modifier.height(cellSpacing))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 没有更早数据的提示组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun NoEarlierDataIndicator(
|
||||||
|
cellSize: Dp,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val outlineColor = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(top = 24.dp, start = 8.dp, end = 16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
repeat(7) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(cellSize)
|
||||||
|
.drawBehind {
|
||||||
|
val strokeWidth = 1.dp.toPx()
|
||||||
|
val stroke = Stroke(
|
||||||
|
width = strokeWidth,
|
||||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(5f, 5f), 0f)
|
||||||
|
)
|
||||||
|
val inset = strokeWidth / 2
|
||||||
|
drawRoundRect(
|
||||||
|
color = outlineColor,
|
||||||
|
style = stroke,
|
||||||
|
topLeft = Offset(inset, inset),
|
||||||
|
size = Size(size.width - strokeWidth, size.height - strokeWidth),
|
||||||
|
cornerRadius = CornerRadius(4.dp.toPx())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
"没有更早数据".forEach { char ->
|
||||||
|
Text(
|
||||||
|
text = char.toString(),
|
||||||
|
fontSize = 9.sp,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
fontWeight = FontWeight.Light
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个日历单元格
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun HeatmapCalendarCell(
|
||||||
|
day: LocalDate,
|
||||||
|
mode: HeatmapMode,
|
||||||
|
dailyReadCounts: Map<LocalDate, Int>,
|
||||||
|
dailyReadTimes: Map<LocalDate, Long>,
|
||||||
|
isSelected: Boolean,
|
||||||
|
config: HeatmapConfig,
|
||||||
|
onDateSelected: (LocalDate) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val level = rememberHeatmapLevel(day, mode, dailyReadCounts, dailyReadTimes)
|
||||||
|
val cellColor = heatmapColorForLevel(level)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(config.cellSize)
|
||||||
|
.clip(RoundedCornerShape(config.cornerRadius))
|
||||||
|
.background(cellColor)
|
||||||
|
.border(
|
||||||
|
width = if (isSelected) 2.dp else 0.dp,
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.onSurface else Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(config.cornerRadius)
|
||||||
|
)
|
||||||
|
.clickable { onDateSelected(day) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一周的日历列(包含月份标签)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun HeatmapWeekColumn(
|
||||||
|
week: List<LocalDate?>,
|
||||||
|
mode: HeatmapMode,
|
||||||
|
dailyReadCounts: Map<LocalDate, Int>,
|
||||||
|
dailyReadTimes: Map<LocalDate, Long>,
|
||||||
|
selectedDate: LocalDate?,
|
||||||
|
config: HeatmapConfig,
|
||||||
|
onDateSelected: (LocalDate) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val firstDayOfMonth = week.firstOrNull { it?.dayOfMonth == 1 }
|
||||||
|
|
||||||
|
Box(modifier = modifier.width(config.cellSize)) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(top = 24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(config.cellSpacing)
|
||||||
|
) {
|
||||||
|
week.forEach { day ->
|
||||||
|
if (day == null) {
|
||||||
|
Spacer(modifier = Modifier.size(config.cellSize))
|
||||||
|
} else {
|
||||||
|
HeatmapCalendarCell(
|
||||||
|
day = day,
|
||||||
|
mode = mode,
|
||||||
|
dailyReadCounts = dailyReadCounts,
|
||||||
|
dailyReadTimes = dailyReadTimes,
|
||||||
|
isSelected = day == selectedDate,
|
||||||
|
config = config,
|
||||||
|
onDateSelected = onDateSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 月份标签
|
||||||
|
if (firstDayOfMonth != null) {
|
||||||
|
Text(
|
||||||
|
text = "${firstDayOfMonth.monthValue}月",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.wrapContentWidth(unbounded = true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热力图图例
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun HeatmapLegend(
|
||||||
|
mode: HeatmapMode,
|
||||||
|
config: HeatmapConfig,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val legendUnit = if (mode == HeatmapMode.COUNT) "次" else "长"
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"少($legendUnit)",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
|
for (level in 0..4) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(config.legendSize)
|
||||||
|
.clip(RoundedCornerShape(2.dp))
|
||||||
|
.background(heatmapColorForLevel(level))
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"多($legendUnit)",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package io.legado.app.ui.widget.components.heatmap
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
enum class HeatmapMode {
|
||||||
|
COUNT, TIME
|
||||||
|
}
|
||||||
|
|
||||||
|
data class HeatmapConfig(
|
||||||
|
val cellSize: Dp = 16.dp,
|
||||||
|
val cellSpacing: Dp = 4.dp,
|
||||||
|
val cornerRadius: Dp = 4.dp,
|
||||||
|
val gradientWidth: Dp = 16.dp,
|
||||||
|
val legendSize: Dp = 12.dp
|
||||||
|
)
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package io.legado.app.ui.widget.components.heatmap
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算日期范围:从最早有数据的日期前1个月到最晚有数据的日期后1个月
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberDateRange(
|
||||||
|
dailyReadCounts: Map<LocalDate, Int>,
|
||||||
|
dailyReadTimes: Map<LocalDate, Long>
|
||||||
|
): Pair<LocalDate, LocalDate> {
|
||||||
|
return remember(dailyReadCounts, dailyReadTimes) {
|
||||||
|
val firstReadDate = listOfNotNull(
|
||||||
|
dailyReadCounts.filterValues { it > 0 }.keys.minOrNull(),
|
||||||
|
dailyReadTimes.filterValues { it > 0L }.keys.minOrNull()
|
||||||
|
).minOrNull()
|
||||||
|
|
||||||
|
val lastReadDate = listOfNotNull(
|
||||||
|
dailyReadCounts.filterValues { it > 0 }.keys.maxOrNull(),
|
||||||
|
dailyReadTimes.filterValues { it > 0L }.keys.maxOrNull()
|
||||||
|
).maxOrNull()
|
||||||
|
|
||||||
|
val startDate = firstReadDate?.minusMonths(1) ?: LocalDate.now()
|
||||||
|
val endDate = lastReadDate ?: startDate
|
||||||
|
|
||||||
|
startDate to endDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成日期范围内的所有日期列表
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberDaysInRange(startDate: LocalDate, endDate: LocalDate): List<LocalDate> {
|
||||||
|
return remember(startDate, endDate) {
|
||||||
|
val list = mutableListOf<LocalDate>()
|
||||||
|
var current = startDate
|
||||||
|
while (!current.isAfter(endDate)) {
|
||||||
|
list.add(current)
|
||||||
|
current = current.plusDays(1)
|
||||||
|
}
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将日期列表按周分组(补全第一周的空白天数)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberWeeks(days: List<LocalDate>, startDate: LocalDate): List<List<LocalDate?>> {
|
||||||
|
return remember(days, startDate) {
|
||||||
|
val firstDayOfWeekOffset = startDate.dayOfWeek.value - 1 // 周一为1
|
||||||
|
val padded = List(firstDayOfWeekOffset) { null } + days
|
||||||
|
padded.chunked(7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算日期对应的热力图等级(0-4)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberHeatmapLevel(
|
||||||
|
day: LocalDate,
|
||||||
|
mode: HeatmapMode,
|
||||||
|
dailyReadCounts: Map<LocalDate, Int>,
|
||||||
|
dailyReadTimes: Map<LocalDate, Long>
|
||||||
|
): Int {
|
||||||
|
val maxCount by remember(dailyReadCounts) {
|
||||||
|
derivedStateOf { dailyReadCounts.values.maxOrNull()?.coerceAtLeast(1) ?: 1 }
|
||||||
|
}
|
||||||
|
val maxTime by remember(dailyReadTimes) {
|
||||||
|
derivedStateOf { dailyReadTimes.values.maxOrNull()?.coerceAtLeast(1L) ?: 1L }
|
||||||
|
}
|
||||||
|
|
||||||
|
return remember(day, mode, maxCount, maxTime) {
|
||||||
|
val fraction = if (mode == HeatmapMode.COUNT) {
|
||||||
|
val value = dailyReadCounts[day] ?: 0
|
||||||
|
value.toFloat() / maxCount
|
||||||
|
} else {
|
||||||
|
val value = dailyReadTimes[day] ?: 0L
|
||||||
|
value.toFloat() / maxTime
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
fraction == 0f -> 0
|
||||||
|
fraction < 0.25f -> 1
|
||||||
|
fraction < 0.5f -> 2
|
||||||
|
fraction < 0.75f -> 3
|
||||||
|
else -> 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据等级获取对应的颜色
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun heatmapColorForLevel(
|
||||||
|
level: Int,
|
||||||
|
primary: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
emptyColor: Color = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
): Color {
|
||||||
|
return when (level) {
|
||||||
|
0 -> emptyColor
|
||||||
|
1 -> primary.copy(alpha = 0.35f)
|
||||||
|
2 -> primary.copy(alpha = 0.55f)
|
||||||
|
3 -> primary.copy(alpha = 0.75f)
|
||||||
|
else -> primary
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package io.legado.app.ui.widget.components.menuItem
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RoundDropdownMenu(
|
||||||
|
expanded: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
shape: Shape = MaterialTheme.shapes.medium,
|
||||||
|
shadowElevation: Dp = 4.dp,
|
||||||
|
verticalSpacing: Dp = 8.dp,
|
||||||
|
content: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit
|
||||||
|
) {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
|
shape = shape,
|
||||||
|
shadowElevation = shadowElevation
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(verticalSpacing)
|
||||||
|
) {
|
||||||
|
content(onDismissRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import androidx.compose.animation.expandVertically
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
@@ -15,11 +14,9 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -28,12 +25,12 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.legado.app.ui.widget.components.AdaptiveAnimatedText
|
import io.legado.app.ui.widget.components.AdaptiveAnimatedText
|
||||||
import io.legado.app.ui.widget.components.AnimatedTextLine
|
import io.legado.app.ui.widget.components.AnimatedTextLine
|
||||||
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
||||||
import io.legado.app.ui.widget.components.SearchBarSection
|
import io.legado.app.ui.widget.components.SearchBarSection
|
||||||
import io.legado.app.ui.widget.components.button.SmallTopBarButton
|
import io.legado.app.ui.widget.components.button.SmallTopBarButton
|
||||||
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.rules.RuleActionState
|
import io.legado.app.ui.widget.components.rules.RuleActionState
|
||||||
|
|
||||||
|
|
||||||
@@ -97,17 +94,11 @@ fun <T> DynamicTopAppBar(
|
|||||||
IconButton(onClick = { showMenu = true }) {
|
IconButton(onClick = { showMenu = true }) {
|
||||||
Icon(Icons.Default.MoreVert, null)
|
Icon(Icons.Default.MoreVert, null)
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
RoundDropdownMenu(
|
||||||
expanded = showMenu,
|
expanded = showMenu,
|
||||||
shape = MaterialTheme.shapes.medium,
|
|
||||||
shadowElevation = 4.dp,
|
|
||||||
onDismissRequest = { showMenu = false }
|
onDismissRequest = { showMenu = false }
|
||||||
) {
|
) { dismiss ->
|
||||||
Column(
|
content(dismiss)
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
content { showMenu = false }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user