Compare commits

..

10 Commits

Author SHA1 Message Date
xfy
8f24706d8c chore: gitignore 添加 docs/superpowers/ 2026-06-08 13:47:35 +08:00
xfy
0868522741 feat: 为版本号添加可点击效果 2026-06-08 13:43:29 +08:00
xfy
c2c8b1c321 feat: 保质期列表添加空状态提示 2026-06-08 13:36:56 +08:00
xfy
4805bd1f0e fix: 修复日期检查器右滑删除卡住问题,列表底部添加间距防止FAB遮挡 2026-06-08 13:30:04 +08:00
xfy
62d3bbabea refactor: 首页 FAB 菜单图标替换为 Material Icons,展开时切换 Close 并添加过渡动画 2026-06-08 13:20:33 +08:00
xfy
a5a4173cc2 refactor: 返回图标从 Canvas 手绘替换为 Material Icons ChevronLeft 2026-06-08 13:10:44 +08:00
xfy
6b15c4b88b feat: 日期检查器 FAB 图标替换为 Material Icons 2026-06-08 11:36:58 +08:00
xfy
0560e81fb2 feat: 日期检查器添加数据持久化和恢复默认按钮
- 新增 DateCheckerStorage,SharedPreferences 持久化生产日期和保质期列表
- 添加 DateCheckerStorageTest 单元测试(含 InMemorySharedPreferences stub)
- 页面退出后保留数据,重新打开自动恢复
- 左下角新增恢复默认 FAB,点击弹出确认弹窗后重置全部数据
- 双 FAB 样式统一(FloatingActionButton + CircleShape + primary 配色)
2026-06-08 11:08:48 +08:00
xfy
fa872caa59 docs: 版本号统一由 gradle.properties 管理,更新发布流程文档 2026-06-02 14:15:07 +08:00
xfy
a36f6c41e1 docs: 更新 README 为纯 Android 描述,调整 CHANGELOG 顺序,添加 RELEASE.md 与图标资源 2026-06-02 13:43:47 +08:00
17 changed files with 517 additions and 280 deletions

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ node_modules/
.omc/ .omc/
logs/ logs/
.claude/ .claude/
docs/superpowers/

View File

