import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/remote/collection_source_remote_ext.dart'; // appendRemoteEntriesFromDb() import 'package:aves/remote/remote_controller.dart'; import 'package:aves/remote/remote_settings.dart'; import 'package:aves/remote/remote_http.dart'; import 'package:aves/remote/remote_sync_bus.dart'; import 'remote_origin.dart'; import 'remote_state_store.dart'; class RemoteSettingsDialog { static Future show(BuildContext context) async { final s = await RemoteSettings.load(); final formKey = GlobalKey(); bool enabled = s.enabled; final baseUrlC = TextEditingController(text: s.baseUrl); final wsUrlC = TextEditingController(text: s.wsUrl); final indexC = TextEditingController(text: s.indexPath); final emailC = TextEditingController(text: s.email); final pwC = TextEditingController(text: s.password); final store = RemoteStateStore(); String deriveWsUrl(String baseUrl) { final uri = Uri.tryParse(baseUrl.trim()); if (uri == null) return ''; final host = uri.host; final scheme = uri.scheme == 'http' ? 'ws' : 'wss'; final parts = host.split('.'); if (parts.isEmpty) return '$scheme://$host'; parts[0] = '${parts[0]}-ws'; return '$scheme://${parts.join('.')}'; } // AUTO se vuoto o uguale al derivato bool wsUrlAuto = wsUrlC.text.trim().isEmpty || wsUrlC.text.trim() == deriveWsUrl(baseUrlC.text.trim()); bool updatingWs = false; String lastBaseForWs = baseUrlC.text.trim(); String resolvedWsUrlForUi() { final w = wsUrlC.text.trim(); if (w.isNotEmpty) return w; return deriveWsUrl(baseUrlC.text.trim()); } String? validateBaseUrl(String? v) { final txt = (v ?? '').trim(); if (txt.isEmpty) return 'Obbligatorio'; final uri = Uri.tryParse(txt); if (uri == null || !(uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https'))) { return 'URL non valida (deve iniziare con http/https)'; } if (RegExp(r'[\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]').hasMatch(txt)) { return 'URL contiene caratteri non validi (invisibili)'; } return null; } String? validateWsUrl(String? v) { final txt = (v ?? '').trim(); if (txt.isEmpty) return null; final uri = Uri.tryParse(txt); if (uri == null || !(uri.hasScheme && (uri.scheme == 'ws' || uri.scheme == 'wss'))) { return 'WS URL non valida (deve iniziare con ws/wss)'; } if (RegExp(r'[\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]').hasMatch(txt)) { return 'WS URL contiene caratteri non validi (invisibili)'; } return null; } String? validateIndex(String? v) { final txt = (v ?? '').trim(); if (txt.isEmpty) return 'Obbligatorio'; return null; } Future applyRuntimeEffects({ required bool newEnabled, required bool oldEnabled, }) async { CollectionSource? source; try { source = context.read(); } catch (_) { source = null; } if (!newEnabled) { RemoteSyncBus.instance.setDisabled(); if (source != null) { final remotesInMemory = source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet(); if (remotesInMemory.isNotEmpty) { source.removeEntriesFromMemory(remotesInMemory); } await RemoteController.instance.onAppStart( source: source, resumeBootstrapIfEnabled: false, ); } return; } if (source == null) { await RemoteController.instance.initBusFromSettings(); return; } await source.appendRemoteEntriesFromDb(); await RemoteController.instance.initBusFromSettings(); await RemoteController.instance.onAppStart( source: source, resumeBootstrapIfEnabled: false, ); } Widget kv(String k, String v) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 135, child: Text(k, style: const TextStyle(fontWeight: FontWeight.w600)), ), Expanded(child: SelectableText(v)), ], ); } Widget buildDebugPanel(StateSetter setStateDialog) { final resolvedWs = resolvedWsUrlForUi(); return Card( elevation: 0, child: ExpansionTile( title: const Text('Debug (WS / Sync)'), subtitle: const Text('Stato runtime, sessione WS, lastSync, ring buffer'), childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), children: [ ValueListenableBuilder( valueListenable: RemoteSyncBus.instance.stateNotifier, builder: (context, st, _) => kv('Sync state', st.name), ), const SizedBox(height: 6), kv('WS URL (usato)', resolvedWs.isEmpty ? '—' : resolvedWs), const SizedBox(height: 6), kv('WS URL mode', wsUrlAuto ? 'AUTO (segue Base URL)' : 'MANUAL'), const SizedBox(height: 6), FutureBuilder( future: store.getLastSyncIso(), builder: (context, snap) { final v = snap.data; return kv('lastSyncIso', (v == null || v.isEmpty) ? '—' : v); }, ), const SizedBox(height: 6), FutureBuilder( future: store.getOrCreateSessionId(), builder: (context, snap) { final v = snap.data; return kv('ws_session_id', (v == null || v.isEmpty) ? '—' : v); }, ), const SizedBox(height: 6), FutureBuilder>( future: store.loadProcessedRing(), builder: (context, snap) { final n = snap.data?.length ?? 0; return kv('processed_ring', '$n'); }, ), const SizedBox(height: 10), const Text( 'Note:\n' '• AUTO: wsUrl segue Base URL.\n' '• MANUAL: wsUrl non viene modificato.\n' '• Cancellando wsUrl torni in AUTO.', style: TextStyle(fontSize: 12), ), ], ), ); } await showDialog( context: context, builder: (_) => StatefulBuilder( builder: (context, setStateDialog) => AlertDialog( title: const Text('Remote Settings'), content: Form( key: formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ SwitchListTile( title: const Text('Abilita remote'), value: enabled, onChanged: (v) => setStateDialog(() => enabled = v), contentPadding: EdgeInsets.zero, ), const SizedBox(height: 8), TextFormField( controller: baseUrlC, decoration: const InputDecoration(labelText: 'Base URL'), validator: validateBaseUrl, onChanged: (v) { final base = v.trim(); if (base.isEmpty) return; if (base == lastBaseForWs) return; if (wsUrlAuto) { final derived = deriveWsUrl(base); updatingWs = true; wsUrlC.text = derived; updatingWs = false; setStateDialog(() {}); } lastBaseForWs = base; }, ), const SizedBox(height: 8), TextFormField( controller: wsUrlC, decoration: const InputDecoration( labelText: 'WebSocket URL (opzionale)', hintText: 'wss://example-ws.org', ), validator: validateWsUrl, onChanged: (v) { if (updatingWs) return; final txt = v.trim(); if (txt.isEmpty) { wsUrlAuto = true; setStateDialog(() {}); return; } wsUrlAuto = false; setStateDialog(() {}); }, ), const SizedBox(height: 8), TextFormField( controller: indexC, decoration: const InputDecoration(labelText: 'Index path'), validator: validateIndex, ), const SizedBox(height: 8), TextFormField( controller: emailC, decoration: const InputDecoration(labelText: 'User/Email'), ), const SizedBox(height: 8), TextFormField( controller: pwC, obscureText: true, decoration: const InputDecoration(labelText: 'Password'), ), const SizedBox(height: 12), buildDebugPanel(setStateDialog), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).maybePop(), child: const Text('Annulla'), ), ElevatedButton.icon( onPressed: () async { if (!formKey.currentState!.validate()) return; final oldEnabled = s.enabled; final newEnabled = enabled; final upd = RemoteSettings( enabled: newEnabled, baseUrl: baseUrlC.text.trim(), indexPath: indexC.text.trim(), email: emailC.text.trim(), password: pwC.text, wsUrl: wsUrlC.text.trim(), ); await upd.save(); await RemoteHttp.refreshFromSettings(); await RemoteHttp.warmUp(); await applyRuntimeEffects(newEnabled: newEnabled, oldEnabled: oldEnabled); if (context.mounted) Navigator.of(context).pop(); }, icon: const Icon(Icons.save), label: const Text('Salva'), ), ], ), ), ); baseUrlC.dispose(); wsUrlC.dispose(); indexC.dispose(); emailC.dispose(); pwC.dispose(); } }