feat: add elastic glass dock motion and Android tablet layouts
This commit is contained in:
@@ -5,6 +5,7 @@ import '../../app/app_theme.dart';
|
||||
import '../../services/diagnostic_log.dart';
|
||||
import '../../services/school_page_diagnostic.dart';
|
||||
import 'feedback_submission.dart';
|
||||
import '../../widgets/adaptive_page.dart';
|
||||
|
||||
typedef FeedbackAttachmentPicker = Future<FeedbackAttachment?> Function();
|
||||
|
||||
@@ -170,287 +171,292 @@ class _FeedbackPageState extends State<FeedbackPage> {
|
||||
canPop: !_submitting,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: const Text('反馈与共建')),
|
||||
body: _success
|
||||
? _successView(context)
|
||||
: _uncertain
|
||||
? _uncertainView()
|
||||
: Form(
|
||||
key: _form,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(22),
|
||||
children: [
|
||||
SegmentedButton<FeedbackKind>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: FeedbackKind.problem,
|
||||
label: Text('反馈问题和建议'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: FeedbackKind.development,
|
||||
label: Text('我要参与开发'),
|
||||
),
|
||||
],
|
||||
selected: {_kind},
|
||||
showSelectedIcon: false,
|
||||
onSelectionChanged: _submitting
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_kind = value.single;
|
||||
_pendingSubmission = null;
|
||||
_error = null;
|
||||
});
|
||||
_form.currentState?.reset();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_kind == FeedbackKind.problem
|
||||
? '告诉我们哪里需要改进'
|
||||
: '一起让课表变得更好',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
body: AdaptivePage(
|
||||
maxWidth: 760,
|
||||
child: _success
|
||||
? _successView(context)
|
||||
: _uncertain
|
||||
? _uncertainView()
|
||||
: Form(
|
||||
key: _form,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(22),
|
||||
children: [
|
||||
SegmentedButton<FeedbackKind>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: FeedbackKind.problem,
|
||||
label: Text('反馈问题和建议'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_kind == FeedbackKind.problem
|
||||
? '请描述遇到的问题或你的建议。联系方式和图片、视频都可以选填。'
|
||||
: '请留下你的学校和联系方式,方便开发者与你联系。',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.7,
|
||||
color: ink.withValues(alpha: 0.6),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: FeedbackKind.development,
|
||||
label: Text('我要参与开发'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (_kind == FeedbackKind.problem)
|
||||
TextFormField(
|
||||
key: const Key('feedback-message'),
|
||||
controller: _message,
|
||||
enabled: !_submitting,
|
||||
minLines: 4,
|
||||
maxLines: 8,
|
||||
maxLength: 5000,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '问题或建议(必填)',
|
||||
alignLabelWithHint: true,
|
||||
],
|
||||
selected: {_kind},
|
||||
showSelectedIcon: false,
|
||||
onSelectionChanged: _submitting
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_kind = value.single;
|
||||
_pendingSubmission = null;
|
||||
_error = null;
|
||||
});
|
||||
_form.currentState?.reset();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_kind == FeedbackKind.problem
|
||||
? '告诉我们哪里需要改进'
|
||||
: '一起让课表变得更好',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
onChanged: _changed,
|
||||
validator: (value) =>
|
||||
(value?.trim().isEmpty ?? true)
|
||||
? '请填写问题或建议'
|
||||
: null,
|
||||
)
|
||||
else
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_kind == FeedbackKind.problem
|
||||
? '请描述遇到的问题或你的建议。联系方式和图片、视频都可以选填。'
|
||||
: '请留下你的学校和联系方式,方便开发者与你联系。',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.7,
|
||||
color: ink.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (_kind == FeedbackKind.problem)
|
||||
TextFormField(
|
||||
key: const Key('feedback-message'),
|
||||
controller: _message,
|
||||
enabled: !_submitting,
|
||||
minLines: 4,
|
||||
maxLines: 8,
|
||||
maxLength: 5000,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '问题或建议(必填)',
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
onChanged: _changed,
|
||||
validator: (value) =>
|
||||
(value?.trim().isEmpty ?? true)
|
||||
? '请填写问题或建议'
|
||||
: null,
|
||||
)
|
||||
else
|
||||
TextFormField(
|
||||
key: const Key('feedback-school'),
|
||||
controller: _school,
|
||||
enabled: !_submitting,
|
||||
maxLength: 200,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '学校(必填)',
|
||||
),
|
||||
onChanged: _changed,
|
||||
validator: (value) =>
|
||||
(value?.trim().isEmpty ?? true)
|
||||
? '请填写学校'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
TextFormField(
|
||||
key: const Key('feedback-school'),
|
||||
controller: _school,
|
||||
key: const Key('feedback-contact'),
|
||||
controller: _contact,
|
||||
enabled: !_submitting,
|
||||
maxLength: 200,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '学校(必填)',
|
||||
decoration: InputDecoration(
|
||||
labelText: _kind == FeedbackKind.problem
|
||||
? '联系方式(选填)'
|
||||
: '联系方式(必填)',
|
||||
hintText: '微信、QQ、邮箱等',
|
||||
),
|
||||
onChanged: _changed,
|
||||
validator: (value) =>
|
||||
(value?.trim().isEmpty ?? true)
|
||||
? '请填写学校'
|
||||
_kind == FeedbackKind.development &&
|
||||
(value?.trim().isEmpty ?? true)
|
||||
? '请填写联系方式'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
TextFormField(
|
||||
key: const Key('feedback-contact'),
|
||||
controller: _contact,
|
||||
enabled: !_submitting,
|
||||
maxLength: 200,
|
||||
decoration: InputDecoration(
|
||||
labelText: _kind == FeedbackKind.problem
|
||||
? '联系方式(选填)'
|
||||
: '联系方式(必填)',
|
||||
hintText: '微信、QQ、邮箱等',
|
||||
),
|
||||
onChanged: _changed,
|
||||
validator: (value) =>
|
||||
_kind == FeedbackKind.development &&
|
||||
(value?.trim().isEmpty ?? true)
|
||||
? '请填写联系方式'
|
||||
: null,
|
||||
),
|
||||
if (_kind == FeedbackKind.problem) ...[
|
||||
CheckboxListTile(
|
||||
key: const Key('feedback-logs'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('附带近10分钟诊断日志'),
|
||||
subtitle: const Text(
|
||||
'包含登录跳转、课表读取及错误状态,帮助排查问题;不包含密码、验证码或课表内容。',
|
||||
),
|
||||
value: _includeLogs,
|
||||
onChanged: _submitting
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_includeLogs = value ?? false;
|
||||
if (!_includeLogs) _includeHtml = false;
|
||||
_pendingSubmission = null;
|
||||
_error = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
key: const Key('feedback-html'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('附带最近一次学校导入页面HTML'),
|
||||
subtitle: const Text(
|
||||
'适用于本科和研究生课表、考试导入。需先勾选日志。网页可能含姓名、学号、课程、教师和考场信息,仅保留最近10分钟,单独作为邮件附件发送。',
|
||||
),
|
||||
value: _includeHtml,
|
||||
onChanged: _submitting || !_includeLogs
|
||||
? null
|
||||
: (value) async {
|
||||
var accepted = value ?? false;
|
||||
if (accepted) {
|
||||
accepted =
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('附带网页排查资料?'),
|
||||
content: const Text(
|
||||
'附件可能包含你的姓名、学号、课程及考试信息,将发送给开发者用于排查导入问题。密码、验证码、Cookie和认证参数不会包含在附件中。',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.pop(
|
||||
context,
|
||||
false,
|
||||
),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () =>
|
||||
Navigator.pop(
|
||||
context,
|
||||
true,
|
||||
),
|
||||
child: const Text('同意附带'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_includeHtml = accepted;
|
||||
_pendingSubmission = null;
|
||||
_error = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'图片或视频(选填)',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'最多 ${widget.limits.maxAttachments} 个,每个 ${_mb(widget.limits.maxFileBytes)} MB,合计 ${_mb(widget.limits.maxTotalBytes)} MB',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: ink.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
for (var i = 0; i < _attachments.length; i++)
|
||||
ListTile(
|
||||
if (_kind == FeedbackKind.problem) ...[
|
||||
CheckboxListTile(
|
||||
key: const Key('feedback-logs'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
_attachments[i].contentType.startsWith('image/')
|
||||
? Icons.image_outlined
|
||||
: Icons.videocam_outlined,
|
||||
title: const Text('附带近10分钟诊断日志'),
|
||||
subtitle: const Text(
|
||||
'包含登录跳转、课表读取及错误状态,帮助排查问题;不包含密码、验证码或课表内容。',
|
||||
),
|
||||
title: Text(
|
||||
_attachments[i].filename,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${(_attachments[i].size / 1024).toStringAsFixed(1)} KB',
|
||||
),
|
||||
trailing: IconButton(
|
||||
tooltip: '移除 ${_attachments[i].filename}',
|
||||
onPressed: _submitting
|
||||
? null
|
||||
: () => setState(() {
|
||||
_attachments.removeAt(i);
|
||||
value: _includeLogs,
|
||||
onChanged: _submitting
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_includeLogs = value ?? false;
|
||||
if (!_includeLogs) _includeHtml = false;
|
||||
_pendingSubmission = null;
|
||||
_error = null;
|
||||
}),
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
key: const Key('feedback-html'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('附带最近一次学校导入页面HTML'),
|
||||
subtitle: const Text(
|
||||
'适用于本科和研究生课表、考试导入。需先勾选日志。网页可能含姓名、学号、课程、教师和考场信息,仅保留最近10分钟,单独作为邮件附件发送。',
|
||||
),
|
||||
value: _includeHtml,
|
||||
onChanged: _submitting || !_includeLogs
|
||||
? null
|
||||
: (value) async {
|
||||
var accepted = value ?? false;
|
||||
if (accepted) {
|
||||
accepted =
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('附带网页排查资料?'),
|
||||
content: const Text(
|
||||
'附件可能包含你的姓名、学号、课程及考试信息,将发送给开发者用于排查导入问题。密码、验证码、Cookie和认证参数不会包含在附件中。',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.pop(
|
||||
context,
|
||||
false,
|
||||
),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () =>
|
||||
Navigator.pop(
|
||||
context,
|
||||
true,
|
||||
),
|
||||
child: const Text('同意附带'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_includeHtml = accepted;
|
||||
_pendingSubmission = null;
|
||||
_error = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'图片或视频(选填)',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'最多 ${widget.limits.maxAttachments} 个,每个 ${_mb(widget.limits.maxFileBytes)} MB,合计 ${_mb(widget.limits.maxTotalBytes)} MB',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: ink.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
key: const Key('feedback-add-media'),
|
||||
onPressed: _submitting || _picking
|
||||
? null
|
||||
: _addAttachment,
|
||||
icon: const Icon(Icons.attach_file_rounded),
|
||||
label: Text(_picking ? '正在读取附件…' : '添加图片或视频'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_error != null) ...[
|
||||
Text(
|
||||
_error!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
semanticsLabel: _error,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
FilledButton(
|
||||
key: const Key('feedback-submit'),
|
||||
onPressed: _submitting || _picking ? null : _submit,
|
||||
child: _submitting
|
||||
? const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
for (var i = 0; i < _attachments.length; i++)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
_attachments[i].contentType.startsWith(
|
||||
'image/',
|
||||
)
|
||||
? Icons.image_outlined
|
||||
: Icons.videocam_outlined,
|
||||
),
|
||||
title: Text(
|
||||
_attachments[i].filename,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${(_attachments[i].size / 1024).toStringAsFixed(1)} KB',
|
||||
),
|
||||
trailing: IconButton(
|
||||
tooltip: '移除 ${_attachments[i].filename}',
|
||||
onPressed: _submitting
|
||||
? null
|
||||
: () => setState(() {
|
||||
_attachments.removeAt(i);
|
||||
_pendingSubmission = null;
|
||||
_error = null;
|
||||
}),
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('正在提交…'),
|
||||
],
|
||||
)
|
||||
: const Text('提交'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'点击提交后,仅发送你在当前类型填写的内容和选择的附件。',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
height: 1.6,
|
||||
color: ink.withValues(alpha: 0.5),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
key: const Key('feedback-add-media'),
|
||||
onPressed: _submitting || _picking
|
||||
? null
|
||||
: _addAttachment,
|
||||
icon: const Icon(Icons.attach_file_rounded),
|
||||
label: Text(_picking ? '正在读取附件…' : '添加图片或视频'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
if (_error != null) ...[
|
||||
Text(
|
||||
_error!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
semanticsLabel: _error,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
FilledButton(
|
||||
key: const Key('feedback-submit'),
|
||||
onPressed: _submitting || _picking ? null : _submit,
|
||||
child: _submitting
|
||||
? const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('正在提交…'),
|
||||
],
|
||||
)
|
||||
: const Text('提交'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'点击提交后,仅发送你在当前类型填写的内容和选择的附件。',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
height: 1.6,
|
||||
color: ink.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -53,6 +53,11 @@ class WeekGrid extends StatelessWidget {
|
||||
final scale = scaler.scale(10) / 10;
|
||||
const gutter = 34.0, gap = 16.0;
|
||||
final width = (constraints.maxWidth - gutter) / 7;
|
||||
final spacious = width >= 76;
|
||||
final nameStyle = _nameStyle.copyWith(fontSize: spacious ? 13 : 10.5);
|
||||
final metadataStyle = _metadataStyle.copyWith(
|
||||
fontSize: spacious ? 11.5 : 9.5,
|
||||
);
|
||||
final header = 56.0 * scale.clamp(1, 2.5);
|
||||
final groups = {for (final e in events) e: _group(e)};
|
||||
final defaultStyle = DefaultTextStyle.of(context).style;
|
||||
@@ -84,14 +89,14 @@ class WeekGrid extends StatelessWidget {
|
||||
var entryHeight = 0.0;
|
||||
for (final value in group) {
|
||||
final required =
|
||||
measure(value.course.name, _nameStyle, available, lines: 2) +
|
||||
measure(value.course.teacher, _metadataStyle, available) +
|
||||
measure(value.course.room, _metadataStyle, available) +
|
||||
measure(value.course.name, nameStyle, available, lines: 2) +
|
||||
measure(value.course.teacher, metadataStyle, available) +
|
||||
measure(value.course.room, metadataStyle, available) +
|
||||
21;
|
||||
final periodHeight = group.length > 1
|
||||
? measure(
|
||||
'${value.course.startPeriod}–${value.course.endPeriod}节',
|
||||
_metadataStyle,
|
||||
metadataStyle,
|
||||
available,
|
||||
) +
|
||||
3
|
||||
@@ -101,7 +106,7 @@ class WeekGrid extends StatelessWidget {
|
||||
final first = group.map((v) => v.course.startPeriod).reduce(math.min);
|
||||
final last = group.map((v) => v.course.endPeriod).reduce(math.max);
|
||||
final warning = group.length > 1
|
||||
? measure('冲突列表', _metadataStyle, width - 12) + 8
|
||||
? measure('冲突列表', metadataStyle, width - 12) + 8
|
||||
: 0.0;
|
||||
rowHeight = math.max(
|
||||
rowHeight,
|
||||
@@ -133,8 +138,8 @@ class WeekGrid extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'${slot.period}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
style: TextStyle(
|
||||
fontSize: spacious ? 13 : 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
@@ -143,7 +148,7 @@ class WeekGrid extends StatelessWidget {
|
||||
child: Text(
|
||||
minuteLabel(slot.startMinute),
|
||||
style: TextStyle(
|
||||
fontSize: 8,
|
||||
fontSize: spacious ? 10 : 8,
|
||||
color: ink.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
@@ -152,7 +157,7 @@ class WeekGrid extends StatelessWidget {
|
||||
child: Text(
|
||||
minuteLabel(slot.endMinute),
|
||||
style: TextStyle(
|
||||
fontSize: 8,
|
||||
fontSize: spacious ? 10 : 8,
|
||||
color: ink.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
@@ -192,7 +197,7 @@ class WeekGrid extends StatelessWidget {
|
||||
][i],
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontSize: spacious ? 12 : 10,
|
||||
color: active
|
||||
? river
|
||||
: ink.withValues(alpha: 0.6),
|
||||
@@ -211,7 +216,7 @@ class WeekGrid extends StatelessWidget {
|
||||
child: Text(
|
||||
'${date.day}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontSize: spacious ? 13 : 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: active ? Colors.white : ink,
|
||||
),
|
||||
@@ -258,7 +263,15 @@ class WeekGrid extends StatelessWidget {
|
||||
),
|
||||
for (final e in events)
|
||||
if (groups[e]!.first == e)
|
||||
_block(context, groups[e]!, width, rowHeight, top),
|
||||
_block(
|
||||
context,
|
||||
groups[e]!,
|
||||
width,
|
||||
rowHeight,
|
||||
top,
|
||||
nameStyle,
|
||||
metadataStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -276,6 +289,8 @@ class WeekGrid extends StatelessWidget {
|
||||
double width,
|
||||
double rowHeight,
|
||||
double Function(int) top,
|
||||
TextStyle nameStyle,
|
||||
TextStyle metadataStyle,
|
||||
) {
|
||||
final first = group.map((e) => e.course.startPeriod).reduce(math.min);
|
||||
final last = group.map((e) => e.course.endPeriod).reduce(math.max);
|
||||
@@ -285,7 +300,12 @@ class WeekGrid extends StatelessWidget {
|
||||
width: width - 4,
|
||||
height: (last - first + 1) * rowHeight - 8,
|
||||
child: group.length == 1
|
||||
? _entry(context, group.first)
|
||||
? _entry(
|
||||
context,
|
||||
group.first,
|
||||
nameStyle: nameStyle,
|
||||
metadataStyle: metadataStyle,
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: river.withValues(alpha: 0.06),
|
||||
@@ -308,7 +328,15 @@ class WeekGrid extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
for (final e in group)
|
||||
Expanded(child: _entry(context, e, inConflict: true)),
|
||||
Expanded(
|
||||
child: _entry(
|
||||
context,
|
||||
e,
|
||||
inConflict: true,
|
||||
nameStyle: nameStyle,
|
||||
metadataStyle: metadataStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -319,6 +347,8 @@ class WeekGrid extends StatelessWidget {
|
||||
BuildContext context,
|
||||
CourseOccurrence e, {
|
||||
bool inConflict = false,
|
||||
required TextStyle nameStyle,
|
||||
required TextStyle metadataStyle,
|
||||
}) => Semantics(
|
||||
label: '${e.course.name},${e.course.teacher},${e.course.room}',
|
||||
button: true,
|
||||
@@ -343,26 +373,24 @@ class WeekGrid extends StatelessWidget {
|
||||
e.course.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _nameStyle,
|
||||
style: nameStyle,
|
||||
),
|
||||
if (inConflict) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
'${e.course.startPeriod}–${e.course.endPeriod}节',
|
||||
style: _metadataStyle,
|
||||
style: metadataStyle,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
e.course.teacher,
|
||||
style: _metadataStyle.copyWith(
|
||||
color: ink.withValues(alpha: 0.85),
|
||||
),
|
||||
style: metadataStyle.copyWith(color: ink.withValues(alpha: 0.85)),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
e.course.room,
|
||||
style: _metadataStyle.copyWith(color: ink.withValues(alpha: 0.7)),
|
||||
style: metadataStyle.copyWith(color: ink.withValues(alpha: 0.7)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user