Add feedback service, reminder diagnostics and smooth week transitions
This commit is contained in:
@@ -6,7 +6,10 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_theme.dart';
|
||||
import '../../app/app_version.dart';
|
||||
import '../../data/schedule_file_import.dart';
|
||||
import '../feedback/feedback_page.dart';
|
||||
import '../feedback/feedback_submission.dart';
|
||||
import 'settings_page.dart';
|
||||
import 'import_preview.dart';
|
||||
|
||||
@@ -19,12 +22,14 @@ class ProfilePage extends StatelessWidget {
|
||||
this.onLogout,
|
||||
this.logoutLabel = '退出学校登录',
|
||||
this.onUpdateTerm,
|
||||
this.feedbackSender = const UnconfiguredFeedbackSender(),
|
||||
});
|
||||
final AppController controller;
|
||||
final VoidCallback onLogin;
|
||||
final VoidCallback? onLogout;
|
||||
final VoidCallback? onUpdateTerm;
|
||||
final String authStatus, logoutLabel;
|
||||
final FeedbackSender feedbackSender;
|
||||
Future<void> _import(BuildContext context) async {
|
||||
try {
|
||||
final file = await FilePicker.pickFile(
|
||||
@@ -212,6 +217,18 @@ class ProfilePage extends StatelessWidget {
|
||||
'无需登录,预览课表样式',
|
||||
controller.showDemo,
|
||||
),
|
||||
Divider(height: 1, color: ink.withValues(alpha: 0.06)),
|
||||
_tile(
|
||||
Icons.chat_bubble_outline_rounded,
|
||||
'反馈与共建',
|
||||
'反馈问题和建议 · 我要参与开发',
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => FeedbackPage(sender: feedbackSender),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -224,6 +241,14 @@ class ProfilePage extends StatelessWidget {
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'版本 $appVersion',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: ink.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'今天也有值得期待的小事。',
|
||||
style: TextStyle(
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_theme.dart';
|
||||
import '../../app/app_version.dart' as release;
|
||||
import '../../services/notification_service.dart';
|
||||
|
||||
class ReminderTools extends StatefulWidget {
|
||||
const ReminderTools({super.key, required this.controller});
|
||||
|
||||
final AppController controller;
|
||||
|
||||
@override
|
||||
State<ReminderTools> createState() => _ReminderToolsState();
|
||||
}
|
||||
|
||||
class _ReminderToolsState extends State<ReminderTools> {
|
||||
bool _busy = false;
|
||||
Map<String, Object?>? _diagnosis;
|
||||
String? _report;
|
||||
|
||||
void _notice(String text) =>
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
|
||||
|
||||
Future<void> _run(Future<void> Function() action) async {
|
||||
if (_busy) return;
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
await action();
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
_notice(
|
||||
error is ReminderSetupException ? error.message : '操作未完成,请检查系统设置后重试',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _test(bool scheduled) => _run(() async {
|
||||
final message = await widget.controller.notifications.sendTestReminder(
|
||||
scheduled: scheduled,
|
||||
);
|
||||
if (mounted) _notice(message);
|
||||
});
|
||||
|
||||
Future<void> _check() => _run(() async {
|
||||
await widget.controller.refreshServices();
|
||||
final diagnosis = await widget.controller.notifications.diagnose();
|
||||
if (!mounted) return;
|
||||
final c = widget.controller;
|
||||
final s = c.snapshot.settings;
|
||||
final rawSystem = diagnosis['system'];
|
||||
final system = rawSystem is Map ? rawSystem : const {};
|
||||
// Copy an explicit allowlist, never raw platform errors, courses or account.
|
||||
final report = <String, Object?>{
|
||||
'appVersion': '${release.appVersion}+${release.appBuildNumber}',
|
||||
'checkedAt': c.clock().toIso8601String(),
|
||||
'remindersEnabled': s.remindersEnabled,
|
||||
'semesterStart': s.semester?.startDate.toIso8601String(),
|
||||
'semesterWeeks': s.semester?.totalWeeks,
|
||||
'reminderOffsets': s.reminderOffsets,
|
||||
'courseCount': c.snapshot.courses.length,
|
||||
'scheduledCount': c.reminderResult.scheduledCount,
|
||||
'reminderStatus': c.reminderResult.status.name,
|
||||
for (final key in [
|
||||
'notifications',
|
||||
'exactAlarm',
|
||||
'pendingCount',
|
||||
'channelImportance',
|
||||
'channelSound',
|
||||
])
|
||||
key: _scalar(diagnosis[key]),
|
||||
'activeQueryWarning': diagnosis['activeQueryWarning'] != null,
|
||||
'errorCode': _errorCode(c.reminderErrorCode),
|
||||
'systemError': _errorCode(diagnosis['systemError']),
|
||||
'system': {
|
||||
for (final key in [
|
||||
'manufacturer',
|
||||
'model',
|
||||
'androidVersion',
|
||||
'sdk',
|
||||
'backgroundRestricted',
|
||||
'batteryOptimizationsIgnored',
|
||||
'powerSaveMode',
|
||||
'interruptionFilter',
|
||||
])
|
||||
key: _scalar(system[key]),
|
||||
},
|
||||
};
|
||||
setState(() {
|
||||
_diagnosis = diagnosis;
|
||||
_report = const JsonEncoder.withIndent(' ').convert(report);
|
||||
});
|
||||
});
|
||||
|
||||
Object? _scalar(Object? value) =>
|
||||
value is String || value is bool || value is num ? value : null;
|
||||
|
||||
String _errorCode(Object? value) {
|
||||
if (value is String && RegExp(r'^[a-zA-Z0-9_]{1,64}$').hasMatch(value)) {
|
||||
return value;
|
||||
}
|
||||
return value == null ? 'none' : 'unknown';
|
||||
}
|
||||
|
||||
Future<void> _copy() => _run(() async {
|
||||
final report = _report;
|
||||
if (report == null) return;
|
||||
await Clipboard.setData(ClipboardData(text: report));
|
||||
if (mounted) _notice('提醒诊断已复制,可粘贴到意见反馈中');
|
||||
});
|
||||
|
||||
String _permission(Object? value) => switch (value) {
|
||||
true => '已允许',
|
||||
false => '未允许',
|
||||
_ => '暂时无法确认',
|
||||
};
|
||||
|
||||
List<String> _summary(Map<String, Object?> diagnosis) {
|
||||
final rawSystem = diagnosis['system'];
|
||||
final system = rawSystem is Map ? rawSystem : const {};
|
||||
final pending = diagnosis['pendingCount'];
|
||||
return [
|
||||
'通知权限:${_permission(diagnosis['notifications'])}',
|
||||
'精确提醒:${_permission(diagnosis['exactAlarm'])}',
|
||||
if (pending is int) '待发送提醒:$pending 条',
|
||||
if (diagnosis['notifications'] == false) '系统通知已关闭,请在通知设置中开启。',
|
||||
if (diagnosis['channelImportance'] == 0) '提醒通道已关闭,请在通知设置中开启。',
|
||||
if (diagnosis['channelSound'] == false) '提醒通道处于静音,请在通知设置中检查声音。',
|
||||
if (diagnosis['exactAlarm'] == false) '精确提醒未获允许,系统发送时间可能延迟。',
|
||||
if (system['backgroundRestricted'] == true ||
|
||||
system['powerSaveMode'] == true)
|
||||
'系统正在限制后台运行,可在后台运行设置中检查。',
|
||||
if (system['interruptionFilter'] is int &&
|
||||
system['interruptionFilter'] != 0 &&
|
||||
system['interruptionFilter'] != 1)
|
||||
'勿扰模式可能影响提醒声音。',
|
||||
if (pending == 0 && widget.controller.snapshot.settings.remindersEnabled)
|
||||
'当前没有待发送提醒,请核对学期、课程日期和提醒提前时间。',
|
||||
if (diagnosis['activeQueryWarning'] != null) '部分系统提醒状态暂时无法读取,可稍后再检查。',
|
||||
if (diagnosis['systemError'] != null) '设备后台状态暂时无法读取,可在系统设置中检查。',
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'测试与检查',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _busy ? null : () => _test(false),
|
||||
child: const Text('立即测试提醒'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _busy ? null : () => _test(true),
|
||||
child: const Text('10 秒后台测试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Text(
|
||||
'后台测试:点击后按 Home 键回到桌面,等待约 10 秒;请勿强行停止应用。',
|
||||
style: TextStyle(fontSize: 12, height: 1.6, color: river),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _busy ? null : _check,
|
||||
child: const Text('检查提醒'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => _run(
|
||||
() => widget.controller.notifications.openSettings(
|
||||
background: false,
|
||||
),
|
||||
),
|
||||
child: const Text('通知设置'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => _run(
|
||||
() => widget.controller.notifications.openSettings(
|
||||
background: true,
|
||||
),
|
||||
),
|
||||
child: const Text('后台运行设置'),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_busy)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
if (_diagnosis != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
for (final line in _summary(_diagnosis!))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 5),
|
||||
child: Text(
|
||||
line,
|
||||
style: const TextStyle(fontSize: 12, height: 1.5),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _busy ? null : _copy,
|
||||
child: const Text('复制提醒诊断'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import '../../app/app_theme.dart';
|
||||
import '../../domain/app_settings.dart';
|
||||
import '../../domain/semester.dart';
|
||||
import '../../services/notification_service.dart';
|
||||
import 'reminder_tools.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({
|
||||
@@ -373,6 +374,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
if (!c.isDemo) ReminderTools(controller: c),
|
||||
if (!c.isDemo) const SizedBox(height: 22),
|
||||
_heading('手机日历'),
|
||||
SurfaceCard(
|
||||
child: Column(
|
||||
|
||||
Reference in New Issue
Block a user