@ -5,6 +5,103 @@ All notable changes to the YaYa project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0] - 2026-06-02
### Added
#### Date Checker Tool
- New "Date Checker" tool page accessible from FAB → Tools menu for tracking item expiration dates
- Swipe-to-delete with animated removal and staggered enter/exit animations
- Expired status display with visual indicators
- Auto-scroll and highlight animation for new entries
#### Tools Page
- New "Tools" entry in FAB menu linking to a dedicated tools landing page
- Date Checker as the first tool module
#### Theme & Visual
- `YaYaTheme` introduced and applied to all Activities for unified theming
- Legal holiday badges now display with colored background and continuous edge rounded corners
- Holiday badge wave-scale entrance animation
- Personal shift badges redesigned with light circle background + centered text
- Shift badge circular base to avoid overlapping with selection ring
#### Year ↔ Month View Transition
- BottomCard slide-in animation and fade effect during year/month view transitions
- Month→year view no longer forces collapse state to expand
#### Performance
- LunarCache LRU cache for lunar/solar term calculations with startup pre-computation
- Macrobenchmark module with automated Baseline Profile generation
- Baseline Profile covering date checker, shift settings, tools page, and core calendar scenarios
- `ComposeTrace` cross-platform trace markers for Perfetto/Systrace
- SolarDay static cache to eliminate repeated object creation
- MiniMonth pure Canvas rendering eliminating 96 Text measurement overhead
- `graphicsLayer(translationY)` replacing `offset(Dp)` to avoid layout passes
- Aggregated `CalendarUiState` to reduce Compose recomposition
- `remember` stabilization for lambdas and computations
- Scene-based `profile.sh` with `--all` batch mode for 15 automated trace scenarios
- Perfetto trace analysis script (`analyze-trace.sh`)
- Trace build type for release + retained trace markers
#### Build & Tooling
- Spotless 8.5.1 code formatter with ktlint integration
- `.editorconfig` for ktlint Composable function naming rules
- Dependency update checker and auto-upgrade tool integration
- `app_icon` shrunk to 512×512 and converted to WebP (446KB savings)
- 152 GIF assets batch-converted to animated WebP format
- `uiTooling` moved to `debugImplementation`; unused `@Preview` and `kotlin-test` entries removed
- `sketch` library for GIF/WebP display (`sketch-compose` + `sketch-animated-webp`)
- PowerShell performance tracing script (`profile.ps1`)
#### Documentation
- Comprehensive `AGENTS.md` at every directory level (root, app, core, scripts, etc.)
- Updated `DEVELOPMENT.md` with Perfetto trace analysis and emulator launch commands
- Updated `CLAUDE.md` to reflect pure Android project structure
### Changed
- Project migrated from Kotlin Multiplatform (KMP/CMP) to pure Android (`:app` + `:core`)
- All Compose UI and business logic consolidated into `:core` module; `:app` remains a thin shell
- Removed KMP/CMP plugins, iOS app module, and `:shared` module
- `androidApp` module renamed to `app`
- Collapse animation refactored: removed fling velocity threshold, now spring-driven
- `CalendarPager``WeekPager` switching uses `AnimatedContent` for smooth crossfade
- Year view page year calculation uses `settledPage` to prevent flicker during swipe
- ViewModel decoupled from Compose runtime, migrated to `StateFlow`
- `LunarCache` made injectable with extracted repeated computations
- MenuItem and ToolItem unified to use `Card(onClick)` pattern
- Holiday badge null checks simplified to Elvis operator
- `@Suppress` annotations cleaned up with deprecated API replacements
- Removed unnecessary P0 code (custom combine, dead StateFlow, duplicate grid algorithms, runBlocking)
- Removed debug logging from LicensesScreen and BottomCard
### Fixed
- Lunar first-day month name no longer appends redundant "月" suffix
- Year view stale year display on enter
- Year view page year flicker during swipe transitions
- Collapse animation flicker when switching between CalendarPager and WeekPager
- Folded state cross-month dates not grayed out in week view
- Date checker swipe-to-delete state misalignment and deprecation warning
- Shared element transition animation loss after year view page change
- Night mode theme transparency issues with explicit background colors
- Predictive back gesture failure and end-of-animation flash on certain devices
- Back animation residual transition eliminated with `snapTo`
- Fast swipe collapse/expand failure, now uses progress threshold detection
- `graphicsLayer` optimization reverted due to excessive GPU compositing overhead on real devices
- Reverted shared element transitions in favor of zoom + fade animation
### Removed
- iOS app module (`iosApp/`) and all related Xcode project files
- `:shared` module and `shared/build.gradle.kts`
- Shared element transition animations (replaced by zoom + fade)
- Year/month scroll wheel picker with haptic feedback (reverted)
- Aliyun Maven mirrors (switched to Maven Central / Google)
- Unused Compose runtime ProGuard keep rules
- Temporary performance monitoring logs (trace markers retained)
## [1.0.0] - 2026-05-20 ## [1.0.0] - 2026-05-20
### Added ### Added
@ -139,103 +236,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Aliyun Maven mirrors (switched back to Maven Central / Google) - Aliyun Maven mirrors (switched back to Maven Central / Google)
- Unused Compose runtime ProGuard keep rules - Unused Compose runtime ProGuard keep rules
## [1.1.0] - 2026-06-02
### Added
#### Date Checker Tool
- New "Date Checker" tool page accessible from FAB → Tools menu for tracking item expiration dates
- Swipe-to-delete with animated removal and staggered enter/exit animations
- Expired status display with visual indicators
- Auto-scroll and highlight animation for new entries
#### Tools Page
- New "Tools" entry in FAB menu linking to a dedicated tools landing page
- Date Checker as the first tool module
#### Theme & Visual
- `YaYaTheme` introduced and applied to all Activities for unified theming
- Legal holiday badges now display with colored background and continuous edge rounded corners
- Holiday badge wave-scale entrance animation
- Personal shift badges redesigned with light circle background + centered text
- Shift badge circular base to avoid overlapping with selection ring
#### Year ↔ Month View Transition
- BottomCard slide-in animation and fade effect during year/month view transitions
- Month→year view no longer forces collapse state to expand
#### Performance
- LunarCache LRU cache for lunar/solar term calculations with startup pre-computation
- Macrobenchmark module with automated Baseline Profile generation
- Baseline Profile covering date checker, shift settings, tools page, and core calendar scenarios
- `ComposeTrace` cross-platform trace markers for Perfetto/Systrace
- SolarDay static cache to eliminate repeated object creation
- MiniMonth pure Canvas rendering eliminating 96 Text measurement overhead
- `graphicsLayer(translationY)` replacing `offset(Dp)` to avoid layout passes
- Aggregated `CalendarUiState` to reduce Compose recomposition
- `remember` stabilization for lambdas and computations
- Scene-based `profile.sh` with `--all` batch mode for 15 automated trace scenarios
- Perfetto trace analysis script (`analyze-trace.sh`)
- Trace build type for release + retained trace markers
#### Build & Tooling
- Spotless 8.5.1 code formatter with ktlint integration
- `.editorconfig` for ktlint Composable function naming rules
- Dependency update checker and auto-upgrade tool integration
- `app_icon` shrunk to 512×512 and converted to WebP (446KB savings)
- 152 GIF assets batch-converted to animated WebP format
- `uiTooling` moved to `debugImplementation`; unused `@Preview` and `kotlin-test` entries removed
- `sketch` library for GIF/WebP display (`sketch-compose` + `sketch-animated-webp`)
- PowerShell performance tracing script (`profile.ps1`)
#### Documentation
- Comprehensive `AGENTS.md` at every directory level (root, app, core, scripts, etc.)
- Updated `DEVELOPMENT.md` with Perfetto trace analysis and emulator launch commands
- Updated `CLAUDE.md` to reflect pure Android project structure
### Changed
- Project migrated from Kotlin Multiplatform (KMP/CMP) to pure Android (`:app` + `:core`)
- All Compose UI and business logic consolidated into `:core` module; `:app` remains a thin shell
- Removed KMP/CMP plugins, iOS app module, and `:shared` module
- `androidApp` module renamed to `app`
- Collapse animation refactored: removed fling velocity threshold, now spring-driven
- `CalendarPager``WeekPager` switching uses `AnimatedContent` for smooth crossfade
- Year view page year calculation uses `settledPage` to prevent flicker during swipe
- ViewModel decoupled from Compose runtime, migrated to `StateFlow`
- `LunarCache` made injectable with extracted repeated computations
- MenuItem and ToolItem unified to use `Card(onClick)` pattern
- Holiday badge null checks simplified to Elvis operator
- `@Suppress` annotations cleaned up with deprecated API replacements
- Removed unnecessary P0 code (custom combine, dead StateFlow, duplicate grid algorithms, runBlocking)
- Removed debug logging from LicensesScreen and BottomCard
### Fixed
- Lunar first-day month name no longer appends redundant "月" suffix
- Year view stale year display on enter
- Year view page year flicker during swipe transitions
- Collapse animation flicker when switching between CalendarPager and WeekPager
- Folded state cross-month dates not grayed out in week view
- Date checker swipe-to-delete state misalignment and deprecation warning
- Shared element transition animation loss after year view page change
- Night mode theme transparency issues with explicit background colors
- Predictive back gesture failure and end-of-animation flash on certain devices
- Back animation residual transition eliminated with `snapTo`
- Fast swipe collapse/expand failure, now uses progress threshold detection
- `graphicsLayer` optimization reverted due to excessive GPU compositing overhead on real devices
- Reverted shared element transitions in favor of zoom + fade animation
### Removed
- iOS app module (`iosApp/`) and all related Xcode project files
- `:shared` module and `shared/build.gradle.kts`
- Shared element transition animations (replaced by zoom + fade)
- Year/month scroll wheel picker with haptic feedback (reverted)
- Aliyun Maven mirrors (switched to Maven Central / Google)
- Unused Compose runtime ProGuard keep rules
- Temporary performance monitoring logs (trace markers retained)
## [Unreleased] ## [Unreleased]
- No unreleased changes at this time. - No unreleased changes at this time.

