299 lines
11 KiB
Dart
299 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import '../../app/app_controller.dart';
|
||
import '../../app/app_theme.dart';
|
||
import '../../domain/schedule_engine.dart';
|
||
import '../../domain/school_identity.dart';
|
||
import '../../widgets/directional_week_transition.dart';
|
||
import '../profile/settings_page.dart';
|
||
import 'week_grid.dart';
|
||
|
||
class TimetablePage extends StatefulWidget {
|
||
const TimetablePage({super.key, required this.controller, this.onSync});
|
||
final AppController controller;
|
||
final VoidCallback? onSync;
|
||
@override
|
||
State<TimetablePage> createState() => _TimetablePageState();
|
||
}
|
||
|
||
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;
|
||
final today = ScheduleEngine.schoolDate(c.clock());
|
||
if (_wasDemo != c.isDemo) {
|
||
_week = null;
|
||
_wasDemo = c.isDemo;
|
||
}
|
||
final current = term == null
|
||
? 1
|
||
: (ScheduleEngine.weekNumber(term, today) ?? 1);
|
||
final week = (_week ?? current).clamp(1, term?.totalWeeks ?? 1);
|
||
final monday = term == null
|
||
? today.subtract(Duration(days: today.weekday - 1))
|
||
: term.firstMonday.add(Duration(days: (week - 1) * 7));
|
||
final sunday = monday.add(const Duration(days: 6));
|
||
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,
|
||
),
|
||
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),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (!c.isDemo && c.snapshot.schoolTerm != null)
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
||
child: Text(
|
||
'${c.snapshot.schoolTerm!.school.label} · ${c.snapshot.schoolTerm!.label}',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: ink.withValues(alpha: 0.5),
|
||
),
|
||
),
|
||
),
|
||
if (!c.isDemo &&
|
||
c.snapshot.schoolTerm != null &&
|
||
c.snapshot.selectedSchool != null &&
|
||
c.snapshot.selectedSchool != c.snapshot.schoolTerm!.school)
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
||
child: Text(
|
||
'当前仍为${c.snapshot.schoolTerm!.school.label}课表;新课表导入成功后才会替换。',
|
||
style: const TextStyle(fontSize: 12, color: river),
|
||
),
|
||
),
|
||
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 Text(
|
||
'不会生成提醒或日历事件',
|
||
style: TextStyle(fontSize: 10, color: Color(0xFF8193AB)),
|
||
),
|
||
TextButton(onPressed: c.leaveDemo, child: const Text('退出示例')),
|
||
],
|
||
),
|
||
),
|
||
if (term != null) ...[
|
||
DirectionalWeekTransition(
|
||
key: ValueKey((c.isDemo, term.firstMonday, term.totalWeeks)),
|
||
week: week,
|
||
child: WeekGrid(
|
||
events: ScheduleEngine.occurrences(
|
||
c.visibleCourses,
|
||
term,
|
||
).where((e) => e.week == week).toList(),
|
||
monday: monday,
|
||
today: today,
|
||
settings: c.visibleSettings,
|
||
periodCount:
|
||
!c.isDemo &&
|
||
c.snapshot.schoolTerm?.school ==
|
||
SupportedSchool.uestcUndergraduate
|
||
? 12
|
||
: 11,
|
||
),
|
||
),
|
||
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),
|
||
),
|
||
),
|
||
const SizedBox(height: 25),
|
||
Center(
|
||
child: FilledButton.icon(
|
||
onPressed: c.showDemo,
|
||
icon: const Icon(Icons.auto_awesome_outlined, size: 18),
|
||
label: const Text('体验示例课表'),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|