Files
shenleiandClaude Opus 4.8 febfd30f49 feat: A1 短材料复现本单元理解词,并加入黑夜模式
课程内容
- 重写全部 21 个 A1 单元的听读材料,把本单元理解词织进听/读文本,
  单元内理解词复现率从约 20% 提升到约 77%(各单元 56–96%)。
- 修正 U01 房间号与机场大巴同为 thirty 的撞车(改为 17/30/40)。
- 校验器容差按级别读取(A1 为 8%),materials 覆盖率、字数、
  选项子串等校验全部通过;course_content_test 通过。

黑夜模式
- app_theme 拆分明/暗两套调色板,AppColors 随亮度切换;
  main 用 theme/darkTheme/themeMode + builder 镜像已解析亮度;
  主题偏好持久化到快照;进度页新增“外观主题”切换。

文档
- COURSE-PACK-JSON.md 更新 A1 词池覆盖(840/933)与材料复现约定。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-18 11:16:09 +09:00

173 lines
3.9 KiB
Dart

import 'package:flutter/material.dart';
import '../core/app_theme.dart';
class AppPage extends StatelessWidget {
const AppPage({
super.key,
required this.child,
this.appBar,
this.bottomNavigationBar,
this.scrollController,
});
final Widget child;
final PreferredSizeWidget? appBar;
final Widget? bottomNavigationBar;
final ScrollController? scrollController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
body: SafeArea(
top: appBar == null,
child: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(16, 20, 16, 24),
child: child,
),
),
);
}
}
class SectionCard extends StatelessWidget {
const SectionCard({
super.key,
required this.child,
this.tint,
this.onTap,
this.padding = const EdgeInsets.all(14),
});
final Widget child;
final Color? tint;
final VoidCallback? onTap;
final EdgeInsets padding;
@override
Widget build(BuildContext context) {
final content = Padding(padding: padding, child: child);
return Material(
color: tint ?? AppColors.surface,
borderRadius: BorderRadius.circular(16),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: AppColors.line),
borderRadius: BorderRadius.circular(16),
),
child: content,
),
),
);
}
}
class PrimaryButton extends StatelessWidget {
const PrimaryButton({
super.key,
required this.label,
required this.onPressed,
this.icon,
});
final String label;
final VoidCallback? onPressed;
final IconData? icon;
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 50,
child: FilledButton.icon(
onPressed: onPressed,
icon: icon == null ? const SizedBox.shrink() : Icon(icon),
label: Text(label),
style: FilledButton.styleFrom(
backgroundColor: AppColors.green,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
),
);
}
}
class SecondaryButton extends StatelessWidget {
const SecondaryButton({
super.key,
required this.label,
required this.onPressed,
});
final String label;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 48,
child: OutlinedButton(
onPressed: onPressed,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.ink,
side: BorderSide(color: AppColors.line),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: Text(label),
),
);
}
}
class Eyebrow extends StatelessWidget {
const Eyebrow(this.text, {super.key});
final String text;
@override
Widget build(BuildContext context) => Text(
text,
style: TextStyle(
color: AppColors.green,
fontWeight: FontWeight.w600,
fontSize: 13,
),
);
}
class SpacedColumn extends StatelessWidget {
const SpacedColumn({
super.key,
required this.children,
this.spacing = 12,
this.crossAxisAlignment = CrossAxisAlignment.start,
});
final List<Widget> children;
final double spacing;
final CrossAxisAlignment crossAxisAlignment;
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: crossAxisAlignment,
children: [
for (var index = 0; index < children.length; index++) ...[
children[index],
if (index < children.length - 1) SizedBox(height: spacing),
],
],
);
}