Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

673 changed files with 3672 additions and 63609 deletions

View File

@ -1,74 +0,0 @@
---
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.

View File

@ -1,5 +0,0 @@
root = true
[*.{kt,kts}]
# 忽略标注 @Composable 的函数命名检查Compose 规范要求 PascalCase
ktlint_function_naming_ignore_when_annotated_with = Composable

4
.gitignore vendored
View File

@ -22,7 +22,3 @@ node_modules/
.omc/
logs/
.claude/
docs/superpowers/*
!docs/superpowers/specs/
.worktrees/
.zcode/

View File

@ -1,137 +0,0 @@
## 目标
修复班次设置页的 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
View File

@ -1,135 +0,0 @@
# 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`.

View File

@ -5,200 +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/),
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
### Added
@ -333,11 +139,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Aliyun Maven mirrors (switched back to Maven Central / Google)
- Unused Compose runtime ProGuard keep rules
## [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

View File

@ -1 +0,0 @@
AGENTS.md

94
CLAUDE.md Normal file
View File

@ -0,0 +1,94 @@
# 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 Normal file
View File

@ -0,0 +1,230 @@
# 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 无替代 APIkotlinx-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` 是否有原因注释?
- [ ] 动画/进度参数是否注明了取值范围?
- [ ] 是否存在废话注释、过时注释或注释掉的代码?

View File

@ -1,99 +1,252 @@
# 开发指南
## 性能追踪
## 环境要求
使用 `scripts/profile.sh` 一键抓取 Perfetto trace、帧统计和内存快照。
- JDK 17+
- Android Studio (Ladybug 或更新版本)
- Xcode 16+ (仅 iOS 构建需要)
- Kotlin Multiplatform 插件 (Android Studio 内置)
```bash
# 默认抓取 8 秒
./scripts/profile.sh
## 项目结构
# 抓取 15 秒
./scripts/profile.sh 15
# 应用已在运行时,不自动启动
./scripts/profile.sh --no-launch
```
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 # 版本目录 — 统一管理依赖版本
```
输出文件保存在 `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
### Android
```bash
# 编译 Android debug APK
./gradlew :app:assembleDebug
# 命令行构建
./gradlew :androidApp:assembleDebug
# 安装到设备
./gradlew :app:installDebug
# 编译 release APK含 Baseline / Startup Profiles
./gradlew :app:assembleRelease
# 安装 benchmark 构建类型 APK
./gradlew :app:installBenchmark
./gradlew :androidApp:installDebug
```
一键生成并复制到 `:core`
或在 Android Studio 中选择 `androidApp` 配置直接运行。
### iOS
1. 先执行一次 Gradle 同步:`./gradlew :shared:generateDummyFramework`
2. 在 Xcode 中打开 `iosApp/iosApp.xcworkspace`
3. 选择目标设备或模拟器,点击 Run
> 首次打开可能需要等待 Xcode 索引完成。如果报 framework 错误,重新执行 Gradle 同步即可。
## 测试
```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` 包下
```bash
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
### Compose 规范
- `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 拖拽手柄,驱动折叠/展开手势
```
手动复制路径:
`macrobenchmark/build/outputs/connected_android_test_additional_output/`
**折叠动画:** `CalendarViewModel.collapseProgress` 控制 0f(月)↔1f(周) 过渡。`BottomCard` 捕获垂直拖拽,释放时超过 50% 则弹簧动画吸附到最近状态。完全折叠后 `WeekPager` 替代 `CalendarPager` 实现高效单周分页。
注意:当前 `benchmark` 构建类型继承自 `release``isMinifyEnabled = true`),因此生成的文本 profile 会包含 R8 混淆后的类/方法名。AGP 在打包 APK/AAB 时会根据最终混淆映射将其转换为二进制 profile这是正常行为。
**分页映射:** 两个 Pager 均使用 `Int.MAX_VALUE` 页数,中心页为 `Int.MAX_VALUE / 2`。页码到日期为算术转换,无索引列表。两者均跳过初始 `snapshotFlow` 发射 (`.drop(1)`) 以保留首次渲染时的"今日"选中
## 模拟器
## 性能排查Perfetto / Systrace
```sh
emulator -avd Pixel_10 \
-no-snapshot \
-no-boot-anim \
-gpu host \
-accel on \
-cores 4 \
-memory 4096 \
-partition-size 2048
```
项目使用 `composeTraceBeginSection` / `composeTraceEndSection` 在关键代码段插入 trace markerAndroid 上会被记录到系统 trace 中。iOS 为空操作。
启动带 adb logcat 的模拟器
已有的 trace section
- `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 抢占或手指短暂离屏。

View File

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

View File

@ -3,10 +3,11 @@ import java.time.format.DateTimeFormatter
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
val baseVersion = findProperty("app.version.base") as? String ?: "1.1.0"
val baseVersion = findProperty("app.version.base") as? String ?: "1.0.0"
val gitHash = try {
providers.exec {
@ -27,9 +28,12 @@ android {
applicationId = "plus.rua.project"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 6
versionCode = 1
versionName = appVersionName
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
}
}
buildTypes {
@ -42,29 +46,9 @@ android {
)
signingConfig = signingConfigs.getByName("debug")
}
// trace 构建类型release 优化 + trace 标记保留,用于性能分析
create("trace") {
initWith(buildTypes.getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
}
// benchmark 构建类型供 macrobenchmark 模块使用
create("benchmark") {
initWith(buildTypes.getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
// isDebuggable=false 使 macrobenchmark 在模拟器上稳定运行,且 Partial 编译模式可用。
// 关闭混淆,保证生成的 baseline-prof.txt / startup-prof.txt 使用原始类名,
// 避免 R8 混淆签名导致 profile 匹配与维护风险。
isDebuggable = false
isMinifyEnabled = false
isShrinkResources = false
}
}
buildFeatures {
compose = true
buildConfig = false
}
@ -76,29 +60,29 @@ android {
packaging {
resources {
excludes += listOf(
"/META-INF/AL2.0",
"/META-INF/LGPL2.1",
"/META-INF/{AL2.0,LGPL2.1}",
"/META-INF/LICENSE*",
"/META-INF/NOTICE*",
"META-INF/DEPENDENCIES",
"**/*.kotlin_metadata",
"**/*.kotlin_module",
)
pickFirsts += listOf(
"META-INF/INDEX.LIST",
"META-INF/io.netty.versions.properties",
)
}
}
bundle {
language { enableSplit = true }
density { enableSplit = true }
abi { enableSplit = true }
}
}
dependencies {
implementation(project(":core"))
implementation(platform(libs.compose.bom))
implementation(project(":shared"))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.profileinstaller)
debugImplementation(libs.compose.uiToolingPreview)
debugImplementation(libs.compose.uiTooling)
implementation(libs.compose.uiToolingPreview)
}

View File

@ -0,0 +1,23 @@
<?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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Some files were not shown because too many files have changed in this diff Show More