feat: add elastic glass dock motion and Android tablet layouts
This commit is contained in:
@@ -7,6 +7,7 @@ import '../../domain/app_settings.dart';
|
||||
import '../../domain/semester.dart';
|
||||
import '../../services/notification_service.dart';
|
||||
import 'reminder_tools.dart';
|
||||
import '../../widgets/adaptive_page.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({
|
||||
@@ -146,362 +147,378 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: AdaptivePage(
|
||||
maxWidth: 760,
|
||||
child: 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),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
_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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
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,
|
||||
),
|
||||
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),
|
||||
if (!c.isDemo) ReminderTools(controller: c),
|
||||
if (!c.isDemo) 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!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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),
|
||||
if (!c.isDemo) ReminderTools(controller: c),
|
||||
if (!c.isDemo) 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 (mounted) {
|
||||
_notice(c.lastError ?? '学期设置已保存');
|
||||
if (date != null && mounted) {
|
||||
setState(() => _start = date);
|
||||
}
|
||||
},
|
||||
child: const Text('保存学期设置'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'开始日所在的周一至周日为第 1 周。开始日前不会生成课程。',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
height: 1.6,
|
||||
color: ink.withValues(alpha: 0.45),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user