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}); final AppController controller; @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State { late TextEditingController _weeks; DateTime? _start; String? _weeksError, _dateError; @override void initState() { super.initState(); _start = widget.controller.snapshot.settings.semester?.startDate; _weeks = TextEditingController( text: '${widget.controller.snapshot.settings.semester?.totalWeeks ?? 20}', ); } @override void dispose() { _weeks.dispose(); super.dispose(); } Future _calendar(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('是否删除河畔创建的课程日程?其他日程不会受影响。'), 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!); } } 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( 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( s.copyWith(colorMode: ColorMode.multicolor), ), ), ChoiceChip( label: const Text('单色'), selected: s.colorMode == ColorMode.single, onSelected: (_) => c.updateSettings( s.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( s.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: [ 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( s.copyWith(remindersEnabled: false), ); } }, ), const SizedBox(height: 5), Wrap( spacing: 6, runSpacing: 7, children: [ for (final minutes in [5, 10, 15, 30, 60]) ChoiceChip( label: Text('$minutes 分钟'), selected: s.reminderMinutes == minutes, onSelected: c.isDemo ? null : (_) => c.updateSettings( s.copyWith(reminderMinutes: 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( s.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), ), ); }