import 'package:commons_core/commons_core.dart'; import 'package:flutter/material.dart'; import '../i18n/strings.g.dart'; import '../services/profile_cache.dart' show shortPubkey; import '../services/trust_referents.dart'; import '../services/wot_settings.dart'; import 'theme.dart'; /// Manage the web of trust: the bootstrap "roots" (Duniter seeds) membership is /// measured from, and the advanced parameters (sigQty / stepMax / validity). /// This is where a young network is seeded and tuned — progressive disclosure, /// reached from the profile. class TrustNetworkScreen extends StatefulWidget { const TrustNetworkScreen({ required this.referents, required this.wotSettings, super.key, }); final TrustReferents referents; final WotSettings wotSettings; @override State createState() => _TrustNetworkScreenState(); } class _TrustNetworkScreenState extends State { final _input = TextEditingController(); Set _roots = {}; WotParams _params = WotParams.duniter; bool _loading = true; String? _error; @override void initState() { super.initState(); _load(); } Future _load() async { final roots = await widget.referents.userAdded(); final params = await widget.wotSettings.params(); if (!mounted) return; setState(() { _roots = roots; _params = params; _loading = false; }); } Future _addRoot() async { final t = context.t; final messenger = ScaffoldMessenger.of(context); try { await widget.referents.add(_input.text); _input.clear(); await _load(); messenger.showSnackBar(SnackBar(content: Text(t.wot.rootAdded))); } on FormatException { setState(() => _error = t.wot.rootInvalid); } } Future _removeRoot(String hex) async { await widget.referents.remove(hex); await _load(); } Future _saveParams(WotParams next) async { await widget.wotSettings.save(next); if (!mounted) return; setState(() => _params = next); ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(context.t.wot.saved))); } @override void dispose() { _input.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final t = context.t; return Scaffold( appBar: AppBar(title: Text(t.wot.title)), body: _loading ? const Center(child: CircularProgressIndicator()) : ListView( padding: const EdgeInsets.all(20), children: [ Text(t.wot.help, style: const TextStyle(color: seedMuted, fontSize: 13)), const SizedBox(height: 20), Text(t.wot.roots, style: const TextStyle( fontWeight: FontWeight.w600, color: seedOnSurface)), const SizedBox(height: 4), Text(t.wot.rootsHelp, style: const TextStyle(color: seedMuted, fontSize: 12)), const SizedBox(height: 8), if (_roots.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text(t.wot.noRoots, style: const TextStyle(color: seedMuted, fontSize: 13)), ), for (final hex in _roots) ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.hub_outlined, color: seedGreen), title: Text(shortPubkey(hex), style: const TextStyle( fontFamily: 'monospace', fontSize: 13)), trailing: IconButton( icon: const Icon(Icons.close, size: 20), tooltip: t.wot.remove, onPressed: () => _removeRoot(hex), ), ), const SizedBox(height: 8), TextField( key: const Key('wot.rootInput'), controller: _input, onChanged: (_) { if (_error != null) setState(() => _error = null); }, decoration: InputDecoration( labelText: t.wot.addRoot, hintText: t.wot.rootHint, errorText: _error, suffixIcon: IconButton( key: const Key('wot.addRoot'), icon: const Icon(Icons.add), onPressed: _addRoot, ), ), ), const SizedBox(height: 12), const Divider(), ExpansionTile( tilePadding: EdgeInsets.zero, title: Text(t.wot.params), subtitle: Text(t.wot.paramsHelp, style: const TextStyle(fontSize: 12)), children: [ _Stepper( label: t.wot.sigQty, value: _params.sigQty, min: 1, onChanged: (v) => _saveParams(_params.copyWith(sigQty: v)), ), _Stepper( label: t.wot.stepMax, value: _params.stepMax, min: 1, onChanged: (v) => _saveParams(_params.copyWith(stepMax: v)), ), _Stepper( label: t.wot.validityDays, value: _params.sigValidity.inDays, min: 1, step: 30, onChanged: (v) => _saveParams( _params.copyWith(sigValidity: Duration(days: v))), ), const SizedBox(height: 8), Align( alignment: AlignmentDirectional.centerStart, child: TextButton( onPressed: () => _saveParams(WotParams.duniter), child: Text(t.wot.reset), ), ), ], ), ], ), ); } } /// A "- value +" row for an integer parameter, clamped to [min]. class _Stepper extends StatelessWidget { const _Stepper({ required this.label, required this.value, required this.onChanged, this.min = 0, this.step = 1, }); final String label; final int value; final int min; final int step; final ValueChanged onChanged; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Expanded(child: Text(label)), IconButton( icon: const Icon(Icons.remove_circle_outline), onPressed: value - step >= min ? () => onChanged(value - step) : null, ), SizedBox( width: 44, child: Text('$value', textAlign: TextAlign.center), ), IconButton( icon: const Icon(Icons.add_circle_outline), onPressed: () => onChanged(value + step), ), ], ), ); } }