Compare commits
10 Commits
5b5d780593
...
8f24706d8c
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f24706d8c | |||
| 0868522741 | |||
| c2c8b1c321 | |||
| 4805bd1f0e | |||
| 62d3bbabea | |||
| a5a4173cc2 | |||
| 6b15c4b88b | |||
| 0560e81fb2 | |||
| fa872caa59 | |||
| a36f6c41e1 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,4 +21,5 @@ node_modules/
|
||||
# OMC runtime state
|
||||
.omc/
|
||||
logs/
|
||||
.claude/
|
||||
.claude/
|
||||
docs/superpowers/
|
||||
|
||||
194
CHANGELOG.md
194
CHANGELOG.md
@ -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/),
|
||||
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
|
||||
|
||||
### 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)
|
||||
- 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]
|
||||
|
||||
- No unreleased changes at this time.
|
||||
|
||||
21
README.md
21
README.md
@ -1,23 +1,26 @@
|
||||
# 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` 的虚拟分页,前后无边界翻页
|
||||
- **完整中式日历** —— 公历 + 农历 + 二十四节气 + 传统节日,ISO 8601 周起始(周一)
|
||||
- **个人排班周期** —— 自定义工作/休息循环,与公共节假日独立
|
||||
- **Material 3 设计** —— 动态配色,深色模式
|
||||
- **流畅的视图切换** —— 月视图、周视图、年视图三种模式,拖拽手势驱动月↔周折叠,弹簧动画自动吸附
|
||||
- **无限滑动分页** —— 基于 `Int.MAX_VALUE` 的虚拟分页,前后无边界翻页
|
||||
- **完整中式日历** —— 公历 + 农历 + 二十四节气 + 传统节日,ISO 8601 周起始(周一)
|
||||
- **个人排班周期** —— 自定义工作/休息循环,与公共节假日独立
|
||||
- **Material 3 设计** —— 动态配色,深色模式
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Kotlin 2.3 · Compose Multiplatform 1.11 · Material 3
|
||||
- Kotlin 2.3 · Jetpack Compose · Material 3
|
||||
- `kotlinx-datetime` 处理所有日期逻辑
|
||||
- `tyme4kt` 提供农历、节气与传统节日
|
||||
- `sketch` 渲染 GIF 动画
|
||||
- 双模块:`:shared`(UI + 逻辑) · `:androidApp`(薄壳)
|
||||
- iOS 入口为 `MainViewController.kt`,Xcode 工程位于 `iosApp/`
|
||||
- 双模块:`:core`(UI + 逻辑) · `:app`(薄壳)
|
||||
|
||||
## 构建
|
||||
|
||||
|
||||
60
RELEASE.md
Normal file
60
RELEASE.md
Normal 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 |
BIN
app/src/main/assets/app_icon_original.png
Normal file
BIN
app/src/main/assets/app_icon_original.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 457 KiB |
@ -63,6 +63,7 @@ dependencies {
|
||||
implementation(libs.compose.runtime)
|
||||
implementation(libs.compose.foundation)
|
||||
implementation(libs.compose.animation)
|
||||
implementation(libs.compose.material.icons)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.ui)
|
||||
|
||||
|
||||
43
core/src/main/kotlin/plus/rua/project/DateCheckerStorage.kt
Normal file
43
core/src/main/kotlin/plus/rua/project/DateCheckerStorage.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
package plus.rua.project.ui
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.size
|
||||
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.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
@ -20,8 +22,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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 com.github.panpf.sketch.AsyncImage
|
||||
import plus.rua.project.AppInfo
|
||||
@ -48,24 +48,10 @@ fun AboutScreen(
|
||||
title = { Text("关于鸭鸭日历") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
val arrowColor = MaterialTheme.colorScheme.onSurface
|
||||
Canvas(modifier = Modifier.size(24.dp)) {
|
||||
val strokeWidth = 2.dp.toPx()
|
||||
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
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ChevronLeft,
|
||||
contentDescription = "返回"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -99,11 +85,13 @@ fun AboutScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "版本:${getAppVersion()}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
TextButton(onClick = { /* TODO */ }) {
|
||||
Text(
|
||||
text = "版本:${getAppVersion()}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
|
||||
@ -18,7 +18,6 @@ import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.shape.CircleShape
|
||||
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.CardDefaults
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
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.testTagsAsResourceId
|
||||
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.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@ -368,8 +369,18 @@ fun CalendarMonthView(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
) {
|
||||
val iconColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
MenuIcon(color = iconColor)
|
||||
AnimatedContent(
|
||||
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:全透明,仅拦截点击关闭菜单,无动画
|
||||
@ -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
|
||||
private fun CalendarPagerArea(
|
||||
selectedDate: LocalDate,
|
||||
|
||||
@ -8,6 +8,7 @@ import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.SwipeToDismissBox
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
@ -47,6 +55,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
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.Color
|
||||
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.LocalContext
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@ -78,6 +90,7 @@ import kotlinx.datetime.number
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.datetime.todayIn
|
||||
import plus.rua.project.DateCheckerStorage
|
||||
|
||||
private data class ExpiryRow(val id: Int, val days: Int? = null)
|
||||
|
||||
@ -120,22 +133,30 @@ private fun ExpiryStatus.containerColor(): Color = when (this) {
|
||||
@Composable
|
||||
fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
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 {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
ExpiryRow(0, 30),
|
||||
ExpiryRow(1, 60),
|
||||
ExpiryRow(2, 180)
|
||||
)
|
||||
(saved?.second ?: defaultRows).mapIndexed { index, days ->
|
||||
ExpiryRow(index, days)
|
||||
}
|
||||
)
|
||||
}
|
||||
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 showDatePicker by remember { mutableStateOf(false) }
|
||||
var datePickerTarget by remember { mutableStateOf<DatePickerTarget?>(null) }
|
||||
var showResetDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val scope = rememberCoroutineScope()
|
||||
@ -154,7 +175,10 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
BackArrowIcon()
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ChevronLeft,
|
||||
contentDescription = "返回"
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@ -178,25 +202,30 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
) {
|
||||
PlusIcon(color = MaterialTheme.colorScheme.onPrimary)
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = "添加",
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
) {
|
||||
ProductionDateCard(
|
||||
date = productionDate,
|
||||
isToday = productionDate == today,
|
||||
onClick = {
|
||||
datePickerTarget = DatePickerTarget.Production
|
||||
showDatePicker = true
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
ProductionDateCard(
|
||||
date = productionDate,
|
||||
isToday = productionDate == today,
|
||||
onClick = {
|
||||
datePickerTarget = DatePickerTarget.Production
|
||||
showDatePicker = true
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
@ -222,23 +251,56 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.verticalScroll(scrollState)
|
||||
.animateContentSize(
|
||||
animationSpec = androidx.compose.animation.core.spring(
|
||||
stiffness = androidx.compose.animation.core.Spring.StiffnessMediumLow
|
||||
if (rows.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
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)
|
||||
)
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
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 ->
|
||||
val isBeingDeleted = row.id in pendingDeleteIds
|
||||
|
||||
key(row.id) {
|
||||
val dismissState = rememberSwipeToDismissBoxState()
|
||||
val dismissState = rememberSwipeToDismissBoxState(
|
||||
confirmValueChange = { newValue ->
|
||||
newValue == SwipeToDismissBoxValue.EndToStart
|
||||
}
|
||||
)
|
||||
|
||||
androidx.compose.runtime.LaunchedEffect(dismissState.currentValue) {
|
||||
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(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) {
|
||||
val initialMillis = when (val target = datePickerTarget) {
|
||||
is DatePickerTarget.Production -> productionDate.toEpochMillis()
|
||||
@ -583,52 +690,6 @@ private fun ExpiryCard(
|
||||
|
||||
// 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
|
||||
private fun CalendarIcon(color: Color, modifier: Modifier = Modifier) {
|
||||
androidx.compose.foundation.Canvas(modifier = modifier.size(24.dp)) {
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
package plus.rua.project.ui
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -19,8 +20,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
@ -41,24 +40,10 @@ fun LicensesScreen(
|
||||
title = { Text("开放源代码许可") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
val arrowColor = MaterialTheme.colorScheme.onSurface
|
||||
Canvas(modifier = Modifier.size(24.dp)) {
|
||||
val strokeWidth = 2.dp.toPx()
|
||||
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
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ChevronLeft,
|
||||
contentDescription = "返回"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
package plus.rua.project.ui
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ChevronLeft
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
@ -20,8 +19,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
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.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
@ -48,24 +45,10 @@ fun ToolsScreen(
|
||||
title = { Text("工具") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
val arrowColor = MaterialTheme.colorScheme.onSurface
|
||||
Canvas(modifier = Modifier.size(24.dp)) {
|
||||
val strokeWidth = 2.dp.toPx()
|
||||
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
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ChevronLeft,
|
||||
contentDescription = "返回"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
BIN
core/src/main/res/drawable-xxhdpi/ic_empty_state.jpg
Normal file
BIN
core/src/main/res/drawable-xxhdpi/ic_empty_state.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
118
core/src/test/kotlin/plus/rua/project/DateCheckerStorageTest.kt
Normal file
118
core/src/test/kotlin/plus/rua/project/DateCheckerStorageTest.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@ org.gradle.parallel=true
|
||||
org.gradle.daemon=true
|
||||
|
||||
#App
|
||||
app.version.base=1.0.0
|
||||
app.version.base=1.1.0
|
||||
|
||||
#Android
|
||||
android.nonTransitiveRClass=true
|
||||
|
||||
@ -31,6 +31,7 @@ androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator",
|
||||
compose-animation = { module = "androidx.compose.animation:animation" }
|
||||
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
||||
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-runtime = { module = "androidx.compose.runtime:runtime" }
|
||||
compose-ui = { module = "androidx.compose.ui:ui" }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user