Skip to content

Commit 674a836

Browse files
committed
fix(app): retain server address when offline
1 parent 82ae3fd commit 674a836

1 file changed

Lines changed: 125 additions & 78 deletions

File tree

flutter_app/lib/features/server/server_config_screen.dart

Lines changed: 125 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter/services.dart';
33
import 'package:flutter_riverpod/flutter_riverpod.dart';
4+
import 'package:go_router/go_router.dart';
45

56
import '../../data/api/api_client.dart';
67
import '../../data/providers/auth_provider.dart';
78

8-
/// 服务器配置页面 —— 首次启动 / 退出登录后切换服务器时显示。
9-
///
10-
/// 触发条件参见 `lib/app/router.dart` 中的 redirect 逻辑:
11-
/// 当 `authState.serverUrl` 为空时会被强制路由到这里。
9+
/// 服务器配置页面 —— 首次启动或切换服务器时显示。
1210
class ServerConfigScreen extends ConsumerStatefulWidget {
1311
const ServerConfigScreen({super.key});
1412

1513
@override
16-
ConsumerState<ServerConfigScreen> createState() => _ServerConfigScreenState();
14+
ConsumerState<ServerConfigScreen> createState() =>
15+
_ServerConfigScreenState();
1716
}
1817

1918
class _ServerConfigScreenState extends ConsumerState<ServerConfigScreen> {
@@ -26,7 +25,7 @@ class _ServerConfigScreenState extends ConsumerState<ServerConfigScreen> {
2625
@override
2726
void initState() {
2827
super.initState();
29-
_loadHistory();
28+
_loadSavedServers();
3029
}
3130

3231
@override
@@ -35,88 +34,117 @@ class _ServerConfigScreenState extends ConsumerState<ServerConfigScreen> {
3534
super.dispose();
3635
}
3736

38-
Future<void> _loadHistory() async {
39-
final history = await loadServerHistory();
37+
Future<void> _loadSavedServers() async {
38+
final results = await Future.wait<dynamic>([
39+
loadServerUrl(),
40+
loadServerHistory(),
41+
]);
4042
if (!mounted) return;
43+
44+
final savedUrl = results[0] as String;
45+
final history = results[1] as List<ServerRecord>;
4146
setState(() {
4247
_history = history;
43-
// 若有历史记录,自动填入最近一次的地址,省得用户再输
44-
if (history.isNotEmpty && _urlCtrl.text == 'http://') {
48+
if (savedUrl.isNotEmpty) {
49+
_urlCtrl.text = savedUrl;
50+
} else if (history.isNotEmpty && _urlCtrl.text == 'http://') {
4551
_urlCtrl.text = history.first.url;
4652
}
4753
});
4854
}
4955

5056
String _normalizeUrl(String raw) {
5157
var url = raw.trim();
52-
// 去尾部斜杠
5358
while (url.endsWith('/')) {
5459
url = url.substring(0, url.length - 1);
5560
}
5661
return url;
5762
}
5863

5964
String? _validateUrl(String? value) {
60-
final v = (value ?? '').trim();
61-
if (v.isEmpty) return '请输入服务器地址';
62-
if (!v.startsWith('http://') && !v.startsWith('https://')) {
65+
final input = (value ?? '').trim();
66+
if (input.isEmpty) return '请输入服务器地址';
67+
if (!input.startsWith('http://') && !input.startsWith('https://')) {
6368
return '地址必须以 http:// 或 https:// 开头';
6469
}
65-
final uri = Uri.tryParse(v);
70+
final uri = Uri.tryParse(input);
6671
if (uri == null || uri.host.isEmpty) return '地址格式不正确';
6772
return null;
6873
}
6974

7075
Future<void> _connect() async {
7176
if (!(_formKey.currentState?.validate() ?? false)) return;
77+
7278
HapticFeedback.lightImpact();
7379
final url = _normalizeUrl(_urlCtrl.text);
74-
7580
setState(() {
7681
_busy = true;
7782
_errorMsg = null;
7883
});
84+
7985
try {
80-
final ok = await ref.read(authProvider.notifier).setServerUrl(url);
86+
final connected =
87+
await ref.read(authProvider.notifier).setServerUrl(url);
8188
if (!mounted) return;
82-
if (!ok) {
83-
final err = ref.read(authProvider).error;
84-
setState(() => _errorMsg = err ?? '无法连接到服务器,请检查地址或网络');
85-
return;
89+
90+
if (!connected) {
91+
final error = ref.read(authProvider).error;
92+
setState(() {
93+
_errorMsg =
94+
error ?? '服务器暂时不可达,地址已保存,可进入离线缓存';
95+
});
8696
}
87-
// 成功后 GoRouter 的 redirect 会根据登录状态自动跳到 /login 或 /
88-
} catch (e) {
97+
} catch (error) {
8998
if (!mounted) return;
90-
setState(() => _errorMsg = '连接失败:$e');
99+
setState(() {
100+
_errorMsg = '连接失败:$error\n服务器地址已保留';
101+
});
91102
} finally {
92-
if (mounted) {
93-
setState(() => _busy = false);
94-
}
103+
if (mounted) setState(() => _busy = false);
95104
}
96105
}
97106

98107
Future<void> _removeHistory(String url) async {
99108
await removeServerRecord(url);
100-
await _loadHistory();
109+
await _loadSavedServers();
101110
}
102111

103112
@override
104113
Widget build(BuildContext context) {
105114
final theme = Theme.of(context);
115+
final auth = ref.watch(authProvider);
116+
106117
return Scaffold(
118+
appBar: AppBar(
119+
title: const Text('服务器设置'),
120+
actions: [
121+
if (auth.isOffline)
122+
TextButton.icon(
123+
onPressed: () => context.go('/cache'),
124+
icon: const Icon(Icons.cloud_off_rounded),
125+
label: const Text('离线书架'),
126+
),
127+
],
128+
),
107129
body: SafeArea(
108130
child: Center(
109131
child: SingleChildScrollView(
110-
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
132+
padding: const EdgeInsets.symmetric(
133+
horizontal: 24,
134+
vertical: 32,
135+
),
111136
child: ConstrainedBox(
112137
constraints: const BoxConstraints(maxWidth: 460),
113138
child: Form(
114139
key: _formKey,
115140
child: Column(
116141
crossAxisAlignment: CrossAxisAlignment.stretch,
117142
children: [
118-
Icon(Icons.dns_rounded,
119-
size: 64, color: theme.colorScheme.primary),
143+
Icon(
144+
Icons.dns_rounded,
145+
size: 64,
146+
color: theme.colorScheme.primary,
147+
),
120148
const SizedBox(height: 16),
121149
Text(
122150
'连接到 Nowen Reader',
@@ -127,7 +155,8 @@ class _ServerConfigScreenState extends ConsumerState<ServerConfigScreen> {
127155
),
128156
const SizedBox(height: 8),
129157
Text(
130-
'请输入你的服务器地址,例如 http://192.168.1.100:3000',
158+
'输入你的服务器地址。地址会先保存在本机,'
159+
'即使当前断网也不会丢失。',
131160
textAlign: TextAlign.center,
132161
style: theme.textTheme.bodyMedium?.copyWith(
133162
color: theme.colorScheme.onSurfaceVariant,
@@ -144,8 +173,8 @@ class _ServerConfigScreenState extends ConsumerState<ServerConfigScreen> {
144173
onFieldSubmitted: (_) => _connect(),
145174
decoration: const InputDecoration(
146175
labelText: '服务器地址',
147-
hintText: 'http://host:port',
148-
prefixIcon: Icon(Icons.link),
176+
hintText: 'http://192.168.1.100:3000',
177+
prefixIcon: Icon(Icons.link_rounded),
149178
border: OutlineInputBorder(),
150179
),
151180
),
@@ -155,19 +184,23 @@ class _ServerConfigScreenState extends ConsumerState<ServerConfigScreen> {
155184
padding: const EdgeInsets.all(12),
156185
decoration: BoxDecoration(
157186
color: theme.colorScheme.errorContainer,
158-
borderRadius: BorderRadius.circular(8),
187+
borderRadius: BorderRadius.circular(10),
159188
),
160189
child: Row(
190+
crossAxisAlignment: CrossAxisAlignment.start,
161191
children: [
162-
Icon(Icons.error_outline,
163-
color: theme.colorScheme.onErrorContainer,
164-
size: 20),
165-
const SizedBox(width: 8),
192+
Icon(
193+
Icons.cloud_off_rounded,
194+
color:
195+
theme.colorScheme.onErrorContainer,
196+
),
197+
const SizedBox(width: 10),
166198
Expanded(
167199
child: Text(
168200
_errorMsg!,
169201
style: TextStyle(
170-
color: theme.colorScheme.onErrorContainer,
202+
color:
203+
theme.colorScheme.onErrorContainer,
171204
),
172205
),
173206
),
@@ -182,56 +215,70 @@ class _ServerConfigScreenState extends ConsumerState<ServerConfigScreen> {
182215
? const SizedBox(
183216
width: 18,
184217
height: 18,
185-
child: CircularProgressIndicator(strokeWidth: 2),
218+
child: CircularProgressIndicator(
219+
strokeWidth: 2,
220+
),
186221
)
187-
: const Icon(Icons.login),
222+
: const Icon(Icons.login_rounded),
188223
label: Padding(
189224
padding: const EdgeInsets.symmetric(vertical: 12),
190225
child: Text(_busy ? '正在连接…' : '连接服务器'),
191226
),
192227
),
228+
if (auth.isOffline) ...[
229+
const SizedBox(height: 12),
230+
OutlinedButton.icon(
231+
onPressed: () => context.go('/cache'),
232+
icon: const Icon(Icons.menu_book_rounded),
233+
label: const Padding(
234+
padding: EdgeInsets.symmetric(vertical: 12),
235+
child: Text('服务器不可达,进入离线书架'),
236+
),
237+
),
238+
],
193239
if (_history.isNotEmpty) ...[
194240
const SizedBox(height: 32),
195-
Row(
196-
children: [
197-
Text(
198-
'最近使用',
199-
style: theme.textTheme.titleSmall?.copyWith(
200-
color: theme.colorScheme.onSurfaceVariant,
201-
),
202-
),
203-
],
241+
Text(
242+
'最近使用',
243+
style: theme.textTheme.titleSmall?.copyWith(
244+
color: theme.colorScheme.onSurfaceVariant,
245+
),
204246
),
205247
const SizedBox(height: 8),
206-
..._history.map((rec) => Card(
207-
margin: const EdgeInsets.symmetric(vertical: 4),
208-
child: ListTile(
209-
leading: const Icon(Icons.history),
210-
title: Text(
211-
rec.url,
212-
maxLines: 1,
213-
overflow: TextOverflow.ellipsis,
214-
),
215-
subtitle: rec.username != null
216-
? Text('用户:${rec.nickname ?? rec.username}')
217-
: null,
218-
trailing: IconButton(
219-
icon: const Icon(Icons.close),
220-
tooltip: '从历史中移除',
221-
onPressed: _busy
222-
? null
223-
: () => _removeHistory(rec.url),
224-
),
225-
onTap: _busy
248+
..._history.map(
249+
(record) => Card(
250+
margin: const EdgeInsets.symmetric(vertical: 4),
251+
child: ListTile(
252+
leading: const Icon(Icons.history_rounded),
253+
title: Text(
254+
record.url,
255+
maxLines: 1,
256+
overflow: TextOverflow.ellipsis,
257+
),
258+
subtitle: record.username != null
259+
? Text(
260+
'用户:'
261+
'${record.nickname ?? record.username}',
262+
)
263+
: null,
264+
trailing: IconButton(
265+
icon: const Icon(Icons.close_rounded),
266+
tooltip: '从历史中移除',
267+
onPressed: _busy
226268
? null
227-
: () {
228-
setState(() {
229-
_urlCtrl.text = rec.url;
230-
_errorMsg = null;
231-
});
232-
},
269+
: () => _removeHistory(record.url),
233270
),
234-
)),
271+
onTap: _busy
272+
? null
273+
: () {
274+
setState(() {
275+
_urlCtrl.text = record.url;
276+
_errorMsg = null;
277+
});
278+
},
279+
),
280+
),
281+
),
235282
],
236283
],
237284
),

0 commit comments

Comments
 (0)