Surface persistence and fetch errors instead of swallowing them

PrefUtil.setValue now returns the underlying SharedPreferences result
and throws on unsupported types; settings save reports failures rather
than always showing success. Device detail and event history report
the real Dio error message and skip cancellations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-05-31 18:43:46 -04:00
co-authored by Claude Sonnet 4.6
parent f7f255e20f
commit e70676a193
5 changed files with 47 additions and 21 deletions
+14 -4
View File
@@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -42,8 +43,9 @@ class _DeviceDetailState extends State<DeviceDetail> {
}); });
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
if (e is DioException && e.type == DioExceptionType.cancel) return;
setState(() { setState(() {
_error = e.toString(); _error = dioErrorToUserMessage(e);
_isLoading = false; _isLoading = false;
}); });
} }
@@ -140,7 +142,10 @@ class _DeviceHeader extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SelectableText(device.ipv4Address, style: theme.textTheme.headlineSmall), SelectableText(
device.ipv4Address,
style: theme.textTheme.headlineSmall,
),
const SizedBox(height: 4), const SizedBox(height: 4),
if (device.isRegistered) if (device.isRegistered)
StatusBadge(label: 'Registered', color: BadgeColor.success) StatusBadge(label: 'Registered', color: BadgeColor.success)
@@ -166,7 +171,10 @@ class _DeviceInfoCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final formatter = FriendlyDateFormatter(); final formatter = FriendlyDateFormatter();
final rows = <(String, String)>[ final rows = <(String, String)>[
('Name', device.name == null || device.name!.isEmpty ? '' : device.name!), (
'Name',
device.name == null || device.name!.isEmpty ? '' : device.name!,
),
('MAC Address', device.macAddress), ('MAC Address', device.macAddress),
('IP Address', device.ipv4Address), ('IP Address', device.ipv4Address),
('Vendor', device.vendor.isEmpty ? '' : device.vendor), ('Vendor', device.vendor.isEmpty ? '' : device.vendor),
@@ -251,7 +259,9 @@ class _InfoRow extends StatelessWidget {
), ),
), ),
), ),
Expanded(child: SelectableText(value, style: theme.textTheme.bodyMedium)), Expanded(
child: SelectableText(value, style: theme.textTheme.bodyMedium),
),
], ],
), ),
); );
@@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@@ -98,8 +99,9 @@ class _DeviceEventHistoryState extends State<DeviceEventHistory> {
}); });
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
if (e is DioException && e.type == DioExceptionType.cancel) return;
setState(() { setState(() {
_error = e.toString(); _error = dioErrorToUserMessage(e);
_isLoading = false; _isLoading = false;
}); });
} }
@@ -115,9 +117,9 @@ class _DeviceEventHistoryState extends State<DeviceEventHistory> {
} }
if (_error != null) { if (_error != null) {
return const Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(child: Text('Failed to load event history')), child: Center(child: Text('Failed to load event history: $_error')),
); );
} }
+4 -3
View File
@@ -44,10 +44,11 @@ class AppState extends ChangeNotifier {
String get themeKey => _themeKey; String get themeKey => _themeKey;
ThemeData get theme => _themes[_themeKey]!; ThemeData get theme => _themes[_themeKey]!;
void setTheme(String key) { Future<bool> setTheme(String key) async {
if (!_themes.containsKey(key) || key == _themeKey) return; if (!_themes.containsKey(key)) return false;
if (key == _themeKey) return true;
_themeKey = key; _themeKey = key;
PrefUtil.setValue('theme', key);
notifyListeners(); notifyListeners();
return PrefUtil.setValue('theme', key);
} }
} }
+16 -6
View File
@@ -67,13 +67,23 @@ class _SettingsState extends State<Settings> {
} }
} }
void _save() { Future<void> _save() async {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
PrefUtil.setValue('base_url', _baseUrlController.text); final urlOk = await PrefUtil.setValue('base_url', _baseUrlController.text);
PrefUtil.setValue('api_key', XOR().xorEncode(_apiKeyController.text)); if (!mounted) return;
BackendAPI.instance.reconfigureFromPrefs(); final keyOk = await PrefUtil.setValue(
context.read<AppState>().setTheme(_selectedTheme); 'api_key',
UISnackbars.showSuccess(context, 'Settings saved successfully'); XOR().xorEncode(_apiKeyController.text),
);
if (!mounted) return;
final themeOk = await context.read<AppState>().setTheme(_selectedTheme);
if (!mounted) return;
if (urlOk && keyOk && themeOk) {
BackendAPI.instance.reconfigureFromPrefs();
UISnackbars.showSuccess(context, 'Settings saved successfully');
} else {
UISnackbars.showError(context, 'Failed to save settings');
}
} }
@override @override
+7 -4
View File
@@ -10,15 +10,18 @@ class PrefUtil {
return preferences; return preferences;
} }
static void setValue(String key, Object value) { static Future<bool> setValue(String key, Object value) {
switch (value) { switch (value) {
case String s: case String s:
preferences.setString(key, s); return preferences.setString(key, s);
case bool b: case bool b:
preferences.setBool(key, b); return preferences.setBool(key, b);
case int i: case int i:
preferences.setInt(key, i); return preferences.setInt(key, i);
default: default:
throw ArgumentError(
'Unsupported pref value type: ${value.runtimeType}',
);
} }
} }