Compare commits

...

275 Commits
v1.0.0 ... main

Author SHA1 Message Date
xfy
313ebe1eaa release: v1.5.0 2026-07-24 13:51:55 +08:00
xfy
214a4364bf fix(ui): place composeTraceEndSection after CalendarMonthPage composition completes 2026-07-24 13:35:28 +08:00
xfy
ac186ed49a perf(ui): defer translationY and alpha to graphicsLayer draw phase during animations 2026-07-24 13:34:42 +08:00
xfy
fc2f2065df perf(core): offload LunarCache precomputing and batch calculation to Dispatchers.Default 2026-07-24 13:34:02 +08:00
xfy
1b2fcfe248 fix(core): recycle intermediate bitmap in PhotoProcessor.loadSampled to prevent memory leak 2026-07-24 13:33:35 +08:00
xfy
7bf16dfe5a feat(ui): 重新设计开放源代码许可页面,集成 M3 Expressive 设计与弹性动画 2026-07-24 13:30:55 +08:00
xfy
4f6dc2f36a refactor(ui): 补全开放源代码许可依赖项并优化页面组件 2026-07-24 13:28:48 +08:00
xfy
adc149c462 fix(ui): eliminate year header layout jitter when showing this year button 2026-07-24 13:20:56 +08:00
xfy
0703be68b4 fix(ui): shift year view grid upwards to make room for bottom FAB 2026-07-24 13:20:08 +08:00
xfy
13360029e1 fix(ui): animate FAB upward in year view to avoid obscuring october mini month 2026-07-24 13:18:25 +08:00
xfy
c80ad488c6 fix(ui): remove duplicate lunar year text in YearHeader 2026-07-24 11:47:20 +08:00
xfy
fd7f79c3d3 refactor(ui): optimize year view performance, animations, and layout 2026-07-24 11:45:35 +08:00
xfy
d7406ef652 fix(shift): 调班设置迷你月历重排起点角标显示班/休状态(起·班/起·休) 2026-07-24 11:36:55 +08:00
xfy
dfe7b5a6c3 refactor(ui): 优化首页左下角 FAB 浮动菜单视觉样式、动画与返回键拦截 2026-07-24 11:34:15 +08:00
xfy
94a4ba1e2a fix(date-recorder): enforce layer-level graphicsLayer RenderNode clip to retain rounded corners during shared element transition 2026-07-24 11:29:17 +08:00
xfy
d40c6d59b3 fix(date-recorder): preserve rounded corners during shared element view mode transition 2026-07-24 11:27:39 +08:00
xfy
1248dff648 refactor(date-recorder): use SharedTransitionLayout for smooth view mode card morphing 2026-07-24 11:24:10 +08:00
xfy
2a3da75859 feat(date-recorder): add view mode switching card scale and layout transition animation 2026-07-24 11:21:30 +08:00
xfy
bff6e37908 fix(recorder): preserve record ID when editing photo of an existing record 2026-07-24 11:16:44 +08:00
xfy
c7aa611e52 feat(photo-editor): 照片主体增加圆角与精致悬浮阴影,且旋转时100%零黑边
- 直接在 Image 的 graphicsLayer 上配置 RoundedCornerShape(16.dp)、clip = true 和 shadowElevation = 6.dp,使圆角和阴影紧贴照片主体。
- 旋转时圆角与阴影随照片主体平滑一体旋转,外部无 Card 黑色底盒与死板边框,实现圆角 + 悬浮阴影 + 零黑边的体验。
2026-07-24 11:13:47 +08:00
xfy
f9e54f36fe refactor(photo-editor): 重构照片编辑旋转体验,干掉外框卡片实现全视口自适应无缝旋转
- 干掉 Card 容器:彻底移除了带 1.dp 灰色 Border、20.dp 圆角黑底及 4.dp 阴影的外层 Card 盒,解除照片被固定容器包裹旋转的视觉限制。
- 视口自适应包围盒算法:在 RotationGeometry 中新增 calculateLayoutSize 纯函数,按任意旋转角度动态计算照片 AABB 包围盒适配合理布局大小,保证 ContentScale.Fit 内部 0 留白 Padding 且 0 黑边。
- 重构 EditableImage:照片直接在全屏视口中心做平滑自适应旋转,手写 Canvas 与裁剪框精准挂载在稳态照片视觉 Bounds 上。
- 补充 RotationGeometryTest 针对 calculateLayoutSize 视口包围盒自适应的单元测试。
2026-07-24 11:12:19 +08:00
xfy
c39cecf343 fix(photo-editor): 修复旋转照片过程中的错位黑边
- 根因:isSwapped 原先直接基于目标旋转角度 state.rotationDegrees 判定,导致在旋转动画起始阶段(如 0° 向 90° 动画时)提前将布局 measure 尺寸转置,Fit 适配产生了内缩黑边 Padding。
- 修复:基于当前动画渲染角度 displayRotation.value 动态计算最近的 90° 基准角 (baseRotation),使布局尺寸转置与旋转动画平滑同步,彻底消除错位黑边。
- 测试:在 RotationGeometryTest 中补充 baseRotation、offsetDegrees、isSwapped 动态角度过渡断言。
2026-07-24 11:09:33 +08:00
xfy
b13e018503 release: v1.4.0 2026-07-23 16:36:13 +08:00
xfy
e16b7e6587 perf: 优化日历翻页重组、农历批量查询与 Canvas 绘制内存分配 2026-07-23 16:30:27 +08:00
xfy
69bffd8d20 perf(core): 更新 Baseline Profile 与 Startup Profile AOT 编译规则文件 2026-07-23 16:27:05 +08:00
xfy
cc831f85cc feat(macrobenchmark): 更新 Baseline Profile 脚本覆盖年月视图穿梭与日期记录器 2026-07-23 16:04:02 +08:00
xfy
a0df5168f6 feat: 将日期记录器默认视图模式修改为网格 2026-07-23 15:57:41 +08:00
xfy
eadb8eb682 fix: add ProGuard dontwarn rule for sketch and fix deprecation warnings 2026-07-23 15:54:15 +08:00
xfy
19c9feb6de fix(ui): 修复滑动切换年份后再进年视图导致的页码联动错位 bug 2026-07-23 15:45:00 +08:00
xfy
e663ed5c8e fix(ui): 修复切换年月视图动画期间滑动导致的小月历错位 bug 2026-07-23 15:16:38 +08:00
xfy
53086f38e0 feat(ui): 基于 SharedTransitionLayout 重构年月视图穿梭缩放动画 2026-07-23 15:14:20 +08:00
xfy
de1e3178f6 fix(ui): 重构排序对话框布局,解决水平溢出挤压错位问题 2026-07-23 15:09:52 +08:00
xfy
80e4e5e86c fix(ui): 修复视图切换器平滑弹簧滑动动画与几何胶囊同心对齐 2026-07-23 15:08:46 +08:00
xfy
37cf5ffc4c feat(ui): 重构日期记录器界面,升级沉浸式时光画廊与三态视图模式 2026-07-23 15:06:42 +08:00
xfy
afd94604ad fix(photo-editor): 修复外层 Card 未按图片比例自适应导致的上下黑边 2026-07-23 15:03:58 +08:00
xfy
b7607eebd9 fix(photo-editor): 修复照片旋转二次叠加与黑边问题 2026-07-23 15:02:11 +08:00
xfy
0a6b9449a3 fix(ui): 修复照片编辑页错位、亮色主题统一与添加切换过渡动画
- 切换动画:ToolPanel 引入 AnimatedContent,在 旋转 / 裁剪 / 涂鸦 模式切换时平滑淡入滑动过渡
- 修复错位:将“撤销”按钮从限制宽高的 OutlinedIconButton 改为 OutlinedButton(contentPadding),解决“撤销”汉字被压缩竖向换行错位的问题
- 亮色主题统一:将界面配色调整为与鸭鸭日历应用一致的亮色 Material 3 Theme (MaterialTheme.colorScheme.surface / surfaceContainer)
2026-07-23 14:59:05 +08:00
xfy
088860861e feat(ui): 重新设计照片编辑界面 (PhotoEditorScreen)
- 采用暗黑模式专业工作室色调 (0xFF121316),提升照片编辑对比度与专注感
- 优化顶部 TopAppBar:重命名为“照片图层编辑”,包含返回与顶部“完成”快捷保存入口
- 优化模式切换栏:SegmentedButton 采用深色背景与选中高亮效果 (旋转 / 裁剪 / 涂鸦)
- 引入 Studio 视口卡片:圆角黑底边框视口 (RoundedCornerShape(20.dp)),带有处理中全屏 Loading 覆盖层
- 升级底部 ToolPanel 操作面板:
  - 旋转模式:双向 90° 旋转按钮 + 实时角度 Chip (如 90°)
  - 裁剪模式:确认裁剪区域与开启裁剪动作引导
  - 涂鸦模式:扩展 7 色绚丽画笔调色盘 (带选中勾选徽章) 与撤销笔触功能
- 完全兼容 editor_save 自动化测试 tag
2026-07-23 14:55:50 +08:00
xfy
57da898ed7 feat(ui): 重新设计记录编辑页面 (RecordEditScreen)
- 重构 RecordEditScreen 布局:新增 Hero 照片卡片,分组卡片化基本信息(标题与笔记)与日期设定(拍摄日期与关联日历)
- 引入图形化 OutlinedTextField(带图标、提示与标题快速清除)与交互式 DatePickerTile(带星期显示与清除关联)
- 优化顶部 TopAppBar 操作:添加顶部右上角“保存”快捷入口,配合底部固定“保存记录”主按钮
- 保持现有所有 testTag 兼容性
2026-07-23 14:54:01 +08:00
xfy
9ad9e39164 feat(ui): 重新设计日期记录器详情页并优化 Flow 实时监听
- 重构 RecordDetailScreen:新增 Hero 大图卡片(带全屏放大与涂鸦编辑快捷入口)、优雅标题 header、双列元信息卡片(带星期与日历关联状态)及格式化笔记卡片
- 优化底部操作栏:提供直观的编辑信息、涂鸦编辑与删除按扭
- ViewModel 变更:将 DateRecordDetailViewModel 从 single-snapshot first() 改为持续 collect,支持编辑返回后实时刷新数据
- 新增 DateRecordDetailViewModelTest 单元测试
2026-07-23 14:51:29 +08:00
xfy
c3510cde39 fix(recorder): 修复日期记录器多选模式下拖动多选中断问题 2026-07-23 14:42:49 +08:00
xfy
c6377d85f9 perf(ui): 优化多选与长按拖拽灵敏度,实现极速响应跨行连续多选 2026-07-23 14:39:27 +08:00
xfy
1700615508 fix(ui): 改用 PointerEventPass.Initial 监听底层手势,彻底解决模拟器与鼠标长按滑动多选失效问题 2026-07-23 14:35:41 +08:00
xfy
51a7a43b47 fix(ui): 移除子卡片事件拦截,恢复模拟器与触摸屏按住滑动多选手势 2026-07-23 14:33:56 +08:00
xfy
4ada46a549 feat(ui): 支持按住跨行滑动连续多选记录 2026-07-23 14:31:33 +08:00
xfy
b333c527da feat(ui): 为日期记录器网格添加元素删除与平移过渡动画 2026-07-23 14:28:01 +08:00
xfy
a2a631c141 feat(ui): 支持长按单个记录直接进入多选模式并选中 2026-07-23 14:26:19 +08:00
xfy
cfac607862 feat(ui): 为日期记录器多选模式添加过渡动画 2026-07-23 14:21:39 +08:00
xfy
b6146a29ae feat(photo-editor): 旋转图片时加平滑动画,裁剪填满零黑边
- 旋转角用 Animatable + tween(300ms) 平滑过渡,容器宽高比用
  animateFloatAsState 在目标稳态比例间过渡,消除硬切
