feat: 完善芽说英语品牌Logo与图标配置,完成跨平台云端同步服务开发与自动化部署
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/app_state.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/sync/sync_coordinator.dart';
|
||||
import '../../core/sync/sync_models.dart';
|
||||
import '../../widgets/app_widgets.dart';
|
||||
|
||||
/// 跨平台同步设置底部面板
|
||||
class SyncSettingsSheet extends StatefulWidget {
|
||||
const SyncSettingsSheet({super.key, required this.state});
|
||||
|
||||
final AppState state;
|
||||
|
||||
static Future<void> show(BuildContext context, AppState state) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SyncSettingsSheet(state: state),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<SyncSettingsSheet> createState() => _SyncSettingsSheetState();
|
||||
}
|
||||
|
||||
class _SyncSettingsSheetState extends State<SyncSettingsSheet> {
|
||||
final _coordinator = SyncCoordinator.instance;
|
||||
late final TextEditingController _serverController;
|
||||
final _userController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
bool _isRegisterMode = false;
|
||||
bool _testingConnection = false;
|
||||
String? _testMessage;
|
||||
bool? _testOk;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serverController = TextEditingController(text: _coordinator.serverUrl);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_serverController.dispose();
|
||||
_userController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleTestConnection() async {
|
||||
setState(() {
|
||||
_testingConnection = true;
|
||||
_testMessage = null;
|
||||
_testOk = null;
|
||||
});
|
||||
final ok = await _coordinator.testConnection(_serverController.text);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_testingConnection = false;
|
||||
_testOk = ok;
|
||||
_testMessage = ok ? '服务器连接正常' : '无法连接到服务器,请检查地址或网络';
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleAuth() async {
|
||||
final server = _serverController.text.trim();
|
||||
final user = _userController.text.trim();
|
||||
final pwd = _passwordController.text;
|
||||
|
||||
if (server.isEmpty || user.isEmpty || pwd.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('请填写完整的服务器地址、账号和密码')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final success = _isRegisterMode
|
||||
? await _coordinator.register(
|
||||
serverUrl: server,
|
||||
username: user,
|
||||
password: pwd,
|
||||
)
|
||||
: await _coordinator.login(
|
||||
serverUrl: server,
|
||||
username: user,
|
||||
password: pwd,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_isRegisterMode ? '注册并登录成功!' : '登录成功!'),
|
||||
backgroundColor: AppColors.green,
|
||||
),
|
||||
);
|
||||
// 登录成功后自动执行一次同步
|
||||
await _coordinator.syncNow(widget.state);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleSyncNow() async {
|
||||
final ok = await _coordinator.syncNow(widget.state);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(ok ? '同步完成,进度已合并。' : '同步失败:${_coordinator.errorMessage}'),
|
||||
backgroundColor: ok ? AppColors.green : Colors.redAccent,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTime(DateTime? time) {
|
||||
if (time == null) return '从未同步';
|
||||
final local = time.toLocal();
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(local);
|
||||
if (diff.inSeconds < 60) return '刚刚';
|
||||
if (diff.inMinutes < 60) return '${diff.inMinutes} 分钟前';
|
||||
if (diff.inHours < 24) return '${diff.inHours} 小时前';
|
||||
return '${local.year}-${local.month.toString().padLeft(2, '0')}-${local.day.toString().padLeft(2, '0')} ${local.hour.toString().padLeft(2, '0')}:${local.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _coordinator,
|
||||
builder: (context, _) {
|
||||
final isLoggedIn = _coordinator.isLoggedIn;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'云同步与多端备份',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
if (isLoggedIn)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _coordinator.state == SyncState.syncing
|
||||
? AppColors.warm
|
||||
: _coordinator.state == SyncState.error
|
||||
? Colors.red.shade50
|
||||
: AppColors.softGreen,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
_coordinator.state == SyncState.syncing
|
||||
? Icons.sync
|
||||
: _coordinator.state == SyncState.error
|
||||
? Icons.error_outline
|
||||
: Icons.cloud_done_outlined,
|
||||
size: 14,
|
||||
color: _coordinator.state == SyncState.syncing
|
||||
? AppColors.warmInk
|
||||
: _coordinator.state == SyncState.error
|
||||
? Colors.redAccent
|
||||
: AppColors.green,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_coordinator.state == SyncState.syncing
|
||||
? '同步中'
|
||||
: _coordinator.state == SyncState.error
|
||||
? '同步异常'
|
||||
: '已连接',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _coordinator.state == SyncState.syncing
|
||||
? AppColors.warmInk
|
||||
: _coordinator.state == SyncState.error
|
||||
? Colors.redAccent
|
||||
: AppColors.green,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Text(
|
||||
'本地优先架构:无网络时不影响学习,联网后自动双向合并学习进度与复习掌握度。',
|
||||
style: TextStyle(fontSize: 13, color: AppColors.muted),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
if (isLoggedIn) ...[
|
||||
// Logged In State
|
||||
SectionCard(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundColor: AppColors.softGreen,
|
||||
child: Icon(Icons.person, color: AppColors.green),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
_coordinator.username ?? '同步用户',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_coordinator.serverUrl,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _coordinator.logout(),
|
||||
child: const Text('退出登录'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('上次同步:'),
|
||||
Text(
|
||||
_formatTime(_coordinator.lastSyncTime),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('自动后台同步'),
|
||||
subtitle: const Text('在关卡完成与复习提交后自动静默同步'),
|
||||
value: _coordinator.autoSyncEnabled,
|
||||
activeThumbColor: AppColors.green,
|
||||
onChanged: (val) => _coordinator.setAutoSyncEnabled(val),
|
||||
),
|
||||
if (_coordinator.errorMessage != null)
|
||||
SectionCard(
|
||||
tint: Colors.red.shade50,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.redAccent),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'同步失败:${_coordinator.errorMessage}',
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
PrimaryButton(
|
||||
label: _coordinator.state == SyncState.syncing
|
||||
? '正在同步...'
|
||||
: '立即同步 (双向合并)',
|
||||
icon: Icons.sync,
|
||||
onPressed: _coordinator.state == SyncState.syncing
|
||||
? null
|
||||
: _handleSyncNow,
|
||||
),
|
||||
] else ...[
|
||||
// Not Logged In State (Login / Register Form)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ChoiceChip(
|
||||
label: const Center(child: Text('登录已有账号')),
|
||||
selected: !_isRegisterMode,
|
||||
onSelected: (val) =>
|
||||
setState(() => _isRegisterMode = false),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ChoiceChip(
|
||||
label: const Center(child: Text('注册新账号')),
|
||||
selected: _isRegisterMode,
|
||||
onSelected: (val) =>
|
||||
setState(() => _isRegisterMode = true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _serverController,
|
||||
keyboardType: TextInputType.url,
|
||||
decoration: InputDecoration(
|
||||
labelText: '自建同步服务器地址',
|
||||
hintText: 'https://syncenglish.slcydia.fun',
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
tooltip: '测试连通性',
|
||||
icon: _testingConnection
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.network_check),
|
||||
onPressed: _testingConnection ? null : _handleTestConnection,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_testMessage != null)
|
||||
Text(
|
||||
_testMessage!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _testOk == true ? AppColors.green : Colors.redAccent,
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _userController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '用户名',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '密码',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
if (_coordinator.errorMessage != null)
|
||||
SectionCard(
|
||||
tint: Colors.red.shade50,
|
||||
child: Text(
|
||||
_coordinator.errorMessage!,
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
label: _coordinator.state == SyncState.syncing
|
||||
? '处理中...'
|
||||
: (_isRegisterMode ? '注册账号并开启同步' : '登录并同步进度'),
|
||||
onPressed: _coordinator.state == SyncState.syncing
|
||||
? null
|
||||
: _handleAuth,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user