| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import '../../services/api_service.dart';
- import '../../providers/app_provider.dart';
- class SettingsPage extends ConsumerStatefulWidget {
- const SettingsPage({super.key});
- @override
- ConsumerState<SettingsPage> createState() => _SettingsPageState();
- }
- class _SettingsPageState extends ConsumerState<SettingsPage> {
- final _urlCtrl = TextEditingController();
- final _tokenCtrl = TextEditingController();
- final _webdavUrlCtrl = TextEditingController();
- final _webdavUserCtrl = TextEditingController();
- final _webdavPassCtrl = TextEditingController();
- bool _isConnecting = false;
- bool _webdavConfigured = false;
- bool _showWebdavForm = false;
- bool _backingUp = false;
- String? _error;
- @override
- void initState() {
- super.initState();
- // Delay to ensure connection is fully initialized, then load
- Future.delayed(const Duration(milliseconds: 500), () {
- if (mounted) _loadWebDAVConfig();
- });
- }
- Future<void> _loadWebDAVConfig() async {
- final api = ref.read(apiServiceProvider);
- try {
- final data = await api.getWebDAVConfig();
- final configured = data['configured'] == true;
- ref.read(webdavConfiguredProvider.notifier).state = configured;
- ref.read(webdavUrlProvider.notifier).state = data['url'] ?? '';
- ref.read(webdavUserProvider.notifier).state = data['username'] ?? '';
- setState(() {
- _webdavConfigured = configured;
- _showWebdavForm = !configured;
- if (configured) {
- _webdavUrlCtrl.text = data['url'] ?? '';
- _webdavUserCtrl.text = data['username'] ?? '';
- }
- });
- } catch (_) {
- // Retry once
- try {
- final data = await api.getWebDAVConfig();
- final configured = data['configured'] == true;
- ref.read(webdavConfiguredProvider.notifier).state = configured;
- setState(() {
- _webdavConfigured = configured;
- _showWebdavForm = !configured;
- });
- } catch (_) {}
- }
- }
- @override
- void dispose() {
- _urlCtrl.dispose(); _tokenCtrl.dispose();
- _webdavUrlCtrl.dispose(); _webdavUserCtrl.dispose(); _webdavPassCtrl.dispose();
- super.dispose();
- }
- Future<void> _connect() async {
- final url = _urlCtrl.text.trim(), token = _tokenCtrl.text.trim();
- if (url.isEmpty || token.isEmpty) { setState(() => _error = '请输入服务器地址和 Token'); return; }
- setState(() { _isConnecting = true; _error = null; });
- final ok = await ref.read(connectionProvider.notifier).connect(url, token);
- if (!mounted) return;
- setState(() { _isConnecting = false; if (!ok) _error = '连接失败'; else _loadWebDAVConfig(); });
- }
- Future<void> _disconnect() async {
- await ref.read(connectionProvider.notifier).disconnect();
- _urlCtrl.clear(); _tokenCtrl.clear();
- _webdavConfigured = false; _showWebdavForm = false;
- }
- Future<void> _triggerFullBackup() async {
- final api = ref.read(apiServiceProvider);
- // Fire and forget — show confirmation immediately
- api.triggerFullBackup();
- if (mounted) {
- showDialog(
- context: context,
- builder: (ctx) => AlertDialog(
- title: const Text('全量备份'),
- content: const Text('已通知服务端开始备份。\n备份完成后将自动推送到 WebDAV。'),
- actions: [
- FilledButton(onPressed: () => Navigator.pop(ctx), child: const Text('确定')),
- ],
- ),
- );
- }
- }
- Future<void> _saveWebDAV() async {
- final api = ref.read(apiServiceProvider);
- try {
- await api.setWebDAVConfig(
- _webdavUrlCtrl.text.trim(),
- _webdavUserCtrl.text.trim(),
- _webdavPassCtrl.text.trim(),
- );
- setState(() {
- _webdavConfigured = true;
- _showWebdavForm = false;
- });
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('WebDAV 配置已保存')),
- );
- }
- } catch (e) {
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('保存失败: $e')),
- );
- }
- }
- }
- @override
- Widget build(BuildContext context) {
- final connState = ref.watch(connectionProvider);
- final isConfigured = connState.status == ConnectionStatus.configured;
- final webdavConfigured = ref.watch(webdavConfiguredProvider);
- return Scaffold(
- appBar: AppBar(title: const Text('设置')),
- body: ListView(
- padding: const EdgeInsets.all(16),
- children: [
- // 连接状态
- Card(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(children: [
- Icon(isConfigured ? Icons.cloud_done : Icons.cloud_off,
- color: isConfigured ? Colors.green : Colors.grey),
- const SizedBox(width: 8),
- Text(isConfigured ? '已连接' : '未连接',
- style: Theme.of(context).textTheme.titleMedium),
- ]),
- if (isConfigured && connState.serverUrl != null)
- Padding(
- padding: const EdgeInsets.only(top: 8),
- child: Text('服务器: ${connState.serverUrl}',
- style: Theme.of(context).textTheme.bodySmall),
- ),
- ],
- ),
- ),
- ),
- const SizedBox(height: 16),
- // 连接表单
- if (!isConfigured) ...[
- TextField(
- controller: _urlCtrl,
- decoration: const InputDecoration(
- labelText: '服务器地址', hintText: 'http://数学.top:8765',
- prefixIcon: Icon(Icons.link), border: OutlineInputBorder(),
- ),
- keyboardType: TextInputType.url,
- ),
- const SizedBox(height: 12),
- TextField(
- controller: _tokenCtrl,
- decoration: const InputDecoration(
- labelText: 'API Token', hintText: '在服务端运行 tiku-server token 获取',
- prefixIcon: Icon(Icons.key), border: OutlineInputBorder(),
- ),
- obscureText: true,
- ),
- if (_error != null)
- Padding(padding: const EdgeInsets.only(top: 8), child: Text(_error!, style: const TextStyle(color: Colors.red))),
- const SizedBox(height: 16),
- SizedBox(
- width: double.infinity, height: 48,
- child: FilledButton(
- onPressed: _isConnecting ? null : _connect,
- child: _isConnecting
- ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
- : const Text('连接'),
- ),
- ),
- ] else
- SizedBox(width: double.infinity, height: 48,
- child: OutlinedButton(onPressed: _disconnect, child: const Text('断开连接'))),
- const SizedBox(height: 24),
- // WebDAV 配置
- if (isConfigured) ...[
- Card(
- child: webdavConfigured && !_showWebdavForm
- ? ListTile(
- leading: const Icon(Icons.cloud_done, color: Colors.green),
- title: const Text('WebDAV 已配置'),
- subtitle: Text(_webdavUrlCtrl.text),
- trailing: TextButton(
- onPressed: () => setState(() => _showWebdavForm = true),
- child: const Text('修改'),
- ),
- )
- : Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('WebDAV 远程备份', style: Theme.of(context).textTheme.titleMedium),
- const SizedBox(height: 8),
- Text('备份 bank.db 到坚果云等 WebDAV 服务器',
- style: Theme.of(context).textTheme.bodySmall),
- const SizedBox(height: 12),
- TextField(
- controller: _webdavUrlCtrl,
- decoration: const InputDecoration(
- labelText: 'WebDAV 地址',
- hintText: 'https://dav.jianguoyun.com/dav/',
- border: OutlineInputBorder(), isDense: true,
- ),
- ),
- const SizedBox(height: 8),
- TextField(
- controller: _webdavUserCtrl,
- decoration: const InputDecoration(
- labelText: '用户名', border: OutlineInputBorder(), isDense: true,
- ),
- ),
- const SizedBox(height: 8),
- TextField(
- controller: _webdavPassCtrl,
- decoration: const InputDecoration(
- labelText: '密码', border: OutlineInputBorder(), isDense: true,
- ),
- obscureText: true,
- ),
- const SizedBox(height: 12),
- Row(
- children: [
- Expanded(
- child: FilledButton.tonal(
- onPressed: _saveWebDAV,
- child: const Text('保存'),
- ),
- ),
- if (_webdavConfigured) ...[
- const SizedBox(width: 8),
- Expanded(
- child: OutlinedButton(
- onPressed: () => setState(() => _showWebdavForm = false),
- child: const Text('取消'),
- ),
- ),
- ],
- ],
- ),
- ],
- ),
- ),
- ),
- ],
- // 全量备份
- if (isConfigured) ...[
- const SizedBox(height: 16),
- Card(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('数据备份', style: Theme.of(context).textTheme.titleMedium),
- const SizedBox(height: 8),
- Text('备份 bank.db + 全部 docx 到本地和 WebDAV',
- style: Theme.of(context).textTheme.bodySmall),
- const SizedBox(height: 12),
- SizedBox(
- width: double.infinity,
- child: OutlinedButton.icon(
- onPressed: _backingUp ? null : _triggerFullBackup,
- icon: _backingUp
- ? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))
- : const Icon(Icons.backup),
- label: Text(_backingUp ? '备份中...' : '全量备份'),
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- const SizedBox(height: 24),
- // 关于
- Card(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('关于', style: Theme.of(context).textTheme.titleMedium),
- const SizedBox(height: 8),
- const Text('某人的题库 v1.0.0'),
- const Text('服务端: Python + FastAPI'),
- const Text('客户端: Flutter'),
- ],
- ),
- ),
- ),
- ],
- ),
- );
- }
- }
|