- 黑边根因:旋转矩形填不满自身 AABB 的四角(45° 时黑色占 51%)。
  用户选「裁剪填满」方案:图片显示 rotatedBitmap(目标态),
  graphicsLayer 旋转 + coverScale 放大使四角覆盖容器 → 零黑边
- 提取旋转几何为纯函数 RotationGeometry(无 Android 依赖):
  stableAspect + coverScale,便于单测
- 回归测试 RotationGeometryTest 锁住「任意角度旋转图片四角
  覆盖容器四角」断言(maxViolation <= 1e-4),防止黑边回归
- 稳态(offset=0) scale=1 零裁剪;动画中(±45°) scale≈1.65 裁边缘

保存逻辑未变:PhotoProcessor.render 仍用 rotationDegrees 烘焙。
2026-07-22 18:31:48 +08:00
xfy
ca00839c9b docs(agents): 新增功能完成后跑 installDebug 的验证步骤
要求每写完一个功能跑一次 ./gradlew :app:installDebug 做冒烟安装,
并明确允许失败(无设备/模拟器等情况),非硬性 gate。
2026-07-22 18:05:41 +08:00
xfy
023ae44364 docs(agents): 新增按功能点提交的工作流规则
要求每完成一个功能点提交一次(不攒批),提交粒度与信息由 AI 自主
决定,但只暂存相关文件、勿 git add .。
2026-07-22 17:39:59 +08:00
xfy
7d8e0fca61 docs(agents): 重写 AGENTS.md 仓库指南
基于对 core 源码、测试、构建配置、脚本文档的并行扫描综合而成。
修正过时的依赖版本(Compose BOM 2026.06.01、tyme4kt 1.5.0),补充
Activity+Intent 导航模型、namespace 怪癖、StateFlow 状态模式、测试
覆盖范围与缺口、benchmark 构建类型、发布流程等。
2026-07-22 17:34:59 +08:00
xfy
ffafee550e Merge branch 'feat/record-detail-image-lightbox' 2026-07-22 15:23:53 +08:00
xfy
70c97ed9d5 revert(photo-editor): 恢复点击裁剪 icon 进入裁剪的交互
撤销"切到裁剪 tab 自动进入裁剪"的改动,保留原来的两步操作:
点击下方裁剪 icon 进入裁剪模式,再次点击 ✓ 确认裁剪。

ViewModel 恢复 toggleCrop,删除无用的 enterCrop/exitCrop;
保留裁剪框拖动的 bug 修复(本地累积值 + rememberUpdatedState)。
2026-07-22 15:20:13 +08:00
xfy
d3ebd6829b fix(photo-editor): 修复裁剪框拖动的闭包快照陷阱
pointerInput(Unit) 的手势 lambda 只在首次组合捕获一次 state,
后续 onDragStart 读的是过期快照,导致再次拖动从中心开始。

改用本地累积值绕开 StateFlow 往返:onDragStart 把 state 快照到
mutableFloatStateOf,onDrag 每帧在累积值上叠加增量,onDragEnd
一次性提交。onDragStart 通过 rememberUpdatedState 读最新 state。

enterCrop 改为幂等(已启用则保留当前框),避免 LaunchedEffect
因 editorState 变化重触时冲掉用户拖动结果。
2026-07-22 15:18:19 +08:00
xfy
b8509381d9 fix(photo-editor): 修复裁剪 tab 流程与裁剪框拖动
两个问题:
1. 切到裁剪 tab 后还要再点裁剪 icon 才进入裁剪——改为切 tab 即自动进入,
   底部按钮由"开关裁剪"改为"✓ 确认裁剪",点击烘焙裁剪并保留在 tab。
2. 裁剪框无法移动——重写 CropOverlay 拖动逻辑:
   - 加 BODY 整体平移(拖框内区域平移整框)
   - onDrag 每帧从 state 读实时裁剪值叠加增量,修复闭包快照导致的抖动
   - dragHandle 内聚到组件内部,消除跨重组延迟
   - 抽出 hitHandle/moveCrop,四角与整体均带边界约束防交叉越界

ViewModel 新增 enterCrop/exitCrop/applyCrop,删除 toggleCrop。
2026-07-22 14:34:42 +08:00
xfy
cec8debff0 feat(date-recorder): 详情页图片灯箱,支持缩放/旋转手势
点击详情页大图弹出全屏深色遮罩,集成 panpf/zoomimage 的
SketchZoomAsyncImage,支持双指缩放、平移、捻合旋转(松手吸附
0/90/180/270°)与双击复位,点空白或返回键关闭。

新增 zoomimage-compose-sketch4 1.6.0 依赖(与项目 sketch 同作者,
SketchZoomAsyncImage 是 AsyncImage 超集,额外提供缩放/旋转/大图分片)。
2026-07-22 14:26:27 +08:00
xfy
d47a38bc19 feat(date-recorder): 美化记录器界面,卡片图上叠加文字、排序菜单精简
- 网格卡片:照片铺满整卡,底部渐变蒙层叠加标题与拍摄日期
- 网格改为自适应约 3 列,间距 8dp→4dp,卡片圆角 12dp→8dp
- 排序菜单 6 项精简为 3 个字段项,点击已选字段切换升降序
- 新增 RecordSortOrder.nextAfter 纯函数及单元测试
2026-07-20 18:28:39 +08:00
xfy
b589800eda refactor: 记录编辑页复用共享 formatLocalDate 2026-07-20 17:25:08 +08:00
xfy
0d83fef3a9 feat: 新建记录标题预填拍摄日期并支持联动 2026-07-20 17:23:33 +08:00
xfy
94e81bffb1 docs: 新建记录标题预填默认值设计规格 2026-07-20 15:54:59 +08:00
xfy
b1064f95b7 feat(date-recorder): use camera and crop icons for record/crop buttons
- DateRecorder FAB: Icons.Filled.Add -> Icons.Filled.PhotoCamera
  (与 onOpenCamera / '拍照记录' 语义一致)
- PhotoEditor crop toggle: 纯文字 -> Icons.Filled.Crop / CropFree 图标
  (Crop 关闭态, CropFree 开启态, contentDescription 保留无障碍语义)
2026-07-17 16:31:39 +08:00
xfy
81aeb8e894 fix(date-recorder): split handwriting strokes on finger lift
endStroke() was a no-op, so addStrokePoint() kept appending every new
pointer-down to the previous stroke (its only segmentation cue was
points.isEmpty()). A second drag after lift got joined into the first,
and toPath() drew them as a single polyline.

Add isFinished flag to HandStroke and implement real endStroke() that
marks the current stroke finished. withAddedPoint() now starts a new
segment when the previous stroke is null, empty, or finished.
2026-07-17 16:01:44 +08:00
xfy
50da18460f fix(date-recorder): correct photo EXIF orientation and fix capture crash
- PhotoProcessor: read EXIF orientation and rotate bitmap on load so
  camera JPEGs display upright
- CameraScreen: set target rotation so CameraX writes correct EXIF
- CameraScreen: fix RejectedExecutionException by not shutting down the
  capture executor on dispose (CameraX may still post to it after)
- CameraScreen: route capture callback through main handler for thread
  safety before triggering Activity navigation
- CameraScreen: defer camera binding until PreviewView is attached to
  avoid repeated Surface abandon/recreate and camera rebind loops
- CameraScreen: use an overlay (not component swap) for the capturing
  state so the bound ImageCapture is not torn down mid-capture
- deps: add androidx.exifinterface for EXIF reading
2026-07-17 15:36:36 +08:00
xfy
b2942a3ca9 chre: lint CLAUDE.md to AGENTS.md 2026-07-17 13:25:13 +08:00
xfy
2e61fea21b chore: add .zcode to git ginore 2026-07-17 13:24:46 +08:00
xfy
a1c71c2b93 chore: remove useless docs 2026-07-17 13:24:18 +08:00
xfy
8963b6162b feat(date-recorder): photo journal tool with camera, editor, and album grid
新增"日期记录器"工具:相册式记录管理,支持拍照→编辑→记录信息的完整流程。

数据层:
- Room 2.8.4 首个数据库(KSP 2.3.10),DateRecord 实体含标题/备注/拍摄日期/关联日期
- DateRecordConverters 处理 kotlinx-datetime ↔ ISO 字符串
- DateRecorderRepository 封装 DAO + filesDir 照片文件管理

UI 层:
- CameraScreen: CameraX 1.5.3 应用内预览,运行时权限请求,前后摄切换
- PhotoEditorScreen: 纯 Compose 自实现旋转/裁剪/手写三 Tab 编辑器
- DateRecorderScreen: LazyVerticalGrid 相册网格 + 多选 + 6 种排序 + 批量删除
- RecordEditScreen / RecordDetailScreen: 记录信息编辑与详情查看

基础设施:
- CAMERA 权限 + FileProvider(filesDir 存储,免存储权限)
- 5 个 Activity 延续项目 Activity+Intent 滑动转场模式
- 2 个单测:排序逻辑 6 种组合 + 降采样计算
2026-07-17 13:21:45 +08:00
xfy
c665adfeff release: v1.3.0 2026-07-09 17:21:57 +08:00
xfy
afea8bf109 feat: refactor shift pattern with atomic rephase and swipeable calendar
班次设置页重构与体验增强:

数据模型重构:
- phaseBreaks + override 两独立字段 → 原子 RephaseFlip(date, flippedTo, rephaseFrom)
- 解决幽灵重排/撤销误删/断点链不级联三大问题

体验改进:
- 角标颜色按类别配对(onPrimary/onError/onTertiary)
- 迷你月历动态行数(复用 getMonthGridInfo)
- 恢复默认改 Snackbar 撤销(首次引入 SnackbarHost)
- 锚点日期与周期行右侧对齐修复
- 点击年月标题弹 DatePicker 选月份跳转
- HorizontalPager 滑动翻月 + 高度插值平滑过渡
2026-07-09 17:16:45 +08:00
xfy
2a3089bf47 feat(shift): smooth height interpolation for mini calendar swipe
滑动翻月时高度平滑过渡,不再跳变:
- 复用主日历的 interpolatedWeeks 行数插值逻辑
  (currentPageOffsetFraction 在当前页与目标页行数间 lerp)
- 行高由 BoxWithConstraints 宽度推算(格宽=宽/7,aspectRatio(1f) 使行高=格宽)
- HorizontalPager 套 Modifier.height(px).clipToBounds(),跟随手指实时变高
- 迷你月历简化版:无折叠动画,只保留展开态 gridHeightPx = rowH * weeks
2026-07-09 17:14:54 +08:00
xfy
7b1290dd66 feat(shift): swipeable mini calendar via HorizontalPager
迷你月历接入 HorizontalPager,支持左右滑动翻月:
- 复用主日历 START_PAGE/pageToYearMonth/yearMonthToPage 工具
- viewYear/viewMonth 由 pagerState.currentPage 派生
- 箭头按钮改为 animateScrollToPage
- DatePicker 选月后 animateScrollToPage 跳转
- 网格提取为 MonthGrid 单页组件
2026-07-09 17:07:38 +08:00
xfy
1cf950817e feat(shift): tap year-month title to pick date in mini calendar
点击迷你月历顶部"年 月"标题弹出 DatePicker,
选定日期后跳转到对应月份,比连点箭头快。
2026-07-09 16:55:22 +08:00
xfy
62ed8bd09a fix(shift): align anchor date with cycle row and trim top padding
- 顶部内边距过高:Scaffold padding 已含 TopAppBar 间距,
  Column 去掉多余 vertical=8.dp,避免叠加
