From 7404bdfc28c591753886bf83d4f7dce859023594 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 16:03:41 +0800 Subject: [PATCH 1/5] feat: add birthday crown vector drawable --- .../main/res/drawable/ic_birthday_crown.xml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 core/src/main/res/drawable/ic_birthday_crown.xml diff --git a/core/src/main/res/drawable/ic_birthday_crown.xml b/core/src/main/res/drawable/ic_birthday_crown.xml new file mode 100644 index 0000000..67c8994 --- /dev/null +++ b/core/src/main/res/drawable/ic_birthday_crown.xml @@ -0,0 +1,30 @@ + + + + + + + + + + From f33e68d1dbc08275842a17ff954621a07c7fbe2c Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 16:16:09 +0800 Subject: [PATCH 2/5] feat: compute birthday flag in LunarCache --- .../kotlin/plus/rua/project/LunarCache.kt | 17 +++++++---- .../rua/project/LunarCacheBirthdayTest.kt | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 core/src/test/kotlin/plus/rua/project/LunarCacheBirthdayTest.kt diff --git a/core/src/main/kotlin/plus/rua/project/LunarCache.kt b/core/src/main/kotlin/plus/rua/project/LunarCache.kt index 3199e23..5ab13af 100644 --- a/core/src/main/kotlin/plus/rua/project/LunarCache.kt +++ b/core/src/main/kotlin/plus/rua/project/LunarCache.kt @@ -75,23 +75,28 @@ class LunarCache( val lunarDay = solarDay.getLunarDay() val lunarMonth = lunarDay.getLunarMonth() val lunarMonthName = lunarMonth.getName() + // 阳历生日:每年 9 月 4 日 + val isSolarBirthday = date.month.number == 9 && date.day == 4 + // 农历生日:每年正月二十一(tyme4kt 中正月 indexInYear = 0) + val isLunarBirthday = lunarMonth.getIndexInYear() == 0 && lunarDay.day == 21 + val isBirthday = isSolarBirthday || isLunarBirthday // 农历传统节日(仅当天) val lunarFestival = lunarDay.getFestival() if (lunarFestival != null) { - return DayCellInfo(lunarFestival.getName(), true, holidayBadge, lunarMonthName) + return DayCellInfo(lunarFestival.getName(), true, holidayBadge, lunarMonthName, isBirthday) } // 节气(当天才显示) val termDay = solarDay.getTermDay() if (termDay.getDayIndex() == 0) { - return DayCellInfo(termDay.getSolarTerm().getName(), true, holidayBadge, lunarMonthName) + return DayCellInfo(termDay.getSolarTerm().getName(), true, holidayBadge, lunarMonthName, isBirthday) } // 公历节日(仅当天) val solarFestival = solarDay.getFestival() if (solarFestival != null) { - return DayCellInfo(solarFestival.getName(), true, holidayBadge, lunarMonthName) + return DayCellInfo(solarFestival.getName(), true, holidayBadge, lunarMonthName, isBirthday) } // 默认:农历日期 @@ -101,7 +106,7 @@ class LunarCache( } else { name } - return DayCellInfo(text, false, holidayBadge, lunarMonthName) + return DayCellInfo(text, false, holidayBadge, lunarMonthName, isBirthday) } companion object { @@ -116,10 +121,12 @@ class LunarCache( * @param annotationText 底部标注文字(农历/节气/节日) * @param isAnnotationHighlight 是否为高亮标注(节日/节气) * @param holidayBadge 法定调休角标("班"/"休"/null) + * @param isBirthday 是否为生日 */ data class DayCellInfo( val annotationText: String, val isAnnotationHighlight: Boolean, val holidayBadge: String?, - val lunarMonthName: String? = null + val lunarMonthName: String? = null, + val isBirthday: Boolean = false ) diff --git a/core/src/test/kotlin/plus/rua/project/LunarCacheBirthdayTest.kt b/core/src/test/kotlin/plus/rua/project/LunarCacheBirthdayTest.kt new file mode 100644 index 0000000..bc7b01c --- /dev/null +++ b/core/src/test/kotlin/plus/rua/project/LunarCacheBirthdayTest.kt @@ -0,0 +1,30 @@ +package plus.rua.project + +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDate +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class LunarCacheBirthdayTest { + private val cache = LunarCache() + + @Test + fun solarBirthdaySeptember4_returnsTrue() = runTest { + val info = cache.getOrCompute(LocalDate(2026, 9, 4)) + assertTrue("阳历 9 月 4 日应为生日", info.isBirthday) + } + + @Test + fun lunarBirthdayFirstMonthDay21_returnsTrue() = runTest { + // 2026 年农历正月二十一对应阳历 2026-03-09 + val info = cache.getOrCompute(LocalDate(2026, 3, 9)) + assertTrue("农历正月二十一应为生日", info.isBirthday) + } + + @Test + fun regularDate_returnsFalse() = runTest { + val info = cache.getOrCompute(LocalDate(2026, 6, 15)) + assertFalse("普通日期不应为生日", info.isBirthday) + } +} From de47509a3232c5c592212743c1ffc67b80abee34 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 16:28:37 +0800 Subject: [PATCH 3/5] feat: show tilted birthday crown with click bounce animation --- .../kotlin/plus/rua/project/ui/DayCell.kt | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt index c11ca24..792e2aa 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt @@ -1,9 +1,12 @@ package plus.rua.project.ui import androidx.compose.animation.animateColor +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.ui.graphics.TransformOrigin @@ -16,14 +19,19 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -31,6 +39,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight @@ -44,6 +53,7 @@ import kotlinx.datetime.number import plus.rua.project.DayCellInfo import plus.rua.project.LunarCache import plus.rua.project.ShiftKind +import plus.rua.project.shared.R enum class DayCellState { NORMAL, OTHER_MONTH, TODAY, SELECTED, SELECTED_TODAY @@ -63,6 +73,9 @@ enum class DayCellState { * @param holidayEdgeInfo 假日在连续序列中的边缘状态,决定背景圆角。null 表示无假日。 * @param cellIndex 单元格在月网格中的线性索引(weekIndex*7+dayIndex),用于法定假日波浪动画延迟。 * @param onClick 点击回调 + * @param interactionSource 点击交互源 + * @param lunarCache 农历缓存实例 + * @param lunarData 预计算的日期信息;null 时内部自动获取 * @param modifier 外部布局修饰符 */ @Composable @@ -76,10 +89,10 @@ fun DayCell( holidayEdgeInfo: HolidayEdgeInfo? = null, cellIndex: Int = 0, onClick: () -> Unit, - modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, lunarCache: LunarCache = LunarCache.default, lunarData: DayCellInfo? = null, + modifier: Modifier = Modifier, ) { if (lunarData != null) { DayCellImpl( @@ -136,6 +149,16 @@ private fun DayCellImpl( interactionSource: MutableInteractionSource, lunarData: DayCellInfo, ) { + val isBirthday = lunarData.isBirthday + var birthdayClickTick by remember(date) { mutableIntStateOf(0) } + val crownScale = remember(date) { Animatable(1f) } + LaunchedEffect(birthdayClickTick) { + if (birthdayClickTick > 0) { + crownScale.animateTo(1.4f, spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + crownScale.animateTo(1f, spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + } + } + val annotationText = lunarData.annotationText val isAnnotationHighlight = lunarData.isAnnotationHighlight val holidayBadge = lunarData.holidayBadge @@ -296,7 +319,10 @@ private fun DayCellImpl( .clickable( interactionSource = interactionSource, indication = null, - onClick = onClick + onClick = { + if (isBirthday) birthdayClickTick += 1 + onClick() + } ), contentAlignment = Alignment.Center ) { @@ -346,5 +372,23 @@ private fun DayCellImpl( ) } } + if (isBirthday) { + Icon( + painter = painterResource(R.drawable.ic_birthday_crown), + contentDescription = "生日", + tint = Color.Unspecified, + modifier = Modifier + .align(Alignment.TopStart) + .zIndex(1f) + .padding(start = 2.dp, top = 2.dp) + .size(14.dp) + .graphicsLayer { + rotationZ = -45f + transformOrigin = TransformOrigin.Center + scaleX = crownScale.value + scaleY = crownScale.value + } + ) + } } } From 94861dc2f7d8945759f9ccf0f9a7d1dfffe43c0e Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 16:41:07 +0800 Subject: [PATCH 4/5] style: apply spotless formatting --- core/src/main/kotlin/plus/rua/project/ui/DayCell.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt index 792e2aa..373996e 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt @@ -60,7 +60,7 @@ enum class DayCellState { } /** - * 单个日期单元格,显示日期数字并支持选中/今天/非当月状态。 + * 单个日期单元格,显示日期数字并支持选中/今天/非当月状态;生日日期左上角显示金色皇冠。 * * @param date 日期 * @param isCurrentMonth 是否属于当前显示月份 @@ -289,10 +289,11 @@ private fun DayCellImpl( modifier = Modifier .fillMaxSize() .semantics { + val birthdaySuffix = if (isBirthday) ",生日" else "" contentDescription = if (isToday) { - "今天 ${date.year}年${date.month.number}月${date.day}日" + "今天 ${date.year}年${date.month.number}月${date.day}日$birthdaySuffix" } else { - "${date.year}年${date.month.number}月${date.day}日" + "${date.year}年${date.month.number}月${date.day}日$birthdaySuffix" } } .clip(CircleShape) From 3ca0d334e758c5f9fa9ab317a4fbde60d96f83ea Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 16:45:58 +0800 Subject: [PATCH 5/5] refactor: remove redundant icon content description and reorder DayCellImpl params --- core/src/main/kotlin/plus/rua/project/ui/DayCell.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt index 373996e..4e5d864 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt @@ -105,9 +105,9 @@ fun DayCell( holidayEdgeInfo = holidayEdgeInfo, cellIndex = cellIndex, onClick = onClick, - modifier = modifier, interactionSource = interactionSource, lunarData = lunarData, + modifier = modifier, ) } else { val computed by produceState( @@ -127,9 +127,9 @@ fun DayCell( holidayEdgeInfo = holidayEdgeInfo, cellIndex = cellIndex, onClick = onClick, - modifier = modifier, interactionSource = interactionSource, lunarData = computed, + modifier = modifier, ) } } @@ -145,9 +145,9 @@ private fun DayCellImpl( holidayEdgeInfo: HolidayEdgeInfo?, cellIndex: Int, onClick: () -> Unit, - modifier: Modifier, interactionSource: MutableInteractionSource, lunarData: DayCellInfo, + modifier: Modifier, ) { val isBirthday = lunarData.isBirthday var birthdayClickTick by remember(date) { mutableIntStateOf(0) } @@ -376,7 +376,7 @@ private fun DayCellImpl( if (isBirthday) { Icon( painter = painterResource(R.drawable.ic_birthday_crown), - contentDescription = "生日", + contentDescription = null, tint = Color.Unspecified, modifier = Modifier .align(Alignment.TopStart)