Make compact Dock persistent and support week swipes

This commit is contained in:
Codex
2026-10-14 09:49:53 +08:00
parent fe23c65fd9
commit caf7abe0c0
11 changed files with 474 additions and 363 deletions
+219 -190
View File
@@ -17,6 +17,7 @@ class TimetablePage extends StatefulWidget {
class _TimetablePageState extends State<TimetablePage> {
int? _week;
bool? _wasDemo;
double _weekDrag = 0;
@override
Widget build(BuildContext context) {
final c = widget.controller, term = c.visibleSettings.semester;
@@ -33,214 +34,242 @@ class _TimetablePageState extends State<TimetablePage> {
? today.subtract(Duration(days: today.weekday - 1))
: term.firstMonday.add(Duration(days: (week - 1) * 7));
final sunday = monday.add(const Duration(days: 6));
return ListView(
key: const PageStorageKey('timetable-scroll'),
padding: EdgeInsets.fromLTRB(
8,
10,
8,
140 + MediaQuery.paddingOf(context).bottom,
),
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 14),
child: Row(
children: [
const Expanded(
child: Text(
'课表',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
),
),
IconButton(
tooltip: '刷新课表',
onPressed: widget.onSync,
icon: const Icon(Icons.sync_rounded),
),
],
),
return GestureDetector(
onHorizontalDragStart: term == null ? null : (_) => _weekDrag = 0,
onHorizontalDragUpdate: term == null
? null
: (details) => _weekDrag += details.delta.dx,
onHorizontalDragCancel: term == null ? null : () => _weekDrag = 0,
onHorizontalDragEnd: term == null
? null
: (details) {
final velocity = details.primaryVelocity ?? 0;
if (_weekDrag.abs() < 48 && velocity.abs() < 400) return;
final direction = _weekDrag.abs() >= 48 ? _weekDrag : velocity;
final next = (week + (direction < 0 ? 1 : -1)).clamp(
1,
term.totalWeeks,
);
if (next != week) setState(() => _week = next);
},
child: ListView(
key: const PageStorageKey('timetable-scroll'),
padding: EdgeInsets.fromLTRB(
8,
10,
8,
140 + MediaQuery.paddingOf(context).bottom,
),
if (!c.isDemo && c.snapshot.schoolTerm != null)
children: [
Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
child: Text(
c.snapshot.schoolTerm!.label,
style: TextStyle(fontSize: 12, color: ink.withValues(alpha: 0.5)),
padding: EdgeInsets.symmetric(horizontal: 14),
child: Row(
children: [
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),
SurfaceCard(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),
child: Row(
children: [
IconButton(
onPressed: term != null && week > 1
? () => setState(() => _week = week - 1)
: null,
icon: const Icon(Icons.chevron_left_rounded),
if (!c.isDemo && c.snapshot.schoolTerm != null)
Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
child: Text(
c.snapshot.schoolTerm!.label,
style: TextStyle(
fontSize: 12,
color: ink.withValues(alpha: 0.5),
),
),
Expanded(
child: Column(
children: [
Text(
term == null ? '本周' : '第 $week 周',
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
),
const SizedBox(height: 10),
SurfaceCard(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),
child: Row(
children: [
IconButton(
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(
'${monday.month}.${monday.day.toString().padLeft(2, '0')} – ${sunday.month}.${sunday.day.toString().padLeft(2, '0')}',
),
const Text(
'不会生成提醒或日历事件',
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(
fontSize: 12,
color: ink.withValues(alpha: 0.5),
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('设置学期开始日'),
),
],
),
),
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('退出示例')),
],
),
),
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(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('设置学期开始日'),
),
],
] else ...[
const SizedBox(height: 38),
const Icon(
Icons.calendar_month_outlined,
size: 66,
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),
),
),
),
] else ...[
const SizedBox(height: 38),
const Icon(
Icons.calendar_month_outlined,
size: 66,
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('体验示例课表'),
),
),
),
const SizedBox(height: 25),
Center(
child: FilledButton.icon(
onPressed: c.showDemo,
icon: const Icon(Icons.auto_awesome_outlined, size: 18),
label: const Text('体验示例课表'),
),
),
],
],
],
),
);
}
}