- 锚点日期右侧不齐:TextButton 自带内边距导致右边缘内缩,
  改为 Text+clickable 与"周期"行纯 Text 结构一致,右侧对齐
2026-07-09 16:46:38 +08:00
xfy
c68944bd55 feat(shift): undoable reset-to-default via Snackbar
恢复默认改为 Snackbar 撤销模式:
- 点恢复默认立即生效 + 显示 5 秒"撤销"Snackbar
- 点撤销恢复之前的 pattern
- 删除 AlertDialog 确认框(更现代,防误操作)
- 首次在项目引入 SnackbarHost
2026-07-09 16:38:57 +08:00
xfy
4b098442c4 fix(shift): badge color contrast and dynamic calendar rows
- 角标文字色按背景类别配对:班=onPrimary,休=onError,起=onTertiary
- 迷你月历复用 getMonthGridInfo 动态算行数(4/5/6),不再固定 6 行
2026-07-09 16:36:36 +08:00
xfy
f8df108bf3 refactor(shift): replace phaseBreaks with atomic RephaseFlip
数据模型重构:把"翻转+重排"从两个独立字段(override+phaseBreak)
改为单一原子结构 RephaseFlip(date, flippedTo, rephaseFrom)。

- 删除 PhaseBreak / phaseBreaks / cycleOffset(UI 未使用 offset!=0)
- kindAt:overrides → rephaseFlip 当天 → 活跃锚点(从 rephaseFrom 取)
- Storage:KEY_BREAKS → KEY_REPHASE,编码 翻转日:值:重排起点
- ShiftCalendarGrid:toggleFlipAndRephase 产出原子记录,撤销按 date 精确匹配
- toggleOverride 加保护:rephaseFlip 翻转日被点时整体移除,防幽灵重排

解决问题:撤销误删独立断点、单独点翻转留幽灵、连续长按断点链不级联。
2026-07-09 16:33:29 +08:00
xfy
d8014b5154 test(shift): add safety-net tests for long-press rephase scenarios
锁定重构前行为:单次长按重排、连续两次长按、撤销。
作为 RephaseFlip 重构的回归基线。
2026-07-09 16:25:23 +08:00
xfy
66a054f2d8 feat(shift): long-press flips and rephases subsequent days
长按语义从"设相位断点"改为"翻转该天并从次日起重排":
- 翻转当天 override + 次日插入 PhaseBreak(重排起点)
- 后续按 cycle 自动顺延,匹配用户直觉
- 再次长按同一天可撤销(移除 override + 关联断点)
- 角标"断"改为"起",文案同步更新
2026-07-09 16:14:41 +08:00
xfy
14a3239b39 feat: shift pattern settings page
个人轮班设置页:独立 Activity + SharedPreferences 持久化 + onResume 立即生效。

数据模型支持:
- 基础周期(锚点 + 班次序列)
- 单日翻转 overrides(调班)
- 相位断点 phaseBreaks(从某天起重排周期相位)

UI:
- FAB 菜单"班次设置"入口
- 基础周期预设(1班1休/2班2休/3班3休/4班4休)+ 锚点 DatePicker
- 迷你月历(点=翻转班休,长按=设/清断点)
- 恢复默认

实现:TDD,6 任务逐个 spec + 代码质量审查。
2026-07-07 09:36:44 +08:00
xfy
7f080f2d90 fix(shift): add color legend and restore manifest trailing newline
- Add legend text (班/休/断点 color meanings) below mini calendar
- Restore trailing newline in AndroidManifest.xml
2026-07-06 19:10:27 +08:00
xfy
32d66f0a42 fix(shift): subscribe shiftPattern to trigger recomposition on refresh 2026-07-06 19:09:26 +08:00
xfy
a5795b16dc feat(shift): add ShiftPatternActivity, Screen, and mini calendar grid 2026-07-06 18:52:16 +08:00
xfy
d528b98cde feat(shift): wire VM factory, onResume reload, settings menu entry 2026-07-06 18:41:07 +08:00
xfy
e08243d0fa test(shift): cover refreshShiftPattern reload after storage change 2026-07-06 18:37:47 +08:00
xfy
ef55ffcf64 feat(shift): inject ShiftPatternStorage into CalendarViewModel 2026-07-06 18:34:12 +08:00
xfy
1f1db17384 fix(shift): persist name field, guard empty cycle, test corrupt-data path 2026-07-06 18:30:40 +08:00
xfy
e4834a52c9 feat(shift): add ShiftPatternStorage for persistence 2026-07-06 18:24:28 +08:00
xfy
eca6c61058 test(shift): cover non-zero cycleOffset and multiple phase breaks; add kindAt KDoc 2026-07-06 18:20:48 +08:00
xfy
778d5dc8be feat(shift): support overrides and phase breaks in ShiftPattern 2026-07-06 18:14:24 +08:00
xfy
5afac5dd04 docs: add shift pattern settings design spec
个人轮班设置页设计:独立 Activity + SharedPreferences + onResume 重读。
数据模型支持单日 override + 相位断点重排,完整覆盖调班场景。
2026-07-06 16:43:03 +08:00
xfy
08a6016479 chore(deps): bump stable dependencies
- composeBom 2026.05.01 -> 2026.06.01
- androidx-lifecycle 2.10.0 -> 2.11.0
- androidx-uiautomator 2.3.0 -> 2.4.0
- tyme4kt 1.4.5 -> 1.5.0
- spotless 8.5.1 -> 8.8.0

Verified via :app:assembleDebug and :core:testDebugUnitTest.
2026-07-06 14:26:48 +08:00
xfy
db31b9eaed chore: add project-level yayacal-release skill 2026-06-18 15:21:48 +08:00
xfy
fae7654452 release: v1.2.0 2026-06-18 15:07:09 +08:00
xfy
4be8cada86 调整应用图标小黄鸭占比 2026-06-18 13:43:50 +08:00
xfy
0fbbdfa444 feat: show rose day icon with priority over birthday crown 2026-06-16 18:06:50 +08:00
xfy
25885d84b3 feat: compute rose day flag in LunarCache 2026-06-16 17:59:00 +08:00
xfy
a8c2637a0c feat: add rose day vector drawable 2026-06-16 17:56:49 +08:00
xfy
283a474eb1 docs: add rose day icon design spec 2026-06-16 17:38:55 +08:00
xfy
e07ccd8b4e docs: update ui AGENTS.md for DogParkScreen 2026-06-16 16:38:20 +08:00
xfy
3fcdeab702 feat: navigate to DogParkActivity from AboutActivity 2026-06-16 16:31:01 +08:00
xfy
ac48a56465 feat: add DogParkActivity with fade-in entry transition 2026-06-16 16:24:10 +08:00
xfy
6532363cdb feat: add DogParkScreen for full-screen easter egg video 2026-06-16 16:15:09 +08:00
xfy
9d2f67b9fb feat: wire up version click easter egg in AboutScreen 2026-06-16 16:03:51 +08:00
xfy
6830c1e510 feat: add easter egg toast message logic with tests 2026-06-16 15:50:46 +08:00
xfy
1e43638fbc res: add fade_in and fade_out animations for dog park entry 2026-06-16 15:47:59 +08:00
xfy
3ab50ec900 assets: add dog park easter egg background video 2026-06-16 15:41:28 +08:00
xfy
1caea057c5 build: add media3 exoplayer dependency for dog park easter egg 2026-06-16 15:38:46 +08:00
xfy
5714802c89 design: add dog park easter egg spec 2026-06-16 15:29:34 +08:00
xfy
bd4dad4794 chore(profile): 重新生成 Baseline/Startup Profile
benchmark buildType 已关闭 R8 混淆,重新生成 profile 后
plus.rua.project 下类名保持原始可读签名,避免 release
打包时 profile 匹配风险。
2026-06-16 14:41:25 +08:00
xfy
a731507b3b feat: 月视图年月支持日期选择器跳转
- MonthHeader 新增 onYearMonthClick 回调,点击年月文字打开日期选择器
- CalendarMonthView 集成 DatePickerDialog,选择日期后调用 selectDate
- 复用现有 CalendarPager 动画,跨月跳转时自动平滑翻页

同时包含之前的改动:
- 临时隐藏 SplashActivity 入口,改由 MainActivity 作为 LAUNCHER
- benchmark build type 关闭 R8 混淆与资源压缩,保证 profile 可读性
2026-06-16 14:19:41 +08:00
xfy
85bfba241c fix(splash): use dedicated raster icon asset for splash screen 2026-06-16 13:32:00 +08:00
xfy
fd8e1fc1cb fix(splash): use raster launcher icon to avoid adaptive-icon crash 2026-06-16 13:25:19 +08:00
xfy
be46d4b512 chore(splash): remove unused core-splashscreen catalog entries and fix docs 2026-06-16 13:20:19 +08:00
xfy
b5a2ccf151 refactor(splash): remove unused SplashScreen API dependency and theme 2026-06-16 13:17:17 +08:00
xfy
b5e60e8bf8 docs(app): update AGENTS.md for SplashActivity 2026-06-16 13:14:35 +08:00
xfy
21327d572b feat(splash): add custom SplashActivity with background image 2026-06-16 13:11:43 +08:00
xfy
de329c2020 feat(splash): add dimmed dark-mode splash background 2026-06-16 12:36:49 +08:00
xfy
8204ae15cf feat(splash): use SplashScreen API with custom background 2026-06-16 12:27:12 +08:00
xfy
fab3c2d763 feat(splash): make SplashActivity the launcher entry 2026-06-16 12:11:53 +08:00
xfy
423e68ec19 feat(splash): add splash theme with opaque windowBackground 2026-06-16 12:09:26 +08:00
xfy
0edcc005bc feat(splash): add SplashActivity with 400ms delay 2026-06-16 12:00:02 +08:00
xfy
1f2fc8644f feat(splash): add reusable SplashScreen composable 2026-06-16 11:51:00 +08:00
xfy
d98579c2bf feat(splash): add launch background webp asset 2026-06-16 11:43:56 +08:00
xfy
f6b1ed7368 chore(baseline): 重新生成 Baseline Profile 与 Startup Profile 2026-06-16 10:51:38 +08:00
xfy
b242fb8ecc feat(benchmark): 拆分 Baseline Profile 与 Startup Profile 生成方法 2026-06-16 10:37:58 +08:00
xfy
ade852208b docs: 同步 Baseline Profile 与 Startup Profile 双产物说明 2026-06-16 10:17:21 +08:00
xfy
6f4fa68f0d build(benchmark): 同时复制 Baseline Profile 与 Startup Profile 到 :core 2026-06-16 09:50:13 +08:00
xfy
bcc59ca7a0 refactor(benchmark): remove dead code and polish comments 2026-06-15 19:18:12 +08:00
xfy
91068ded1c chore(baseline): regenerate baseline profile after startup optimizations 2026-06-15 19:17:16 +08:00
xfy
499a2eb0c7 build(benchmark): make baseline generation stable on emulator 2026-06-15 19:17:11 +08:00
xfy
66be0be3c4 test(benchmark): add StartupBenchmark for cold launch timing 2026-06-15 17:30:27 +08:00
xfy
ccffcfb052 feat(app): integrate SplashScreen and reportFullyDrawn in MainActivity 2026-06-15 17:17:20 +08:00
xfy
7ae0e80baf feat(theme): add Theme.YaYa.Starting aligned with Compose background 2026-06-15 17:08:17 +08:00
xfy
e97c2f3015 build: add androidx.core:core-splashscreen 1.0.1 2026-06-15 17:00:53 +08:00
xfy
5bf8a85f7a Merge branch 'feature-birthday-crown' 2026-06-15 16:48:24 +08:00
xfy
3ca0d334e7 refactor: remove redundant icon content description and reorder DayCellImpl params 2026-06-15 16:45:58 +08:00
xfy
94861dc2f7 style: apply spotless formatting 2026-06-15 16:41:07 +08:00
xfy
de47509a32 feat: show tilted birthday crown with click bounce animation 2026-06-15 16:36:13 +08:00
xfy
564e4e3960 refactor: 清理无效 ProGuard 规则、调试日志,trace 解耦 VM
P0 正确性与工程卫生修复:

