Make compact Dock persistent and support week swipes
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
# TPSchedule
|
# TPSchedule
|
||||||
|
|
||||||
学生课表 APP,Flutter 实现,目前接入电子科技大学研究生系统。Android 0.5.3+11,Android 7.0 及以上。
|
学生课表 APP,Flutter 实现,目前接入电子科技大学研究生系统。Android 0.5.4+12,Android 7.0 及以上。
|
||||||
|
|
||||||
使用用户提供的原始 SVG 图标,蓝色渐变、黄色日历框及白色笔画保持一致;启动画面使用相同配色。0.5.1修复重复启动页闪烁:初始化期间保留原生启动屏,数据就绪后直接显示课表首帧,不再叠加Flutter品牌页或淡出动画。Android12+ 原生启动屏使用同色纯蓝背景及图标,旧版采用蓝色渐变。
|
使用用户提供的原始 SVG 图标,蓝色渐变、黄色日历框及白色笔画保持一致;启动画面使用相同配色。0.5.1修复重复启动页闪烁:初始化期间保留原生启动屏,数据就绪后直接显示课表首帧,不再叠加Flutter品牌页或淡出动画。Android12+ 原生启动屏使用同色纯蓝背景及图标,旧版采用蓝色渐变。
|
||||||
|
|
||||||
三个图标标签页:课表、考试、个人设置。界面统一蓝色,三个页面标题对齐。透明蓝色液态玻璃风格悬浮 Dock 缩窄至264,选中滑块为灰色玻璃,图标为实心蓝色,静止边距为3。支持点击图标或拖动选中滑块切换;按压和拖动时放大、松手弹簧回弹,支持的渲染模式启用实时背景折射。页面切换采用短距离移动与淡入淡出,并保留每页滚动位置。页面本身不支持左右滑动切换。课表和考试页面向下滚动时 Dock 用360ms过渡滑出屏幕底部,向上滚动恢复;个人设置页面保持显示。系统“减少动画”开启时减少切页与回弹动画。
|
三个标签页:课表、考试、个人设置。界面统一蓝色,三个页面标题对齐。透明蓝色液态玻璃风格悬浮 Dock 为264×58,选中滑块为灰色玻璃,图标为实心蓝色,静止边距为3。支持点击图标或拖动选中滑块切换;按压和拖动时放大、松手弹簧回弹,支持的渲染模式启用实时背景折射。页面切换采用短距离移动与淡入淡出,并保留每页滚动位置。Dock在三个页面常驻,上下滚动均保持可见;课表左右滑动切换周次,不切换标签页。系统“减少动画”开启时减少切页与回弹动画。
|
||||||
|
|
||||||
课表周一至周日固定七列同屏,无横向滚动。同节数课程条等长;课程名可省略,老师和教室完整换行,点击查看全部信息。课程条默认长度125%,在「个人设置 → 课表设置 → 课程条长度」调节80%–200%。课前提醒、手机日历及学期设置继续保留。
|
课表周一至周日固定七列同屏,无横向滚动。同节数课程条等长;课程名可省略,老师和教室完整换行,点击查看全部信息。课程条默认长度125%,在「个人设置 → 课表设置 → 课程条长度」调节80%–200%。课前提醒、手机日历及学期设置继续保留。
|
||||||
|
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
0.5.3为蓝色图标增加轻微柔光,下方显示“课表、考试、我的”。文字可随系统字号放大,并在Dock固定尺寸内自动适配。灰色静止滑块、按压放大和回弹保持。详见[0.5.3验证记录](docs/tpschedule-0.5.3-validation.md)。
|
0.5.3为蓝色图标增加轻微柔光,下方显示“课表、考试、我的”。文字可随系统字号放大,并在Dock固定尺寸内自动适配。灰色静止滑块、按压放大和回弹保持。详见[0.5.3验证记录](docs/tpschedule-0.5.3-validation.md)。
|
||||||
|
|
||||||
|
0.5.4主要缩小Dock高度,由68降至58,宽度保持264。Dock改为常驻。课表向左滑动下一周、向右滑动上一周,一次手势只换一周,限制在设置的学期周数内;箭头和“本周”按钮保留。未设置学期开始日时仍提示设置。详见[0.5.4验证记录](docs/tpschedule-0.5.4-validation.md)。
|
||||||
|
|
||||||
## 安装与学校登录
|
## 安装与学校登录
|
||||||
|
|
||||||
安装 `dist/TPSchedule.apk`(release运行模式),或使用 `dist/TPSchedule-debug.apk` 测试。Android包名已按用户确认改为 `com.tpschedule.app`,显示名称为TPSchedule。它与旧 `com.example.hepan_everyday` 是两个独立应用,不能覆盖安装旧RSchedule;旧应用与数据保留,新应用需要重新导入课程并设置日期。
|
安装 `dist/TPSchedule.apk`(release运行模式),或使用 `dist/TPSchedule-debug.apk` 测试。Android包名已按用户确认改为 `com.tpschedule.app`,显示名称为TPSchedule。它与旧 `com.example.hepan_everyday` 是两个独立应用,不能覆盖安装旧RSchedule;旧应用与数据保留,新应用需要重新导入课程并设置日期。
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# TPSchedule Android 0.5.4+12:常驻 Dock 与滑动换周
|
||||||
|
|
||||||
|
日期:2026-10-14。
|
||||||
|
|
||||||
|
Dock宽度保持264,高度由68降至58,约减少15%。图标由27调整为25,图标与文字间距由3调整为2;“课表、考试、我的”、蓝色微光、灰色静止滑块、按压放大与弹簧回位保留。静止触摸区高度52。
|
||||||
|
|
||||||
|
移除课表和考试滚动时隐藏Dock的监听与过渡容器,三个页面的Dock保持常驻。课表横向拖动释放后切换周次:向左下一周、向右上一周;48像素位移或400像素/秒速度触发,一次手势只换一周,限制在1至学期总周数。上下滚动、课程详情点击、箭头和“本周”按钮保留。未设置学期日期时保持设置提示。页面横向滑动不会切换标签页。
|
||||||
|
|
||||||
|
实现前的新测试复现旧行为:课表左右滑动不换周,滚动时Dock隐藏。实现后145项完整Flutter测试通过,使用--enable-impeller;静态分析无问题。新测试覆盖不同周课程、左右换周、首尾边界、一次换一周、返回本周、课程点击与未设置学期。既有测试覆盖Dock拖动、点击、尺寸、滚动常驻、系统字号和材质。
|
||||||
|
|
||||||
|
玻璃测试按58高度裁剪,预览导出保持3倍等比例尺寸,颜色采样避开图标与名称。蓝红对比下限按既有最强34%灰色覆盖与5%底座覆盖计算:原始210级对比保留约63%,下限130;产品材质参数未更改。精细纹理柔化与较大背景轮廓保留检查继续通过。
|
||||||
|
|
||||||
|
独立源码审查确认手势、边界、触摸尺寸与运动参数无功能问题,指出预览导出高度仍为旧值,已修正。
|
||||||
|
|
||||||
|
包名com.tpschedule.app及签名配置保留,私钥不更换或导出。
|
||||||
|
|
||||||
|
Android API36模拟器覆盖安装通过,原课程保留。实际左右滑动确认第1周→第2周→第1周;向下滚动后Dock三个触摸区域仍在原位置且可点击,点击“我的”进入TPSchedule个人页。课表、下一周、滚动与个人页截图位于dist/screenshots。模拟器使用软件兼容渲染;支持shader的折射效果由像素测试验证。
|
||||||
|
|
||||||
|
发布与调试包均构建成功。发布包versionName0.5.4/versionCode12,签名指纹检查通过,与0.5.3证书DER和公钥完全一致。发布APK SHA256:6707D68DBDAA352E4629994104C1D38450D48F9FB51B2458000B1EBA778AA3E2。
|
||||||
@@ -59,31 +59,9 @@ class _Shell extends StatefulWidget {
|
|||||||
|
|
||||||
class _ShellState extends State<_Shell> with WidgetsBindingObserver {
|
class _ShellState extends State<_Shell> with WidgetsBindingObserver {
|
||||||
int _tab = 0;
|
int _tab = 0;
|
||||||
bool _dockHidden = false;
|
|
||||||
double _scrollTravel = 0;
|
|
||||||
void _selectTab(int index) {
|
void _selectTab(int index) {
|
||||||
if (index == _tab) return;
|
if (index == _tab) return;
|
||||||
setState(() {
|
setState(() => _tab = index);
|
||||||
_tab = index;
|
|
||||||
_dockHidden = false;
|
|
||||||
_scrollTravel = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _scroll(ScrollNotification notification) {
|
|
||||||
if (_tab == 2 || notification.metrics.axis != Axis.vertical) return false;
|
|
||||||
if (notification is ScrollUpdateNotification &&
|
|
||||||
notification.dragDetails != null) {
|
|
||||||
final delta = notification.scrollDelta ?? 0;
|
|
||||||
if (delta.sign != _scrollTravel.sign) _scrollTravel = 0;
|
|
||||||
_scrollTravel += delta;
|
|
||||||
final hide = notification.metrics.pixels > 0 && _scrollTravel > 24;
|
|
||||||
final reveal = notification.metrics.pixels <= 0 || _scrollTravel < -16;
|
|
||||||
if ((hide && !_dockHidden) || (reveal && _dockHidden)) {
|
|
||||||
setState(() => _dockHidden = hide);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -274,35 +252,32 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: NotificationListener<ScrollNotification>(
|
child: TabDeck(
|
||||||
onNotification: _scroll,
|
index: _tab,
|
||||||
child: TabDeck(
|
children: [
|
||||||
index: _tab,
|
TimetablePage(
|
||||||
children: [
|
controller: widget.controller,
|
||||||
TimetablePage(
|
onSync: _school,
|
||||||
controller: widget.controller,
|
),
|
||||||
onSync: _school,
|
ExamsPage(
|
||||||
),
|
controller: widget.controller,
|
||||||
ExamsPage(
|
onSync: () {
|
||||||
controller: widget.controller,
|
_school(exams: true);
|
||||||
onSync: () {
|
},
|
||||||
_school(exams: true);
|
),
|
||||||
},
|
ProfilePage(
|
||||||
),
|
controller: widget.controller,
|
||||||
ProfilePage(
|
onLogin: _school,
|
||||||
controller: widget.controller,
|
onUpdateTerm: () => _school(chooseTerm: true),
|
||||||
onLogin: _school,
|
authStatus: widget.school?.statusLabel ?? '未登录',
|
||||||
onUpdateTerm: () => _school(chooseTerm: true),
|
logoutLabel: widget.school?.logoutFailed == true
|
||||||
authStatus: widget.school?.statusLabel ?? '未登录',
|
? '重试退出学校登录'
|
||||||
logoutLabel: widget.school?.logoutFailed == true
|
: '退出学校登录',
|
||||||
? '重试退出学校登录'
|
onLogout: widget.school?.canLogout == true
|
||||||
: '退出学校登录',
|
? _logout
|
||||||
onLogout: widget.school?.canLogout == true
|
: null,
|
||||||
? _logout
|
),
|
||||||
: null,
|
],
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -313,25 +288,10 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
|
|||||||
right: 0,
|
right: 0,
|
||||||
bottom: 16 + MediaQuery.paddingOf(context).bottom,
|
bottom: 16 + MediaQuery.paddingOf(context).bottom,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: IgnorePointer(
|
child: GlassDock(
|
||||||
ignoring: _dockHidden,
|
key: const Key('floating-dock'),
|
||||||
child: ExcludeSemantics(
|
selectedIndex: _tab,
|
||||||
excluding: _dockHidden,
|
onSelected: _selectTab,
|
||||||
child: AnimatedSlide(
|
|
||||||
key: const Key('floating-dock'),
|
|
||||||
offset: _dockHidden
|
|
||||||
? const Offset(0, 1.8)
|
|
||||||
: Offset.zero,
|
|
||||||
duration: MediaQuery.disableAnimationsOf(context)
|
|
||||||
? Duration.zero
|
|
||||||
: const Duration(milliseconds: 360),
|
|
||||||
curve: Curves.easeInOutCubic,
|
|
||||||
child: GlassDock(
|
|
||||||
selectedIndex: _tab,
|
|
||||||
onSelected: _selectTab,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class TimetablePage extends StatefulWidget {
|
|||||||
class _TimetablePageState extends State<TimetablePage> {
|
class _TimetablePageState extends State<TimetablePage> {
|
||||||
int? _week;
|
int? _week;
|
||||||
bool? _wasDemo;
|
bool? _wasDemo;
|
||||||
|
double _weekDrag = 0;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final c = widget.controller, term = c.visibleSettings.semester;
|
final c = widget.controller, term = c.visibleSettings.semester;
|
||||||
@@ -33,214 +34,242 @@ class _TimetablePageState extends State<TimetablePage> {
|
|||||||
? today.subtract(Duration(days: today.weekday - 1))
|
? today.subtract(Duration(days: today.weekday - 1))
|
||||||
: term.firstMonday.add(Duration(days: (week - 1) * 7));
|
: term.firstMonday.add(Duration(days: (week - 1) * 7));
|
||||||
final sunday = monday.add(const Duration(days: 6));
|
final sunday = monday.add(const Duration(days: 6));
|
||||||
return ListView(
|
return GestureDetector(
|
||||||
key: const PageStorageKey('timetable-scroll'),
|
onHorizontalDragStart: term == null ? null : (_) => _weekDrag = 0,
|
||||||
padding: EdgeInsets.fromLTRB(
|
onHorizontalDragUpdate: term == null
|
||||||
8,
|
? null
|
||||||
10,
|
: (details) => _weekDrag += details.delta.dx,
|
||||||
8,
|
onHorizontalDragCancel: term == null ? null : () => _weekDrag = 0,
|
||||||
140 + MediaQuery.paddingOf(context).bottom,
|
onHorizontalDragEnd: term == null
|
||||||
),
|
? null
|
||||||
children: [
|
: (details) {
|
||||||
Padding(
|
final velocity = details.primaryVelocity ?? 0;
|
||||||
padding: EdgeInsets.symmetric(horizontal: 14),
|
if (_weekDrag.abs() < 48 && velocity.abs() < 400) return;
|
||||||
child: Row(
|
final direction = _weekDrag.abs() >= 48 ? _weekDrag : velocity;
|
||||||
children: [
|
final next = (week + (direction < 0 ? 1 : -1)).clamp(
|
||||||
const Expanded(
|
1,
|
||||||
child: Text(
|
term.totalWeeks,
|
||||||
'课表',
|
);
|
||||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
|
if (next != week) setState(() => _week = next);
|
||||||
),
|
},
|
||||||
),
|
child: ListView(
|
||||||
IconButton(
|
key: const PageStorageKey('timetable-scroll'),
|
||||||
tooltip: '刷新课表',
|
padding: EdgeInsets.fromLTRB(
|
||||||
onPressed: widget.onSync,
|
8,
|
||||||
icon: const Icon(Icons.sync_rounded),
|
10,
|
||||||
),
|
8,
|
||||||
],
|
140 + MediaQuery.paddingOf(context).bottom,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (!c.isDemo && c.snapshot.schoolTerm != null)
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
padding: EdgeInsets.symmetric(horizontal: 14),
|
||||||
child: Text(
|
child: Row(
|
||||||
c.snapshot.schoolTerm!.label,
|
children: [
|
||||||
style: TextStyle(fontSize: 12, color: ink.withValues(alpha: 0.5)),
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'课表',
|
||||||
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: '刷新课表',
|
||||||
|
onPressed: widget.onSync,
|
||||||
|
icon: const Icon(Icons.sync_rounded),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
if (!c.isDemo && c.snapshot.schoolTerm != null)
|
||||||
SurfaceCard(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),
|
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
||||||
child: Row(
|
child: Text(
|
||||||
children: [
|
c.snapshot.schoolTerm!.label,
|
||||||
IconButton(
|
style: TextStyle(
|
||||||
onPressed: term != null && week > 1
|
fontSize: 12,
|
||||||
? () => setState(() => _week = week - 1)
|
color: ink.withValues(alpha: 0.5),
|
||||||
: null,
|
),
|
||||||
icon: const Icon(Icons.chevron_left_rounded),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: Column(
|
const SizedBox(height: 10),
|
||||||
children: [
|
SurfaceCard(
|
||||||
Text(
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),
|
||||||
term == null ? '本周' : '第 $week 周',
|
child: Row(
|
||||||
style: const TextStyle(
|
children: [
|
||||||
fontSize: 17,
|
IconButton(
|
||||||
fontWeight: FontWeight.w700,
|
onPressed: term != null && week > 1
|
||||||
|
? () => setState(() => _week = week - 1)
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.chevron_left_rounded),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
term == null ? '本周' : '第 $week 周',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${monday.month}.${monday.day.toString().padLeft(2, '0')} – ${sunday.month}.${sunday.day.toString().padLeft(2, '0')}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: ink.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: term != null && week < term.totalWeeks
|
||||||
|
? () => setState(() => _week = week + 1)
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.chevron_right_rounded),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => setState(() => _week = null),
|
||||||
|
child: const Text('本周'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
if (c.isDemo)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 7,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE2EEFF),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'示例课表',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Color(0xFF356AA8),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
),
|
||||||
Text(
|
const Text(
|
||||||
'${monday.month}.${monday.day.toString().padLeft(2, '0')} – ${sunday.month}.${sunday.day.toString().padLeft(2, '0')}',
|
'不会生成提醒或日历事件',
|
||||||
|
style: TextStyle(fontSize: 10, color: Color(0xFF8193AB)),
|
||||||
|
),
|
||||||
|
TextButton(onPressed: c.leaveDemo, child: const Text('退出示例')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (term != null) ...[
|
||||||
|
WeekGrid(
|
||||||
|
events: ScheduleEngine.occurrences(
|
||||||
|
c.visibleCourses,
|
||||||
|
term,
|
||||||
|
).where((e) => e.week == week).toList(),
|
||||||
|
monday: monday,
|
||||||
|
today: today,
|
||||||
|
settings: c.visibleSettings,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'点击课程查看完整信息',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: ink.withValues(alpha: 0.35),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else if (c.visibleCourses.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 36),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||||
|
child: SurfaceCard(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.event_available_rounded,
|
||||||
|
size: 44,
|
||||||
|
color: river,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
const Text(
|
||||||
|
'课表已导入,请设置学期开始日',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 18,
|
||||||
color: ink.withValues(alpha: 0.5),
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'${c.visibleCourses.length} 条课程安排已保存',
|
||||||
|
style: const TextStyle(fontSize: 13, color: river),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text(
|
||||||
|
'课程按教学周保存。设置真实的学期开始日后,\n才能显示每周课表并安排课前提醒。',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 12, height: 1.8),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) =>
|
||||||
|
SettingsPage(controller: c, focusSemester: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.edit_calendar_rounded, size: 18),
|
||||||
|
label: const Text('设置学期开始日'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
onPressed: term != null && week < term.totalWeeks
|
|
||||||
? () => setState(() => _week = week + 1)
|
|
||||||
: null,
|
|
||||||
icon: const Icon(Icons.chevron_right_rounded),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => setState(() => _week = null),
|
|
||||||
child: const Text('本周'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
if (c.isDemo)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Wrap(
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 7,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFE2EEFF),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'示例课表',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Color(0xFF356AA8),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Text(
|
|
||||||
'不会生成提醒或日历事件',
|
|
||||||
style: TextStyle(fontSize: 10, color: Color(0xFF8193AB)),
|
|
||||||
),
|
|
||||||
TextButton(onPressed: c.leaveDemo, child: const Text('退出示例')),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
] else ...[
|
||||||
if (term != null) ...[
|
const SizedBox(height: 38),
|
||||||
WeekGrid(
|
const Icon(
|
||||||
events: ScheduleEngine.occurrences(
|
Icons.calendar_month_outlined,
|
||||||
c.visibleCourses,
|
size: 66,
|
||||||
term,
|
color: Color(0xFFB7CDF0),
|
||||||
).where((e) => e.week == week).toList(),
|
),
|
||||||
monday: monday,
|
const SizedBox(height: 22),
|
||||||
today: today,
|
const Text(
|
||||||
settings: c.visibleSettings,
|
'让课表,陪你开启每一天',
|
||||||
),
|
textAlign: TextAlign.center,
|
||||||
const SizedBox(height: 12),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||||
Text(
|
),
|
||||||
'点击课程查看完整信息',
|
const SizedBox(height: 12),
|
||||||
textAlign: TextAlign.center,
|
Text(
|
||||||
style: TextStyle(fontSize: 11, color: ink.withValues(alpha: 0.35)),
|
'在「个人设置」设置学期并导入课程,\n或先看看示例课表。',
|
||||||
),
|
textAlign: TextAlign.center,
|
||||||
] else if (c.visibleCourses.isNotEmpty) ...[
|
style: TextStyle(
|
||||||
const SizedBox(height: 36),
|
fontSize: 13,
|
||||||
Padding(
|
height: 1.8,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
color: ink.withValues(alpha: 0.5),
|
||||||
child: SurfaceCard(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.event_available_rounded,
|
|
||||||
size: 44,
|
|
||||||
color: river,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 18),
|
|
||||||
const Text(
|
|
||||||
'课表已导入,请设置学期开始日',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Text(
|
|
||||||
'${c.visibleCourses.length} 条课程安排已保存',
|
|
||||||
style: const TextStyle(fontSize: 13, color: river),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
const Text(
|
|
||||||
'课程按教学周保存。设置真实的学期开始日后,\n才能显示每周课表并安排课前提醒。',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 12, height: 1.8),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: () => Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute<void>(
|
|
||||||
builder: (_) =>
|
|
||||||
SettingsPage(controller: c, focusSemester: true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.edit_calendar_rounded, size: 18),
|
|
||||||
label: const Text('设置学期开始日'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 25),
|
||||||
] else ...[
|
Center(
|
||||||
const SizedBox(height: 38),
|
child: FilledButton.icon(
|
||||||
const Icon(
|
onPressed: c.showDemo,
|
||||||
Icons.calendar_month_outlined,
|
icon: const Icon(Icons.auto_awesome_outlined, size: 18),
|
||||||
size: 66,
|
label: const Text('体验示例课表'),
|
||||||
color: Color(0xFFB7CDF0),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 22),
|
|
||||||
const Text(
|
|
||||||
'让课表,陪你开启每一天',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
'在「个人设置」设置学期并导入课程,\n或先看看示例课表。',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
height: 1.8,
|
|
||||||
color: ink.withValues(alpha: 0.5),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(height: 25),
|
|
||||||
Center(
|
|
||||||
child: FilledButton.icon(
|
|
||||||
onPressed: c.showDemo,
|
|
||||||
icon: const Icon(Icons.auto_awesome_outlined, size: 18),
|
|
||||||
label: const Text('体验示例课表'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class GlassDock extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
|
class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
|
||||||
static const _width = 264.0, _height = 68.0, _inset = 3.0, _travel = 86.0;
|
static const _width = 264.0, _height = 58.0, _inset = 3.0, _travel = 86.0;
|
||||||
static const _blue = Color(0xFF007AFF);
|
static const _blue = Color(0xFF007AFF);
|
||||||
late final AnimationController _motion = AnimationController.unbounded(
|
late final AnimationController _motion = AnimationController.unbounded(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
@@ -294,7 +294,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
|
|||||||
duration: const Duration(milliseconds: 130),
|
duration: const Duration(milliseconds: 130),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 27,
|
size: 25,
|
||||||
color: index == _position.round().clamp(0, 2)
|
color: index == _position.round().clamp(0, 2)
|
||||||
? _blue
|
? _blue
|
||||||
: _blue.withValues(alpha: 0.48),
|
: _blue.withValues(alpha: 0.48),
|
||||||
@@ -310,7 +310,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 2),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: hepan_everyday
|
name: hepan_everyday
|
||||||
description: TPSchedule 学生课表
|
description: TPSchedule 学生课表
|
||||||
publish_to: none
|
publish_to: none
|
||||||
version: 0.5.3+11
|
version: 0.5.4+12
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.13.5
|
sdk: ^3.13.5
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
final glass = find.byType(GlassRefraction);
|
final glass = find.byType(GlassRefraction);
|
||||||
expect(glass, findsNWidgets(2));
|
expect(glass, findsNWidgets(2));
|
||||||
expect(tester.getSize(glass.first), const Size(264, 68));
|
expect(tester.getSize(glass.first), const Size(264, 58));
|
||||||
expect(
|
expect(
|
||||||
tester.widget<GlassRefraction>(glass.first).strength,
|
tester.widget<GlassRefraction>(glass.first).strength,
|
||||||
greaterThan(0),
|
greaterThan(0),
|
||||||
@@ -101,42 +101,41 @@ void main() {
|
|||||||
expect(tester.getSize(lens).width, closeTo(resting.width, 0.5));
|
expect(tester.getSize(lens).width, closeTo(resting.width, 0.5));
|
||||||
expect(tester.getSize(lens).height, closeTo(resting.height, 0.5));
|
expect(tester.getSize(lens).height, closeTo(resting.height, 0.5));
|
||||||
});
|
});
|
||||||
testWidgets(
|
testWidgets('Dock stays stationary and tappable while scrolling timetable', (
|
||||||
'Dock stays visible while sliding down instead of abruptly fading',
|
tester,
|
||||||
(tester) async {
|
) async {
|
||||||
tester.view.physicalSize = const Size(390, 844);
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
final c = uiController();
|
final c = uiController();
|
||||||
await c.initialize();
|
await c.initialize();
|
||||||
await c.showDemo();
|
await c.showDemo();
|
||||||
await tester.pumpWidget(HepanApp(controller: c));
|
await tester.pumpWidget(HepanApp(controller: c));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
final dock = find.byType(GlassDock);
|
final dock = find.byType(GlassDock);
|
||||||
final before = tester.getRect(dock);
|
final before = tester.getRect(dock);
|
||||||
final gesture = await tester.startGesture(
|
final gesture = await tester.startGesture(
|
||||||
tester.getCenter(find.text('周三')),
|
tester.getCenter(find.text('周三')),
|
||||||
);
|
);
|
||||||
await gesture.moveBy(const Offset(0, -80));
|
await gesture.moveBy(const Offset(0, -80));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 120));
|
await tester.pump(const Duration(milliseconds: 120));
|
||||||
final fading = find.ancestor(
|
final fading = find.ancestor(
|
||||||
of: dock,
|
of: dock,
|
||||||
matching: find.byType(FadeTransition),
|
matching: find.byType(FadeTransition),
|
||||||
);
|
);
|
||||||
final opacity = fading.evaluate().isEmpty
|
final opacity = fading.evaluate().isEmpty
|
||||||
? 1.0
|
? 1.0
|
||||||
: tester.widget<FadeTransition>(fading.first).opacity.value;
|
: tester.widget<FadeTransition>(fading.first).opacity.value;
|
||||||
expect(opacity, greaterThan(0.95));
|
expect(opacity, greaterThan(0.95));
|
||||||
expect(tester.getRect(dock).top, greaterThan(before.top));
|
expect(tester.getRect(dock), before);
|
||||||
expect(tester.getRect(dock).top, lessThan(844));
|
expect(tester.getRect(dock).top, lessThan(844));
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(dock.hitTestable(), findsNothing);
|
expect(dock.hitTestable(), findsOneWidget);
|
||||||
c.dispose();
|
c.dispose();
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _dock({bool reduceMotion = false}) {
|
Widget _dock({bool reduceMotion = false}) {
|
||||||
|
|||||||
@@ -9,46 +9,46 @@ import 'navigation_test.dart' show uiController;
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
setUpAll(tzdata.initializeTimeZones);
|
setUpAll(tzdata.initializeTimeZones);
|
||||||
testWidgets(
|
testWidgets('dock drags switch tabs while body swipes only change week', (
|
||||||
'glass cursor drags in both directions, taps work, body swipes do not navigate',
|
tester,
|
||||||
(tester) async {
|
) async {
|
||||||
final c = uiController();
|
final c = uiController();
|
||||||
await c.initialize();
|
await c.initialize();
|
||||||
await c.showDemo();
|
await c.showDemo();
|
||||||
await tester.pumpWidget(HepanApp(controller: c));
|
await tester.pumpWidget(HepanApp(controller: c));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(TimetablePage),
|
of: find.byType(TimetablePage),
|
||||||
matching: find.text('课表'),
|
matching: find.text('课表'),
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
expect(tester.getRect(find.text('周日')).right, lessThanOrEqualTo(800));
|
expect(tester.getRect(find.text('周日')).right, lessThanOrEqualTo(800));
|
||||||
await tester.drag(find.text('周三'), const Offset(-250, 0));
|
await tester.drag(find.text('周三'), const Offset(-250, 0));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('登录学校账户'), findsNothing);
|
expect(find.text('第 2 周'), findsOneWidget);
|
||||||
await tester.drag(
|
expect(find.text('登录学校账户'), findsNothing);
|
||||||
find.byKey(const Key('dock-cursor')),
|
await tester.drag(
|
||||||
const Offset(185, 0),
|
find.byKey(const Key('dock-cursor')),
|
||||||
);
|
const Offset(185, 0),
|
||||||
await tester.pumpAndSettle();
|
);
|
||||||
expect(find.text('登录学校账户'), findsOneWidget);
|
await tester.pumpAndSettle();
|
||||||
await tester.drag(find.text('连接学校课表'), const Offset(250, 0));
|
expect(find.text('登录学校账户'), findsOneWidget);
|
||||||
await tester.pumpAndSettle();
|
await tester.drag(find.text('连接学校课表'), const Offset(250, 0));
|
||||||
expect(find.text('登录学校账户'), findsOneWidget);
|
await tester.pumpAndSettle();
|
||||||
await tester.drag(
|
expect(find.text('登录学校账户'), findsOneWidget);
|
||||||
find.byKey(const Key('dock-cursor')),
|
await tester.drag(
|
||||||
const Offset(-185, 0),
|
find.byKey(const Key('dock-cursor')),
|
||||||
);
|
const Offset(-185, 0),
|
||||||
await tester.pumpAndSettle();
|
);
|
||||||
expect(find.text('示例课表'), findsOneWidget);
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('我的'));
|
expect(find.text('示例课表'), findsOneWidget);
|
||||||
await tester.pumpAndSettle();
|
await tester.tap(find.text('我的'));
|
||||||
expect(find.text('登录学校账户'), findsOneWidget);
|
await tester.pumpAndSettle();
|
||||||
c.dispose();
|
expect(find.text('登录学校账户'), findsOneWidget);
|
||||||
},
|
c.dispose();
|
||||||
);
|
});
|
||||||
testWidgets('cancelled cursor drag keeps selected tab', (tester) async {
|
testWidgets('cancelled cursor drag keeps selected tab', (tester) async {
|
||||||
int selected = 0;
|
int selected = 0;
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ void main() {
|
|||||||
preview: true,
|
preview: true,
|
||||||
);
|
);
|
||||||
// Sample beside the visible label, away from icon glow and glass rims.
|
// Sample beside the visible label, away from icon glow and glass rims.
|
||||||
final selected = _channel(resting, 22, 52, 0);
|
final selected = _channel(resting, 16, 43, 0);
|
||||||
final base = _channel(resting, 160, 52, 0);
|
final base = _channel(resting, 160, 43, 0);
|
||||||
expect(base - selected, greaterThan(20));
|
expect(base - selected, greaterThan(20));
|
||||||
expect((_channel(resting, 22, 52, 2) - selected).abs(), lessThan(12));
|
expect((_channel(resting, 16, 43, 2) - selected).abs(), lessThan(12));
|
||||||
final held = await _render(
|
final held = await _render(
|
||||||
tester,
|
tester,
|
||||||
GlassDock(selectedIndex: 0, onSelected: (_) {}),
|
GlassDock(selectedIndex: 0, onSelected: (_) {}),
|
||||||
@@ -30,7 +30,7 @@ void main() {
|
|||||||
held: true,
|
held: true,
|
||||||
preview: true,
|
preview: true,
|
||||||
);
|
);
|
||||||
expect(_channel(held, 22, 52, 0) - selected, greaterThan(18));
|
expect(_channel(held, 16, 43, 0) - selected, greaterThan(18));
|
||||||
});
|
});
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'glass softens fine background detail without whitening its colour',
|
'glass softens fine background detail without whitening its colour',
|
||||||
@@ -61,12 +61,14 @@ void main() {
|
|||||||
);
|
);
|
||||||
// Below the symbol and away from the rim, the selected lens should retain
|
// Below the symbol and away from the rim, the selected lens should retain
|
||||||
// this saturated cyan backdrop while applying the new resting grey tint.
|
// this saturated cyan backdrop while applying the new resting grey tint.
|
||||||
final red = _channel(pixels, 22, 52, 0);
|
final red = _channel(pixels, 16, 40, 0);
|
||||||
final green = _channel(pixels, 22, 52, 1);
|
final green = _channel(pixels, 16, 40, 1);
|
||||||
final blue = _channel(pixels, 22, 52, 2);
|
final blue = _channel(pixels, 16, 40, 2);
|
||||||
expect(red, lessThan(85));
|
expect(red, lessThan(85));
|
||||||
expect(green - red, greaterThan(95));
|
expect(green - red, greaterThan(95));
|
||||||
expect(blue - red, greaterThan(140));
|
// The strongest resting tint (34%) and base veil (5%) retain about 63%
|
||||||
|
// of the source's 210-level blue/red contrast, including the shorter rim.
|
||||||
|
expect(blue - red, greaterThan(130));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
for (final start in [32, 160]) {
|
for (final start in [32, 160]) {
|
||||||
final reds = [
|
final reds = [
|
||||||
for (var x = start; x < start + 20; x++) _channel(pixels, x, 60, 0),
|
for (var x = start; x < start + 20; x++) _channel(pixels, x, 50, 0),
|
||||||
];
|
];
|
||||||
final contrast =
|
final contrast =
|
||||||
reds.reduce((a, b) => a > b ? a : b) -
|
reds.reduce((a, b) => a > b ? a : b) -
|
||||||
@@ -104,7 +106,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
for (final start in [24, 160]) {
|
for (final start in [24, 160]) {
|
||||||
final reds = [
|
final reds = [
|
||||||
for (var x = start; x < start + 40; x++) _channel(pixels, x, 60, 0),
|
for (var x = start; x < start + 40; x++) _channel(pixels, x, 50, 0),
|
||||||
];
|
];
|
||||||
final contrast =
|
final contrast =
|
||||||
reds.reduce((a, b) => a > b ? a : b) -
|
reds.reduce((a, b) => a > b ? a : b) -
|
||||||
@@ -173,6 +175,7 @@ Future<ByteData> _render(
|
|||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
final key = GlobalKey(), glassKey = GlobalKey();
|
final key = GlobalKey(), glassKey = GlobalKey();
|
||||||
|
final height = glass is GlassDock ? 58 : 68;
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
RepaintBoundary(
|
RepaintBoundary(
|
||||||
key: key,
|
key: key,
|
||||||
@@ -183,7 +186,7 @@ Future<ByteData> _render(
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
key: glassKey,
|
key: glassKey,
|
||||||
width: 264,
|
width: 264,
|
||||||
height: 68,
|
height: height.toDouble(),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
@@ -235,11 +238,11 @@ Future<ByteData> _render(
|
|||||||
ui.Canvas(recorder).drawImageRect(
|
ui.Canvas(recorder).drawImageRect(
|
||||||
image,
|
image,
|
||||||
region,
|
region,
|
||||||
const Rect.fromLTWH(0, 0, 792, 204),
|
Rect.fromLTWH(0, 0, 792, height * 3.0),
|
||||||
Paint()..filterQuality = FilterQuality.high,
|
Paint()..filterQuality = FilterQuality.high,
|
||||||
);
|
);
|
||||||
final picture = recorder.endRecording();
|
final picture = recorder.endRecording();
|
||||||
final previewImage = await picture.toImage(792, 204);
|
final previewImage = await picture.toImage(792, height * 3);
|
||||||
final png = (await previewImage.toByteData(
|
final png = (await previewImage.toByteData(
|
||||||
format: ui.ImageByteFormat.png,
|
format: ui.ImageByteFormat.png,
|
||||||
))!;
|
))!;
|
||||||
@@ -254,8 +257,8 @@ Future<ByteData> _render(
|
|||||||
previewImage.dispose();
|
previewImage.dispose();
|
||||||
picture.dispose();
|
picture.dispose();
|
||||||
}
|
}
|
||||||
final cropped = ByteData(264 * 68 * 4);
|
final cropped = ByteData(264 * height * 4);
|
||||||
for (var y = 0; y < 68; y++) {
|
for (var y = 0; y < height; y++) {
|
||||||
for (var x = 0; x < 264; x++) {
|
for (var x = 0; x < 264; x++) {
|
||||||
final source =
|
final source =
|
||||||
((region.top.round() + y) * image.width + region.left.round() + x) *
|
((region.top.round() + y) * image.width + region.left.round() + x) *
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import 'navigation_test.dart' show uiController;
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
setUpAll(tzdata.initializeTimeZones);
|
setUpAll(tzdata.initializeTimeZones);
|
||||||
testWidgets('three icon tabs and vertical scroll hide and restore Dock', (
|
testWidgets('three tabs remain reachable while pages scroll', (tester) async {
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
tester.view.physicalSize = const Size(390, 844);
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
@@ -30,7 +28,7 @@ void main() {
|
|||||||
expect(find.bySemanticsLabel('考试'), findsOneWidget);
|
expect(find.bySemanticsLabel('考试'), findsOneWidget);
|
||||||
await tester.drag(find.text('周三'), const Offset(0, -200));
|
await tester.drag(find.text('周三'), const Offset(0, -200));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.bySemanticsLabel('考试').hitTestable(), findsNothing);
|
expect(find.bySemanticsLabel('考试').hitTestable(), findsOneWidget);
|
||||||
await tester.drag(find.byType(ListView).first, const Offset(0, 100));
|
await tester.drag(find.byType(ListView).first, const Offset(0, 100));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.bySemanticsLabel('考试').hitTestable(), findsOneWidget);
|
expect(find.bySemanticsLabel('考试').hitTestable(), findsOneWidget);
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:timezone/data/latest.dart' as tzdata;
|
||||||
|
import 'package:hepan_everyday/app/app_controller.dart';
|
||||||
|
import 'package:hepan_everyday/domain/app_snapshot.dart';
|
||||||
|
import 'package:hepan_everyday/domain/app_settings.dart';
|
||||||
|
import 'package:hepan_everyday/domain/schedule_engine.dart';
|
||||||
|
import 'package:hepan_everyday/features/timetable/timetable_page.dart';
|
||||||
|
|
||||||
|
import '../support/fakes.dart';
|
||||||
|
|
||||||
|
AppController _controller({bool hasSemester = true}) {
|
||||||
|
Course lesson(int week) => Course(
|
||||||
|
id: 'week-$week',
|
||||||
|
name: '第$week周课程',
|
||||||
|
teacher: '测试教师',
|
||||||
|
room: 'A101',
|
||||||
|
weekday: 3,
|
||||||
|
startPeriod: 1,
|
||||||
|
endPeriod: 2,
|
||||||
|
weeks: {week},
|
||||||
|
source: CourseSource.file,
|
||||||
|
);
|
||||||
|
return AppController(
|
||||||
|
MemoryStore(
|
||||||
|
AppSnapshot(
|
||||||
|
courses: [lesson(1), lesson(2), lesson(3)],
|
||||||
|
settings: AppSettings(
|
||||||
|
semester: hasSemester
|
||||||
|
? Semester(startDate: DateTime(2026, 10, 12), totalWeeks: 3)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FakeNotifications(),
|
||||||
|
FakeCalendar(),
|
||||||
|
() => DateTime.utc(2026, 10, 14),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _open(WidgetTester tester, AppController controller) async {
|
||||||
|
await controller.initialize();
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(body: TimetablePage(controller: controller)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUpAll(tzdata.initializeTimeZones);
|
||||||
|
testWidgets(
|
||||||
|
'horizontal course swipe changes week and course taps still open',
|
||||||
|
(tester) async {
|
||||||
|
await _open(tester, _controller());
|
||||||
|
expect(find.text('第 1 周'), findsOneWidget);
|
||||||
|
expect(find.text('第1周课程'), findsOneWidget);
|
||||||
|
await tester.drag(find.text('第1周课程'), const Offset(-180, 0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('第 2 周'), findsOneWidget);
|
||||||
|
expect(find.text('第1周课程'), findsNothing);
|
||||||
|
expect(find.text('第2周课程'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('第2周课程'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('知道了'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('知道了'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.drag(find.text('周三'), const Offset(180, 0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('第 1 周'), findsOneWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
testWidgets('one swipe advances one week and stops at semester boundaries', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await _open(tester, _controller());
|
||||||
|
await tester.drag(find.text('周三'), const Offset(300, 0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('第 1 周'), findsOneWidget);
|
||||||
|
for (final week in [2, 3, 3]) {
|
||||||
|
await tester.drag(find.text('周三'), const Offset(-300, 0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('第 $week 周'), findsOneWidget);
|
||||||
|
}
|
||||||
|
await tester.tap(find.text('本周'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('第 1 周'), findsOneWidget);
|
||||||
|
});
|
||||||
|
testWidgets('swiping an imported timetable without a semester keeps prompt', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await _open(tester, _controller(hasSemester: false));
|
||||||
|
await tester.drag(find.byType(TimetablePage), const Offset(-180, 0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('课表已导入,请设置学期开始日'), findsOneWidget);
|
||||||
|
expect(find.text('第 2 周'), findsNothing);
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user