Compare commits
275 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 313ebe1eaa | |||
| 214a4364bf | |||
| ac186ed49a | |||
| fc2f2065df | |||
| 1b2fcfe248 | |||
| 7bf16dfe5a | |||
| 4f6dc2f36a | |||
| adc149c462 | |||
| 0703be68b4 | |||
| 13360029e1 | |||
| c80ad488c6 | |||
| fd7f79c3d3 | |||
| d7406ef652 | |||
| dfe7b5a6c3 | |||
| 94a4ba1e2a | |||
| d40c6d59b3 | |||
| 1248dff648 | |||
| 2a3da75859 | |||
| bff6e37908 | |||
| c7aa611e52 | |||
| f9e54f36fe | |||
| c39cecf343 | |||
| b13e018503 | |||
| e16b7e6587 | |||
| 69bffd8d20 | |||
| cc831f85cc | |||
| a0df5168f6 | |||
| eadb8eb682 | |||
| 19c9feb6de | |||
| e663ed5c8e | |||
| 53086f38e0 | |||
| de1e3178f6 | |||
| 80e4e5e86c | |||
| 37cf5ffc4c | |||
| afd94604ad | |||
| b7607eebd9 | |||
| 0a6b9449a3 | |||
| 088860861e | |||
| 57da898ed7 | |||
| 9ad9e39164 | |||
| c3510cde39 | |||
| c6377d85f9 | |||
| 1700615508 | |||
| 51a7a43b47 | |||
| 4ada46a549 | |||
| b333c527da | |||
| a2a631c141 | |||
| cfac607862 | |||
| b6146a29ae | |||
| ca00839c9b | |||
| 023ae44364 | |||
| 7d8e0fca61 | |||
| ffafee550e | |||
| 70c97ed9d5 | |||
| d3ebd6829b | |||
| b8509381d9 | |||
| cec8debff0 | |||
| d47a38bc19 | |||
| b589800eda | |||
| 0d83fef3a9 | |||
| 94e81bffb1 | |||
| b1064f95b7 | |||
| 81aeb8e894 | |||
| 50da18460f | |||
| b2942a3ca9 | |||
| 2e61fea21b | |||
| a1c71c2b93 | |||
| 8963b6162b | |||
| c665adfeff | |||
| afea8bf109 | |||
| 2a3089bf47 | |||
| 7b1290dd66 | |||
| 1cf950817e | |||
| 62ed8bd09a | |||
| c68944bd55 | |||
| 4b098442c4 | |||
| f8df108bf3 | |||
| d8014b5154 | |||
| 66a054f2d8 | |||
| 14a3239b39 | |||
| 7f080f2d90 | |||
| 32d66f0a42 | |||
| a5795b16dc | |||
| d528b98cde | |||
| e08243d0fa | |||
| ef55ffcf64 | |||
| 1f1db17384 | |||
| e4834a52c9 | |||
| eca6c61058 | |||
| 778d5dc8be | |||
| 5afac5dd04 | |||
| 08a6016479 | |||
| db31b9eaed | |||
| fae7654452 | |||
| 4be8cada86 | |||
| 0fbbdfa444 | |||
| 25885d84b3 | |||
| a8c2637a0c | |||
| 283a474eb1 | |||
| e07ccd8b4e | |||
| 3fcdeab702 | |||
| ac48a56465 | |||
| 6532363cdb | |||
| 9d2f67b9fb | |||
| 6830c1e510 | |||
| 1e43638fbc | |||
| 3ab50ec900 | |||
| 1caea057c5 | |||
| 5714802c89 | |||
| bd4dad4794 | |||
| a731507b3b | |||
| 85bfba241c | |||
| fd8e1fc1cb | |||
| be46d4b512 | |||
| b5a2ccf151 | |||
| b5e60e8bf8 | |||
| 21327d572b | |||
| de329c2020 | |||
| 8204ae15cf | |||
| fab3c2d763 | |||
| 423e68ec19 | |||
| 0edcc005bc | |||
| 1f2fc8644f | |||
| d98579c2bf | |||
| f6b1ed7368 | |||
| b242fb8ecc | |||
| ade852208b | |||
| 6f4fa68f0d | |||
| bcc59ca7a0 | |||
| 91068ded1c | |||
| 499a2eb0c7 | |||
| 66be0be3c4 | |||
| ccffcfb052 | |||
| 7ae0e80baf | |||
| e97c2f3015 | |||
| 5bf8a85f7a | |||
| 3ca0d334e7 | |||
| 94861dc2f7 | |||
| de47509a32 | |||
| 564e4e3960 | |||
| f33e68d1db | |||
| ef785a3ca7 | |||
| 06ac30e6d2 | |||
| 7404bdfc28 | |||
| 5c8839dea2 | |||
| 2427ae18a4 | |||
| c82ab26944 | |||
| 825eb526de | |||
| 9a09be56ef | |||
| 9d1999661c | |||
| 3f8eb1db05 | |||
| aba2086c22 | |||
| c591bc389b | |||
| 6e1c2a39ae | |||
| ac41eef37b | |||
| 58a97d1725 | |||
| 4c8084c176 | |||
| 87d51b253a | |||
| 3d39e609df | |||
| 7d6ed1428e | |||
| 1cb35d2752 | |||
| 7628f299c2 | |||
| 1438b405a9 | |||
| 2085c22987 | |||
| 55d50c3e1d | |||
| 8f24706d8c | |||
| 0868522741 | |||
| c2c8b1c321 | |||
| 4805bd1f0e | |||
| 62d3bbabea | |||
| a5a4173cc2 | |||
| 6b15c4b88b | |||
| 0560e81fb2 | |||
| fa872caa59 | |||
| a36f6c41e1 | |||
| 5b5d780593 | |||
| b3abe92b2c | |||
| ecf8a70f5d | |||
| 51d6e383b0 | |||
| d933ca3e41 | |||
| 829f89eb7c | |||
| e249700ee5 | |||
| 484045950f | |||
| 877a3f9aa4 | |||
| 23a7db1057 | |||
| aeb147c6a7 | |||
| c9bdff9063 | |||
| 24cb8fd3fe | |||
| f0975f119d | |||
| bc9c10d82e | |||
| b12206dc88 | |||
| 23909e855f | |||
| be023a00a9 | |||
| f34e34e5ae | |||
| 2b0491d7d7 | |||
| a8717dec7d | |||
| 6446d6636e | |||
| c72120ee0a | |||
| acb737dd25 | |||
| 4a8480be64 | |||
| 1930bbcb7f | |||
| 6dffaf4c91 | |||
| 3c1a04580c | |||
| 281abcf66b | |||
| 4de00e35dc | |||
| 4eadc479eb | |||
| 9a0222b4a2 | |||
| 20425e392c | |||
| 2592a5fa55 | |||
| 4c53f234cf | |||
| ea7558dec8 | |||
| 47876a233b | |||
|
|
dfb820d610 | ||
|
|
0b6d9ea87a | ||
|
|
bbe51051ae | ||
|
|
6fac313fdf | ||
|
|
ce84c614de | ||
|
|
5158b99800 | ||
|
|
80ab328906 | ||
|
|
a84e1b9528 | ||
| bb0a72006d | |||
| 73b176a95e | |||
| 6a4907f465 | |||
| 4664d352bd | |||
| 297a7a54d4 | |||
| 55000f64c9 | |||
| 01a7d7aed8 | |||
| da46204de4 | |||
| e175bb07da | |||
| 6ab63eae53 | |||
| a431dcdfbf | |||
| 28f777abe4 | |||
| e934d33cfa | |||
| dead12fdf4 | |||
| 5bac114c16 | |||
| 57eae41c65 | |||
| 93c02810bf | |||
| e30540b2dc | |||
| 134fc5a9c6 | |||
| e97909ec34 | |||
| b1185da27a | |||
| 7f639f6c24 | |||
| 8e65ae6f94 | |||
| 9bd1f57daf | |||
| e02459e8c9 | |||
| 5acad54384 | |||
| 1be1948f49 | |||
| dd072730f1 | |||
| 2fc89d08c7 | |||
| ca02784d39 | |||
| 0cdac663c9 | |||
| 7f9db1dc1d | |||
| baedf878b4 | |||
| fb7e19ddc9 | |||
| 6f4d62b78f | |||
| 774e03a928 | |||
| bf28008d17 | |||
| 2bd7d5ee19 | |||
| 98f3b71c4f | |||
| 2771e3733b | |||
| 6582d5970e | |||
| 6aefaf33a6 | |||
| f6b5e62284 | |||
| 27ba4f9f54 | |||
| 2028897ce3 | |||
| e1345cc071 | |||
| 9ad619c105 | |||
| f5cc4ef2e5 | |||
| cf9315cfe3 | |||
| c19916c2ec | |||
| 0806c11fcc | |||
| d3229f90d0 | |||
| d0f4980e01 | |||
| df0aa16d8e | |||
| 50a42be9bc |
74
.agents/skills/yayacal-release/SKILL.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: yayacal-release
|
||||||
|
description: Use when the user asks to release a new version of the yayacal Android calendar app
|
||||||
|
---
|
||||||
|
|
||||||
|
# YaYa Release
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
One-shot local release workflow for the yayacal Android app. All steps run on the developer machine using `gh` CLI.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- User says something like "发布 1.2.0", "release v1.3.0", "发一个新版本".
|
||||||
|
- Working in `/Users/issuser/Developer/xfy/yayacal`.
|
||||||
|
|
||||||
|
## Release Steps
|
||||||
|
|
||||||
|
1. **Confirm version number** with the user if not provided.
|
||||||
|
2. **Update version numbers**:
|
||||||
|
- `gradle.properties`: `app.version.base=x.y.z`
|
||||||
|
- `app/build.gradle.kts`: increment `versionCode` by 1
|
||||||
|
3. **Update `CHANGELOG.md`**:
|
||||||
|
- Compare commits since the last tag: `git log <last-tag>..HEAD --pretty=format:"%s"`
|
||||||
|
- Group into Added / Changed / Fixed
|
||||||
|
- Insert `## [x.y.z] - YYYY-MM-DD` below `## [Unreleased]`
|
||||||
|
- Append `[x.y.z]: https://github.com/xfy/yayacal/releases/tag/vx.y.z` at the bottom
|
||||||
|
4. **Commit** with the exact message:
|
||||||
|
```bash
|
||||||
|
git add CHANGELOG.md gradle.properties app/build.gradle.kts
|
||||||
|
git commit -m "release: vx.y.z"
|
||||||
|
```
|
||||||
|
5. **Tag and push** (lightweight tag, matching previous releases):
|
||||||
|
```bash
|
||||||
|
git tag vx.y.z
|
||||||
|
git push origin main --tags
|
||||||
|
```
|
||||||
|
6. **Build release APK**:
|
||||||
|
```bash
|
||||||
|
./gradlew :app:assembleRelease
|
||||||
|
```
|
||||||
|
7. **Create GitHub Release** with the current version's CHANGELOG as body:
|
||||||
|
```bash
|
||||||
|
python3 - <<'PY'
|
||||||
|
import re
|
||||||
|
content = open('CHANGELOG.md').read()
|
||||||
|
m = re.search(r'## \[x.y.z\] - .*?\n(.*?)\n## \[', content, re.DOTALL)
|
||||||
|
open('/tmp/vx.y.z-notes.md', 'w').write(m.group(1).strip())
|
||||||
|
PY
|
||||||
|
gh release create vx.y.z \
|
||||||
|
app/build/outputs/apk/release/app-release.apk \
|
||||||
|
--title "YaYa vx.y.z" \
|
||||||
|
--notes-file /tmp/vx.y.z-notes.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|---|---|
|
||||||
|
| Version base | `gradle.properties` → `app.version.base` |
|
||||||
|
| Version code | `app/build.gradle.kts` → `versionCode` |
|
||||||
|
| APK path | `app/build/outputs/apk/release/app-release.apk` |
|
||||||
|
| Commit message | `release: vx.y.z` |
|
||||||
|
| Tag format | lightweight `vx.y.z` |
|
||||||
|
| Release title | `YaYa vx.y.z` |
|
||||||
|
| Signing | Keep existing debug signing; do **not** add release keystore logic |
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
- **Wrong commit message**: Do not use `chore(release): ...` or `build: ...`. Use exactly `release: vx.y.z`.
|
||||||
|
- **Annotated tag**: Previous releases use lightweight tags; do not add `-a`.
|
||||||
|
- **Empty release body**: Always extract the current version's CHANGELOG section and pass it to `--notes-file`.
|
||||||
|
- **Releasing from a dirty tree**: Run `git status` first; only release from a clean `main` branch.
|
||||||
|
- **Forgetting to push the tag**: `git push origin main --tags` is required.
|
||||||
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*.{kt,kts}]
|
||||||
|
# 忽略标注 @Composable 的函数命名检查,Compose 规范要求 PascalCase
|
||||||
|
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||||
6
.gitignore
vendored
@ -21,4 +21,8 @@ node_modules/
|
|||||||
# OMC runtime state
|
# OMC runtime state
|
||||||
.omc/
|
.omc/
|
||||||
logs/
|
logs/
|
||||||
.claude/
|
.claude/
|
||||||
|
docs/superpowers/*
|
||||||
|
!docs/superpowers/specs/
|
||||||
|
.worktrees/
|
||||||
|
.zcode/
|
||||||
|
|||||||
137
.zcode/plans/plan-sess_ace6e49c-6557-47c4-9e42-2b0bbfd83e00.md
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
## 目标
|
||||||
|
|
||||||
|
修复班次设置页的 6 个问题:
|
||||||
|
1. 长按撤销误删巧合重合的独立断点
|
||||||
|
2. 长按产生的 override 与 phaseBreak 解耦,单独点翻转留幽灵重排
|
||||||
|
3. 连续长按产生断点链,撤销老断点不级联
|
||||||
|
4. `kindAt` 每格每次重组重算(无 remember 缓存)
|
||||||
|
5. "恢复默认"无撤销,立即落盘
|
||||||
|
6. 角标颜色硬编码 `onPrimary`、固定 6 行
|
||||||
|
|
||||||
|
根因:问题 1-3 都是"长按产生的 override + phaseBreak 是两个独立数据,但语义上是原子操作"。彻底解法是重构数据模型。
|
||||||
|
|
||||||
|
## 总体方案
|
||||||
|
|
||||||
|
**引入 `RephaseFlip` 原子结构**,把"翻转某天 + 从次日起重排"绑定成一个不可分割的数据单元。`overrides`(纯单日翻转)和 `phaseBreaks` 两字段保留,但长按操作改为产出 `RephaseFlip` 而非拆成两个独立字段。
|
||||||
|
|
||||||
|
### 新数据模型
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// 新增:原子化的"翻转并重排"操作记录
|
||||||
|
data class RephaseFlip(
|
||||||
|
val date: LocalDate, // 被翻转的天
|
||||||
|
val flippedTo: ShiftKind, // 翻转后的值
|
||||||
|
val rephaseFrom: LocalDate // 重排起点(= date + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ShiftPattern(
|
||||||
|
val anchorDate: LocalDate,
|
||||||
|
val cycle: List<ShiftKind>,
|
||||||
|
val overrides: Map<LocalDate, ShiftKind> = emptyMap(), // 保留:纯单日翻转(点)
|
||||||
|
val rephaseFlips: List<RephaseFlip> = emptyList(), // 新增:翻转并重排(长按)
|
||||||
|
val name: String = "默认"
|
||||||
|
) {
|
||||||
|
fun kindAt(date: LocalDate): ShiftKind? {
|
||||||
|
if (cycle.isEmpty()) return null
|
||||||
|
// 1. 纯单日翻转优先
|
||||||
|
overrides[date]?.let { return it }
|
||||||
|
// 2. rephaseFlips 中被翻转的当天
|
||||||
|
rephaseFlips.find { it.date == date }?.let { return it.flippedTo }
|
||||||
|
// 3. 找活跃锚点:rephaseFlips 的 rephaseFrom <= 当天 的最大那个;无则基础锚点
|
||||||
|
val (anchor, offset) = activeAnchor(date)
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键变化**:
|
||||||
|
- `phaseBreaks` 字段**删除**,替换为 `rephaseFlips`
|
||||||
|
- `activeAnchor` 改为从 `rephaseFlips` 取 `rephaseFrom` 作锚点
|
||||||
|
- kindAt 优先级:overrides → rephaseFlip 当天 → 活跃锚点 cycle
|
||||||
|
|
||||||
|
**撤销语义变干净**:长按 7/10 产生 `RephaseFlip(7/10, OFF, 7/11)`。再次长按 7/10 → 按 `date == 7/10` 精确匹配整个原子记录删除,**不会误删**别的(解决问题 1)。单独点 7/10 翻转时,如果存在 `rephaseFlip.date == 7/10`,提示或阻止(解决问题 2)。
|
||||||
|
|
||||||
|
### 存储格式
|
||||||
|
|
||||||
|
App 未正式发布(git 历史显示个人项目,SharedPreferences 刚引入)。**不做向后兼容迁移**:
|
||||||
|
- 存储格式改:`KEY_BREAKS` → `KEY_REPHASE`,编码 `日期:flippedTo:rephaseFrom`(`1/0:ISO日期`)
|
||||||
|
- 旧数据(含 `KEY_BREAKS` 的)在 `load()` 时解析 `KEY_REPHASE` 失败返回 null → 回退 `DEFAULT_PATTERN`(load 已有 try/catch)
|
||||||
|
- 用户唯一数据是默认 2班2休,丢失无感知
|
||||||
|
|
||||||
|
### 测试策略(先建安全网)
|
||||||
|
|
||||||
|
**Task 0 优先**:在改模型前,先用端到端测试锁定当前所有行为(kindAt 的全部输出),确保重构后这些断言仍然成立。新增针对长按场景(翻转+重排、撤销、连续长按)的测试,先在旧模型上跑过(记录当前行为),再重构后验证不回归。
|
||||||
|
|
||||||
|
## 实施步骤(7 个 Task,TDD)
|
||||||
|
|
||||||
|
### Task 0: 建立测试安全网(不改产品代码)
|
||||||
|
|
||||||
|
在改任何产品代码前,补充测试覆盖当前行为,作为重构的回归基线:
|
||||||
|
- `ShiftPatternTest`:补充"长按翻转+重排"端到端断言(用 override+phaseBreak 组合模拟当前长按产出,断言后续序列)。包括:单次长按、连续两次长按、撤销场景。
|
||||||
|
- `ShiftPatternStorageTest`:补充往返测试覆盖 phaseBreak。
|
||||||
|
- 这些测试在旧模型上**必须先通过**,记录为"重构前行为"。
|
||||||
|
|
||||||
|
### Task 1: 重构 ShiftPattern 数据模型(RephaseFlip)
|
||||||
|
|
||||||
|
- 新增 `RephaseFlip` data class
|
||||||
|
- `ShiftPattern` 删除 `phaseBreaks`,加 `rephaseFlips: List<RephaseFlip>`
|
||||||
|
- 重写 `kindAt`:overrides → rephaseFlip 当天 → 活跃锚点(从 rephaseFlips.rephaseFrom 取)
|
||||||
|
- 重写 `activeAnchor`:从 `rephaseFlips` 取 `rephaseFrom <= date` 的最大
|
||||||
|
- 删除 `PhaseBreak` 类
|
||||||
|
- **所有 Task 0 测试必须通过**(行为不变)
|
||||||
|
- 补充新测试:rephaseFlip 撤销精确匹配、多个 rephaseFlip 共存
|
||||||
|
|
||||||
|
### Task 2: 重构 ShiftPatternStorage 存储格式
|
||||||
|
|
||||||
|
- `KEY_BREAKS` → `KEY_REPHASE`
|
||||||
|
- save/load 改为序列化 `rephaseFlips`(`日期:flippedTo:rephaseFrom`)
|
||||||
|
- `parseRephase` 替代 `parseBreaks`
|
||||||
|
- 往返测试更新
|
||||||
|
|
||||||
|
### Task 3: 重写 ShiftCalendarGrid 交互逻辑
|
||||||
|
|
||||||
|
- `toggleFlipAndRephase`:产出 `RephaseFlip(date, flippedTo, date+1)`,撤销按 `date` 精确匹配
|
||||||
|
- `togglePhaseBreak` 删除
|
||||||
|
- `toggleOverride` 加保护:若该天存在 rephaseFlip,移除整个 rephaseFlip(防止幽灵重排,解决问题 2)
|
||||||
|
- 角标 `isRephaseStart` 改为检测 `rephaseFlips.any { it.rephaseFrom == date }`
|
||||||
|
- ShiftDayCell 加 `remember(pattern, date) { pattern.kindAt(date) }` 缓存(解决问题 4)
|
||||||
|
|
||||||
|
### Task 4: 修复角标颜色(问题 6)
|
||||||
|
|
||||||
|
- 角标文字色按类别:班=`onPrimary`、休=`onError`、起=`onTertiary`
|
||||||
|
- 对齐 DayCell 风格:9sp、TopEnd、CircleShape 胶囊背景
|
||||||
|
|
||||||
|
### Task 5: 修复固定行数(问题 7)
|
||||||
|
|
||||||
|
- `ShiftCalendarGrid` 改用 `getMonthGridInfo(viewYear, viewMonth).rows` 动态算行数
|
||||||
|
- 删除硬编码 `(0 until 6)`,改为 `(0 until rows)`
|
||||||
|
|
||||||
|
### Task 6: "恢复默认"加撤销(问题 5)
|
||||||
|
|
||||||
|
- 引入 `SnackbarHost`(项目首次使用,需在 Scaffold 加 `snackbarHost` 参数)
|
||||||
|
- 点恢复默认 → 存旧 pattern 到临时变量 → 显示 Snackbar "已恢复默认,撤销" 5 秒
|
||||||
|
- 点撤销 → 恢复旧 pattern
|
||||||
|
- 不再用 AlertDialog 确认(Snackbar 撤销比确认对话框更现代,且防误操作)
|
||||||
|
|
||||||
|
### Task 7: 全量验证
|
||||||
|
|
||||||
|
- `./gradlew :app:assembleDebug :core:testDebugUnitTest` 通过
|
||||||
|
- `./gradlew spotlessApply`
|
||||||
|
- 手动验证清单(模拟器):长按翻转重排、再次长按撤销、连续长按、恢复默认撤销
|
||||||
|
|
||||||
|
## 改动文件清单
|
||||||
|
|
||||||
|
| 文件 | Task | 改动 |
|
||||||
|
|------|------|------|
|
||||||
|
| `core/.../ShiftPattern.kt` | 1 | 删 PhaseBreak,加 RephaseFlip,重写 kindAt |
|
||||||
|
| `core/.../ShiftPatternStorage.kt` | 2 | 存储格式改 |
|
||||||
|
| `core/.../ui/ShiftCalendarGrid.kt` | 3,4,5 | 交互逻辑+角标颜色+动态行数 |
|
||||||
|
| `core/.../ui/ShiftPatternScreen.kt` | 6 | Snackbar 撤销 |
|
||||||
|
| `core/test/.../ShiftPatternTest.kt` | 0,1 | 安全网+重构后验证 |
|
||||||
|
| `core/test/.../ShiftPatternStorageTest.kt` | 0,2 | 往返测试更新 |
|
||||||
|
|
||||||
|
## 风险
|
||||||
|
|
||||||
|
- **kindAt 行为变化**:`rephaseFlips` 的 `rephaseFrom` 与旧 `phaseBreaks.date` 在"长按场景"下语义等价(都是次日),但"纯 phaseBreak"(offset!=0)不再支持。当前 UI 不产生 offset!=0 的断点(长按总是 offset=0),所以无实际影响。Task 0 测试会验证。
|
||||||
|
- **存储不兼容**:旧 `KEY_BREAKS` 数据被忽略,用户重置为默认。已确认可接受(个人项目,数据可重建)。
|
||||||
|
- **Snackbar 首次引入**:需确认 Material3 Scaffold 的 snackbarHost 用法正确,不破坏现有布局。
|
||||||
135
AGENTS.md
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
Guide for AI assistants working in **YaYa (鸭鸭日历)** — a pure-Android Jetpack Compose lunar/shift calendar app. Remote: `github.com/DefectingCat/yayacal` (+ `git.rua.plus` mirror). UI text is Chinese; date logic is `kotlinx-datetime` only (`java.util.Calendar` is banned).
|
||||||
|
|
||||||
|
> Note: `CLAUDE.md` is a symlink to this file. `COMMENTS.md` is referenced from several docs but does not exist yet — treat the KDoc rules in [Code Conventions](#code-conventions--common-patterns) as authoritative until it is created.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
YaYa is a single-app Android calendar focused on: Chinese lunar calendar, solar terms (节气) and traditional festivals (via **tyme4kt**), a personal WORK/OFF shift cycle, month/week/year views with infinite paging, and a photo-journal ("date recorder") feature with an in-app photo editor. Latest release `1.3.0` (see `CHANGELOG.md`); base version lives in `gradle.properties` (`app.version.base`).
|
||||||
|
|
||||||
|
## Architecture & Data Flow
|
||||||
|
|
||||||
|
**Three Gradle modules** (`settings.gradle.kts`, typesafe accessors on):
|
||||||
|
|
||||||
|
| Module | Type | Responsibility |
|
||||||
|
|--------|------|----------------|
|
||||||
|
| `:core` | `com.android.library` | **All** Compose UI, ViewModels, business logic, Room data layer |
|
||||||
|
| `:app` | `com.android.application` | Thin shell: Activities + Manifest + theme. **Iron rule: no business logic here.** |
|
||||||
|
| `:macrobenchmark` | `com.android.test` | Baseline Profile / Startup Profile generation |
|
||||||
|
|
||||||
|
Dependency chain: `:app` → `:core`; `:macrobenchmark` → `:app`.
|
||||||
|
|
||||||
|
**Navigation is Activity + Intent, NOT Compose Navigation.** Each screen has a 1:1 Activity in `:app` that just does `setContent { YaYaTheme { SomeScreen() } }`. `core/.../ui/DateRecorderNav.kt` centralizes the Intent-extra contract (`EXTRA_TEMP_PHOTO_PATH`, `EXTRA_FINAL_PHOTO_PATH`, `EXTRA_RECORD_ID`). Slide/fade transitions come from `app/.../BaseActivity.kt`.
|
||||||
|
|
||||||
|
**State pattern (all ViewModels):** private `MutableStateFlow` backing fields exposed as read-only `StateFlow` via `asStateFlow()`; many aggregate into a single `uiState: StateFlow<UiState>` through `combine(...).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), initial)`. The aggregation is an explicit design goal to minimize Compose recomposition. Tests read `stateFlow.value` directly (no Turbine).
|
||||||
|
|
||||||
|
**Home-screen data flow:** `CalendarViewModel` owns `selectedDate`, `isCollapsed`, `collapseProgress` (0=month↔1=week), `isYearView`, `shiftPattern`. It pulls lunar data from `LunarCache` (LRU `LinkedHashMap` + coroutine `Mutex`) which builds `DayCellInfo` per cell from tyme4kt `SolarDay`. `CalendarMonthView` lifts the shared `PagerState` and renders `CalendarPager` → `CalendarMonthPage` → `DayCell`.
|
||||||
|
|
||||||
|
**Persistence:** Room (`DateRecordDatabase` v1, `exportSchema = true` → `core/schemas/`). Repository stores relative photo paths under `filesDir/Pictures/date_recorder/` and deletes photos on record delete. Non-DB prefs (shift pattern, date checker) use SharedPreferences with **custom string encoding** (no JSON dependency) in `*Storage.kt`.
|
||||||
|
|
||||||
|
## Key Directories
|
||||||
|
|
||||||
|
```
|
||||||
|
app/src/main/
|
||||||
|
kotlin/plus/rua/project/ # Activities (shell only): MainActivity, BaseActivity, *Activity
|
||||||
|
AndroidManifest.xml # Activity registry; SplashActivity present but disabled
|
||||||
|
res/anim/, res/values/ # slide transitions, themes (Theme.Material.* NoActionBar)
|
||||||
|
core/src/main/
|
||||||
|
kotlin/plus/rua/project/ # ViewModels, business logic, data, storage, tracing
|
||||||
|
kotlin/plus/rua/project/ui/ # All Composable screens + calendar grid + nav contract
|
||||||
|
kotlin/plus/rua/project/ui/theme/ # YaYaTheme
|
||||||
|
assets/animations/*.webp # scanned at config time → BuildConfig.WEBP_FILES
|
||||||
|
baseline-prof.txt / baselineProfiles/startup-prof.txt # generated profiles
|
||||||
|
core/src/test/kotlin/plus/rua/project[/ui]/ # the only unit tests
|
||||||
|
macrobenchmark/src/main/java/.../baseline/ # StartupBenchmark, BaselineProfileGenerator
|
||||||
|
scripts/ # profile.sh, analyze-trace.sh, resize_duck_icon.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Namespace quirk:** `:core`'s `android.namespace` is `plus.rua.project.shared`, so core's `R` and `BuildConfig` live there — but the code package is `plus.rua.project`.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:assembleDebug # build debug APK
|
||||||
|
./gradlew :app:installDebug # install to device/emulator
|
||||||
|
./gradlew :core:testDebugUnitTest # all unit tests (JVM, no emulator)
|
||||||
|
./gradlew :core:testDebugUnitTest --tests "plus.rua.project.ui.CalendarUtilsTest" # one class
|
||||||
|
./gradlew spotlessApply # format (ktlint) — run before committing
|
||||||
|
```
|
||||||
|
|
||||||
|
Profiling & profiles (device required):
|
||||||
|
```bash
|
||||||
|
./gradlew :macrobenchmark:updateBaselineProfile # generates + copies both profiles into :core
|
||||||
|
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest # benchmarks only (no copy)
|
||||||
|
./scripts/profile.sh # Perfetto trace, default 8s → logs/
|
||||||
|
./scripts/profile.sh --scenario month_browse --trace 15 # specific scenario, trace build
|
||||||
|
./scripts/profile.sh --list-scenarios # list named scenarios
|
||||||
|
./scripts/analyze-trace.sh [logs/trace_*.perfetto-trace] # post-hoc SQL analysis
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
- **每完成一个功能点,提交一次。** 不要攒着最后一起提交。一个"功能点"是一个可独立说明的小块改动(一个 bug 修复、一个 Composable、一组测试、一次重构),而不是整次会话的全部产出。
|
||||||
|
- **提交粒度与信息由 AI 自主决定**:自行判断该功能点属于 `feat` / `fix` / `refactor` / `docs` / `test` / `chore`,写清摘要;改动小则合并提交,改动跨多步则在每步落盘。
|
||||||
|
- 只暂存本次功能点相关的文件,不要 `git add .` 把无关改动(如会话前遗留的脏文件)一并带入。提交前看一眼 `git status`。
|
||||||
|
- 当前在 `main` 分支:按仓库惯例可以直接在 `main` 上提交常规改动;如改动涉及发布或需评审,先开分支。
|
||||||
|
- **每写完一个功能后跑 `./gradlew :app:installDebug`** 做一次真机/模拟器冒烟安装,验证能编过、能装上。**允许失败**:无连接设备、无模拟器、签名/环境问题等导致失败属正常,不必阻塞后续工作——记下失败原因即可继续。这条是为了在能装上时尽早暴露问题,不是硬性 gate。
|
||||||
|
|
||||||
|
## Code Conventions & Common Patterns
|
||||||
|
|
||||||
|
- **KDoc required** on every public `@Composable`: document parameters and *when callbacks fire*. (This is the missing `COMMENTS.md` contract.)
|
||||||
|
- **`Modifier` always last** in Composable signatures.
|
||||||
|
- **Callbacks use `on` prefix** (`onDateClick`, `onDragEnd`).
|
||||||
|
- **Clickable list items:** use `Card(onClick = …)` with `CardDefaults.cardElevation(defaultElevation = 0.dp)` — **not** bare `Modifier.clickable()`.
|
||||||
|
- **`@Suppress("DEPRECATION")`** must carry an inline comment explaining why (currently used for `monthNumber`).
|
||||||
|
- **Dates:** `kotlinx-datetime` everywhere. `java.util.Calendar` is forbidden.
|
||||||
|
- **Testability seams:** `Clock` is constructor-injectable (tests pass a `FixedClock`); SharedPreferences are faked with in-memory impls written per-test-file (duplicated deliberately to avoid same-package private-class name clashes).
|
||||||
|
- **Pagers:** `HorizontalPager` with `pageCount = { Int.MAX_VALUE }` centered at `START_PAGE = Int.MAX_VALUE/2`; page↔month via `pageToYearMonth`/`yearMonthToPage` in `ui/CalendarUtils.kt`; sync via `snapshotFlow { pagerState.settledPage }.drop(1)`.
|
||||||
|
- **Trace markers:** `ComposeTrace.kt` wraps `android.os.Trace`, gated by `BuildConfig.ENABLE_TRACE` (on in `debug`/`trace`, off in `release`). Key markers: `MonthView:Compose`, `YearView:Compose`, `CalendarPager:Page:<year>-<month>`, `WeekPager:Page`, `VM:collapseProgress:*`, `YearGridView:*`. Full catalog in `DEVELOPMENT.md`.
|
||||||
|
- **Formatting:** Spotless + ktlint cover `src/**/*.kt` and root `*.gradle.kts` (not `.toml`/`.md`/scripts). `.editorconfig` permits PascalCase `@Composable` function names.
|
||||||
|
|
||||||
|
## Important Files
|
||||||
|
|
||||||
|
Central / load-bearing source (paths relative to repo root):
|
||||||
|
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt` — home-screen state: collapse math, shift resolution, ISO week, grid generation. The heart of the app.
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt` — home Composable: month↔year transition, FAB menu, pager wiring (largest UI file).
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/ui/CalendarUtils.kt` — all page↔date math + layout constants (`START_PAGE`, `COLLAPSE_THRESHOLD`).
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/LunarCache.kt` — tyme4kt integration; produces `DayCellInfo` (the per-cell render contract).
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/ShiftPattern.kt` — shift resolution (`kindAt`: overrides → rephaseFlips → active-anchor cycle). Most rigorously tested file.
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt`, `ui/WeekPager.kt` — the two infinite pagers.
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt`, `ui/DayCell.kt` — per-page grid + cell rendering with collapse animation.
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/ui/YearGridView.kt` — year overview grid.
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/PhotoProcessor.kt` + `PhotoEditorState.kt` + `PhotoEditorViewModel.kt` — photo editor pipeline (load/rotate/crop/render with `HandStroke` overlay).
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/{DateRecord,DateRecordDao,DateRecordDatabase,DateRecordConverters,DateRecorderRepository}.kt` — Room data layer for the photo journal.
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/ComposeTrace.kt` — perf tracing shim.
|
||||||
|
- `core/src/main/kotlin/plus/rua/project/ui/DateRecorderNav.kt` — Intent extra keys (cross-Activity contract).
|
||||||
|
- `app/src/main/kotlin/plus/rua/project/{MainActivity,BaseActivity}.kt` — entry point + transition base.
|
||||||
|
- `gradle/libs.versions.toml` — all dependency versions.
|
||||||
|
- `app/build.gradle.kts` — build types, dynamic versioning (`baseVersion_gitHash_buildDate`).
|
||||||
|
|
||||||
|
Other docs: `DEVELOPMENT.md` (perf/profile workflow + trace marker catalog), `README.md` (user intro), `CHANGELOG.md` (Keep-a-Changelog; `[Unreleased]` + `[1.3.0] - 2026-07-09`). Module rules: `app/AGENTS.md`, `core/AGENTS.md`, `macrobenchmark/AGENTS.md`.
|
||||||
|
|
||||||
|
## Runtime/Tooling Preferences
|
||||||
|
|
||||||
|
- **JDK 17** (`VERSION_17` in all modules). Gradle wrapper **9.5.1**.
|
||||||
|
- **AGP 9.2.1 · Kotlin 2.3.21 · KSP 2.3.10 · Compose BOM 2026.06.01.** compileSdk/targetSdk **37**, minSdk **24**.
|
||||||
|
- Key libs: **kotlinx-datetime 0.8.0 · tyme4kt 1.5.0 · sketch 4.4.0 · zoomimage 1.6.0 · Room 2.8.4 · lifecycle 2.11.0 · activity-compose 1.13.0 · CameraX 1.5.3 · Media3 1.6.1.**
|
||||||
|
- **Gradle caches on:** configuration cache + build cache + parallel (`gradle.properties`). R8 full mode enabled.
|
||||||
|
- **Build types:** `debug` (default, trace on) · `release` (R8 + resource shrink, trace off, debug-signed) · `trace` (release + trace markers) · `benchmark` (release base, **no** minify — so generated profile names aren't obfuscated).
|
||||||
|
- **Profiling needs a device/emulator** with GPU acceleration (software renderer can't produce `gfxinfo` framestats). Real benchmarks need a physical device on a release target.
|
||||||
|
- No CI/CD exists — all builds, tests, profiling, and releases run locally.
|
||||||
|
|
||||||
|
## Testing & QA
|
||||||
|
|
||||||
|
- **All automated tests are pure-JVM unit tests** in `core/src/test/kotlin/plus/rua/project[/ui]/` — **no `androidTest` source set, no Compose UI tests, no Robolectric/Turbine/Mockk.** Run on JVM 17, no emulator.
|
||||||
|
- **Frameworks:** `kotlin-test-junit` (+ JUnit 4 in a few classes) and `kotlinx-coroutines-test` (`runTest`). `androidx.room:room-testing` is declared but unused.
|
||||||
|
- **Covered well:** date math (`CalendarUtilsTest`), the shift engine (`ShiftPatternTest` — cycle/override/`RephaseFlip`), `CalendarViewModel` observable state + grid generation, storage round-trips, record sorting, lunar birthday/rose-day flags, `PhotoProcessor.calculateInSampleSize`, `HandStroke` segmentation.
|
||||||
|
- **Coverage gaps to be aware of:** no Compose UI/instrumented tests (UI exercised only via the macrobenchmark journey); Room DAO/Database/migration logic untested; `Flow` emission sequences untested (only synchronous `.value` snapshots); `DateRecordDetailViewModel`/`PhotoEditorViewModel` and the edit-record path of `RecordEditViewModel` have no dedicated tests.
|
||||||
|
- **Test conventions:** classes `*Test.kt`; methods `method_condition_result`; in-memory SharedPreferences fakes written per file; fixtures inline (no shared helpers). Comments/KDoc are in Chinese and state scope explicitly.
|
||||||
|
- **Macrobenchmark** (`macrobenchmark/src/main/.../baseline/`) provides the only on-device coverage — `StartupBenchmark` (cold start) and `BaselineProfileGenerator` (drives a 12-step user journey to emit `baseline-prof.txt` + `startup-prof.txt`).
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
Driven by the `yayacal-release` skill (`.agents/skills/yayacal-release/SKILL.md`). Flow: bump `app.version.base` in `gradle.properties` + `versionCode` in `app/build.gradle.kts` → update `CHANGELOG.md` from `git log <last-tag>..HEAD` → commit exactly `release: vx.y.z` → lightweight tag `vx.y.z` → push `main --tags` → `assembleRelease` → `gh release create` with extracted notes. Release only from clean `main`; keep debug signing (no release keystore). Note: the skill's doc links to `github.com/xfy/yayacal` — the actual remote is `DefectingCat/yayacal`.
|
||||||
203
CHANGELOG.md
@ -5,6 +5,200 @@ 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).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.5.0] - 2026-07-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 开源许可页面重构:重新设计开放源代码许可页面 (`LicensesScreen`),集成 M3 Expressive 设计规范、弹簧弹性动画与依赖项组件补全。
|
||||||
|
- 时光画廊视图切换动画:日期记录器 (`DateRecorderScreen`) 视图模式切换接入 `SharedTransitionLayout`,实现瀑布流/网格/列表视图切换时的卡片形变与层级圆角过渡。
|
||||||
|
- 照片编辑自适应无缝旋转:重构 `PhotoEditorScreen` 旋转体验,干掉外框卡片限制,实现全视口自适应旋转,照片主体增加精细悬浮阴影与圆角。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 渲染与绘制性能优化:动画平移与透明度变更改用 `graphicsLayer` 移至 Drawing 阶段,跳过 Composition 与 Layout 测量;农历/节气预计算及批量查询解耦至 `Dispatchers.Default` 后台协程池。
|
||||||
|
- 首页 FAB 浮动菜单升级:重新设计左下角 FAB 浮动菜单视觉样式、淡入淡出动画以及 Back 键拦截体验。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 内存泄露与 Bitmap 回收:修复 `PhotoProcessor.loadSampled` 中 EXIF 旋转前的中间 `Bitmap` 未回收导致的 Native 内存泄露。
|
||||||
|
- 照片编辑错位与黑边:修复照片旋转时的错位黑边,以及编辑已有记录时丢掉 `recordId` 的问题。
|
||||||
|
- 年视图布局与抖动:修复年视图标题切换今年按钮时的布局抖动、重复农历年份显示,以及 FAB 遮挡 10 月迷你月历的问题。
|
||||||
|
- 调班设置重排角标:调班设置迷你月历重排起点显式展示 `起·班` / `起·休` 状态角标。
|
||||||
|
- 性能 Trace 范围修正:修正 `CalendarMonthPage` 中 Trace Section 的结束判定边界。
|
||||||
|
## [1.4.0] - 2026-07-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 日期记录器重构与时光画廊:全面重构 `DateRecorderScreen`,升级沉浸式时光画廊,支持瀑布流/网格/列表三态视图模式切换。
|
||||||
|
- 日期记录手势与多选:新增长按记录进入多选模式、支持按住跨行滑动连续多选记录,并带有平滑过渡与网格删除平移动画。
|
||||||
|
- 记录详情页图片灯箱:日期记录详情页图片支持点击全屏查看灯箱(Lightbox),支持双指缩放与平移手势。
|
||||||
|
- 新建记录联动预填:新建日期记录时自动提取图片拍摄日期(EXIF)并预填联动记录标题与日期。
|
||||||
|
- 年月视图穿梭动画:基于 `SharedTransitionLayout` 重新设计主日历月份与年份视图间的缩放穿梭动画。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 照片编辑与记录编辑重构:重新设计照片编辑界面 (`PhotoEditorScreen`) 与记录编辑页面 (`RecordEditScreen`),优化全屏预览、平滑旋转裁剪与黑边自动填充。
|
||||||
|
- 渲染与性能深度优化:优化日历翻页重组范围、农历 LRU 缓存与批量查询机制,减少 Canvas 绘制内存分配;更新 Baseline Profile 与 Startup Profile 规则。
|
||||||
|
- 视图切换器样式调整:优化顶部视图切换器平滑弹簧滑动动画与几何胶囊同心对齐。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 拖拽手势与多选响应:改用 `PointerEventPass.Initial` 监听底层手势,解决模拟器、鼠标及触摸屏长按滑动多选中断问题。
|
||||||
|
- 照片编辑错位与黑边:修复照片旋转二次叠加、裁剪框闭包快照陷阱、外层 Card 自适应比例及照片 EXIF 方向错位引起的崩溃与黑边。
|
||||||
|
- 日历视图联动错位:修复滑动切换年份后再进入年视图导致的页码联动错位,以及动画期间滑动导致的迷你月历错位问题。
|
||||||
|
- 布局排版与 ProGuard:重构排序对话框防止水平溢出挤压错位,添加 Sketch 库 dontwarn 规则规避编译警告。
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-07-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 班次设置页:新增独立设置页,支持编辑基础周期(锚点日期 + 预设方案 1班1休/2班2休/3班3休/4班4休)。
|
||||||
|
- 班次设置页:迷你月历支持点击翻转班/休(单日)、长按翻转并从次日起重排后续周期。
|
||||||
|
- 班次设置页:迷你月历支持左右滑动翻月,点击年月标题弹 DatePicker 快速跳转月份。
|
||||||
|
- 班次设置页:滑动翻月时高度插值平滑过渡,复用主日历的行数插值机制。
|
||||||
|
- 班次设置页:恢复默认改为 Snackbar 撤销模式,5 秒内可恢复。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 班次数据模型重构:`phaseBreaks` + `override` 两独立字段合并为原子结构 `RephaseFlip(date, flippedTo, rephaseFrom)`,解决撤销误删、幽灵重排、断点链不级联问题。
|
||||||
|
- 个人轮班设置持久化到 SharedPreferences,设置返回主界面后 `onResume` 立即生效。
|
||||||
|
- 迷你月历角标颜色按类别配对(班=onPrimary,休=onError,重排起点=onTertiary)。
|
||||||
|
- 迷你月历动态计算行数(4/5/6 行自适应),不再固定 6 行。
|
||||||
|
- 依赖升级:Compose BOM 2026.06.01、Lifecycle 2.11.0、UiAutomator 2.4.0、tyme4kt 1.5.0、Spotless 8.8.0。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 班次设置页:修复 `shiftPattern` StateFlow 未被 `collectAsState` 订阅导致设置返回后日历不立即刷新。
|
||||||
|
- 班次设置页:修复锚点日期 TextButton 内边距导致与周期行右侧不对齐。
|
||||||
|
- 班次设置页:修复顶部内边距比四周偏大的叠加问题。
|
||||||
|
|
||||||
|
## [1.2.0] - 2026-06-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 启动页:新增自定义 `SplashActivity` 与启动主题,集成 `core-splashscreen` 与 `reportFullyDrawn`。
|
||||||
|
- 月视图:年月标题支持日期选择器快速跳转。
|
||||||
|
- 关于页:连续点击版本号 7 次触发「小狗乐园」全屏视频彩蛋。
|
||||||
|
- 日期图标:新增玫瑰节(Rose Day)与生日皇冠动画。
|
||||||
|
- 日期检查器:添加数据持久化、空状态提示、恢复默认按钮,以及生产日期/保质期非法日期禁选。
|
||||||
|
- 图标:新增 API 26+ Adaptive Icon。
|
||||||
|
- Baseline Profile:拆分并同时集成 Baseline Profile 与 Startup Profile 到 `:core`。
|
||||||
|
- Macrobenchmark:新增 StartupBenchmark 冷启动耗时测试。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 应用图标:调整小黄鸭占比,优化 Adaptive Icon 前景尺寸。
|
||||||
|
- 资源整理:GIF 目录重命名为 `animations`,`AnimatedGif` 改为 `AnimatedWebp`,WebP 文件列表由 BuildConfig 注入以消除硬编码。
|
||||||
|
- 构建清理:移除无效 ProGuard 规则、调试日志,trace 标记解耦 ViewModel。
|
||||||
|
- UI 统一:日期检查器、FAB 菜单、返回图标等手绘 Canvas 图标替换为 Material Icons。
|
||||||
|
- 关于页背景图改为 WebP 格式。
|
||||||
|
- 版本号统一由 `gradle.properties` 管理。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 启动页:修复 SplashScreen API 在某些设备上的崩溃/黑屏问题。
|
||||||
|
- 图标:修复 Adaptive Icon 前景加载失败及含背景问题。
|
||||||
|
- 日期检查器:修复历史负保质期数据导致列表异常,修复右滑删除卡住及 FAB 遮挡底部问题。
|
||||||
|
|
||||||
|
## [1.1.0] - 2026-06-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
#### Date Checker Tool
|
||||||
|
- New "Date Checker" tool page accessible from FAB → Tools menu for tracking item expiration dates
|
||||||
|
- Swipe-to-delete with animated removal and staggered enter/exit animations
|
||||||
|
- Expired status display with visual indicators
|
||||||
|
- Auto-scroll and highlight animation for new entries
|
||||||
|
|
||||||
|
#### Tools Page
|
||||||
|
- New "Tools" entry in FAB menu linking to a dedicated tools landing page
|
||||||
|
- Date Checker as the first tool module
|
||||||
|
|
||||||
|
#### Theme & Visual
|
||||||
|
- `YaYaTheme` introduced and applied to all Activities for unified theming
|
||||||
|
- Legal holiday badges now display with colored background and continuous edge rounded corners
|
||||||
|
- Holiday badge wave-scale entrance animation
|
||||||
|
- Personal shift badges redesigned with light circle background + centered text
|
||||||
|
- Shift badge circular base to avoid overlapping with selection ring
|
||||||
|
|
||||||
|
#### Year ↔ Month View Transition
|
||||||
|
- BottomCard slide-in animation and fade effect during year/month view transitions
|
||||||
|
- Month→year view no longer forces collapse state to expand
|
||||||
|
|
||||||
|
#### Performance
|
||||||
|
- LunarCache LRU cache for lunar/solar term calculations with startup pre-computation
|
||||||
|
- Macrobenchmark module with automated Baseline Profile generation
|
||||||
|
- Baseline Profile covering date checker, shift settings, tools page, and core calendar scenarios
|
||||||
|
- `ComposeTrace` cross-platform trace markers for Perfetto/Systrace
|
||||||
|
- SolarDay static cache to eliminate repeated object creation
|
||||||
|
- MiniMonth pure Canvas rendering eliminating 96 Text measurement overhead
|
||||||
|
- `graphicsLayer(translationY)` replacing `offset(Dp)` to avoid layout passes
|
||||||
|
- Aggregated `CalendarUiState` to reduce Compose recomposition
|
||||||
|
- `remember` stabilization for lambdas and computations
|
||||||
|
- Scene-based `profile.sh` with `--all` batch mode for 15 automated trace scenarios
|
||||||
|
- Perfetto trace analysis script (`analyze-trace.sh`)
|
||||||
|
- Trace build type for release + retained trace markers
|
||||||
|
|
||||||
|
#### Build & Tooling
|
||||||
|
- Spotless 8.5.1 code formatter with ktlint integration
|
||||||
|
- `.editorconfig` for ktlint Composable function naming rules
|
||||||
|
- Dependency update checker and auto-upgrade tool integration
|
||||||
|
- `app_icon` shrunk to 512×512 and converted to WebP (446KB savings)
|
||||||
|
- 152 GIF assets batch-converted to animated WebP format
|
||||||
|
- `uiTooling` moved to `debugImplementation`; unused `@Preview` and `kotlin-test` entries removed
|
||||||
|
- `sketch` library for GIF/WebP display (`sketch-compose` + `sketch-animated-webp`)
|
||||||
|
- PowerShell performance tracing script (`profile.ps1`)
|
||||||
|
|
||||||
|
#### Documentation
|
||||||
|
- Comprehensive `AGENTS.md` at every directory level (root, app, core, scripts, etc.)
|
||||||
|
- Updated `DEVELOPMENT.md` with Perfetto trace analysis and emulator launch commands
|
||||||
|
- Updated `CLAUDE.md` to reflect pure Android project structure
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Project migrated from Kotlin Multiplatform (KMP/CMP) to pure Android (`:app` + `:core`)
|
||||||
|
- All Compose UI and business logic consolidated into `:core` module; `:app` remains a thin shell
|
||||||
|
- Removed KMP/CMP plugins, iOS app module, and `:shared` module
|
||||||
|
- `androidApp` module renamed to `app`
|
||||||
|
- Collapse animation refactored: removed fling velocity threshold, now spring-driven
|
||||||
|
- `CalendarPager` ↔ `WeekPager` switching uses `AnimatedContent` for smooth crossfade
|
||||||
|
- Year view page year calculation uses `settledPage` to prevent flicker during swipe
|
||||||
|
- ViewModel decoupled from Compose runtime, migrated to `StateFlow`
|
||||||
|
- `LunarCache` made injectable with extracted repeated computations
|
||||||
|
- MenuItem and ToolItem unified to use `Card(onClick)` pattern
|
||||||
|
- Holiday badge null checks simplified to Elvis operator
|
||||||
|
- `@Suppress` annotations cleaned up with deprecated API replacements
|
||||||
|
- Removed unnecessary P0 code (custom combine, dead StateFlow, duplicate grid algorithms, runBlocking)
|
||||||
|
- Removed debug logging from LicensesScreen and BottomCard
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Lunar first-day month name no longer appends redundant "月" suffix
|
||||||
|
- Year view stale year display on enter
|
||||||
|
- Year view page year flicker during swipe transitions
|
||||||
|
- Collapse animation flicker when switching between CalendarPager and WeekPager
|
||||||
|
- Folded state cross-month dates not grayed out in week view
|
||||||
|
- Date checker swipe-to-delete state misalignment and deprecation warning
|
||||||
|
- Shared element transition animation loss after year view page change
|
||||||
|
- Night mode theme transparency issues with explicit background colors
|
||||||
|
- Predictive back gesture failure and end-of-animation flash on certain devices
|
||||||
|
- Back animation residual transition eliminated with `snapTo`
|
||||||
|
- Fast swipe collapse/expand failure, now uses progress threshold detection
|
||||||
|
- `graphicsLayer` optimization reverted due to excessive GPU compositing overhead on real devices
|
||||||
|
- Reverted shared element transitions in favor of zoom + fade animation
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- iOS app module (`iosApp/`) and all related Xcode project files
|
||||||
|
- `:shared` module and `shared/build.gradle.kts`
|
||||||
|
- Shared element transition animations (replaced by zoom + fade)
|
||||||
|
- Year/month scroll wheel picker with haptic feedback (reverted)
|
||||||
|
- Aliyun Maven mirrors (switched to Maven Central / Google)
|
||||||
|
- Unused Compose runtime ProGuard keep rules
|
||||||
|
- Temporary performance monitoring logs (trace markers retained)
|
||||||
|
|
||||||
## [1.0.0] - 2026-05-20
|
## [1.0.0] - 2026-05-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -139,10 +333,11 @@ 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
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
- No unreleased changes at this time.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
[1.5.0]: https://github.com/xfy/yayacal/releases/tag/v1.5.0
|
||||||
|
[1.4.0]: https://github.com/xfy/yayacal/releases/tag/v1.4.0
|
||||||
|
[1.3.0]: https://github.com/xfy/yayacal/releases/tag/v1.3.0
|
||||||
|
[1.2.0]: https://github.com/xfy/yayacal/releases/tag/v1.2.0
|
||||||
|
[1.1.0]: https://github.com/xfy/yayacal/releases/tag/v1.1.0
|
||||||
[1.0.0]: https://github.com/xfy/yayacal/releases/tag/v1.0.0
|
[1.0.0]: https://github.com/xfy/yayacal/releases/tag/v1.0.0
|
||||||
|
|||||||
94
CLAUDE.md
@ -1,94 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
YaYa is a calendar app built with Kotlin Multiplatform (KMP) + Compose Multiplatform, targeting Android and iOS. The shared UI is written entirely in Compose Multiplatform with Material 3.
|
|
||||||
|
|
||||||
## Build Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build Android debug APK
|
|
||||||
./gradlew :androidApp:assembleDebug
|
|
||||||
|
|
||||||
# Install Android debug APK to connected device
|
|
||||||
./gradlew :androidApp:installDebug
|
|
||||||
|
|
||||||
# Run all shared module tests
|
|
||||||
./gradlew :shared:allTests
|
|
||||||
|
|
||||||
# Run shared module tests on Android host only
|
|
||||||
./gradlew :shared:testAndroidHostTest
|
|
||||||
|
|
||||||
# Run a single test class
|
|
||||||
./gradlew :shared:testAndroidHostTest --tests "plus.rua.project.ui.CalendarUtilsTest"
|
|
||||||
|
|
||||||
# Generate iOS framework (required before first Xcode open or after clean)
|
|
||||||
./gradlew :shared:generateDummyFramework
|
|
||||||
|
|
||||||
# Build iOS app — open iosApp/iosApp.xcworkspace in Xcode and run from there
|
|
||||||
```
|
|
||||||
|
|
||||||
Gradle configuration cache and build cache are enabled by default (`gradle.properties`).
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
**Two-module structure:**
|
|
||||||
- `:shared` — all Compose UI, ViewModel, and business logic (KMP library)
|
|
||||||
- `:androidApp` — thin Android shell (`MainActivity` → `App()`)
|
|
||||||
|
|
||||||
iOS entry point is `MainViewController.kt` in `shared/src/iosMain/`, consumed by the Xcode project in `iosApp/`.
|
|
||||||
|
|
||||||
**Shared source sets:**
|
|
||||||
- `commonMain` — all Compose UI and ViewModel code
|
|
||||||
- `commonTest` — shared tests (run via `:shared:allTests` or `:shared:androidHostTest`)
|
|
||||||
- `androidMain` — Android-specific platform impl + preview tooling
|
|
||||||
- `iosMain` — `ComposeUIViewController` factory
|
|
||||||
|
|
||||||
**Calendar UI composition** (all in `plus.rua.project.ui`):
|
|
||||||
```
|
|
||||||
CalendarMonthView ← top-level screen (MonthHeader + WeekdayHeader + pager + BottomCard)
|
|
||||||
├── MonthHeader ← year/month label + ISO week number
|
|
||||||
├── WeekdayHeader ← fixed "一二三四五六日" row
|
|
||||||
├── CalendarPager ← HorizontalPager with Int.MAX_VALUE pages (month view)
|
|
||||||
│ └── CalendarMonthPage ← 6×7 grid of DayCell with collapse animation
|
|
||||||
│ └── DayCell ← single day circle with selection/today states
|
|
||||||
├── WeekPager ← HorizontalPager for single-week view (collapsed state)
|
|
||||||
│ └── DayCell
|
|
||||||
├── YearGridView ← 4×3 mini-month grid with year navigation (year view)
|
|
||||||
│ └── MiniMonth ← compact month: title + weekday row + day numbers
|
|
||||||
└── BottomCard ← drag handle card, drives collapse/expand gestures
|
|
||||||
```
|
|
||||||
|
|
||||||
`ShiftPattern` (in `plus.rua.project`) defines personal shift cycles (WORK/OFF) independent of public holidays. Uses modular arithmetic: `(date - anchorDate) mod cycle.size`.
|
|
||||||
|
|
||||||
`CalendarUtils` (in `plus.rua.project.ui`) holds pager constants (`START_PAGE = Int.MAX_VALUE/2`, `COLLAPSE_THRESHOLD = 0.25f`) and page↔date arithmetic (`pageToYearMonth`, `yearMonthToPage`, `pageToWeekMonday`).
|
|
||||||
|
|
||||||
**Collapse/expand animation:** `CalendarMonthView` supports month↔week transition via `CalendarViewModel.collapseProgress` (0f=month, 1f=week). `BottomCard` captures vertical drag gestures and calls `viewModel.onDrag()`/`onExpandDrag()`. When progress crosses 50% on release, a spring animation snaps to the nearest state. `CalendarMonthPage` compresses non-selected weeks toward zero height during collapse. When fully collapsed, `WeekPager` replaces `CalendarPager` for efficient single-week paging.
|
|
||||||
|
|
||||||
**Pager page mapping:** Both `CalendarPager` and `WeekPager` use `Int.MAX_VALUE` pages centered at `Int.MAX_VALUE / 2`. Page-to-date conversion is arithmetic — no index-based list. `CalendarPager` maps pages to yearMonth; `WeekPager` maps pages to week-Monday dates. Both skip the initial `snapshotFlow` emission (`.drop(1)`) to preserve the "today" selection on first render.
|
|
||||||
|
|
||||||
`CalendarViewModel` holds `selectedDate` and `isCollapsed` state, computes month day grids (6×7=42 cells) and ISO week numbers. Week starts on Monday (ISO 8601).
|
|
||||||
|
|
||||||
**Performance tracing:** `ComposeTrace.kt` provides `composeTraceBeginSection`/`composeTraceEndSection` via expect/actual — Android routes to `android.os.Trace`, iOS is a no-op. Custom markers are inserted at key points (e.g., `MonthView:Compose`, `YearView:Compose`, `VM:collapseProgress`) for Perfetto/Systrace analysis. See `DEVELOPMENT.md` for trace recording and Python parsing scripts.
|
|
||||||
|
|
||||||
## Key Dependencies
|
|
||||||
|
|
||||||
- Kotlin 2.3.21, Compose Multiplatform 1.11.0, Material 3 1.10.0-alpha05
|
|
||||||
- `kotlinx-datetime` 0.8.0 for all date logic (no java.util.Calendar)
|
|
||||||
- `tyme4kt` for Chinese traditional calendar (lunar dates, solar terms, festivals)
|
|
||||||
- `sketch` 4.4.0 for animated GIF display (`AsyncImage` with `sketch-animated-gif`)
|
|
||||||
- AGP 9.2.1, compileSdk/targetSdk 37, minSdk 24
|
|
||||||
- JVM target: 17
|
|
||||||
- R8 full mode enabled (`android.enableR8.fullMode=true`)
|
|
||||||
|
|
||||||
## Conventions
|
|
||||||
|
|
||||||
- Package: `plus.rua.project` (shared), `plus.rua.project.ui` (UI composables)
|
|
||||||
- Version catalog at `gradle/libs.versions.toml` — all dependency versions declared there
|
|
||||||
- `@Suppress("DEPRECATION")` used for `monthNumber` access on `kotlinx.datetime.LocalDate` — must include inline comment explaining reason
|
|
||||||
- UI text is in Chinese (weekday labels, month header format "2026年5月")
|
|
||||||
- Public `@Composable` functions require KDoc per `COMMENTS.md`
|
|
||||||
- `Modifier` parameter always last in composable signatures
|
|
||||||
- Callback parameters use `on` prefix (`onDateClick`, `onMonthChanged`)
|
|
||||||
230
COMMENTS.md
@ -1,230 +0,0 @@
|
|||||||
# YaYa 注释规范
|
|
||||||
|
|
||||||
基于 [KDoc](https://kotlinlang.org/docs/kotlin-doc.html) 规范与 Compose 语义化约定,结合项目实际情况制定。
|
|
||||||
|
|
||||||
## 核心原则
|
|
||||||
|
|
||||||
1. **注释解释「为什么」,而非「做什么」**——代码本身应能说明做什么,注释补充意图、约束和决策原因
|
|
||||||
2. **宁可没有注释,也不要废话注释**——当前项目风格是自描述代码 + 极少注释,这是好的基线
|
|
||||||
3. **公共 API 必须有 KDoc,内部实现按需注释**
|
|
||||||
|
|
||||||
## 何时必须写注释
|
|
||||||
|
|
||||||
### 1. Public Composable 函数——KDoc 必需
|
|
||||||
|
|
||||||
所有 `public` 的 `@Composable` 函数必须写 KDoc,说明用途、参数含义和回调触发时机。
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
/**
|
|
||||||
* 月度日历视图,支持周/月切换和折叠动画。
|
|
||||||
*
|
|
||||||
* @param selectedDate 当前选中日期
|
|
||||||
* @param today 今天的日期,用于高亮标记
|
|
||||||
* @param onDateClick 日期点击回调
|
|
||||||
* @param collapseProgress 折叠进度,0f=展开,1f=折叠
|
|
||||||
* @param modifier 外部布局修饰符
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun CalendarMonthPage(
|
|
||||||
year: Int,
|
|
||||||
month: Int,
|
|
||||||
selectedDate: LocalDate,
|
|
||||||
today: LocalDate,
|
|
||||||
onDateClick: (LocalDate) -> Unit,
|
|
||||||
collapseProgress: Float,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**约定:**
|
|
||||||
- `Modifier` 参数放最后,KDoc 中注明"外部布局修饰符"
|
|
||||||
- 回调参数用 `on` 前缀命名(`onDateClick` 而非 `dateClick`),KDoc 说明触发时机
|
|
||||||
- `Float` 进度/比例参数注明取值范围和含义
|
|
||||||
|
|
||||||
### 2. 非显而易见的算法和计算
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// 6行×7列=42格,覆盖跨月首尾周,保证网格完整
|
|
||||||
return (0 until 42).map { i -> ... }
|
|
||||||
|
|
||||||
// 折叠时选中行上方行上移、下方行下移,模拟"挤压"效果
|
|
||||||
val offsetY = when {
|
|
||||||
isAboveSelected -> -progress * 200f
|
|
||||||
isBelowSelected -> progress * 200f
|
|
||||||
else -> 0f
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Magic Number
|
|
||||||
|
|
||||||
用命名常量 + 注释,而非裸数字:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// 好
|
|
||||||
val MaxPagerPages = Int.MAX_VALUE // 无限分页,中心页为起始月
|
|
||||||
val CenterPage = MaxPagerPages / 2
|
|
||||||
|
|
||||||
// 坏
|
|
||||||
HorizontalPager(pageCount = 2147483647) { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Workaround / Hack / 平台限制
|
|
||||||
|
|
||||||
必须注明原因和追踪信息:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
@Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-datetime 尚未提供新接口
|
|
||||||
currentMonth = weekMonday.monthNumber
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 状态与副作用的业务意图
|
|
||||||
|
|
||||||
对 `remember`、`LaunchedEffect`、`DisposableEffect` 等解释业务意图,而非技术实现:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// 仅在首次展开时记录完整日历高度,折叠后不再覆盖
|
|
||||||
if (!viewModel.isCollapsed && viewModel.collapseProgress < 0.01f) {
|
|
||||||
expandedCalendarHeightPx = size.height
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 动画参数与业务状态的映射
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// collapseProgress: 0f=月视图(6行), 1f=周视图(1行)
|
|
||||||
// 折叠偏移量 = 进度 × 展开高度的5/6(保留1行可见)
|
|
||||||
val collapseOffsetPx = -(viewModel.collapseProgress * expandedCalendarHeightPx * 5f / 6f).toInt()
|
|
||||||
```
|
|
||||||
|
|
||||||
## 何时不需要注释
|
|
||||||
|
|
||||||
### 1. 代码本身已足够清晰
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// 坏——废话注释
|
|
||||||
// 设置水平 padding 为 16dp
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
|
||||||
|
|
||||||
// 好——无需注释
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 函数名已说明用途
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// 坏
|
|
||||||
// 获取 ISO 周号
|
|
||||||
fun getIsoWeekNumber(date: LocalDate): Int
|
|
||||||
|
|
||||||
// 好——函数名已足够
|
|
||||||
fun getIsoWeekNumber(date: LocalDate): Int
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 简单的属性赋值和数据类
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// 不需要注释
|
|
||||||
data class DayData(
|
|
||||||
val date: LocalDate,
|
|
||||||
val isCurrentMonth: Boolean
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## KDoc 格式规范
|
|
||||||
|
|
||||||
### Composable 函数模板
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
/**
|
|
||||||
* 一句话描述组件用途。
|
|
||||||
*
|
|
||||||
* 可选:补充说明重组行为、副作用等。
|
|
||||||
*
|
|
||||||
* @param param1 参数说明
|
|
||||||
* @param param2 参数说明
|
|
||||||
* @param modifier 外部布局修饰符
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun MyComponent(
|
|
||||||
param1: Type,
|
|
||||||
param2: Type,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
### ViewModel / 工具函数模板
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
/**
|
|
||||||
* 一句话描述功能。
|
|
||||||
*
|
|
||||||
* @param input 输入说明
|
|
||||||
* @return 返回值说明
|
|
||||||
*/
|
|
||||||
fun calculateSomething(input: Int): Int { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 文件级注释
|
|
||||||
|
|
||||||
仅在文件包含多个紧密关联的组件且关系不直观时使用,大多数文件不需要:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
/**
|
|
||||||
* 日历分页组件,包含月视图和周视图的 HorizontalPager 实现。
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 项目特定约定
|
|
||||||
|
|
||||||
| 场景 | 规范 |
|
|
||||||
|------|------|
|
|
||||||
| `@Suppress("DEPRECATION")` | 必须附带行内注释说明原因(当前主要用于 `monthNumber`) |
|
|
||||||
| Pager 页码映射 | `pageToYearMonth()` / `yearMonthToPage()` 等算术映射需注释公式逻辑 |
|
|
||||||
| 折叠动画参数 | 注释 `collapseProgress` 的取值范围和物理含义 |
|
|
||||||
| 尺寸测量(px) | 注释为何需要 px 而非 dp(如:动画偏移量需要像素精度) |
|
|
||||||
| `remember` key | 当 key 列表不直观时,注释为何选择这些 key |
|
|
||||||
| `Int.MAX_VALUE` 分页 | 注释无限分页的设计意图和中心页计算方式 |
|
|
||||||
|
|
||||||
## Preview 注释
|
|
||||||
|
|
||||||
Preview 函数不需要 KDoc,但 `@Preview` 注解应提供有意义的 `name`:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
@Preview(name = "Month View - Expanded", showBackground = true)
|
|
||||||
@Preview(name = "Month View - Collapsed", showBackground = true)
|
|
||||||
@Composable
|
|
||||||
private fun CalendarMonthViewPreview() { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
## 反模式清单
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// ❌ 翻译代码
|
|
||||||
// 遍历每一周
|
|
||||||
weeks.forEachIndexed { ... }
|
|
||||||
|
|
||||||
// ❌ 过时注释
|
|
||||||
// TODO: 后续优化(已完成但未删除)
|
|
||||||
|
|
||||||
// ❌ 注释掉的代码
|
|
||||||
// val oldImplementation = ...
|
|
||||||
|
|
||||||
// ❌ 用 // 替代 KDoc
|
|
||||||
// 这个函数渲染日历
|
|
||||||
@Composable fun Calendar() { ... }
|
|
||||||
|
|
||||||
// ❌ 无原因的 Suppress
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun foo() { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
## 检查清单
|
|
||||||
|
|
||||||
- [ ] Public Composable 是否有 KDoc?
|
|
||||||
- [ ] KDoc 是否说明了参数含义和回调触发时机?
|
|
||||||
- [ ] `Modifier` 参数是否在最后?
|
|
||||||
- [ ] 非显而易见的计算是否有行内注释?
|
|
||||||
- [ ] Magic Number 是否有命名常量 + 注释?
|
|
||||||
- [ ] `@Suppress` 是否有原因注释?
|
|
||||||
- [ ] 动画/进度参数是否注明了取值范围?
|
|
||||||
- [ ] 是否存在废话注释、过时注释或注释掉的代码?
|
|
||||||
305
DEVELOPMENT.md
@ -1,252 +1,99 @@
|
|||||||
# 开发指南
|
# 开发指南
|
||||||
|
|
||||||
## 环境要求
|
## 性能追踪
|
||||||
|
|
||||||
- JDK 17+
|
使用 `scripts/profile.sh` 一键抓取 Perfetto trace、帧统计和内存快照。
|
||||||
- Android Studio (Ladybug 或更新版本)
|
|
||||||
- Xcode 16+ (仅 iOS 构建需要)
|
|
||||||
- Kotlin Multiplatform 插件 (Android Studio 内置)
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
YaYa/
|
|
||||||
├── shared/ # 共享模块 — 所有 UI 和业务逻辑
|
|
||||||
│ ├── src/commonMain/kotlin/ # 跨平台代码
|
|
||||||
│ │ └── plus/rua/project/
|
|
||||||
│ │ ├── App.kt # 应用入口
|
|
||||||
│ │ ├── CalendarViewModel.kt # 日历状态管理
|
|
||||||
│ │ └── ui/
|
|
||||||
│ │ ├── CalendarMonthView.kt # 顶层日历屏幕
|
|
||||||
│ │ ├── CalendarMonthPage.kt # 单月网格页
|
|
||||||
│ │ ├── CalendarPager.kt # 月视图无限分页
|
|
||||||
│ │ ├── WeekPager.kt # 周视图无限分页
|
|
||||||
│ │ ├── DayCell.kt # 单日圆圈组件
|
|
||||||
│ │ ├── MonthHeader.kt # 年月标题 + 周数
|
|
||||||
│ │ ├── WeekdayHeader.kt # 星期标题行
|
|
||||||
│ │ └── BottomCard.kt # 底部拖拽卡片
|
|
||||||
│ ├── src/commonTest/kotlin/ # 共享测试
|
|
||||||
│ ├── src/androidMain/kotlin/ # Android 预览工具
|
|
||||||
│ └── src/iosMain/kotlin/ # iOS ViewController 工厂
|
|
||||||
├── androidApp/ # Android 薄壳 — MainActivity → App()
|
|
||||||
├── iosApp/ # iOS 入口 — Xcode 项目
|
|
||||||
└── gradle/libs.versions.toml # 版本目录 — 统一管理依赖版本
|
|
||||||
```
|
|
||||||
|
|
||||||
## 运行
|
|
||||||
|
|
||||||
### Android
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 命令行构建
|
# 默认抓取 8 秒
|
||||||
./gradlew :androidApp:assembleDebug
|
./scripts/profile.sh
|
||||||
|
|
||||||
|
# 抓取 15 秒
|
||||||
|
./scripts/profile.sh 15
|
||||||
|
|
||||||
|
# 应用已在运行时,不自动启动
|
||||||
|
./scripts/profile.sh --no-launch
|
||||||
|
```
|
||||||
|
|
||||||
|
输出文件保存在 `logs/` 目录:
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
| ------------------------ | ----------------------------------------------- |
|
||||||
|
| `trace_*.perfetto-trace` | Perfetto trace,在 https://ui.perfetto.dev 打开 |
|
||||||
|
| `framestats_*.txt` | GPU 帧统计 |
|
||||||
|
| `meminfo_*.txt` | 内存快照 |
|
||||||
|
| `report_*.md` | 追踪报告摘要 |
|
||||||
|
|
||||||
|
trace 中包含自定义标记:
|
||||||
|
|
||||||
|
- `MonthView:Compose` — 月视图重组
|
||||||
|
- `CalendarPagerArea` — 日历分页器区域
|
||||||
|
- `CalendarPager:Page:*` — 月视图单页重组
|
||||||
|
- `CalendarMonthPage:*` — 月页面数据计算(含折叠动画准备)
|
||||||
|
- `WeekPager:Page` — 周视图单页重组
|
||||||
|
- `YearView:Compose` — 年视图重组
|
||||||
|
- `YearGridView:*` — 年视图网格组合(首帧耗时关键指标)
|
||||||
|
- `generateMiniMonthDays:*` — 月份网格计算
|
||||||
|
- `MonthView→YearView` / `YearView→MonthView` — 视图切换
|
||||||
|
- `YearView:SelectMonth` — 年视图选月
|
||||||
|
- `getMonthDays:*` — ViewModel 月份网格计算
|
||||||
|
- `VM:collapseProgress:*` — 折叠动画拖拽(onDrag/onDragEnd/onExpandDrag/onExpandDragEnd)
|
||||||
|
|
||||||
|
## Baseline Profile / Startup Profile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译 Android debug APK
|
||||||
|
./gradlew :app:assembleDebug
|
||||||
|
|
||||||
# 安装到设备
|
# 安装到设备
|
||||||
./gradlew :androidApp:installDebug
|
./gradlew :app:installDebug
|
||||||
|
|
||||||
|
# 编译 release APK(含 Baseline / Startup Profiles)
|
||||||
|
./gradlew :app:assembleRelease
|
||||||
|
|
||||||
|
# 安装 benchmark 构建类型 APK
|
||||||
|
./gradlew :app:installBenchmark
|
||||||
```
|
```
|
||||||
|
|
||||||
或在 Android Studio 中选择 `androidApp` 配置直接运行。
|
一键生成并复制到 `:core`:
|
||||||
|
|
||||||
### iOS
|
|
||||||
|
|
||||||
1. 先执行一次 Gradle 同步:`./gradlew :shared:generateDummyFramework`
|
|
||||||
2. 在 Xcode 中打开 `iosApp/iosApp.xcworkspace`
|
|
||||||
3. 选择目标设备或模拟器,点击 Run
|
|
||||||
|
|
||||||
> 首次打开可能需要等待 Xcode 索引完成。如果报 framework 错误,重新执行 Gradle 同步即可。
|
|
||||||
|
|
||||||
## 测试
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 运行所有共享模块测试
|
./gradlew :macrobenchmark:updateBaselineProfile
|
||||||
./gradlew :shared:allTests
|
|
||||||
|
|
||||||
# 运行单个测试类 (Android host)
|
|
||||||
./gradlew :shared:androidHostTest --tests "plus.rua.project.ComposeAppCommonTest"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开发约定
|
生成后会得到两份产物:
|
||||||
|
|
||||||
### 代码组织
|
| 产物 | 目标路径 | 用途 |
|
||||||
|
|------|--------|------|
|
||||||
|
| Baseline Profile | `core/src/main/baseline-prof.txt` | 指导 ART 做 AOT 编译 |
|
||||||
|
| Startup Profile | `core/src/main/baselineProfiles/startup-prof.txt` | 指导 AGP 做 DEX layout 优化 |
|
||||||
|
|
||||||
- 所有 Compose UI 和 ViewModel 代码放在 `shared/commonMain`,不按平台拆分
|
仅运行基准测试(不自动复制):
|
||||||
- 平台特定代码仅放在对应的 `androidMain` / `iosMain`
|
|
||||||
- UI 组件统一在 `plus.rua.project.ui` 包下
|
|
||||||
|
|
||||||
### Compose 规范
|
```bash
|
||||||
|
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
|
||||||
- `Modifier` 参数始终放在最后
|
|
||||||
- 回调参数使用 `on` 前缀:`onDateClick`、`onMonthChanged`
|
|
||||||
- 公开 `@Composable` 函数需要 KDoc 注释(详见 `COMMENTS.md`)
|
|
||||||
|
|
||||||
### 日期处理
|
|
||||||
|
|
||||||
- 统一使用 `kotlinx-datetime`,禁止使用 `java.util.Calendar`
|
|
||||||
- 周起始为周一 (ISO 8601)
|
|
||||||
- `monthNumber` 访问需要 `@Suppress("DEPRECATION")` 并附行内注释说明原因
|
|
||||||
|
|
||||||
### UI 文案
|
|
||||||
|
|
||||||
- 界面文字为中文(星期标题 "一二三四五六日",月份格式 "2026年5月")
|
|
||||||
|
|
||||||
### 依赖管理
|
|
||||||
|
|
||||||
- 所有版本声明在 `gradle/libs.versions.toml`,不硬编码
|
|
||||||
- 新增依赖先在版本目录添加条目,再在 `build.gradle.kts` 中引用
|
|
||||||
|
|
||||||
## 架构概览
|
|
||||||
|
|
||||||
```
|
|
||||||
CalendarMonthView (顶层屏幕)
|
|
||||||
├── MonthHeader 年月标签 + ISO 周数
|
|
||||||
├── WeekdayHeader 固定星期行
|
|
||||||
├── CalendarPager 月视图无限分页 (Int.MAX_VALUE 页)
|
|
||||||
│ └── CalendarMonthPage 6×7 DayCell 网格,折叠时压缩非选中行
|
|
||||||
│ └── DayCell 单日圆圈,选中/今日状态
|
|
||||||
├── WeekPager 周视图无限分页 (折叠态)
|
|
||||||
│ └── DayCell
|
|
||||||
└── BottomCard 拖拽手柄,驱动折叠/展开手势
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**折叠动画:** `CalendarViewModel.collapseProgress` 控制 0f(月)↔1f(周) 过渡。`BottomCard` 捕获垂直拖拽,释放时超过 50% 则弹簧动画吸附到最近状态。完全折叠后 `WeekPager` 替代 `CalendarPager` 实现高效单周分页。
|
手动复制路径:
|
||||||
|
`macrobenchmark/build/outputs/connected_android_test_additional_output/`
|
||||||
|
|
||||||
**分页映射:** 两个 Pager 均使用 `Int.MAX_VALUE` 页数,中心页为 `Int.MAX_VALUE / 2`。页码到日期为算术转换,无索引列表。两者均跳过初始 `snapshotFlow` 发射 (`.drop(1)`) 以保留首次渲染时的"今日"选中。
|
注意:当前 `benchmark` 构建类型继承自 `release`(`isMinifyEnabled = true`),因此生成的文本 profile 会包含 R8 混淆后的类/方法名。AGP 在打包 APK/AAB 时会根据最终混淆映射将其转换为二进制 profile,这是正常行为。
|
||||||
|
|
||||||
## 性能排查(Perfetto / Systrace)
|
## 模拟器
|
||||||
|
|
||||||
项目使用 `composeTraceBeginSection` / `composeTraceEndSection` 在关键代码段插入 trace marker,Android 上会被记录到系统 trace 中。iOS 为空操作。
|
```sh
|
||||||
|
emulator -avd Pixel_10 \
|
||||||
|
-no-snapshot \
|
||||||
|
-no-boot-anim \
|
||||||
|
-gpu host \
|
||||||
|
-accel on \
|
||||||
|
-cores 4 \
|
||||||
|
-memory 4096 \
|
||||||
|
-partition-size 2048
|
||||||
|
```
|
||||||
|
|
||||||
已有的 trace section:
|
启动带 adb logcat 的模拟器
|
||||||
- `MonthView:Compose` / `YearView:Compose` — 顶层重组耗时
|
|
||||||
- `YearView→MonthView` / `MonthView→YearView` — 年视图切换动画
|
|
||||||
- `YearGridView:$year` / `generateMiniMonthDays:$year-$month` — 年网格渲染
|
|
||||||
- `getMonthDays:$year-$month` — 月网格数据生成
|
|
||||||
|
|
||||||
### 分析折叠器卡顿的方法
|
```sh
|
||||||
|
./gradlew :app:installDebug && rm -rf logs/1.logcat && adb logcat | tee logs/1.logcat
|
||||||
1. **录制 trace**:Android Studio → Profiler → CPU → 选择 "Trace Java Methods" 或命令行:
|
```
|
||||||
```bash
|
|
||||||
adb shell perfetto -c - --txt \<<EOF
|
|
||||||
buffers: { size_kb: 65536 }
|
|
||||||
data_sources: {
|
|
||||||
config {
|
|
||||||
name: "linux.ftrace"
|
|
||||||
ftrace_config {
|
|
||||||
ftrace_events: "ftrace/print"
|
|
||||||
ftrace_events: "sched/sched_switch"
|
|
||||||
buffer_size_kb: 8192
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data_sources: {
|
|
||||||
config {
|
|
||||||
name: "android.packages_list"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
duration_ms: 10000
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **用 Python 分析 trace**(无需 Perfetto UI):
|
|
||||||
|
|
||||||
```python
|
|
||||||
def read_varint(data, offset):
|
|
||||||
result = 0; shift = 0
|
|
||||||
while offset < len(data):
|
|
||||||
byte = data[offset]
|
|
||||||
result |= (byte & 0x7F) << shift
|
|
||||||
offset += 1
|
|
||||||
if not (byte & 0x80): break
|
|
||||||
shift += 7
|
|
||||||
return result, offset
|
|
||||||
|
|
||||||
def parse_trace(path):
|
|
||||||
with open(path, 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
|
|
||||||
# 1) 读取所有 TracePacket
|
|
||||||
packets = []
|
|
||||||
offset = 0
|
|
||||||
while offset < len(data):
|
|
||||||
if data[offset] != 0x0a:
|
|
||||||
offset += 1; continue
|
|
||||||
offset += 1
|
|
||||||
try:
|
|
||||||
length, new_offset = read_varint(data, offset)
|
|
||||||
if 0 < length < 1_000_000 and new_offset + length <= len(data):
|
|
||||||
packets.append(data[new_offset:new_offset + length])
|
|
||||||
offset = new_offset + length
|
|
||||||
else:
|
|
||||||
offset = new_offset
|
|
||||||
except:
|
|
||||||
offset += 1
|
|
||||||
|
|
||||||
# 2) 在 ftrace_events 中搜索自定义 marker
|
|
||||||
events = []
|
|
||||||
for pkt in packets:
|
|
||||||
# 找 field 2 (ftrace_events bundle)
|
|
||||||
po = 0
|
|
||||||
while po < len(pkt):
|
|
||||||
if po >= len(pkt): break
|
|
||||||
tag = pkt[po]; po += 1
|
|
||||||
fn = tag >> 3; wt = tag & 0x07
|
|
||||||
if wt == 0:
|
|
||||||
_, po = read_varint(pkt, po)
|
|
||||||
elif wt == 2:
|
|
||||||
length, po = read_varint(pkt, po)
|
|
||||||
chunk = pkt[po:po + length]
|
|
||||||
if fn == 2:
|
|
||||||
# 扫描 bundle 内的 FtraceEvent (field 1, 0x0a)
|
|
||||||
eo = 0
|
|
||||||
while eo < len(chunk):
|
|
||||||
if chunk[eo] != 0x0a:
|
|
||||||
eo += 1; continue
|
|
||||||
eo += 1
|
|
||||||
try:
|
|
||||||
el, eno = read_varint(chunk, eo)
|
|
||||||
if el > 0 and eno + el <= len(chunk):
|
|
||||||
evt = chunk[eno:eno + el]
|
|
||||||
# 提取 timestamp (field 1, varint)
|
|
||||||
if len(evt) > 1 and evt[0] == 0x08:
|
|
||||||
ts, _ = read_varint(evt, 1)
|
|
||||||
# 搜索 marker 字符串
|
|
||||||
for pat in [b'BC:', b'VM:', b'MonthView:']:
|
|
||||||
idx = evt.find(pat)
|
|
||||||
if idx >= 0:
|
|
||||||
me = idx
|
|
||||||
while me < len(evt) and 32 <= evt[me] < 127:
|
|
||||||
me += 1
|
|
||||||
name = evt[idx:me].decode()
|
|
||||||
events.append((ts, name))
|
|
||||||
break
|
|
||||||
eo = eno + el
|
|
||||||
else:
|
|
||||||
eo = eno
|
|
||||||
except:
|
|
||||||
eo += 1
|
|
||||||
po += length
|
|
||||||
elif wt in (1, 5):
|
|
||||||
po += 8 if wt == 1 else 4
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
events.sort()
|
|
||||||
return events
|
|
||||||
|
|
||||||
# 使用
|
|
||||||
events = parse_trace('cpu-perfetto-xxxx.trace')
|
|
||||||
for ts, name in events:
|
|
||||||
print(f"{ts}: {name}")
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **关注点**:
|
|
||||||
- **触摸事件间隔**:统计相邻 `BC:delta` marker 的时间差。理想间隔 ≤16ms;若出现 >33ms 说明丢帧,>100ms 说明触摸断流。
|
|
||||||
- **重组耗时**:`VM:collapseProgress` → `MonthView:Compose` 的间隔,应在亚毫秒级。
|
|
||||||
- **ViewModel → Compose 延迟**:从 `snapTo` 调用到下一帧重组完成的间隔。
|
|
||||||
|
|
||||||
### 已知排查结论(2026-05-19)
|
|
||||||
|
|
||||||
对折叠器 trace 的分析显示:
|
|
||||||
- **重组本身很快**(VM progress → Compose 约 500μs),不是卡顿来源。
|
|
||||||
- **触摸事件采样间隔不均匀**是主要问题。某些拖拽序列中出现 30-50ms 的触摸事件间隔,偶尔有 >100ms 的断流。这属于系统/模拟器层的事件分发问题,而非 Compose 代码问题。
|
|
||||||
- 若在真机上复现,建议检查是否有 CPU 抢占或手指短暂离屏。
|
|
||||||
|
|||||||
51
README.md
@ -1,20 +1,51 @@
|
|||||||
# YaYa
|
# YaYa
|
||||||
|
|
||||||
基于 Kotlin Multiplatform 与 Compose Multiplatform 的跨平台日历应用,Android 与 iOS 共享同一套 UI 与业务逻辑。
|
纯 Android + Jetpack Compose 日历应用,支持农历/节气/节日、个人班次排期,提供月/周/年三种视图。
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img src="core/src/main/assets/app_icon.webp" width="128" height="128" />
|
||||||
|
</div>
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- **流畅的视图切换** —— 月视图、周视图、年视图三种模式,拖拽手势驱动月↔周折叠,弹簧动画自动吸附
|
- **流畅的视图切换** —— 月视图、周视图、年视图三种模式,拖拽手势驱动月↔周折叠,弹簧动画自动吸附
|
||||||
- **无限滑动分页** —— 基于 `Int.MAX_VALUE` 的虚拟分页,前后无边界翻页
|
- **无限滑动分页** —— 基于 `Int.MAX_VALUE` 的虚拟分页,前后无边界翻页
|
||||||
- **完整中式日历** —— 公历 + 农历 + 二十四节气 + 传统节日,ISO 8601 周起始(周一)
|
- **完整中式日历** —— 公历 + 农历 + 二十四节气 + 传统节日,ISO 8601 周起始(周一)
|
||||||
- **个人排班周期** —— 自定义工作/休息循环,与公共节假日独立
|
- **个人排班周期** —— 自定义工作/休息循环,与公共节假日独立
|
||||||
- **Material 3 设计** —— 动态配色,深色模式
|
- **Material 3 设计** —— 动态配色,深色模式
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- Kotlin 2.3 · Compose Multiplatform 1.11 · Material 3
|
- Kotlin 2.3 · Jetpack Compose · Material 3
|
||||||
- `kotlinx-datetime` 处理所有日期逻辑
|
- `kotlinx-datetime` 处理所有日期逻辑
|
||||||
- `tyme4kt` 提供农历、节气与传统节日
|
- `tyme4kt` 提供农历、节气与传统节日
|
||||||
- `sketch` 渲染 GIF 动画
|
- `sketch` 渲染动画 WebP
|
||||||
- 双模块:`:shared`(UI + 逻辑) · `:androidApp`(薄壳)
|
- 三模块:`:core`(UI + 逻辑) · `:app`(薄壳) · `:macrobenchmark`(Baseline Profile / Startup Profile 生成)
|
||||||
- iOS 入口为 `MainViewController.kt`,Xcode 工程位于 `iosApp/`
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debug
|
||||||
|
./gradlew :app:assembleDebug # 构建 debug APK
|
||||||
|
./gradlew :app:installDebug # 安装 debug APK 到设备
|
||||||
|
|
||||||
|
# Release
|
||||||
|
./gradlew :app:assembleRelease # 构建 release APK
|
||||||
|
./gradlew :app:installBenchmark # 安装 benchmark(release + 可调试)APK
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
./gradlew :core:testDebugUnitTest # 运行全部测试
|
||||||
|
./gradlew :core:testDebugUnitTest --tests "plus.rua.project.ui.CalendarUtilsTest" # 运行单个测试
|
||||||
|
|
||||||
|
# Baseline Profile / Startup Profile(需要连接设备)
|
||||||
|
./gradlew :macrobenchmark:updateBaselineProfile # 生成并复制两份 Profile 到 :core
|
||||||
|
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest # 仅运行基准测试
|
||||||
|
|
||||||
|
# 性能 Profiling(需要连接设备)
|
||||||
|
./scripts/profile.sh # 默认 8 秒
|
||||||
|
./scripts/profile.sh 15 # 自定义时长
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物位于 `app/build/outputs/apk/<variant>/` 目录。
|
||||||
|
|
||||||
|
线条小狗表情包来自 https://www.douban.com/group/topic/264788645/?_i=9181692phrDzjR,9241256phrDzjR
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.YaYa">
|
|
||||||
<activity
|
|
||||||
android:exported="true"
|
|
||||||
android:name=".MainActivity">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
Before Width: | Height: | Size: 457 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 66 KiB |