View File

@ -1,23 +1,26 @@
# YaYa # YaYa
基于 Kotlin Multiplatform 与 Compose Multiplatform 的跨平台日历应用,Android 与 iOS 共享同一套 UI 与业务逻辑。 纯 Android + Jetpack Compose 日历应用,支持农历/节气/节日、个人班次排期,提供月/周/年三种视图。
<div>
<img src="app/src/main/assets/app_icon.png" />
</div>
## 特性 ## 特性
- **流畅的视图切换** —— 月视图、周视图、年视图三种模式,拖拽手势驱动月↔周折叠,弹簧动画自动吸附 - **流畅的视图切换** —— 月视图、周视图、年视图三种模式,拖拽手势驱动月↔周折叠,弹簧动画自动吸附
- **无限滑动分页** —— 基于 `Int.MAX_VALUE` 的虚拟分页,前后无边界翻页 - **无限滑动分页** —— 基于 `Int.MAX_VALUE` 的虚拟分页前后无边界翻页
- **完整中式日历** —— 公历 + 农历 + 二十四节气 + 传统节日,ISO 8601 周起始(周一) - **完整中式日历** —— 公历 + 农历 + 二十四节气 + 传统节日ISO 8601 周起始(周一)
- **个人排班周期** —— 自定义工作/休息循环,与公共节假日独立 - **个人排班周期** —— 自定义工作/休息循环与公共节假日独立
- **Material 3 设计** —— 动态配色,深色模式 - **Material 3 设计** —— 动态配色深色模式
## 技术栈 ## 技术栈
- Kotlin 2.3 · Compose Multiplatform 1.11 · Material 3 - Kotlin 2.3 · Jetpack Compose · Material 3
- `kotlinx-datetime` 处理所有日期逻辑 - `kotlinx-datetime` 处理所有日期逻辑
- `tyme4kt` 提供农历、节气与传统节日 - `tyme4kt` 提供农历、节气与传统节日
- `sketch` 渲染 GIF 动画 - `sketch` 渲染 GIF 动画
- 双模块:`:shared`(UI + 逻辑) · `:androidApp`(薄壳) - 双模块:`:core`UI + 逻辑) · `:app`(薄壳)
- iOS 入口为 `MainViewController.kt`,Xcode 工程位于 `iosApp/`
## 构建 ## 构建