- 删除 core/proguard-rules.pro 中全部无效的 -keepclassmembers 规则
  (LunarCache.getOrCompute 实为实例 suspend 方法,generateMonthDays 实为
   private,签名均不匹配,属 placebo);同时移除 core/build.gradle.kts
  release block 内重复的 consumerProguardFiles 声明
- 将 7 处 composeTrace 调用从 CalendarViewModel 移至 Compose 层
  (MenuItem.onClick / onMonthClick / BottomCard 回调),VM 不再依赖
  android.os.Trace,可在纯 JVM 环境测试无需兜底
- 删除 CalendarViewModel 与 4 个 UI 文件中约 30 处 logd 调用及其辅助
  变量/ SideEffect/ DisposableEffect (AnimLog.kt 工具函数保留)
- 删除 CalendarViewModel.getIsoWeekNumber 不可达的 weekNumber < 1 递归分支
- 修正 MainActivity.kt setContent 块缩进错位
- 同步 README: sketch 渲染 GIF→动画 WebP,补提 :macrobenchmark 模块;
  删除 AGENTS.md 顶部过时的「README 与实际不符」注释

验证: spotlessApply UP-TO-DATE, testDebugUnitTest 全过,
       assembleDebug + assembleRelease (R8) 均成功
2026-06-15 16:24:38 +08:00
xfy
f33e68d1db feat: compute birthday flag in LunarCache 2026-06-15 16:22:47 +08:00
xfy
ef785a3ca7 docs: correct tyme4kt API details in birthday crown spec and plan 2026-06-15 16:18:38 +08:00
xfy
06ac30e6d2 feat: compute birthday flag in LunarCache 2026-06-15 16:10:22 +08:00
xfy
7404bdfc28 feat: add birthday crown vector drawable 2026-06-15 16:03:41 +08:00
xfy
5c8839dea2 chore: ignore .worktrees directory 2026-06-15 16:01:38 +08:00
xfy
2427ae18a4 docs: add birthday crown implementation plan 2026-06-15 15:59:17 +08:00
xfy
c82ab26944 docs: add birthday crown feature design spec 2026-06-15 15:53:37 +08:00
xfy
825eb526de feat(about): 将关于页面背景图优化为 WebP 2026-06-15 15:40:34 +08:00
xfy
9a09be56ef feat(about): 将 xiaogou_bg1.jpg 设为关于页面背景 2026-06-15 15:04:58 +08:00
xfy
9d1999661c refactor(app): 缩小 Adaptive Icon 前景鸭子尺寸
- 将鸭子+日历缩放至 60dp 安全圆内,留出更多浅蓝背景边距
- 保持透明前景,背景由 @drawable/ic_launcher_background 单独提供
2026-06-15 14:51:10 +08:00
xfy
3f8eb1db05 fix(app): 修复 Adaptive Icon 前景加载失败及前景含背景问题
- mipmap-anydpi-v26 XML 直接引用 @mipmap/ic_launcher_foreground
- 删除非法的 drawable/ic_launcher_foreground.xml(<mipmap> 不是合法 drawable 根元素)
- 移除无效的 <monochrome> 引用(彩色位图不能用作主题化单色层)
- 重新生成带透明通道的前景 WebP,仅保留鸭子+日历
2026-06-15 14:46:12 +08:00
xfy
aba2086c22 Merge branch 'fix/image-management-cleanup' into main
图片资源管理优化: 清理无用资源、消除硬编码、补齐 Adaptive Icon、校正文档

7 个提交:
- 删除 app/assets 中无引用的 app_icon PNG (设备 -516KB)
- 删除 core/assets/gifs/.omc 工具残留 (gitignored, 物理清理)
- gifs/→animations/、AnimatedGif→AnimatedWebp (名实相符)
- WebP 文件列表改构建期 BuildConfig 注入 + 测试守卫 (消除 (1..152) 硬编码)
- 新增 Adaptive Icon (anydpi-v26, 修复 API 26+ 启动器裁切)
- 校正 4 份 assets/AGENTS.md (消除 GIF/镜像错误描述)
- 修复审查发现 (README 坏链 + 原图归宿说明)

验证:
- 146 单元测试 0 失败
- :app:assembleDebug + spotlessCheck 全过
- APK -489KB (24,416,752 → 23,915,583)
- Adaptive Icon 模拟器实机验证 (圆形遮罩下背景填满、主体居中无裁切)
- Task 4 守卫突变验证 (硬编码 1..150 时测试立即 FAIL)
2026-06-15 14:15:49 +08:00
xfy
c591bc389b docs: 修复代码审查发现的问题(README 坏链 + 原图归宿说明)
审查发现 2 个问题:

1. README.md:6 坏链 [必须修]
   - <img src="app/src/main/assets/app_icon.png"> 指向 Task 1 已删的 PNG
   - 改为 core/src/main/assets/app_icon.webp (现存的应用内图标, 512px)
   - 加 width/height=128 限制展示尺寸 (原图无尺寸约束会撑满)

2. app/assets/AGENTS.md 原图归宿说明模糊 [建议修]
   - 原写"源文件放仓库外或 .assets-source/" 但两者都不存在, 误导
   - 改为明确: 原图 app_icon_original.png (2048px) 保留在 git 历史
     commit a36f6c4, 用 git show 恢复, 生成脚本见实施计划 Task 5

审查驳回的建议 (附理由):
- <mipmap> 间接引用: 审查自承"通常能工作", 且 Task 5 已模拟器实机
  验证渲染正常 (analyze_image 确认黄色主体居中无裁切)。
  <mipmap> 标签按密度选桶是启动器前景的标准做法, 不改。
- baseline-prof lambda hash: Task 3 commit 已说明"下次 updateBaselineProfile
  校准", 且 hash 失效是静默降级不报错, 不阻塞合并。

验证: ./gradlew :app:assembleDebug → BUILD SUCCESSFUL
2026-06-15 14:13:26 +08:00
xfy
6e1c2a39ae docs: 校正 assets AGENTS.md,消除 GIF/镜像等错误描述
问题
- 4 份 assets/AGENTS.md 与 Task 1-5 后的实际状态全部相反:
  - 声称 core/app "内容同步/镜像" → 实际职责不同 (core 管 UI/WebP, app 管启动器)
  - 声称文件是 .gif → 实际是 .webp 动画
  - 声称由 AnimatedGif 加载 → 实际是 AnimatedWebp
  - app/assets/gifs/ 只剩过时 AGENTS.md (PNG 已被 Task 1 删)

改动
- core/assets/AGENTS.md: 移除"与 app 镜像"错误说法, 说明职责划分, 列 app_icon.webp
- core/assets/animations/AGENTS.md: GIF→WebP 正名, 说明构建期扫描机制 + 测试守卫,
  保留"历史命名曾为 gifs/"历史说明
- app/assets/AGENTS.md: 反映清理后为空, 警告勿再放图标源 PNG
- 删除 app/assets/gifs/ 过时空目录 (仅剩 1 个过时 AGENTS.md)

验证
- grep 全 assets 无真正错误描述 (剩余命中均为有意的纠正/历史说明)
- ./gradlew :app:assembleDebug → BUILD SUCCESSFUL
2026-06-15 14:08:18 +08:00
xfy
ac41eef37b feat: 新增 Adaptive Icon(anydpi-v26),修复 API 26+ 启动器裁切
问题
- minSdk 24, 但只有 mipmap-*/ic_launcher.png (无 anydpi-v26)
- ic_launcher.png 与 ic_launcher_round.png MD5 完全相同, 圆形版只是方形副本
- API 26+ 用户在 OEM 圆形/水滴/方形启动器上图标会被硬切, 主体缺失

新增资源
- drawable/ic_launcher_background.xml: 矢量色块 #83D0F1 (原图四角采样 RGB 131,208,241)
- drawable/ic_launcher_foreground.xml: <mipmap> 引用密度分桶位图
- mipmap-anydpi-v26/ic_launcher.xml + ic_launcher_round.xml: adaptive-icon 定义
  (含 <monochrome> 支持 Android 13+ 主题化图标)
- mipmap-{xhdpi,xxhdpi,xxxhdpi}/ic_launcher_foreground.webp:
  从 app_icon_original.png (2048px) 中心裁 80% + cwebp -q 90 生成
  体积比对应 ic_launcher.png 小 60-70% (xxhdpi: 7.4KB vs 22.5KB)

设计决策
- foreground 用位图而非矢量: 原图主体是写实风格, 矢量化会损失质量
- foreground 源图用 git 历史临时恢复生成, 不重新入库 (保持 Task 1 清理成果)
- 保留 mipmap-*/ic_launcher.png 作 API 24-25 回退 (不删)
- AndroidManifest.xml 无需改: @mipmap/ic_launcher 在 API 26+ 自动取 anydpi-v26

