Compare commits
No commits in common. "8f24706d8c9588bd044af63ee9cce4b940023938" and "5b5d7805937ba5282f3cd46ae8a9b0e81b2b17b5" have entirely different histories.
8f24706d8c
...
5b5d780593
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,5 +21,4 @@ node_modules/
|
|||||||
# OMC runtime state
|
# OMC runtime state
|
||||||
.omc/
|
.omc/
|
||||||
logs/
|
logs/
|
||||||
.claude/
|
.claude/
|
||||||
docs/superpowers/
|
|
||||||
194
CHANGELOG.md
194
CHANGELOG.md
@ -5,103 +5,6 @@ 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
|
||||||
@ -236,6 +139,103 @@ 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.
|
||||||
|
|||||||
21
README.md
21
README.md
@ -1,26 +1,23 @@
|
|||||||
# YaYa
|
# YaYa
|
||||||
|
|
||||||
纯 Android + Jetpack Compose 日历应用,支持农历/节气/节日、个人班次排期,提供月/周/年三种视图。
|
基于 Kotlin Multiplatform 与 Compose Multiplatform 的跨平台日历应用,Android 与 iOS 共享同一套 UI 与业务逻辑。
|
||||||
|
|
||||||
<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 · Jetpack Compose · Material 3
|
- Kotlin 2.3 · Compose Multiplatform 1.11 · Material 3
|
||||||
- `kotlinx-datetime` 处理所有日期逻辑
|
- `kotlinx-datetime` 处理所有日期逻辑
|
||||||
- `tyme4kt` 提供农历、节气与传统节日
|
- `tyme4kt` 提供农历、节气与传统节日
|
||||||
- `sketch` 渲染 GIF 动画
|
- `sketch` 渲染 GIF 动画
|
||||||
- 双模块:`:core`(UI + 逻辑) · `:app`(薄壳)
|
- 双模块:`:shared`(UI + 逻辑) · `:androidApp`(薄壳)
|
||||||
|
- iOS 入口为 `MainViewController.kt`,Xcode 工程位于 `iosApp/`
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|
||||||
|
|||||||
60
RELEASE.md
60
RELEASE.md
@ -1,60 +0,0 @@
|
|||||||
# 发布流程
|
|
||||||
|
|
||||||
## 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: 59 KiB After Width: | Height: | Size: 457 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 457 KiB |
@ -63,7 +63,6 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
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,5 +1,6 @@
|
|||||||
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
|
||||||
@ -7,10 +8,7 @@ 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
|
||||||
@ -22,6 +20,8 @@ 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,10 +48,24 @@ fun AboutScreen(
|
|||||||
title = { Text("关于鸭鸭日历") },
|
title = { Text("关于鸭鸭日历") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
val arrowColor = MaterialTheme.colorScheme.onSurface
|
||||||
imageVector = Icons.Filled.ChevronLeft,
|
Canvas(modifier = Modifier.size(24.dp)) {
|
||||||
contentDescription = "返回"
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -85,13 +99,11 @@ fun AboutScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
TextButton(onClick = { /* TODO */ }) {
|
Text(
|
||||||
Text(
|
text = "版本:${getAppVersion()}",
|
||||||
text = "版本:${getAppVersion()}",
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ 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
|
||||||
@ -37,14 +38,10 @@ 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
|
||||||
@ -66,6 +63,8 @@ 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
|
||||||
@ -369,18 +368,8 @@ fun CalendarMonthView(
|
|||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
) {
|
) {
|
||||||
AnimatedContent(
|
val iconColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
targetState = isMenuExpanded,
|
MenuIcon(color = iconColor)
|
||||||
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:全透明,仅拦截点击关闭菜单,无动画
|
||||||
@ -477,6 +466,24 @@ 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,
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import androidx.compose.animation.expandVertically
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.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
|
||||||
@ -28,20 +27,13 @@ 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
|
||||||
@ -55,7 +47,6 @@ 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
|
||||||
@ -67,10 +58,7 @@ 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
|
||||||
@ -90,7 +78,6 @@ 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)
|
||||||
|
|
||||||
@ -133,30 +120,22 @@ 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) }
|
|
||||||
|
|
||||||
val saved = remember { storage.load() }
|
var productionDate by remember { mutableStateOf(today) }
|
||||||
val defaultRows = listOf(30, 60, 180)
|
|
||||||
|
|
||||||
var productionDate by remember { mutableStateOf(saved?.first ?: today) }
|
|
||||||
var rows by remember {
|
var rows by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
(saved?.second ?: defaultRows).mapIndexed { index, days ->
|
listOf(
|
||||||
ExpiryRow(index, days)
|
ExpiryRow(0, 30),
|
||||||
}
|
ExpiryRow(1, 60),
|
||||||
|
ExpiryRow(2, 180)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
var nextId by remember { mutableIntStateOf(rows.size) }
|
var nextId by remember { mutableIntStateOf(3) }
|
||||||
|
|
||||||
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()
|
||||||
@ -175,10 +154,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
BackArrowIcon()
|
||||||
imageVector = Icons.Filled.ChevronLeft,
|
|
||||||
contentDescription = "返回"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
@ -202,30 +178,25 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
containerColor = MaterialTheme.colorScheme.primary,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
) {
|
) {
|
||||||
Icon(
|
PlusIcon(color = MaterialTheme.colorScheme.onPrimary)
|
||||||
imageVector = Icons.Filled.Add,
|
|
||||||
contentDescription = "添加",
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
ProductionDateCard(
|
||||||
ProductionDateCard(
|
date = productionDate,
|
||||||
date = productionDate,
|
isToday = productionDate == today,
|
||||||
isToday = productionDate == today,
|
onClick = {
|
||||||
onClick = {
|
datePickerTarget = DatePickerTarget.Production
|
||||||
datePickerTarget = DatePickerTarget.Production
|
showDatePicker = true
|
||||||
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))
|
||||||
|
|
||||||
@ -251,56 +222,23 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
if (rows.isEmpty()) {
|
Column(
|
||||||
Box(
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.weight(1f)
|
||||||
.weight(1f),
|
.verticalScroll(scrollState)
|
||||||
contentAlignment = Alignment.Center
|
.animateContentSize(
|
||||||
) {
|
animationSpec = androidx.compose.animation.core.spring(
|
||||||
Column(
|
stiffness = androidx.compose.animation.core.Spring.StiffnessMediumLow
|
||||||
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(
|
)
|
||||||
text = "暂无保质期记录",
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
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) {
|
||||||
@ -391,55 +329,10 @@ 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()
|
||||||
@ -690,6 +583,52 @@ 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)) {
|
||||||
|
|||||||
@ -1,17 +1,16 @@
|
|||||||
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
|
||||||
@ -20,6 +19,8 @@ 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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,10 +41,24 @@ fun LicensesScreen(
|
|||||||
title = { Text("开放源代码许可") },
|
title = { Text("开放源代码许可") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
val arrowColor = MaterialTheme.colorScheme.onSurface
|
||||||
imageVector = Icons.Filled.ChevronLeft,
|
Canvas(modifier = Modifier.size(24.dp)) {
|
||||||
contentDescription = "返回"
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
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
|
||||||
@ -19,6 +20,8 @@ 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
|
||||||
@ -45,10 +48,24 @@ fun ToolsScreen(
|
|||||||
title = { Text("工具") },
|
title = { Text("工具") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
val arrowColor = MaterialTheme.colorScheme.onSurface
|
||||||
imageVector = Icons.Filled.ChevronLeft,
|
Canvas(modifier = Modifier.size(24.dp)) {
|
||||||
contentDescription = "返回"
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@ -1,118 +0,0 @@
|
|||||||
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
|
org.gradle.daemon=true
|
||||||
|
|
||||||
#App
|
#App
|
||||||
app.version.base=1.1.0
|
app.version.base=1.0.0
|
||||||
|
|
||||||
#Android
|
#Android
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
|||||||
@ -31,7 +31,6 @@ 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" }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user