- 管理页:单元集 / 计划 / 算法三个维度,卡片带元数据 - 账户页:资料、学习数据、竖直分组的四个功能入口 - 新增设置、文档与帮助(底部可上滑助手)、反馈、关于四个页面 - 抽出 StatBlock 与 GroupedListSection 共用组件 - 关闭 Android 预测式返回,转场改为 M3 水平滑入
222 lines
6.0 KiB
Dart
222 lines
6.0 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import '../app_info.dart';
|
||
|
||
/// 设置页,按 Android 系统设置的分组列表组织。
|
||
class SettingsPage extends StatefulWidget {
|
||
const SettingsPage({super.key});
|
||
|
||
@override
|
||
State<SettingsPage> createState() => _SettingsPageState();
|
||
}
|
||
|
||
class _SettingsPageState extends State<SettingsPage> {
|
||
bool _requeueOnWrong = true;
|
||
bool _readAloud = true;
|
||
bool _autoSync = true;
|
||
bool _wifiOnly = true;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('设置')),
|
||
body: ListView(
|
||
padding: const EdgeInsets.only(bottom: 32),
|
||
children: <Widget>[
|
||
const _SectionHeader('学习'),
|
||
_ValueTile(
|
||
icon: Icons.school_outlined,
|
||
title: '每日新识记上限',
|
||
value: '35 条',
|
||
onTap: () => _pending('每日新识记上限'),
|
||
),
|
||
_ValueTile(
|
||
icon: Icons.replay_rounded,
|
||
title: '每日复习上限',
|
||
value: '200 条',
|
||
onTap: () => _pending('每日复习上限'),
|
||
),
|
||
_ValueTile(
|
||
icon: Icons.schedule_rounded,
|
||
title: '学习时段',
|
||
value: '08:00–23:00',
|
||
onTap: () => _pending('学习时段'),
|
||
),
|
||
_SwitchTile(
|
||
icon: Icons.restart_alt_rounded,
|
||
title: '答错后重新排队',
|
||
subtitle: '同一轮里再出现一次',
|
||
value: _requeueOnWrong,
|
||
onChanged: (bool value) => setState(() => _requeueOnWrong = value),
|
||
),
|
||
|
||
const _SectionHeader('测验'),
|
||
_ValueTile(
|
||
icon: Icons.quiz_outlined,
|
||
title: '默认谜题类型',
|
||
value: '完形填空',
|
||
onTap: () => _pending('默认谜题类型'),
|
||
),
|
||
_SwitchTile(
|
||
icon: Icons.volume_up_outlined,
|
||
title: '自动朗读',
|
||
subtitle: '进入谜题时播放语音',
|
||
value: _readAloud,
|
||
onChanged: (bool value) => setState(() => _readAloud = value),
|
||
),
|
||
|
||
const _SectionHeader('外观'),
|
||
_ValueTile(
|
||
icon: Icons.dark_mode_outlined,
|
||
title: '主题',
|
||
value: '跟随系统',
|
||
onTap: () => _pending('主题'),
|
||
),
|
||
_ValueTile(
|
||
icon: Icons.text_fields_rounded,
|
||
title: '字号',
|
||
value: '标准',
|
||
onTap: () => _pending('字号'),
|
||
),
|
||
|
||
const _SectionHeader('同步'),
|
||
_SwitchTile(
|
||
icon: Icons.cloud_sync_outlined,
|
||
title: '自动同步',
|
||
subtitle: '学完一轮后自动上传',
|
||
value: _autoSync,
|
||
onChanged: (bool value) => setState(() => _autoSync = value),
|
||
),
|
||
_SwitchTile(
|
||
icon: Icons.wifi_rounded,
|
||
title: '仅在 Wi-Fi 下同步',
|
||
value: _wifiOnly,
|
||
onChanged: (bool value) => setState(() => _wifiOnly = value),
|
||
),
|
||
_ValueTile(
|
||
icon: Icons.sync_rounded,
|
||
title: '立即同步',
|
||
onTap: () => _pending('立即同步'),
|
||
),
|
||
|
||
const _SectionHeader('关于'),
|
||
_ValueTile(
|
||
icon: Icons.info_outline_rounded,
|
||
title: '版本',
|
||
value: appVersion,
|
||
),
|
||
_ValueTile(
|
||
icon: Icons.description_outlined,
|
||
title: '开源许可',
|
||
onTap: () => _pending('开源许可'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _pending(String feature) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('$feature 尚未接入内核'),
|
||
behavior: SnackBarBehavior.floating,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SectionHeader extends StatelessWidget {
|
||
const _SectionHeader(this.title);
|
||
|
||
final String title;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ThemeData theme = Theme.of(context);
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 6),
|
||
child: Text(
|
||
title,
|
||
style: theme.textTheme.labelLarge?.copyWith(
|
||
color: theme.colorScheme.primary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ValueTile extends StatelessWidget {
|
||
const _ValueTile({
|
||
required this.icon,
|
||
required this.title,
|
||
this.value,
|
||
this.onTap,
|
||
});
|
||
|
||
final IconData icon;
|
||
final String title;
|
||
final String? value;
|
||
final VoidCallback? onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ColorScheme colors = Theme.of(context).colorScheme;
|
||
final String? value = this.value;
|
||
|
||
return ListTile(
|
||
leading: Icon(icon, color: colors.onSurfaceVariant),
|
||
title: Text(title),
|
||
trailing: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: <Widget>[
|
||
if (value != null)
|
||
Text(
|
||
value,
|
||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||
color: colors.onSurfaceVariant,
|
||
),
|
||
),
|
||
if (onTap != null) ...<Widget>[
|
||
const SizedBox(width: 4),
|
||
Icon(
|
||
Icons.chevron_right_rounded,
|
||
size: 20,
|
||
color: colors.onSurfaceVariant,
|
||
),
|
||
],
|
||
],
|
||
),
|
||
onTap: onTap,
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SwitchTile extends StatelessWidget {
|
||
const _SwitchTile({
|
||
required this.icon,
|
||
required this.title,
|
||
required this.value,
|
||
required this.onChanged,
|
||
this.subtitle,
|
||
});
|
||
|
||
final IconData icon;
|
||
final String title;
|
||
final String? subtitle;
|
||
final bool value;
|
||
final ValueChanged<bool> onChanged;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ColorScheme colors = Theme.of(context).colorScheme;
|
||
return SwitchListTile(
|
||
secondary: Icon(icon, color: colors.onSurfaceVariant),
|
||
title: Text(title),
|
||
subtitle: subtitle == null ? null : Text(subtitle!),
|
||
value: value,
|
||
onChanged: onChanged,
|
||
);
|
||
}
|
||
}
|