设备验证 (emulator API 37, Pixel Launcher)
- 安装 + 回桌面, hotseat 最右 YaYa 图标显示正常
- analyze_image 确认: 圆形遮罩下背景填满, 黄色主体居中无裁切
- 颜色与原设计一致 (主体 RGB~254,213,74, 背景 #83D0F1)

构建验证
- unzip -l 确认 APK 含 anydpi-v26/*.xml + drawable/ic_launcher_*.xml + 各密度 foreground.webp
- ./gradlew :app:assembleDebug → BUILD SUCCESSFUL
- APK 累计 -489KB (baseline 24,416,752 → 23,915,694, 主要是 Task 1 删 PNG)
2026-06-15 14:06:21 +08:00
xfy
58a97d1725 refactor: WebP 文件列表改由构建期 BuildConfig 注入,消除 (1..152) 硬编码
问题
- AnimatedWebp.kt 原写死 WEBP_FILES = (1..152).map{...}, 与磁盘文件耦合却无校验
- 加 153.webp → 永远不会被随机到; 删某个 → random() 偶发命中不存在的资源
- 这是静默失败, 运行期无报错, 难排查

改动
- core/build.gradle.kts defaultConfig: 构建期扫描 assets/animations/ 生成
  buildConfigField("String[]", "WEBP_FILES", "new String[]{...}")
  含 require(webpFiles.isNotEmpty()) 防空目录构建
- AnimatedWebp.kt: WEBP_FILES 从 private 硬编码改为 internal val = BuildConfig.WEBP_FILES.toList()
  (internal 让同模块测试可访问)
- 新增 AnimatedWebpFilesTest: 2 个守卫测试
  1. webpFilesMatchDirectoryContents: WEBP_FILES 必须与 assets/animations/ 一一对应
  2. webpFilesUseZeroPaddedThreeDigitNames: 锁定 NNN.webp 命名约定

TDD 流程
- 先写测试 → 编译失败(WEBP_FILES 是 private) → 红灯成立
- 改实现 → 测试 PASS → 绿灯
- 突变验证: 临时改 WEBP_FILES = (1..150) 漏掉 151/152
  → webpFilesMatchDirectoryContents 立即 FAIL, 守卫有效
- 恢复后全量 146 个测试 0 失败

设计说明
- 当前实现里 WEBP_FILES 与目录都来自同一次构建期扫描, 二者天然一致
- 测试的核心价值是锁定「两者一致」不变量, 防止有人回退成硬编码
  (突变验证已证明: 回退后测试立即失败)
- BuildConfig 生成结果验证: String[] 含 152 个元素 001~152.webp

验证
- ./gradlew :core:testDebugUnitTest → 146 tests, 0 failures, 0 errors
- ./gradlew :app:assembleDebug → BUILD SUCCESSFUL
- BuildConfig.java: public static final String[] WEBP_FILES = new String[]{"001.webp",...,"152.webp"}
2026-06-15 14:02:21 +08:00
xfy
4c8084c176 fix: 恢复 scripts/profile.sh 可执行权限
上一个提交 (87d51b2) 编辑该文件时丢失了 100755 → 100644 的 mode 位,
纯 mode 修正, 无内容变更。
2026-06-15 13:57:20 +08:00
xfy
87d51b253a refactor: gifs/→animations/、AnimatedGif→AnimatedWebp, 名实相符
问题
- 目录名 gifs/ 与类名 AnimatedGif 长期描述 GIF, 但实际格式是 WebP 动画
  (依赖 sketch-animated-webp, 文件后缀也是 .webp)
- 名实不符持续误导阅读者, 也让 GIF 相关搜索返回错误结果

改动(仅重命名, 不改运行时行为)
- core/src/main/assets/gifs/ → animations/ (git mv, 152 个 webp + AGENTS.md)
- core/.../ui/AnimatedGif.kt → AnimatedWebp.kt, 函数 AnimatedGif() → AnimatedWebp()
- Platform.kt:13 getWebpUri 路径 gifs/ → animations/
- BottomCard.kt:175 调用点 AnimatedGif( → AnimatedWebp(
- CalendarMonthView.kt:583 注释同步
- scripts/profile.sh:228,1103 两处场景描述/trace 文档同步
- baseline-prof.txt: 19 处 AnimatedGifKt → AnimatedWebpKt (R8 类名)
  注: lambda hash 可能需下次 updateBaselineProfile 重新生成校准, 当前不影响构建

验证
- grep 全仓库 (排除 build/AGENTS.md/baseline-prof) 无 AnimatedGif/ 残留
- ./gradlew :app:assembleDebug → BUILD SUCCESSFUL

备注
- WEBP_FILES = (1..152) 硬编码暂保留, 下一个提交替换为构建期 BuildConfig
- AGENTS.md 文档校正留待 Task 6 统一处理
2026-06-15 13:57:09 +08:00
xfy
3d39e609df chore(app): 删除 app/assets 中无引用的 app_icon PNG 图标
问题
- app/src/main/assets/app_icon.png (61KB) 与 app_icon_original.png (468KB)
  无任何代码引用(grep 全仓库确认),但作为 assets/ 文件会被原样打进 APK
- 实际应用内图标用的是 core/src/main/assets/app_icon.webp
  (Platform.kt:15 getAppIconUri() 引用),与这两个 PNG 无关

验证
- grep -rn "app_icon_original\|assets/app_icon\.png" app/src core/src
  → 无输出(仅 AGENTS.md 提及,已排除)
- unzip -l 对比: assets/ 总量 4,286,121 → 3,757,710 字节(-528,411B ≈ 516KB)
- ./gradlew :app:assembleDebug → BUILD SUCCESSFUL

影响
- 设备安装体积减少 ~516KB(APK 是 ZIP, PNG 已压缩, 故 APK 文件本身仅 -139B;
  但 assets 安装时解压展开, 设备占用是实打实少 516KB)
- 不影响任何运行时行为
2026-06-15 13:52:21 +08:00
xfy
7d6ed1428e refactor: 日期检查器 Canvas 手绘图标替换为 Material Icons
CalendarIcon/ArrowRightIcon 改用 Icons.Filled.DateRange 和
Icons.AutoMirrored.Filled.ArrowForward,统一图标风格并支持
RTL 自动镜像;删除冗余 Canvas 绘制代码及关联 import(Offset/
StrokeCap/Stroke)。
2026-06-15 13:26:34 +08:00
xfy
1cb35d2752 Merge branch 'fix/date-checker-negative-days'
修复日期检查器保质期天数负数 bug: clampExpiryDays 纯函数钳制非负(含 4 单测);两个日期写入路径(选择器确认+行内输入)兜底;DatePicker SelectableDates 禁选早于生产日期(仅 Row);加载清理历史持久化负数旧数据
2026-06-15 11:43:34 +08:00
xfy
7628f299c2 fix: 加载保质期列表时钳制旧数据,清理历史持久化的负数天数 2026-06-15 11:40:26 +08:00
xfy
1438b405a9 feat: 日期检查器 Row 日期选择器禁选早于生产日期 2026-06-15 11:30:43 +08:00
xfy
2085c22987 fix: 日期检查器写入保质期天数前钳制为非负,防止负数落库 2026-06-15 11:27:35 +08:00
xfy
55d50c3e1d feat: 添加 clampExpiryDays 纯函数钳制保质期天数为非负 2026-06-15 11:24:38 +08:00
xfy
8f24706d8c chore: gitignore 添加 docs/superpowers/ 2026-06-08 13:47:35 +08:00
xfy
0868522741 feat: 为版本号添加可点击效果 2026-06-08 13:43:29 +08:00
xfy
c2c8b1c321 feat: 保质期列表添加空状态提示 2026-06-08 13:36:56 +08:00
xfy
4805bd1f0e fix: 修复日期检查器右滑删除卡住问题,列表底部添加间距防止FAB遮挡 2026-06-08 13:30:04 +08:00
xfy
62d3bbabea refactor: 首页 FAB 菜单图标替换为 Material Icons,展开时切换 Close 并添加过渡动画 2026-06-08 13:20:33 +08:00
xfy
a5a4173cc2 refactor: 返回图标从 Canvas 手绘替换为 Material Icons ChevronLeft 2026-06-08 13:10:44 +08:00
xfy
6b15c4b88b feat: 日期检查器 FAB 图标替换为 Material Icons 2026-06-08 11:36:58 +08:00
xfy
0560e81fb2 feat: 日期检查器添加数据持久化和恢复默认按钮
- 新增 DateCheckerStorage,SharedPreferences 持久化生产日期和保质期列表
- 添加 DateCheckerStorageTest 单元测试(含 InMemorySharedPreferences stub)
- 页面退出后保留数据,重新打开自动恢复
- 左下角新增恢复默认 FAB,点击弹出确认弹窗后重置全部数据
- 双 FAB 样式统一(FloatingActionButton + CircleShape + primary 配色)
2026-06-08 11:08:48 +08:00
xfy
fa872caa59 docs: 版本号统一由 gradle.properties 管理,更新发布流程文档 2026-06-02 14:15:07 +08:00
xfy
a36f6c41e1 docs: 更新 README 为纯 Android 描述,调整 CHANGELOG 顺序,添加 RELEASE.md 与图标资源 2026-06-02 13:43:47 +08:00
xfy
5b5d780593 release: v1.1.0 2026-06-02 13:29:41 +08:00
xfy
b3abe92b2c feat: 更新 Baseline Profile,覆盖日期检查器/调休/工具页等场景 2026-06-02 13:10:49 +08:00
xfy
ecf8a70f5d docs: 更新 AGENTS.md,标准化命令/规范/包名说明 2026-06-02 10:29:05 +08:00
xfy
51d6e383b0 feat: profile.sh 支持 --all 批量录制全部 15 个自动场景
将之前需手动操作的 date_select / cross_month / date_checker / full_flow
也改为自动执行(基于屏幕百分比坐标估算点击位置),
--all 可一次性录制全部场景并生成汇总对比报告
2026-06-01 18:30:09 +08:00
xfy
d933ca3e41 feat: 将 profile.sh 重写为场景化版本,支持按交互场景录制 Perfetto trace
- 引入 --scenario 参数,支持 15 种预设交互场景
- 自动场景通过 adb 自动执行操作(滑动翻页、拖拽折叠、切换视图等)
- 手动场景提供操作指引并保留倒计时准备
- 批量录制模式(--all)自动遍历所有场景并生成汇总报告
- 输出文件按场景名命名,支持场景级对比分析
2026-06-01 18:22:56 +08:00
xfy
829f89eb7c refactor: 删除 P0 冗余代码(自定义combine/死StateFlow/网格重复算法/runBlocking)
- 删除自定义 6 参数 combine + Quintuple,改用标准库嵌套 combine + Triple
- 删除 yearViewProgress 死 StateFlow 及关联测试
- generateMonthDays/generateMiniMonthDays 复用 getMonthGridInfo
- WeekPager 移除 runBlocking,交由 DayCell 自行异步计算
- 修复 AnimLog 在 JVM 单元测试中因 android.util.Log 不可用而崩溃
2026-06-01 18:03:27 +08:00
xfy
e249700ee5 refactor: 清理不必要的 @Suppress 注解并替换弃用 API
移除 23 处不必要或可通过改写避免的 @Suppress。
- 删除 12 处完全不必要(误加/重构遗留)
- 替换 9 处 monthNumber/dayOfMonth → month.number/day
- 替换 4 处弃用构造函数 LocalDate(year,month,1) → LocalDate(year,Month(month),1)
- 保留 6 处必要 @Suppress(NOTHING_TO_INLINE、Android API 兼容等)
2026-06-01 17:55:18 +08:00
xfy
484045950f fix: 修复日期检查器滑动删除 state 错位并消除 deprecation warning
- 将 dismissState 移入 key(row.id) 内部,修复删除后 state 被错误复用的问题
- 新增 pendingDeleteIds 延迟删除机制,滑动删除播放 shrinkVertically + fadeOut 退场动画
- 移除废弃的 confirmValueChange 参数,改用 LaunchedEffect 监听 dismissState.currentValue
2026-06-01 17:21:42 +08:00
xfy
877a3f9aa4 refactor: 日期检查器滑动删除添加动画等待完成后移除数据 2026-06-01 17:15:14 +08:00
xfy
23a7db1057 docs: 重写 AGENTS.md,精简为模块概览、命令、规范等核心信息 2026-06-01 16:59:32 +08:00
xfy
aeb147c6a7 feat: 日期检查器添加行级入场/出场动画与 animateContentSize 2026-06-01 16:56:19 +08:00
xfy
c9bdff9063 docs: 添加模拟器启动与 adb logcat 命令 2026-06-01 16:41:36 +08:00
xfy
24cb8fd3fe refactor: 移除日期检查器新行入场动画与高亮动画
- 移除 ExpiryRow.isNew 字段及相关入场淡入+上滑动效
- 移除 highlightedRowId 高亮逻辑及相关动画
- simplify animateItem() 为默认参数
- 用 Spacer 替代 verticalArrangement 实现行间距
- 用 animateScrollToItem 替代 scrollToItem + delay 模式
2026-06-01 16:41:33 +08:00
xfy
f0975f119d feat: 日期检查器新行添加入场动画(淡入+上滑)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 15:54:40 +08:00
xfy
bc9c10d82e feat: 日期检查器添加新行自动滚动与高亮动画
- 点击 FAB 添加新行后自动滚动到列表底部
- 新增行短暂高亮显示(primaryContainer 背景色)
- LazyColumn 添加 animateItem() 提升列表动画体验

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 15:39:43 +08:00
xfy
b12206dc88 feat: 日期检查器 UI 改版,添加过期状态显示
- 生产日期改为渐变卡片展示,支持点击切换日期
- 保质期条目改为 ExpiryCard,显示过期状态标签
- 新增 ExpiryStatus 枚举(SAFE/WARNING/URGENT/EXPIRED)
- 新增 ArrowRightIcon 自定义图标
- 添加中文日期格式化辅助函数
- 改进 TopAppBar 和 FAB 样式

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 14:12:41 +08:00
xfy
23909e855f refactor: 简化 BottomCard 年视图切换动画并移除调试日志
- 将 slideAnim 初始值固定为 1f(屏幕外),简化年↔月切换逻辑
- 移除 BottomCard 区域的调试 logd 日志
- fadeIn easing 从 FastOutSlowInEasing 改为 LinearOutSlowInEasing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 14:00:18 +08:00
xfy
be023a00a9 refactor: 优化年份视图切换动画,使用 Animatable 精确控制
- 用 Animatable + LaunchedEffect 替换 animateFloatAsState
  实现滑出/滑入分阶段动画序列(先滑出再滑入)
- 统一缩放/淡入淡出动画时长为 350ms
- 底部卡片滑出偏移从 200.dp 增加到 300.dp

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 13:47:36 +08:00
xfy
f34e34e5ae feat: 引入 YaYaTheme 并应用到所有 Activity
新增 Theme.kt 提供 Android 12+ 动态颜色支持,
为全部 5 个 Activity 的 setContent 包裹 YaYaTheme。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 13:25:27 +08:00
xfy
2b0491d7d7 chore: 添加 .editorconfig 配置 ktlint Composable 函数命名规则
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 13:20:55 +08:00
xfy
a8717dec7d refactor: MenuItem 和 ToolItem 统一使用 Card(onClick) 2026-06-01 13:18:27 +08:00
xfy
6446d6636e refactor: 移除 LicensesScreen 调试日志
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 11:44:15 +08:00
xfy
c72120ee0a fix: 开源许可列表项改用 Card(onClick) 以支持点击动画
原实现使用 Column + Modifier.clickable 在 LazyColumn 中无法显示
ripple 反馈。改用 Material3 Card 的 onClick 参数后,点击水波纹
动画正常显示。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 11:42:49 +08:00
xfy
acb737dd25 refactor: 简化 holiday badge 空值检查为 Elvis 运算符 2026-06-01 11:18:10 +08:00
xfy
4a8480be64 feat: 移除共享元素转场,月/年视图切换改用缩放+淡入淡出动画 2026-06-01 11:11:45 +08:00
xfy
1930bbcb7f feat: 添加 Perfetto trace 分析脚本并更新开发文档 2026-05-27 18:10:53 +08:00
xfy
6dffaf4c91 feat: 法定假日背景添加波浪缩放动画
点击"显示调休"后,假日格子背景从左上到右下依次缩放弹出(每格延迟15ms),
关闭时反向缩放消失,使用 graphicsLayer 实现高性能动画。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 17:41:18 +08:00
xfy
3c1a04580c 更新 baseline profile 和优化基准测试生成器 2026-05-27 17:13:16 +08:00
xfy
281abcf66b perf: 添加性能追踪标记并改进基线配置文件生成器 2026-05-27 16:06:05 +08:00
xfy
4de00e35dc Update baseline profile for performance optimization 2026-05-27 11:14:07 +08:00
xfy
4eadc479eb feat: 添加 testTag 基础设施,扩展 Baseline Profile 覆盖路径
为 CalendarPager、BottomCard、FAB、工具页、日期检查器等关键 UI 元素添加 testTag,
启用 testTagsAsResourceId 支持 UI Automator 通过 resource-id 定位。
BaselineProfileGenerator 使用 testTag 选择器重写,覆盖工具页、日期检查器、
DatePicker 等更多导航路径。清理 DEVELOPMENT.md 移除已过时的性能瓶颈描述。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 10:03:16 +08:00
xfy
9a0222b4a2 perf: 引入 logd 条件日志工具,增强动画调试并优化 sharedElement 缓存
- 新增 AnimLog.kt,提供 BuildConfig.DEBUG 条件控制的 logd 日志工具,
  支持 lambda 延迟求值以避免 release 模式下的字符串拼接开销
- 全模块替换 android.util.Log.d 为 logd,并将日志重构为 lambda 形式
- CalendarViewModel: toggleYearView / selectMonthFromYearView 添加纳秒级
  耗时追踪日志
- CalendarMonthView / CalendarMonthPage / CalendarPager / YearGridView:
  增加重组/进入/离开/页面切换/动画状态变化的详细调试日志
- 折叠动画/滑动进度日志添加状态去重,避免频繁重复输出
- P0: 缓存 sharedElement tween 实例(CalendarMonthView + YearGridView),
  避免每次重组创建新实例导致动画重新计算
- P0: YearPager pageYear 使用 remember 稳定计算,避免 settledPage 与
  yearViewYear 不同步导致抖动
- 移除 YearPager crossFadeAlpha,改为无透明度渐变
- app/build.gradle.kts 添加 profileinstaller 依赖

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:27:07 +08:00
xfy
20425e392c fix: 恢复折叠动画 Phase 2 下方行偏移,移除 showWeekPager 切换
- 恢复 CalendarMonthPage 中 isBelow 行的 `- phase2 * belowRowsHeight`
  (2592a5f 误删导致展开时下方行原地淡入而非滑出)
- 移除 CalendarMonthView 的 showWeekPager + AnimatedContent 切换
  (完全折叠后下拉拖拽时只显示 WeekPager 单周,上下空白)
- 回退到始终显示 CalendarPager,由 CalendarMonthPage 处理折叠动画
- 添加详细动画调试日志(Page/WeekRow/View 三层)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:39:40 +08:00
xfy
2592a5fa55 feat: 使用 AnimatedContent 平滑切换 CalendarPager ↔ WeekPager
- 用 AnimatedContent 包装 pager 切换,添加 fadeIn/fadeOut 过渡
- 延迟 50ms 切换避免折叠 spring 动画期间的视觉跳跃
- 修复 WeekRow 下方行的 yOffset 计算(移除 phase2 项)
- WeekPager 添加农历缓存支持
- 添加折叠动画调试日志

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:18:50 +08:00
xfy
4c53f234cf fix: 去掉 CalendarPager ↔ WeekPager 切换,修复折叠闪烁
CalendarPager 在 collapseProgress=1.0 时只渲染 anchor 行,
效果和 WeekPager 相同,但避免了切换导致的视觉跳跃。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:54:02 +08:00
ea7558dec8
Merge pull request #9 from xunrua/main
优化年月切换卡顿
2026-05-26 09:37:41 +08:00
xfy
47876a233b feat: 添加 PowerShell 性能追踪脚本
一键抓取 Perfetto trace、帧统计、内存信息并生成 Markdown 报告

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 08:12:48 +08:00
meyou
dfb820d610
Revert "feat: 添加年月滚轮选择器,支持触觉反馈"
This reverts commit bbe51051ae83d56fda8ad965f7b73a014ddea7b8.
2026-05-26 07:33:15 +08:00
meyou
0b6d9ea87a
fix: 滚轮选择器仅在滚动停止后触发选中变更和触觉反馈
快速滑动时不再每帧更新 selectedIndex,改为等滚动停止后
计算最终中心项再触发回调,消除来回抽搐问题。
2026-05-25 23:38:59 +08:00
meyou
bbe51051ae
feat: 添加年月滚轮选择器,支持触觉反馈
- 新增 WheelPicker composable:惯性吸附 + 触觉反馈滚轮
- 新增 MonthYearPickerDialog:年月双滚轮选择弹窗
- 新增 YearPickerDialog:年份滚轮选择弹窗
- MonthHeader:点击年月文字弹出年月选择器
- YearHeader:点击年份文字弹出年份选择器
- 滚动时触发 HapticFeedbackConstants.CLOCK_TICK 触觉反馈
2026-05-25 23:34:25 +08:00
meyou
6fac313fdf
perf: 延迟 YearGridView 文本测量到第二帧,补充性能分析文档
YearGridView 首帧 168ms 中约 24ms 来自 remember 同步文本测量。
将 dayLayouts/titleLayouts/weekdayLayouts 改为 produceState,
首帧 Canvas 渲染为空(sharedElement 结构不变),第二帧填充内容。

DEVELOPMENT.md 补充已知性能瓶颈分析和 Baseline Profile 覆盖建议。
2026-05-25 23:19:58 +08:00
meyou
ce84c614de
fix: 恢复 AnimatedGif 弹跳动画和 YearGridView 全量 sharedElement
- AnimatedGif: 恢复原始 scale 弹跳入场动画,仅保留 repeatCount 限制
- YearGridView: 恢复所有 12 个月的 sharedElement,保证点击任意
  月份都有正确的共享元素转场动画
- CalendarPager: beyondViewportPageCount 1→0 保留(无视觉影响)
2026-05-25 23:06:15 +08:00
meyou
5158b99800
perf: 优化年月切换卡顿
Perfetto trace 分析:
- YearGridView 首帧 168ms: 12 个 MiniMonth 各创建 sharedElement
  节点,但仅 1 个 key 匹配 CalendarPager
- compose:lazy:prefetch 最长 703ms: CalendarPager 预加载相邻页

修复:
- YearGridView: 仅选中的月份使用 sharedElement(11→1 个),
  减少首次组合的 modifier 节点创建开销
- CalendarPager: beyondViewportPageCount 1→0,消除预加载卡顿
2026-05-25 22:57:18 +08:00
meyou
80ab328906
perf: 限制 WebP 动画重复次数并简化入场动画,减少帧丢失
Perfetto trace 分析显示 75.6% 帧延迟,根因为 250x250 WebP
持续以 11-14 FPS 无限循环解码。每次解码周期导致 4-26 帧延迟。

- AnimatedGif: repeatCount(2) 限制动画播放 3 次后停止
- AnimatedGif: 移除两阶段弹跳动画,改为 150ms 淡入
- README: 添加构建命令和产物路径
2026-05-25 22:43:45 +08:00
meyou
a84e1b9528
fix: 移除农历初一月份名称重复的后缀"月"
lunarMonth.getName() 返回的月份名称已包含"月"字,
如"三月"、"四月",无需再手动拼接。
2026-05-25 22:01:06 +08:00
xfy
bb0a72006d revert: 回退 graphicsLayer 优化,实机 GPU 合成开销过大
graphicsLayer 创建的离屏 layer 在实体机上导致 slow draw commands 激增,
整体体验比重组更卡。回退以下改动:

- WeekRow: graphicsLayer → offset + alpha
- CalendarPager: graphicsLayer → alpha
- WeekPager: graphicsLayer → alpha
- YearGridView: graphicsLayer → alpha
- BottomCard: graphicsLayer → offset + alpha
- layoutReady: graphicsLayer → alpha

保留的优化:
- DayCell produceState 上移(42 协程 → 1 协程)
- remember 稳定化 lambda 和计算

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:25:22 +08:00
xfy
73b176a95e perf: remember 稳定化 lambda 和计算,减少重组传播
- weekNumber: remember(selectedDate) 缓存 getIsoWeekNumber 结果
- onDateClick/onMonthChanged/shiftKindAt: remember 稳定化,避免每次重组创建新 lambda
- onRowHeightMeasured: remember 稳定化

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:58:46 +08:00
xfy
6a4907f465 perf: layoutReady alpha() → graphicsLayer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:55:15 +08:00
xfy
4664d352bd cleanup: 移除 CalendarMonthView 冗余的 offset 导入
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:51:12 +08:00
xfy
297a7a54d4 perf: 年视图/BottomCard alpha+offset → graphicsLayer
- YearGridView crossFadeAlpha: alpha() → graphicsLayer(年视图翻页淡入淡出)
- BottomCard slideProgress: offset()+alpha() → graphicsLayer(年视图切换滑出)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:50:02 +08:00
xfy
55000f64c9 perf: WeekPager alpha() → graphicsLayer 避免滑动重组
与 CalendarPager 同样的问题:周视图滑动时 alpha 每帧变化触发重组。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:48:09 +08:00
xfy
01a7d7aed8 perf: CalendarPager alpha() → graphicsLayer 避免滑动重组
月份切换滑动时 pageOffset 每帧变化,alpha() Modifier 触发重组。
改用 graphicsLayer 在 draw 阶段更新透明度,跳过 Composition/Layout。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:47:04 +08:00
xfy
da46204de4 perf: DayCell produceState 上移到页面级,减少协程数量
CalendarMonthPage 统一预计算整月 42 天的 lunarData(1 个协程),
通过参数传入 DayCell,避免每个单元格独立启动 produceState(42 协程)。

- holidayBadges produceState 扩展为 lunarDataMap,存储完整 DayCellInfo
- DayCell 添加可选 lunarData 参数,条件分支调用 DayCellImpl
- WeekPager 保持向后兼容(不传 lunarData 时内部获取)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:44:06 +08:00
xfy
e175bb07da perf: WeekRow 改用 graphicsLayer 避免折叠动画重组
将 offset() + alpha() 替换为 graphicsLayer { translationY / alpha },
collapseProgress 变化时跳过 Composition/Layout,仅更新 Draw 阶段。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:38:04 +08:00
xfy
6ab63eae53 fix(profile.sh): 修复帧率和内存数据提取逻辑
- Janky frames 百分比:grep -oP 替换为 BSD awk 兼容的 substr+index
- Slow UI thread / Slow draw commands / High input latency:改用 $NF 提取最后一个字段,
  避免 "Slow UI thread" 等中间空格导致 awk 列错位
- Java Heap / Native Heap:改用 $3 提取,避免 "Java Heap:" 中 $2 是 "Heap:" 而非数值
- 所有指标改用 awk + exit 取第一个匹配(整体统计),避免 tail -1 取到 per-window 数据

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:27:21 +08:00
xfy
a431dcdfbf feat: 添加 trace 构建类型 + 增强性能追踪脚本
- ComposeTrace 改用 BuildConfig.ENABLE_TRACE 控制,release 关闭,debug/trace 开启
- core/app 添加 trace buildType:release 优化(R8 + shrink resources)+ 保留 trace 标记
- CalendarMonthView/WeekPager 添加更多自定义 trace marker,覆盖年月视图切换路径
- profile.sh 增强:
  - 添加 --trace 参数支持 trace 构建
  - Perfetto 配置增加 gpu.memory 和 surfaceflinger.frametimeline 数据源
  - 报告自动提取帧率百分位、jank 比例、内存摘要
  - 添加年月视图切换专项分析指南和基线对比方法

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:19:21 +08:00
xfy
28f777abe4 perf: 优化 Compose 渲染性能
- DayCell: 5 个 animate*AsState 合并为 updateTransition,减少 80% 动画状态对象
- CalendarMonthPage: produceState/remember key 从 List/Map 改为 year/month 原始值
- CalendarPager: derivedStateOf 提取 currentPageOffsetFraction,beyondViewportPageCount 设为 1
- CalendarViewModel: 三层嵌套 combine 扁平化为 6 参数 combine

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:36:18 +08:00
xfy
e934d33cfa cleanup: ComposeTrace 仅在 debug 构建中启用
- core/build.gradle.kts: 启用 buildConfig 生成
- ComposeTrace: 添加 BuildConfig.DEBUG 条件,release 构建中空操作
  避免 R8 full mode 下 Trace 调用的生产环境开销

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:15:29 +08:00
xfy
dead12fdf4 cleanup: 提取 Activity 基类、修复 Compose 状态、精简资源
A. 提取 Activity 共用代码:
- 新增 BaseActivity 基类,统一处理 enableEdgeToEdge 和 slide 转场动画
- 5 个子 Activity 从 ~212 行简化为 ~50 行,减少 150+ 行重复代码

B. 修复 Compose 状态问题:
- BottomCardArea: 移除对 selectedDate/isCollapsed 的独立 StateFlow 订阅,
  统一使用 uiState 聚合流
- 替换脆弱的 SideEffect 帧计数器为显式 LaunchedEffect + delay(32)

C. 资源精简:
- 删除 mipmap-mdpi 和 mipmap-hdpi 密度(现代设备已被覆盖)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:08:00 +08:00
xfy
5bac114c16 cleanup: 移除冗余依赖、死代码和构建配置
- core: 移除未使用的 uiTooling/uiToolingPreview debug 依赖
- app: 删除冗余 ndk abiFilters、bundle.language、netty pickFirsts
- app: 修复 packaging.excludes 花括号展开语法
- Platform: 删除死代码 AppPredictiveBackHandler 及未使用 import
- CalendarMonthView: 删除重复 import animateFloatAsState

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:53:39 +08:00
xfy
57eae41c65 feat: 日期检查器页面实现商品过期检查功能
- 顶部生产日期选择器(TextField + DatePickerDialog)
- 默认三行(30/60/180天),天数与到期日期双向联动
- 滑动删除行
- 底部右侧 FAB 添加新行
- 日期格式 ISO YYYY-MM-DD

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:58:17 +08:00
xfy
93c02810bf feat: 工具页面添加"日期检查器"入口,支持跳转详情页
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:24:12 +08:00
xfy
e30540b2dc feat: FAB 菜单添加"工具"入口,跳转空白工具页面
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:20:36 +08:00
xfy
134fc5a9c6 fix: 排班标记添加圆形底,避免与选中圆环重叠
- 将右上角排班 Text 包裹在 Box 中
- Box 添加 CircleShape + 背景色,遮挡选中状态的描边圆环
- 非选中时背景色与 cell 背景一致,视觉上仍是裸文字

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:12:58 +08:00
xfy
e97909ec34 feat: 法定假日改为背景色显示,支持连续边缘圆角
- 移除右上角 Text 角标,改用整格淡色背景("休"淡红、"班"淡蓝)
- 连续假日自动边缘圆角:开头左边、结尾右边、中间无、单个四边
- 排班标记固定右上角,不再随 showLegalHoliday 切换位置

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:06:38 +08:00
xfy
b1185da27a feat: 个人排班标记改为淡色圆形底+居中文字
- showLegalHoliday=true 时左上角标记改为 16dp 圆形淡色底
  背景 alpha 0.12,文字用原色居中
- showLegalHoliday=false 时保持右上角无背景直接浮于单元格
- 移除不再使用的 RoundedCornerShape import

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:01:57 +08:00
xfy
7f639f6c24 feat: FAB 菜单添加"显示调休"切换,修复个人排班标记样式
- CalendarViewModel 新增 toggleShowLegalHoliday() 方法
- FAB 菜单新增"显示调休"选项,选中高亮反馈开关状态
- DayCell 修复 showLegalHoliday=true 时个人排班标记样式:
  从"浅色背景+浅色文字"改为"accent 色实心胶囊+白色文字"
  默认模式(showLegalHoliday=false)恢复为无背景、直接浮于单元格

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:57:43 +08:00
xfy
8e65ae6f94 build: 缩小 app_icon 为 512x512 并转为 WebP,节省 446KB
原 2048x2048 PNG (457KB) → 512x512 WebP (11KB)。
同步更新 Platform.kt 中的资源引用。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:26:14 +08:00
xfy
9bd1f57daf build: 将 152 个 GIF 动画资源批量转换为 WebP 格式
使用 gif2webp -mixed -mt 批量转换,资源从 5.65MB 降至 3.60MB,
APK release 包从 7.8MB 降至 5.8MB(减少 26%)。

同步更新:
- 依赖:sketch-animated-gif → sketch-animated-webp
- 代码:GIF_FILES → WEBP_FILES,getGifUri → getWebpUri

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:04:36 +08:00
xfy
e02459e8c9 build: 删除未引用的 kotlin-test 条目,移除 @Preview,uiTooling 改为 debugImplementation
- 删除 libs.versions.toml 中未被任何模块引用的 kotlin-test 孤儿条目
- 移除所有 @Preview 注解和 Preview 函数(5 个文件)
- app/core 模块的 compose-uiTooling 从 implementation 改为
  debugImplementation,避免打包到 release APK

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:53:05 +08:00
xfy
5acad54384 build: 集成依赖更新检查与自动升级工具
- 添加 ben-manes/gradle-versions-plugin(0.54.0)用于检测依赖更新
- 添加 nl.littlerobots/version-catalog-update(1.1.0)用于自动升级版本目录
- 升级 Compose BOM 2025.05.01 → 2026.05.01
- 升级 profileinstaller 1.4.0 → 1.4.1
- 版本目录按字母顺序重新排序

使用方式:
  ./gradlew dependencyUpdates       # 检查更新
  ./gradlew versionCatalogUpdate    # 自动升级版本号

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:31:23 +08:00
xfy
1be1948f49 build: 集成 Spotless 8.5.1 代码格式化工具
- 在版本目录中添加 spotless 插件 8.5.1
- 配置 Kotlin 源码和 Gradle Kotlin DSL 脚本格式化
- 使用 ktlint 引擎,启用 trimTrailingWhitespace 和 endWithNewline

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:37:29 +08:00
xfy
dd072730f1 fix: 年视图 pageYear 使用 settledPage 计算,修复滑动切换时年份闪烁
pageYear = yearViewYear + (page - settledPage) 代替 currentPage,
因为 yearViewYear 由 settledPage 驱动,两者始终同步。
用 currentPage 会在滑动过半时出现不匹配,导致小月份
短暂显示错误年份数据。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:09:42 +08:00
xfy
2fc89d08c7 fix: 修复年视图翻页后共享元素转场动画失效
- 移除 baseYearForPager 和 scrollToPage(START_PAGE)
- HorizontalPager 的 pageYear 改用 yearViewYear + 相对偏移计算
- snapshotFlow 改为跟踪 settled page 差值更新 yearViewYear
- 避免再次进入年视图时强制滚动导致动画期间重组

Fixes #8

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:46:50 +08:00
xfy
ca02784d39 fix: 月视图和年视图添加背景色,修复共享元素过渡动画
在 CalendarMonthView 的月视图和年视图根布局添加
.background(MaterialTheme.colorScheme.background),确保共享元素
过渡时非共享元素有正确的背景动画(fade+slide)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:15:44 +08:00
xfy
0cdac663c9 refactor: 移除 fling 速度阈值,改用 spring 动画驱动折叠过渡
- ViewModel onDragEnd/onExpandDragEnd 移除 velocity 参数
- BottomCard 移除 VelocityTracker,简化回调签名
- CalendarMonthView 添加 animateFloatAsState spring 动画
- 更新单元测试:移除 fling 测试,调整阈值边界

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:36:58 +08:00
xfy
7f9db1dc1d perf: 聚合 CalendarUiState 减少 Compose 重组
- ViewModel 新增 CalendarUiState 数据类,通过 combine + stateIn
  将 6 个独立 StateFlow 合并为单一 uiState 流
- CalendarMonthView 从 5 个分散 collectAsState 改为单个
  uiState.collectAsState,减少重组次数
- CalendarPagerArea 改为显式传参,解耦对 ViewModel 的直接引用
- CalendarMonthPage 中 DayCell 外层包裹 key(dayData.date),
  稳定跨帧重组

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:09:05 +08:00
xfy
baedf878b4 perf: 动画性能优化 + 性能追踪工具
- CalendarMonthPage: 提取 WeekRow 子 Composable,graphicsLayer → offset + alpha
- DayCell: updateTransition → 独立 animateFloatAsState/animateColorAsState,
  共享 MutableInteractionSource 减少重组
- CalendarMonthView/WeekPager: graphicsLayer → offset + alpha,共享 interactionSource
- DEVELOPMENT.md: 补充性能追踪使用说明
- scripts/profile.sh: 新增 Perfetto trace 一键抓取脚本

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:29:58 +08:00
xfy
fb7e19ddc9 test: Wave 4 — 补充 fling 速度阈值交互测试
- test: onDragEnd 高速上滑强制折叠(无视 progress 阈值)
- test: onDragEnd 高速下滑强制展开(无视 progress 阈值)
- test: onExpandDragEnd 高速下滑强制展开
- test: onExpandDragEnd 高速上滑保持折叠

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:18:34 +08:00
xfy
6f4d62b78f refactor: Wave 3 — ViewModel 解耦 Compose 运行时,迁移到 StateFlow
- refactor: CalendarViewModel 从 mutableStateOf/Animatable 迁移到 StateFlow
- refactor: 继承 Android ViewModel,使用 viewModelScope 管理生命周期
- refactor: 拖拽/动画方法改为同步修改 StateFlow(移除协程 launch)
- refactor: onDragEnd/onExpandDragEnd 实现 fling 速度阈值判断
- refactor: BottomCard 解耦为纯参数驱动
- refactor: CalendarMonthView 使用 viewModel() + collectAsState()
- test: 适配 StateFlow API,139 测试全部通过

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:16:20 +08:00
xfy
774e03a928 refactor: Wave 2 — LunarCache 可注入化 + 重复计算提取
- refactor: LunarCache 从 object 单例改为 class + Mutex,方法改为 suspend
- refactor: DayCell 参数注入 lunarCache,remember → produceState
- refactor: 提取 MonthGridInfo/getMonthGridInfo 到 CalendarUtils
- refactor: CalendarViewModel init 块和 getMonthDays 复用 getMonthGridInfo
- refactor: calculateWeeksCount 委托给 getMonthGridInfo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:58:35 +08:00
xfy
bf28008d17 refactor: Wave 1 — 构建清理、测试修复、文档修正、a11y 改进
- build: 清理版本目录死依赖(appcompat、core、material3)
- build: 精简 ProGuard keep 规则,仅保留热点路径和第三方库
- refactor: 重命名 PredictiveBackHandler → AppPredictiveBackHandler
- test: formatLunarDate 精确断言 + 闰月测试
- test: 统一测试命名风格(下划线),Unconfined → StandardTestDispatcher
- docs: 修正文档为纯 Android + Jetpack Compose 描述
- feat(a11y): 添加可访问性语义标注(WeekdayHeader heading、BottomCard 拖拽描述)
- feat: 为 DayCell、MonthHeader、WeekdayHeader、BottomCard 添加 @Preview

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:51:12 +08:00
xfy
2bd7d5ee19 perf: 移除临时性能监控日志,保留 Perfetto trace 标记
清理 ViewModel 和各 UI 组件中用于调试的 println 性能日志、
重组计数器和 System.nanoTime() 计时代码。正式的
composeTraceBeginSection/EndSection Perfetto/Systrace 标记继续保留,
用于生产环境性能分析。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:46:12 +08:00
xfy
98f3b71c4f perf: 升级 Macrobenchmark 至 1.4.1,适配新 API 并重新生成 Baseline Profile
- benchmarkMacro 1.3.4 → 1.4.1
- BaselineProfileGenerator: 使用 Direction 枚举替代坐标 swipe;
  用 executeShellCommand 绕过 startActivityAndWait(模拟器 software renderer
  不支持 gfxinfo framestats,会导致 amStartAndWait 超时)
- updateBaselineProfile task: 适配 1.4+ 的 startup-prof.txt 文件名格式,
  使用 layout.buildDirectory 预先计算路径(configuration cache 兼容)
- 重新生成 baseline-prof.txt(全量 AOT 覆盖)
- benchmark build type 设为 isDebuggable=true(配合模拟器运行)
- 将 Baseline Profile 使用文档从 README 迁移至 DEVELOPMENT.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:37:37 +08:00
xfy
2771e3733b perf: 集成 Macrobenchmark 模块,自动生成全量 Baseline Profile
- 新增 :macrobenchmark 模块,提供自动 Baseline Profile 生成
- 扩展 proguard-rules.pro,保留所有业务类方法名确保 profile 匹配
- 全量 AOT 覆盖:冷启动 + 交互路径(年视图、周折叠、关于页等)
- app/build.gradle.kts 新增 benchmark 构建类型
- README 简化 Baseline Profiles 维护指南

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:50:01 +08:00
xfy
6582d5970e perf: 集成 Baseline Profiles,优化启动性能与文档
- 添加 core/proguard-rules.pro 保留关键类方法签名
- 添加 core/src/main/baseline-prof.txt 预编译热点路径
- 引入 androidx.profileinstaller 依赖
- LunarCache 新增 formatLunarDate() 复用缓存数据
- BottomCard 使用 remember 缓存农历描述避免重复计算
- CalendarMonthView 延迟一帧显示 BottomCard,避免首帧阻塞
- README 补充 Baseline Profiles 维护指南与性能监控说明

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:22:43 +08:00
xfy
6aefaf33a6 perf: 提取农历/节气计算为 LunarCache LRU 缓存,添加启动预计算
- 新增 LunarCache 单例,LinkedHashMap(accessOrder=true) 实现 LRU 语义,
  @Synchronized 保护并发,容量 800 条,超限时淘汰 20%
- CalendarViewModel init 中在 Default 线程预计算当前月前后各 1 个月
- DayCell 移除内联 computeDayCellInfo 和 mutableMap 缓存,统一使用 LunarCache

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:50:48 +08:00
xfy
f6b5e62284 perf: 添加组件级性能监控日志
- CalendarViewModel: toggle/select 方法耗时打点
- CalendarMonthPage/CalendarPager/WeekPager/YearGridView: 重组次数与耗时日志
- CalendarMonthView: 使用 graphicsLayer 驱动 BottomCard 滑入动画

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:31:48 +08:00
xfy
27ba4f9f54 refactor: 月→年视图切换时不再强制展开折叠态
折叠状态下直接切换视图,sharedElement 从周视图缩小到
MiniMonth 的转场更自然。同时修复了重复的 trace begin。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:50:31 +08:00
xfy
2028897ce3 docs: 同步 AGENTS.md 文档,反映纯 Android 项目结构
- 更新所有层级 AGENTS.md 中的模块描述(:app / :core)
- 移除 KMP/iOS 相关引用,统一为纯 Android + Jetpack Compose
- 同步构建命令和测试路径

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:46:17 +08:00
xfy
e1345cc071 feat: 年月视图转场添加 BottomCard 滑入动画和淡入淡出效果
- CalendarMonthView: 为 BottomCard 添加滑入/淡出动效
- 优化 AnimatedContent 过渡:fade + slideInVertically/slideOutVertically
- YearGridView: 提取 sharedKey 变量

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:46:09 +08:00
xfy
9ad619c105 feat: 优化年月视图共享元素转场动画
- 月→年切换时自动展开折叠状态
- 将 sharedElement 精确绑定到日历网格区域
- YearGridView 为每个 MiniMonth 添加共享元素转场

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:17:16 +08:00
xfy
f5cc4ef2e5 refactor: 完成纯 Android 迁移,清理旧 KMP 模块并添加 app/core 新模块
- 删除遗留的 androidApp/、shared/ 等 KMP 结构文件
- 将新的 app/、core/ 纯 Android 模块纳入版本控制

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:55:08 +08:00
xfy
cf9315cfe3 feat: US-007~010 实现年月视图共享元素转场动画
- 引入 SharedTransitionLayout + AnimatedContent 管理月/年视图切换
- 使用 sharedBounds + rememberSharedContentState 标记共享元素
- 转场动画 400ms,FastOutSlowInEasing
- 添加 compose-animation 依赖支持 SharedTransition API

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:17:18 +08:00
xfy
c19916c2ec fix: US-006 修复构建错误,首次构建验证通过
- 移除 androidApp/shared 中已弃用的 kotlinOptions 块
- material3 改为通过 Compose BOM 管理版本
- 清理残留的 composeResources 目录

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 14:58:06 +08:00
xfy
0806c11fcc refactor: US-002~005 迁移为纯 Android 项目
- shared/build.gradle.kts: KMP 库 → Android Library,使用 Compose BOM
- androidApp/build.gradle.kts: 移除 CMP 插件,使用标准 Android App 配置
- 源码目录: commonMain → main, commonTest → test
- expect/actual 内联: Platform.kt + ComposeTrace.kt 合并为普通实现
- 资源迁移: 152 个 GIF + app_icon → assets/
- 删除: iosApp/、androidMain/、commonMain/、commonTest/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 14:51:59 +08:00
xfy
d3229f90d0 refactor: US-001 清理 Gradle 根配置,移除 KMP/CMP 插件和依赖
- build.gradle.kts: 移除 composeMultiplatform、kotlinMultiplatform 插件
- settings.gradle.kts: 移除 JetBrains maven 仓库
- libs.versions.toml: CMP 依赖替换为 AndroidX Compose BOM
- Licenses.kt: 更新依赖列表

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 14:47:56 +08:00
xfy
d0f4980e01 fix: 修复快速滑动时折叠/展开失效,改用 progress 阈值判定
- 移除 velocity 判定:change.position 是本地坐标,卡片移动会导致
  velocityTracker 计算的速度方向错误,快速滑动时被误判方向而回弹
- onDragEnd 改为 progress > 0.3f 判定折叠,onExpandDragEnd 改为
  progress < 0.7f 判定展开
- 修复 onDrag/onExpandDrag 的 race condition:将 _collapseAnimatable.value
  的读取从 launch 外部移到协程内部,避免快速滑动时多回调并发读到旧值

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 14:14:18 +08:00
xfy
df0aa16d8e feat: 新增关于页面与开源许可页面
- 新增 AboutActivity、LicensesActivity 及对应的 Slide 转场动画
- MainActivity 接入 CalendarMonthView,支持导航到关于页面
- 移除 composeResources 下的 AGENTS.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:55:21 +08:00
xfy
50a42be9bc docs: 新增项目各目录 AGENTS.md 文档
为各模块和子目录添加 AGENTS.md,便于 AI 助手理解项目结构。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:26:19 +08:00
673 changed files with 63614 additions and 3677 deletions

View 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
View File

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

4
.gitignore vendored
View File

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

View 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
View 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`.

View File

@ -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

View File

@ -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`)

1
CLAUDE.md Symbolic link
View File

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

View File

@ -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 无替代 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,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 markerAndroid 上会被记录到系统 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 抢占或手指短暂离屏。

View File

@ -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 # 安装 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

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

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