import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../app/app_controller.dart'; import '../../app/app_theme.dart'; import '../../domain/app_settings.dart'; import '../../domain/semester.dart'; import '../../services/notification_service.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({ super.key, required this.controller, this.focusSemester = false, }); final AppController controller; final bool focusSemester; @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State { late TextEditingController _weeks; DateTime? _start; double? _courseHeightDraft; bool _savingHeight = false; String? _weeksError, _dateError; final _scroll = ScrollController(); @override void initState() { super.initState(); _start = widget.controller.snapshot.settings.semester?.startDate; _weeks = TextEditingController( text: '${widget.controller.snapshot.settings.semester?.totalWeeks ?? 20}', ); if (widget.focusSemester) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && _scroll.hasClients) { _scroll.jumpTo(_scroll.position.maxScrollExtent); } }); } } @override void dispose() { _weeks.dispose(); _scroll.dispose(); super.dispose(); } Future _calendar(bool enabled) async { try { await _calendarAction(enabled); } catch (_) { if (mounted) { _notice('日历暂时不可用,请检查权限后重试'); } } } Future _calendarAction(bool enabled) async { final c = widget.controller; if (enabled) { if (!await c.calendar.requestAccess()) { if (mounted) _notice('请允许日历访问后重试'); return; } final calendars = await c.calendar.listWritableCalendars(); if (!mounted) return; if (calendars.isEmpty) { _notice('手机中没有可写入的日历,请先在日历应用中添加日历'); return; } final id = await showDialog( context: context, builder: (context) => SimpleDialog( title: const Text('选择手机日历'), children: calendars .map( (cal) => SimpleDialogOption( onPressed: () => Navigator.pop(context, cal.id), child: Text(cal.name), ), ) .toList(), ), ); if (id != null) { await c.enableCalendarFromUserAction(id); } } else { final delete = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('关闭日历同步'), content: const Text('是否删除 TPSchedule 创建的课程日程?其他日程不会受影响。'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('保留日程'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('删除课程日程'), ), ], ), ); if (delete != null) { await c.disableCalendar(deleteEvents: delete); } } if (mounted && c.calendarSyncError != null) { _notice(c.calendarSyncError!); } } Future _saveCourseHeight(double value) async { setState(() => _savingHeight = true); await widget.controller.updateSettings( (current) => current.copyWith(courseHeightScale: value), ); if (!mounted) return; setState(() { _courseHeightDraft = null; _savingHeight = false; }); if (widget.controller.lastError != null) { _notice(widget.controller.lastError!); } } void _notice(String text) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text))); @override Widget build(BuildContext context) => AnimatedBuilder( animation: widget.controller, builder: (context, _) { final c = widget.controller, s = c.snapshot.settings; return Scaffold( appBar: AppBar( title: const Text( '课表设置', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w700), ), ), body: ListView( controller: _scroll, padding: const EdgeInsets.fromLTRB(20, 12, 20, 40), children: [ if (c.isDemo) const Padding( padding: EdgeInsets.only(bottom: 18), child: Text( '示例模式中,提醒与日历功能不会应用到示例课程。', style: TextStyle(fontSize: 12, color: river), ), ), _heading('课表配色'), SurfaceCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 10, runSpacing: 8, children: [ ChoiceChip( label: const Text('多彩'), selected: s.colorMode == ColorMode.multicolor, onSelected: (_) => c.updateSettings( (current) => current.copyWith(colorMode: ColorMode.multicolor), ), ), ChoiceChip( label: const Text('单色'), selected: s.colorMode == ColorMode.single, onSelected: (_) => c.updateSettings( (current) => current.copyWith(colorMode: ColorMode.single), ), ), ], ), const SizedBox(height: 15), Wrap( spacing: 14, runSpacing: 12, children: [ for (final color in [ 0xFF348F91, 0xFF6E83B1, 0xFFB18A6E, 0xFF8C789E, ]) Semantics( label: '选择课表颜色', button: true, child: InkWell( onTap: () => c.updateSettings( (current) => current.copyWith( singleColorArgb: color, colorMode: ColorMode.single, ), ), borderRadius: BorderRadius.circular(30), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: Color(color), shape: BoxShape.circle, border: Border.all( color: s.singleColorArgb == color ? ink : Colors.transparent, width: 2, ), ), child: s.singleColorArgb == color ? const Icon( Icons.check_rounded, color: Colors.white, size: 20, ) : null, ), ), ), ], ), ], ), ), const SizedBox(height: 22), _heading('课程条长度'), SurfaceCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( '${((_courseHeightDraft ?? s.courseHeightScale) * 100).round()}%', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: river, ), ), ), TextButton( onPressed: _savingHeight ? null : () => _saveCourseHeight( AppSettings.defaultCourseHeightScale, ), child: const Text('恢复默认'), ), ], ), Slider( key: const Key('course-height-slider'), value: _courseHeightDraft ?? s.courseHeightScale, min: AppSettings.minCourseHeightScale, max: AppSettings.maxCourseHeightScale, divisions: 24, label: '${((_courseHeightDraft ?? s.courseHeightScale) * 100).round()}%', semanticFormatterCallback: (value) => '${(value * 100).round()}%', onChanged: _savingHeight ? null : (value) => setState(() => _courseHeightDraft = value), onChangeEnd: _saveCourseHeight, ), const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('较短', style: TextStyle(fontSize: 11)), Text('较长', style: TextStyle(fontSize: 11)), ], ), const SizedBox(height: 10), Text( '统一调整课程条长度,老师和教室始终完整显示。', style: TextStyle( fontSize: 11, height: 1.6, color: ink.withValues(alpha: 0.55), ), ), ], ), ), const SizedBox(height: 22), _heading('课前提醒'), SurfaceCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile.adaptive( contentPadding: EdgeInsets.zero, title: const Text('提醒我去上课', style: TextStyle(fontSize: 15)), value: s.remindersEnabled, onChanged: c.isDemo ? null : (v) async { if (v) { await c.enableRemindersFromUserAction(); } else { await c.updateSettings( (current) => current.copyWith(remindersEnabled: false), ); } }, ), const SizedBox(height: 5), Wrap( spacing: 6, runSpacing: 7, children: [ for (final minutes in AppSettings.allowedReminderMinutes) FilterChip( label: Text('$minutes 分钟'), selected: s.reminderOffsets.contains(minutes), onSelected: c.isDemo ? null : (selected) { if (!selected && s.reminderOffsets.length == 1) { _notice('请至少保留一个提醒时间'); return; } c.updateSettings( (current) => current.copyWith( reminderOffsets: [ for (final value in current.reminderOffsets) if (value != minutes) value, if (selected) minutes, ], ), ); }, ), ], ), const SizedBox(height: 10), Text( c.reminderResult.message.isNotEmpty ? c.reminderResult.message : c.reminderResult.status == ReminderStatus.permissionDenied ? '尚未允许通知权限,点击下方按钮开启。' : '可多选提前时间,连续课程按每个选中的时间提醒。', style: TextStyle( fontSize: 11, height: 1.6, color: ink.withValues(alpha: 0.5), ), ), if (!c.isDemo && s.remindersEnabled) TextButton( onPressed: c.enableRemindersFromUserAction, child: const Text('允许课前提醒'), ), ], ), ), const SizedBox(height: 22), _heading('手机日历'), SurfaceCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile.adaptive( contentPadding: EdgeInsets.zero, title: const Text('导入手机日历', style: TextStyle(fontSize: 15)), subtitle: const Text( '按实际上课周次同步,不重复提醒', style: TextStyle(fontSize: 11), ), value: s.calendarEnabled, onChanged: c.isDemo ? null : _calendar, ), if (c.calendarSyncError != null) Text( c.calendarSyncError!, style: const TextStyle( fontSize: 12, color: Color(0xFFB45D54), ), ), if (c.snapshot.calendarCleanupPending) TextButton( onPressed: c.retryCalendarCleanup, child: const Text('重试清理课程日程'), ), ], ), ), const SizedBox(height: 22), _heading('学期安排'), SurfaceCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( contentPadding: EdgeInsets.zero, title: const Text('学期开始日', style: TextStyle(fontSize: 15)), subtitle: Text( _start == null ? '请选择学校实际开学日期' : '${_start!.year} 年 ${_start!.month} 月 ${_start!.day} 日', style: const TextStyle(fontSize: 12), ), trailing: const Icon( Icons.date_range_outlined, color: river, ), onTap: () async { final date = await showDatePicker( context: context, initialDate: _start ?? c.clock(), firstDate: DateTime(2020), lastDate: DateTime(2040), ); if (date != null && mounted) { setState(() => _start = date); } }, ), if (_dateError != null) Text( _dateError!, style: const TextStyle( fontSize: 12, color: Color(0xFFB45D54), ), ), const SizedBox(height: 16), TextField( key: const Key('semester-weeks'), controller: _weeks, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(2), ], decoration: InputDecoration( labelText: '学期总周数', hintText: '1–30', errorText: _weeksError, ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: FilledButton( onPressed: () async { final weeks = int.tryParse(_weeks.text); setState(() { _weeksError = weeks == null || weeks < 1 || weeks > 30 ? '周数应为 1–30' : null; _dateError = _start == null ? '请先选择学期开始日' : null; }); if (_weeksError != null || _dateError != null) return; await c.updateSettings( (current) => current.copyWith( semester: Semester( startDate: _start!, totalWeeks: weeks!, ), ), ); if (mounted) { _notice(c.lastError ?? '学期设置已保存'); } }, child: const Text('保存学期设置'), ), ), const SizedBox(height: 12), Text( '开始日所在的周一至周日为第 1 周。开始日前不会生成课程。', style: TextStyle( fontSize: 11, height: 1.6, color: ink.withValues(alpha: 0.45), ), ), ], ), ), ], ), ); }, ); Widget _heading(String text) => Padding( padding: const EdgeInsets.only(left: 4, bottom: 10), child: Text( text, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), ), ); }