60
RELEASE.md Normal file
View File

@ -0,0 +1,60 @@
# 发布流程
## 1. 更新 CHANGELOG.md
按倒序(新版在前)在 `[Unreleased]` 下方添加新版本条目,格式遵循 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)。
底部添加版本链接:
```
[x.y.z]: https://github.com/DefectingCat/yayacal/releases/tag/vx.y.z
```
## 2. 更新版本号
编辑 `gradle.properties`
- `app.version.base` 改为新版本号(如 `1.2.0`
编辑 `app/build.gradle.kts`
- `versionCode` 递增 `+1`
> `app.version.base` 优先于 `build.gradle.kts` 中的默认值,因此以 `gradle.properties` 为准。
## 3. 构建 Release APK
```bash
./gradlew :app:assembleRelease
```
产物路径:`app/build/outputs/apk/release/app-release.apk`
## 4. 提交、打 Tag、推送
```bash
git add CHANGELOG.md gradle.properties app/build.gradle.kts
git commit -m "release: vx.y.z"
git tag vx.y.z
git push origin main --tags
```
## 5. 创建 GitHub Release
```bash
gh release create vx.y.z \
app/build/outputs/apk/release/app-release.apk \
--title "YaYa vx.y.z" \
--notes-file CHANGELOG.md
```
`--notes-file` 会读取 CHANGELOG.md 全文作为 Release body。
## 检查清单
- [ ] CHANGELOG.md 新版本条目已添加(倒序,新版在前)
- [ ] CHANGELOG.md 底部链接已添加
- [ ] `gradle.properties``app.version.base``app/build.gradle.kts``versionCode` 已更新
- [ ] Release APK 构建成功
- [ ] Git tag 已推送
- [ ] GitHub Release 已创建且包含 APK

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

View File

@ -63,6 +63,7 @@ dependencies {
implementation(libs.compose.runtime) implementation(libs.compose.runtime)
implementation(libs.compose.foundation) implementation(libs.compose.foundation)
implementation(libs.compose.animation) implementation(libs.compose.animation)
implementation(libs.compose.material.icons)
implementation(libs.compose.material3) implementation(libs.compose.material3)
implementation(libs.compose.ui) implementation(libs.compose.ui)

View File

@ -0,0 +1,43 @@
package plus.rua.project
import android.content.Context
import android.content.SharedPreferences
import kotlinx.datetime.LocalDate
class DateCheckerStorage(private val prefs: SharedPreferences) {
companion object {
private const val KEY_PRODUCTION_DATE = "production_date"
private const val KEY_ROWS = "rows"
private const val ROWS_SEPARATOR = ","
fun fromContext(context: Context): DateCheckerStorage {
val prefs = context.getSharedPreferences("date_checker", Context.MODE_PRIVATE)
return DateCheckerStorage(prefs)
}
}
fun save(productionDate: LocalDate, rows: List<Int?>) {
val nonNullRows = rows.filterNotNull()
prefs.edit()
.putString(KEY_PRODUCTION_DATE, productionDate.toString())
.putString(KEY_ROWS, nonNullRows.joinToString(ROWS_SEPARATOR))
.apply()
}
fun load(): Pair<LocalDate, List<Int>>? {
val dateStr = prefs.getString(KEY_PRODUCTION_DATE, null) ?: return null
val rowsStr = prefs.getString(KEY_ROWS, null) ?: return null
val date = LocalDate.parse(dateStr)
val rows = if (rowsStr.isBlank()) {
emptyList()
} else {
rowsStr.split(ROWS_SEPARATOR).map { it.toInt() }
}
return date to rows
}
fun clear() {
prefs.edit().clear().apply()
}
}

View File

@ -1,6 +1,5 @@
package plus.rua.project.ui package plus.rua.project.ui
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -8,7 +7,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -20,8 +22,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment 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.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.github.panpf.sketch.AsyncImage import com.github.panpf.sketch.AsyncImage
import plus.rua.project.AppInfo import plus.rua.project.AppInfo
@ -48,24 +48,10 @@ fun AboutScreen(
title = { Text("关于鸭鸭日历") }, title = { Text("关于鸭鸭日历") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
val arrowColor = MaterialTheme.colorScheme.onSurface Icon(
Canvas(modifier = Modifier.size(24.dp)) { imageVector = Icons.Filled.ChevronLeft,
val strokeWidth = 2.dp.toPx() contentDescription = "返回"
drawLine( )
color = arrowColor,
start = Offset(size.width * 0.75f, size.height * 0.15f),
end = Offset(size.width * 0.25f, size.height * 0.5f),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
drawLine(
color = arrowColor,
start = Offset(size.width * 0.25f, size.height * 0.5f),
end = Offset(size.width * 0.75f, size.height * 0.85f),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
}
} }
} }
) )
@ -99,11 +85,13 @@ fun AboutScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( TextButton(onClick = { /* TODO */ }) {
text = "版本:${getAppVersion()}", Text(
style = MaterialTheme.typography.bodyMedium, text = "版本:${getAppVersion()}",
color = MaterialTheme.colorScheme.onSurfaceVariant style = MaterialTheme.typography.bodyMedium,
) color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(48.dp)) Spacer(modifier = Modifier.height(48.dp))

View File

@ -18,7 +18,6 @@ import androidx.compose.animation.scaleOut
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
@ -38,10 +37,14 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -63,8 +66,6 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@ -368,8 +369,18 @@ fun CalendarMonthView(
containerColor = MaterialTheme.colorScheme.primaryContainer, containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) { ) {
val iconColor = MaterialTheme.colorScheme.onPrimaryContainer AnimatedContent(
MenuIcon(color = iconColor) targetState = isMenuExpanded,
transitionSpec = {
fadeIn(tween(200)) togetherWith fadeOut(tween(200))
},
label = "fab_icon"
) { expanded ->
Icon(
imageVector = if (expanded) Icons.Filled.Close else Icons.Filled.Menu,
contentDescription = if (expanded) "关闭菜单" else "打开菜单"
)
}
} }
// Scrim全透明仅拦截点击关闭菜单无动画 // Scrim全透明仅拦截点击关闭菜单无动画
@ -466,24 +477,6 @@ fun CalendarMonthView(
} }
} }
@Composable
private fun MenuIcon(color: Color, modifier: Modifier = Modifier) {
Canvas(modifier = modifier.size(24.dp)) {
val strokeWidth = 2.dp.toPx()
val lineSpacing = 4.dp.toPx()
val totalHeight = strokeWidth * 3 + lineSpacing * 2
val startY = (size.height - totalHeight) / 2
repeat(3) { i ->
drawLine(
color = color,
start = Offset(0f, startY + i * (strokeWidth + lineSpacing)),
end = Offset(size.width, startY + i * (strokeWidth + lineSpacing)),
strokeWidth = strokeWidth
)
}
}
}
@Composable @Composable
private fun CalendarPagerArea( private fun CalendarPagerArea(
selectedDate: LocalDate, selectedDate: LocalDate,

View File

@ -8,6 +8,7 @@ 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.Image
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
@ -27,13 +28,20 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.SwipeToDismissBoxValue
@ -47,6 +55,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -58,7 +67,10 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -78,6 +90,7 @@ import kotlinx.datetime.number
import kotlinx.datetime.plus import kotlinx.datetime.plus
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.todayIn import kotlinx.datetime.todayIn
import plus.rua.project.DateCheckerStorage
private data class ExpiryRow(val id: Int, val days: Int? = null) private data class ExpiryRow(val id: Int, val days: Int? = null)
@ -120,22 +133,30 @@ private fun ExpiryStatus.containerColor(): Color = when (this) {
@Composable @Composable
fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) { fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) } val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) }
val context = LocalContext.current.applicationContext
val storage = remember { DateCheckerStorage.fromContext(context) }
var productionDate by remember { mutableStateOf(today) } val saved = remember { storage.load() }
val defaultRows = listOf(30, 60, 180)
var productionDate by remember { mutableStateOf(saved?.first ?: today) }
var rows by remember { var rows by remember {
mutableStateOf( mutableStateOf(
listOf( (saved?.second ?: defaultRows).mapIndexed { index, days ->
ExpiryRow(0, 30), ExpiryRow(index, days)
ExpiryRow(1, 60), }
ExpiryRow(2, 180)
)
) )
} }
var nextId by remember { mutableIntStateOf(3) } var nextId by remember { mutableIntStateOf(rows.size) }
LaunchedEffect(productionDate, rows) {
storage.save(productionDate, rows.map { it.days })
}
var pendingDeleteIds by remember { mutableStateOf(setOf<Int>()) } var pendingDeleteIds by remember { mutableStateOf(setOf<Int>()) }
var showDatePicker by remember { mutableStateOf(false) } var showDatePicker by remember { mutableStateOf(false) }
var datePickerTarget by remember { mutableStateOf<DatePickerTarget?>(null) } var datePickerTarget by remember { mutableStateOf<DatePickerTarget?>(null) }
var showResetDialog by remember { mutableStateOf(false) }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -154,7 +175,10 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
BackArrowIcon() Icon(
imageVector = Icons.Filled.ChevronLeft,
contentDescription = "返回"
)
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
@ -178,25 +202,30 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = MaterialTheme.colorScheme.onPrimary
) { ) {
PlusIcon(color = MaterialTheme.colorScheme.onPrimary) Icon(
imageVector = Icons.Filled.Add,
contentDescription = "添加",
tint = MaterialTheme.colorScheme.onPrimary
)
} }
}, },
containerColor = MaterialTheme.colorScheme.surface containerColor = MaterialTheme.colorScheme.surface
) { innerPadding -> ) { innerPadding ->
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding) .padding(innerPadding)
) { ) {
ProductionDateCard( Column(modifier = Modifier.fillMaxSize()) {
date = productionDate, ProductionDateCard(
isToday = productionDate == today, date = productionDate,
onClick = { isToday = productionDate == today,
datePickerTarget = DatePickerTarget.Production onClick = {
showDatePicker = true datePickerTarget = DatePickerTarget.Production
}, showDatePicker = true
modifier = Modifier.padding(horizontal = 16.dp) },
) modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@ -222,23 +251,56 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Column( if (rows.isEmpty()) {
modifier = Modifier Box(
.fillMaxWidth() modifier = Modifier
.weight(1f) .fillMaxWidth()
.verticalScroll(scrollState) .weight(1f),
.animateContentSize( contentAlignment = Alignment.Center
animationSpec = androidx.compose.animation.core.spring( ) {
stiffness = androidx.compose.animation.core.Spring.StiffnessMediumLow Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Image(
painter = painterResource(id = plus.rua.project.shared.R.drawable.ic_empty_state),
contentDescription = "空状态",
modifier = Modifier.size(120.dp)
) )
) Text(
.padding(horizontal = 16.dp, vertical = 4.dp) text = "暂无保质期记录",
) { style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "点击右下角 + 添加",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
}
}
} else {
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.verticalScroll(scrollState)
.animateContentSize(
animationSpec = androidx.compose.animation.core.spring(
stiffness = androidx.compose.animation.core.Spring.StiffnessMediumLow
)
)
.padding(horizontal = 16.dp, vertical = 4.dp)
) {
rows.forEachIndexed { index, row -> rows.forEachIndexed { index, row ->
val isBeingDeleted = row.id in pendingDeleteIds val isBeingDeleted = row.id in pendingDeleteIds
key(row.id) { key(row.id) {
val dismissState = rememberSwipeToDismissBoxState() val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { newValue ->
newValue == SwipeToDismissBoxValue.EndToStart
}
)
androidx.compose.runtime.LaunchedEffect(dismissState.currentValue) { androidx.compose.runtime.LaunchedEffect(dismissState.currentValue) {
if (dismissState.currentValue == SwipeToDismissBoxValue.EndToStart && !isBeingDeleted) { if (dismissState.currentValue == SwipeToDismissBoxValue.EndToStart && !isBeingDeleted) {
@ -329,10 +391,55 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
} }
} }
Spacer(modifier = Modifier.height(80.dp))
}
}
}
FloatingActionButton(
onClick = { showResetDialog = true },
modifier = Modifier
.align(Alignment.BottomStart)
.padding(start = 16.dp, bottom = 16.dp),
shape = CircleShape,
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = "重置",
tint = MaterialTheme.colorScheme.onPrimary
)
} }
} }
} }
if (showResetDialog) {
AlertDialog(
onDismissRequest = { showResetDialog = false },
title = { Text("恢复默认") },
text = { Text("将重置生产日期和保质期列表为默认值") },
confirmButton = {
TextButton(onClick = {
productionDate = today
rows = defaultRows.mapIndexed { index, days ->
ExpiryRow(index, days)
}
nextId = defaultRows.size
showResetDialog = false
}) {
Text("确定")
}
},
dismissButton = {
TextButton(onClick = { showResetDialog = false }) {
Text("取消")
}
}
)
}
if (showDatePicker) { if (showDatePicker) {
val initialMillis = when (val target = datePickerTarget) { val initialMillis = when (val target = datePickerTarget) {
is DatePickerTarget.Production -> productionDate.toEpochMillis() is DatePickerTarget.Production -> productionDate.toEpochMillis()
@ -583,52 +690,6 @@ private fun ExpiryCard(
// region Icons // region Icons
@Composable
private fun BackArrowIcon(modifier: Modifier = Modifier) {
val color = MaterialTheme.colorScheme.onSurface
androidx.compose.foundation.Canvas(modifier = modifier.size(24.dp)) {
val strokeWidth = 2.dp.toPx()
drawLine(
color = color,
start = Offset(size.width * 0.75f, size.height * 0.15f),
end = Offset(size.width * 0.25f, size.height * 0.5f),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
drawLine(
color = color,
start = Offset(size.width * 0.25f, size.height * 0.5f),
end = Offset(size.width * 0.75f, size.height * 0.85f),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
}
}
@Composable
private fun PlusIcon(color: Color, modifier: Modifier = Modifier) {
androidx.compose.foundation.Canvas(modifier = modifier.size(24.dp)) {
val strokeWidth = 2.dp.toPx()
val cx = size.width / 2
val cy = size.height / 2
val half = size.minDimension * 0.35f
drawLine(
color = color,
start = Offset(cx, cy - half),
end = Offset(cx, cy + half),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
drawLine(
color = color,
start = Offset(cx - half, cy),
end = Offset(cx + half, cy),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
}
}
@Composable @Composable
private fun CalendarIcon(color: Color, modifier: Modifier = Modifier) { private fun CalendarIcon(color: Color, modifier: Modifier = Modifier) {
androidx.compose.foundation.Canvas(modifier = modifier.size(24.dp)) { androidx.compose.foundation.Canvas(modifier = modifier.size(24.dp)) {

View File

@ -1,16 +1,17 @@
package plus.rua.project.ui package plus.rua.project.ui
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -19,8 +20,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
/** /**
@ -41,24 +40,10 @@ fun LicensesScreen(
title = { Text("开放源代码许可") }, title = { Text("开放源代码许可") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
val arrowColor = MaterialTheme.colorScheme.onSurface Icon(
Canvas(modifier = Modifier.size(24.dp)) { imageVector = Icons.Filled.ChevronLeft,
val strokeWidth = 2.dp.toPx() contentDescription = "返回"
drawLine( )
color = arrowColor,
start = Offset(size.width * 0.75f, size.height * 0.15f),
end = Offset(size.width * 0.25f, size.height * 0.5f),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
drawLine(
color = arrowColor,
start = Offset(size.width * 0.25f, size.height * 0.5f),
end = Offset(size.width * 0.75f, size.height * 0.85f),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
}
} }
} }
) )

View File

@ -1,18 +1,17 @@
package plus.rua.project.ui package plus.rua.project.ui
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -20,8 +19,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.semantics.testTagsAsResourceId
@ -48,24 +45,10 @@ fun ToolsScreen(
title = { Text("工具") }, title = { Text("工具") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
val arrowColor = MaterialTheme.colorScheme.onSurface Icon(
Canvas(modifier = Modifier.size(24.dp)) { imageVector = Icons.Filled.ChevronLeft,
val strokeWidth = 2.dp.toPx() contentDescription = "返回"
drawLine( )
color = arrowColor,
start = Offset(size.width * 0.75f, size.height * 0.15f),
end = Offset(size.width * 0.25f, size.height * 0.5f),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
drawLine(
color = arrowColor,
start = Offset(size.width * 0.25f, size.height * 0.5f),
end = Offset(size.width * 0.75f, size.height * 0.85f),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
}
} }
} }
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,118 @@
package plus.rua.project
import android.content.SharedPreferences
import kotlinx.datetime.LocalDate
import kotlin.test.Test
import kotlin.test.assertEquals
class DateCheckerStorageTest {
private val prefs = InMemorySharedPreferences()
private val storage = DateCheckerStorage(prefs)
@Test
fun load_noSavedData_returnsDefault() {
storage.clear()
val result = storage.load()
assertEquals(null, result)
}
@Test
fun saveAndLoad_roundTrips_correctly() {
storage.clear()
val date = LocalDate(2026, 5, 15)
val rows = listOf(30, 60, 180, 365)
storage.save(date, rows)
val result = storage.load()
assertEquals(date to rows, result)
}
@Test
fun saveAndLoad_nullDaysNotPersisted() {
storage.clear()
val date = LocalDate(2026, 6, 1)
val rows = listOf(30, null, 180)
storage.save(date, rows)
val result = storage.load()
assertEquals(date to listOf(30, 180), result)
}
@Test
fun saveAndLoad_emptyRows_savesSuccessfully() {
storage.clear()
val date = LocalDate(2026, 1, 1)
storage.save(date, emptyList())
val result = storage.load()
assertEquals(date to emptyList(), result)
}
}
private class InMemorySharedPreferences : SharedPreferences {
private val data = mutableMapOf<String, Any?>()
override fun getAll(): Map<String, *> = data.toMap()
override fun getString(key: String, defValue: String?): String? =
data[key] as? String ?: defValue
override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
data[key] as? Set<String> ?: defValues
override fun getInt(key: String, defValue: Int): Int =
data[key] as? Int ?: defValue
override fun getLong(key: String, defValue: Long): Long =
data[key] as? Long ?: defValue
override fun getFloat(key: String, defValue: Float): Float =
data[key] as? Float ?: defValue
override fun getBoolean(key: String, defValue: Boolean): Boolean =
data[key] as? Boolean ?: defValue
override fun contains(key: String): Boolean = data.containsKey(key)
override fun edit(): SharedPreferences.Editor = InMemoryEditor(data)
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {}
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {}
}
private class InMemoryEditor(private val data: MutableMap<String, Any?>) : SharedPreferences.Editor {
private val pending = mutableMapOf<String, Any?>()
private var clearPending = false
override fun putString(key: String, value: String?): SharedPreferences.Editor = apply {
pending[key] = value
}
override fun putStringSet(key: String, values: Set<String>?): SharedPreferences.Editor = apply {
pending[key] = values
}
override fun putInt(key: String, value: Int): SharedPreferences.Editor = apply { pending[key] = value }
override fun putLong(key: String, value: Long): SharedPreferences.Editor = apply { pending[key] = value }
override fun putFloat(key: String, value: Float): SharedPreferences.Editor = apply { pending[key] = value }
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor = apply { pending[key] = value }
override fun remove(key: String): SharedPreferences.Editor = apply { pending[key] = null }
override fun clear(): SharedPreferences.Editor = apply { clearPending = true }
override fun commit(): Boolean {
apply()
return true
}
override fun apply() {
if (clearPending) {
data.clear()
clearPending = false
}
data.putAll(pending)
pending.clear()
}
}

View File

@ -10,7 +10,7 @@ org.gradle.parallel=true
org.gradle.daemon=true org.gradle.daemon=true
#App #App
app.version.base=1.0.0 app.version.base=1.1.0
#Android #Android
android.nonTransitiveRClass=true android.nonTransitiveRClass=true

View File

@ -31,6 +31,7 @@ androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator",
compose-animation = { module = "androidx.compose.animation:animation" } compose-animation = { module = "androidx.compose.animation:animation" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
compose-foundation = { module = "androidx.compose.foundation:foundation" } compose-foundation = { module = "androidx.compose.foundation:foundation" }
compose-material-icons = { module = "androidx.compose.material:material-icons-extended" }
compose-material3 = { module = "androidx.compose.material3:material3" } compose-material3 = { module = "androidx.compose.material3:material3" }
compose-runtime = { module = "androidx.compose.runtime:runtime" } compose-runtime = { module = "androidx.compose.runtime:runtime" }
compose-ui = { module = "androidx.compose.ui:ui" } compose-ui = { module = "androidx.compose.ui:ui" }