feat: login登录优化&通用报错

This commit is contained in:
chaos-zhu
2026-06-06 22:03:24 +08:00
parent 7d0dfe3c48
commit 9f32ebdfbc
22 changed files with 226 additions and 178 deletions
+24
View File
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import '../../l10n/app_localizations.dart';
Future<void> runRefreshWithFeedback(
BuildContext context,
Future<void> Function() refresh,
) async {
try {
await refresh();
} catch (error) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
).trf('common.refreshFailed', [error.toString()]),
),
behavior: SnackBarBehavior.floating,
),
);
}
}
+24 -71
View File
@@ -43,7 +43,6 @@ class _LoginPageState extends State<LoginPage> {
LoginExpiry _expiry = LoginExpiry.currentDay;
bool _savePassword = false;
bool _httpRiskAccepted = false;
bool _showHttpRiskBanner = false;
bool _submitting = false;
String? _errorMessage;
@@ -108,7 +107,11 @@ class _LoginPageState extends State<LoginPage> {
if (!mounted) return;
if (result.requiresHttpRiskConfirmation) {
setState(() => _showHttpRiskBanner = true);
setState(() => _submitting = false);
final accepted = await _confirmHttpRisk();
if (!mounted || accepted != true) return;
setState(() => _httpRiskAccepted = true);
await _submit();
return;
}
if (!result.success || result.session == null) {
@@ -121,12 +124,25 @@ class _LoginPageState extends State<LoginPage> {
}
}
Future<void> _acceptHttpRiskAndSubmit() async {
setState(() {
_httpRiskAccepted = true;
_showHttpRiskBanner = false;
});
await _submit();
Future<bool?> _confirmHttpRisk() {
final l = AppLocalizations.of(context);
return showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(l.tr('login.httpRiskTitle')),
content: Text(l.tr('login.httpRiskBody')),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(l.tr('common.cancel')),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(l.tr('common.continue')),
),
],
),
);
}
@override
@@ -170,10 +186,6 @@ class _LoginPageState extends State<LoginPage> {
onChanged: (value) =>
setState(() => _savePassword = value),
),
if (_showHttpRiskBanner) ...[
const SizedBox(height: 14),
_HttpRiskBanner(onConfirm: _acceptHttpRiskAndSubmit),
],
if (_errorMessage != null) ...[
const SizedBox(height: 14),
_ErrorBox(message: _errorMessage!),
@@ -490,65 +502,6 @@ class _LoginHero extends ConsumerWidget {
}
}
class _HttpRiskBanner extends StatelessWidget {
const _HttpRiskBanner({required this.onConfirm});
final VoidCallback onConfirm;
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFFFE8BF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE6A23C)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.warning_amber, color: Color(0xFF7A4A00), size: 20),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l.tr('login.httpRiskTitle'),
style: const TextStyle(
color: Color(0xFF7A4A00),
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
l.tr('login.httpRiskBody'),
style: const TextStyle(
color: Color(0xFF7A4A00),
fontSize: 13,
height: 1.35,
),
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: onConfirm,
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF7A4A00),
),
child: Text(l.tr('common.continue')),
),
),
],
),
),
],
),
);
}
}
class _ErrorBox extends StatelessWidget {
const _ErrorBox({required this.message});
+2 -1
View File
@@ -4,6 +4,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import '../../core/api/api_result.dart';
import '../../core/ui/palette.dart';
import '../../core/ui/refresh_feedback.dart';
import '../../l10n/app_localizations.dart';
import '../../state/api_providers.dart';
import '../../state/auth_notifier.dart';
@@ -55,7 +56,7 @@ class _ServersTabState extends ConsumerState<ServersTab> {
}
Future<void> _refresh() async {
await refreshServerSharedData(ref);
await runRefreshWithFeedback(context, () => refreshServerSharedData(ref));
}
Future<void> _connect(ServerModel server) async {
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/api/api_result.dart';
import '../../core/ui/palette.dart';
import '../../core/ui/refresh_feedback.dart';
import '../../l10n/app_localizations.dart';
import '../../state/api_providers.dart';
import '../../state/credential_list_notifier.dart';
@@ -206,8 +207,12 @@ class _CredentialsPageState extends ConsumerState<CredentialsPage> {
),
Expanded(
child: RefreshIndicator(
onRefresh: () =>
ref.read(credentialListProvider.notifier).refresh(),
onRefresh: () => runRefreshWithFeedback(
context,
() => ref
.read(credentialListProvider.notifier)
.refresh(throwOnError: true),
),
child: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => _ErrorBody(
@@ -5,6 +5,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../core/api/api_result.dart';
import '../../core/ui/palette.dart';
import '../../core/ui/refresh_feedback.dart';
import '../../l10n/app_localizations.dart';
import '../../state/api_providers.dart';
import '../../state/plus_info_notifier.dart';
@@ -127,10 +128,12 @@ class _PlusSubscriptionPageState extends ConsumerState<PlusSubscriptionPage> {
],
),
body: RefreshIndicator(
onRefresh: () async {
await ref.read(plusInfoProvider.notifier).refresh();
onRefresh: () => runRefreshWithFeedback(context, () async {
await ref
.read(plusInfoProvider.notifier)
.refresh(throwOnError: true);
await _loadInitial();
},
}),
child: plusAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => _ErrorBody(
+7 -1
View File
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/api/api_result.dart';
import '../../core/ui/palette.dart';
import '../../core/ui/refresh_feedback.dart';
import '../../l10n/app_localizations.dart';
import '../../state/api_providers.dart';
import '../../state/plus_info_notifier.dart';
@@ -247,7 +248,12 @@ class _ProxyPageState extends ConsumerState<ProxyPage> {
),
Expanded(
child: RefreshIndicator(
onRefresh: () => ref.read(proxyListProvider.notifier).refresh(),
onRefresh: () => runRefreshWithFeedback(
context,
() => ref
.read(proxyListProvider.notifier)
.refresh(throwOnError: true),
),
child: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => _ErrorBody(
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/api/api_result.dart';
import '../../core/ui/palette.dart';
import '../../core/ui/refresh_feedback.dart';
import '../../l10n/app_localizations.dart';
import '../../state/api_providers.dart';
import '../../state/auth_notifier.dart';
@@ -212,7 +213,11 @@ class _SessionsPageState extends ConsumerState<SessionsPage> {
],
),
body: RefreshIndicator(
onRefresh: () => ref.read(loginLogProvider.notifier).refresh(),
onRefresh: () => runRefreshWithFeedback(
context,
() =>
ref.read(loginLogProvider.notifier).refresh(throwOnError: true),
),
child: logAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => _ErrorBody(
+10 -4
View File
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/api/api_result.dart';
import '../../core/ui/refresh_feedback.dart';
import '../../features/scripts/script_form_page.dart';
import '../../features/scripts/script_group_model.dart';
import '../../features/scripts/script_groups_page.dart';
@@ -39,10 +40,15 @@ class _ScriptsTabState extends ConsumerState<ScriptsTab> {
}
Future<void> _refresh() async {
await Future.wait([
ref.read(scriptListProvider.notifier).refresh(),
ref.read(scriptGroupListProvider.notifier).refresh(),
]);
await runRefreshWithFeedback(
context,
() => Future.wait([
ref.read(scriptListProvider.notifier).refresh(throwOnError: true),
ref
.read(scriptGroupListProvider.notifier)
.refresh(throwOnError: true),
]),
);
}
Future<void> _openForm({ScriptModel? script}) async {
+10 -9
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/ui/palette.dart';
import '../../core/ui/refresh_feedback.dart';
import '../../l10n/app_localizations.dart';
import '../../state/auth_notifier.dart';
import '../../state/credential_list_notifier.dart';
@@ -184,14 +185,14 @@ class SettingsTab extends ConsumerWidget {
);
}
Future<void> _refresh(WidgetRef ref) async {
await Future.wait([
ref.read(hostListProvider.notifier).refresh(),
ref.read(credentialListProvider.notifier).refresh(),
ref.read(scriptListProvider.notifier).refresh(),
ref.read(plusInfoProvider.notifier).refresh(),
ref.read(plusDiscountProvider.notifier).refresh(),
]);
Future<void> _refresh(BuildContext context, WidgetRef ref) async {
await runRefreshWithFeedback(context, () => Future.wait([
ref.read(hostListProvider.notifier).refresh(throwOnError: true),
ref.read(credentialListProvider.notifier).refresh(throwOnError: true),
ref.read(scriptListProvider.notifier).refresh(throwOnError: true),
ref.read(plusInfoProvider.notifier).refresh(throwOnError: true),
ref.read(plusDiscountProvider.notifier).refresh(throwOnError: true),
]));
}
@override
@@ -218,7 +219,7 @@ class SettingsTab extends ConsumerWidget {
body: SafeArea(
bottom: false,
child: RefreshIndicator(
onRefresh: () => _refresh(ref),
onRefresh: () => _refresh(context, ref),
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
+5 -1
View File
@@ -8,6 +8,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../../core/api/api_result.dart';
import '../../core/ui/refresh_feedback.dart';
import '../../features/servers/server_model.dart';
import '../../l10n/app_localizations.dart';
import '../../state/api_providers.dart';
@@ -30,7 +31,10 @@ class SftpTab extends ConsumerStatefulWidget {
class _SftpTabState extends ConsumerState<SftpTab> {
bool _connecting = false;
Future<void> _refresh() => ref.read(hostListProvider.notifier).refresh();
Future<void> _refresh() => runRefreshWithFeedback(
context,
() => ref.read(hostListProvider.notifier).refresh(throwOnError: true),
);
Future<void> _openServerPicker() async {
final manager = ref.read(sftpSessionManagerProvider);
+1
View File
@@ -24,6 +24,7 @@ const Map<String, String> stringsEn = {
'common.exitApp': 'Exit',
'common.exitAppTitle': 'Exit app?',
'common.exitAppBody': 'Are you sure you want to exit the app?',
'common.refreshFailed': 'Refresh failed: {0}',
// Login page
'login.serverAddress': 'Server address',
+2
View File
@@ -24,6 +24,8 @@ const Map<String, String> stringsZh = {
'common.exitAppTitle': '退出 App?',
'common.exitAppBody': '确认退出 App?',
'common.refreshFailed': '刷新失败:{0}',
// Login
'login.serverAddress': '服务端地址',
'login.serverAddressHint': 'https://example.com',
+10 -5
View File
@@ -13,13 +13,18 @@ class CredentialListNotifier
return _fetch(ref.watch(apiClientProvider));
}
Future<void> refresh() async {
Future<void> refresh({bool throwOnError = false}) async {
final previous = state.valueOrNull;
if (previous == null) state = const AsyncLoading();
state = await AsyncValue.guard(
() => _fetch(ref.read(apiClientProvider)),
);
if (state.hasError && previous != null) state = AsyncData(previous);
try {
state = AsyncData(await _fetch(ref.read(apiClientProvider)));
} catch (error, stackTrace) {
state = previous == null
? AsyncError(error, stackTrace)
: AsyncData(previous);
if (!throwOnError) return;
rethrow;
}
}
Future<List<ServerCredentialModel>> _fetch(ApiClient api) async {
+14 -11
View File
@@ -18,21 +18,24 @@ class GroupListNotifier extends AsyncNotifier<List<ServerGroupModel>> {
}
}
Future<void> refresh() async {
Future<void> refresh({bool throwOnError = false}) async {
final previous = state.valueOrNull;
if (previous == null) {
state = const AsyncLoading();
}
state = await AsyncValue.guard(() async {
try {
return await ref.read(serverRepositoryProvider).fetchGroups();
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
rethrow;
}
});
if (state.hasError && previous != null) {
state = AsyncData(previous);
try {
final groups = await ref.read(serverRepositoryProvider).fetchGroups();
state = AsyncData(groups);
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
if (!throwOnError) return;
rethrow;
} catch (error, stackTrace) {
state = previous == null
? AsyncError(error, stackTrace)
: AsyncData(previous);
if (!throwOnError) return;
rethrow;
}
}
}
+14 -11
View File
@@ -20,21 +20,24 @@ class HostListNotifier extends AsyncNotifier<List<ServerModel>> {
}
}
Future<void> refresh() async {
Future<void> refresh({bool throwOnError = false}) async {
final previous = state.valueOrNull;
if (previous == null) {
state = const AsyncLoading();
}
state = await AsyncValue.guard(() async {
try {
return await ref.read(serverRepositoryProvider).fetchHosts();
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
rethrow;
}
});
if (state.hasError && previous != null) {
state = AsyncData(previous);
try {
final hosts = await ref.read(serverRepositoryProvider).fetchHosts();
state = AsyncData(hosts);
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
if (!throwOnError) return;
rethrow;
} catch (error, stackTrace) {
state = previous == null
? AsyncError(error, stackTrace)
: AsyncData(previous);
if (!throwOnError) return;
rethrow;
}
}
}
+15 -10
View File
@@ -16,18 +16,23 @@ class LoginLogNotifier extends AsyncNotifier<LoginLogData> {
}
}
Future<void> refresh() async {
Future<void> refresh({bool throwOnError = false}) async {
final previous = state.valueOrNull;
if (previous == null) state = const AsyncLoading();
state = await AsyncValue.guard(() async {
try {
return await ref.read(settingsRepositoryProvider).getLoginLog();
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
rethrow;
}
});
if (state.hasError && previous != null) state = AsyncData(previous);
try {
final log = await ref.read(settingsRepositoryProvider).getLoginLog();
state = AsyncData(log);
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
if (!throwOnError) return;
rethrow;
} catch (error, stackTrace) {
state = previous == null
? AsyncError(error, stackTrace)
: AsyncData(previous);
if (!throwOnError) return;
rethrow;
}
}
}
+12 -5
View File
@@ -11,12 +11,19 @@ class PlusDiscountNotifier extends AsyncNotifier<PlusDiscount> {
return ref.watch(settingsRepositoryProvider).getPlusDiscount();
}
Future<void> refresh() async {
Future<void> refresh({bool throwOnError = false}) async {
final previous = state.valueOrNull;
state = await AsyncValue.guard(
() => ref.read(settingsRepositoryProvider).getPlusDiscount(),
);
if (state.hasError && previous != null) state = AsyncData(previous);
try {
state = AsyncData(
await ref.read(settingsRepositoryProvider).getPlusDiscount(),
);
} catch (error, stackTrace) {
state = previous == null
? AsyncError(error, stackTrace)
: AsyncData(previous);
if (!throwOnError) return;
rethrow;
}
}
}
+15 -12
View File
@@ -17,20 +17,23 @@ class PlusInfoNotifier extends AsyncNotifier<PlusInfo> {
}
}
Future<void> refresh() async {
Future<void> refresh({bool throwOnError = false}) async {
final previous = state.valueOrNull;
if (previous == null) state = const AsyncLoading();
state = await AsyncValue.guard(
() async {
try {
return await ref.read(settingsRepositoryProvider).getPlusInfo();
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
rethrow;
}
},
);
if (state.hasError && previous != null) state = AsyncData(previous);
try {
final plusInfo = await ref.read(settingsRepositoryProvider).getPlusInfo();
state = AsyncData(plusInfo);
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
if (!throwOnError) return;
rethrow;
} catch (error, stackTrace) {
state = previous == null
? AsyncError(error, stackTrace)
: AsyncData(previous);
if (!throwOnError) return;
rethrow;
}
}
}
+10 -5
View File
@@ -12,13 +12,18 @@ class ProxyListNotifier extends AsyncNotifier<List<ServerProxyModel>> {
return _fetch(ref.watch(apiClientProvider));
}
Future<void> refresh() async {
Future<void> refresh({bool throwOnError = false}) async {
final previous = state.valueOrNull;
if (previous == null) state = const AsyncLoading();
state = await AsyncValue.guard(
() => _fetch(ref.read(apiClientProvider)),
);
if (state.hasError && previous != null) state = AsyncData(previous);
try {
state = AsyncData(await _fetch(ref.read(apiClientProvider)));
} catch (error, stackTrace) {
state = previous == null
? AsyncError(error, stackTrace)
: AsyncData(previous);
if (!throwOnError) return;
rethrow;
}
}
Future<List<ServerProxyModel>> _fetch(ApiClient api) async {
@@ -20,21 +20,24 @@ class ScriptGroupListNotifier extends AsyncNotifier<List<ScriptGroupModel>> {
}
}
Future<void> refresh() async {
Future<void> refresh({bool throwOnError = false}) async {
final previous = state.valueOrNull;
if (previous == null) {
state = const AsyncLoading();
}
state = await AsyncValue.guard(() async {
try {
return await ref.read(scriptRepositoryProvider).fetchGroups();
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
rethrow;
}
});
if (state.hasError && previous != null) {
state = AsyncData(previous);
try {
final groups = await ref.read(scriptRepositoryProvider).fetchGroups();
state = AsyncData(groups);
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
if (!throwOnError) return;
rethrow;
} catch (error, stackTrace) {
state = previous == null
? AsyncError(error, stackTrace)
: AsyncData(previous);
if (!throwOnError) return;
rethrow;
}
}
}
+14 -11
View File
@@ -21,21 +21,24 @@ class ScriptListNotifier extends AsyncNotifier<List<ScriptModel>> {
}
}
Future<void> refresh() async {
Future<void> refresh({bool throwOnError = false}) async {
final previous = state.valueOrNull;
if (previous == null) {
state = const AsyncLoading();
}
state = await AsyncValue.guard(() async {
try {
return await ref.read(scriptRepositoryProvider).fetchScripts();
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
rethrow;
}
});
if (state.hasError && previous != null) {
state = AsyncData(previous);
try {
final scripts = await ref.read(scriptRepositoryProvider).fetchScripts();
state = AsyncData(scripts);
} on UnauthorizedFailure {
await ref.read(authProvider.notifier).signOut();
if (!throwOnError) return;
rethrow;
} catch (error, stackTrace) {
state = previous == null
? AsyncError(error, stackTrace)
: AsyncData(previous);
if (!throwOnError) return;
rethrow;
}
}
}
+4 -4
View File
@@ -7,9 +7,9 @@ import 'proxy_list_notifier.dart';
Future<void> refreshServerSharedData(WidgetRef ref) async {
await Future.wait([
ref.read(groupListProvider.notifier).refresh(),
ref.read(hostListProvider.notifier).refresh(),
ref.read(credentialListProvider.notifier).refresh(),
ref.read(proxyListProvider.notifier).refresh(),
ref.read(groupListProvider.notifier).refresh(throwOnError: true),
ref.read(hostListProvider.notifier).refresh(throwOnError: true),
ref.read(credentialListProvider.notifier).refresh(throwOnError: true),
ref.read(proxyListProvider.notifier).refresh(throwOnError: true),
]);
}