import 'package:flutter/material.dart'; import 'package:uuid/uuid.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/settings/logs_settings/logs_settings.dart'; import 'package:adguard_home_manager/widgets/load_status_widgets.dart'; import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/master_switch.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/models/statistics_config.dart'; import 'package:adguard_home_manager/constants/enums.dart'; class StatisticsSettings extends StatefulWidget { const StatisticsSettings({super.key}); @override State createState() => _StatisticsSettingsState(); } class _StatisticsSettingsState extends State { final Uuid uuid = const Uuid(); LoadStatus _loadStatus = LoadStatus.loading; bool _generalSwitch = false; final List _retentionItems = [ "custom", "86400000", "604800000", "2592000000", "7776000000" ]; final _customTimeController = TextEditingController(); String? _customTimeError; String? _retentionTime; List _ignoredDomainsControllers = []; void loadData() async { final serversProvider = Provider.of(context, listen: false); final result = await serversProvider.apiClient2!.getStatisticsConfig(); if (!mounted) return; if (result.successful == true) { final data = result.content as StatisticsConfig; setState(() { _generalSwitch = data.enabled ?? false; if (_retentionItems.contains(data.interval.toString())) { _retentionTime = data.interval.toString(); } else if (data.interval != null) { _retentionTime = "custom"; _customTimeController.text = Duration(milliseconds: data.interval!).inHours.toString(); } if (data.ignored != null) { _ignoredDomainsControllers = data.ignored!.map((e) => DomainListItemController( id: uuid.v4(), controller: TextEditingController(text: e), error: false )).toList(); } _loadStatus = LoadStatus.loaded; }); } else { setState(() => _loadStatus = LoadStatus.error); } } @override void initState() { loadData(); super.initState(); } @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); final List dropdownItemTranslation = [ AppLocalizations.of(context)!.custom, AppLocalizations.of(context)!.hours24, AppLocalizations.of(context)!.days7, AppLocalizations.of(context)!.days30, AppLocalizations.of(context)!.days90, ]; void validateDomain(String value, String id) { final domainRegex = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$'); bool error = false; if (domainRegex.hasMatch(value)) { error = false; } else { error = true; } setState(() { _ignoredDomainsControllers = _ignoredDomainsControllers.map((entry) { if (entry.id != id) return entry; return DomainListItemController( id: id, controller: entry.controller, error: error ); }).toList(); }); } void validateCustomTime(String v) { try { final regex = RegExp(r'^\d+$'); final parsed = int.parse(v); if (!regex.hasMatch(v)) { setState(() => _customTimeError = AppLocalizations.of(context)!.invalidTime); } else if (parsed < 1) { setState(() => _customTimeError = AppLocalizations.of(context)!.notLess1Hour); } else { setState(() => _customTimeError = null); } } catch (_) { setState(() => _customTimeError = AppLocalizations.of(context)!.invalidTime); } } void updateConfig() async { ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.updatingSettings); final result = await serversProvider.apiClient2!.updateStatisticsSettings( body: { "enabled": _generalSwitch, "interval": _retentionTime == "custom" ? Duration(hours: int.parse(_customTimeController.text)).inMilliseconds : int.parse(_retentionTime!), "ignored": _ignoredDomainsControllers.map((e) => e.controller.text).toList() } ); processModal.close(); if (!mounted) return; if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.logsConfigUpdated, color: Colors.green ); } else { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.logsConfigNotUpdated, color: Colors.red ); } } final validValues = _ignoredDomainsControllers.where( (d) => d.controller.text == "" || d.error == true ).isEmpty && (_retentionTime != "custom" || (_retentionTime == "custom" && _customTimeController.text != "" && _customTimeError == null)); return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.statisticsSettings), actions: [ IconButton( onPressed: validValues ? () => updateConfig() : null, icon: const Icon(Icons.save_rounded), tooltip: AppLocalizations.of(context)!.save, ), const SizedBox(width: 8) ], ), body: Builder( builder: (context) { switch (_loadStatus) { case LoadStatus.loading: return LoadingData(text: AppLocalizations.of(context)!.loadingStatisticsSettings); case LoadStatus.loaded: return ListView( children: [ const SizedBox(height: 8), MasterSwitch( label: AppLocalizations.of(context)!.enableLog, value: _generalSwitch, onChange: (v) => setState(() => _generalSwitch = v) ), const SizedBox(height: 24), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: DropdownButtonFormField( items: _retentionItems.asMap().entries.map((item) => DropdownMenuItem( value: item.value, child: Text(dropdownItemTranslation[item.key]), )).toList(), value: _retentionTime, onChanged: (value) => setState(() { if (value != null && value != "custom") { _customTimeError = null; _customTimeController.text = ""; }; _retentionTime = value; }), decoration: InputDecoration( border: const OutlineInputBorder( borderRadius: BorderRadius.all( Radius.circular(10) ) ), label: Text(AppLocalizations.of(context)!.retentionTime) ), borderRadius: BorderRadius.circular(20), ), ), if (_retentionTime == "custom") Padding( padding: const EdgeInsets.only( left: 16, right: 16, top: 24 ), child: TextFormField( controller: _customTimeController, onChanged: validateCustomTime, decoration: InputDecoration( prefixIcon: const Icon(Icons.schedule_rounded), border: const OutlineInputBorder( borderRadius: BorderRadius.all( Radius.circular(10) ) ), labelText: AppLocalizations.of(context)!.customTimeInHours, errorText: _customTimeError ), keyboardType: TextInputType.number, ), ), Padding( padding: const EdgeInsets.only(top: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SectionLabel( label: AppLocalizations.of(context)!.ignoredDomains, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), ), Padding( padding: const EdgeInsets.only(right: 6), child: IconButton( onPressed: () => setState(() => _ignoredDomainsControllers = [ ..._ignoredDomainsControllers, DomainListItemController( id: uuid.v4(), controller: TextEditingController(), error: false ), ]), icon: const Icon(Icons.add), tooltip: AppLocalizations.of(context)!.addDomain, ), ) ], ), ), if (_ignoredDomainsControllers.isNotEmpty) ..._ignoredDomainsControllers.map((controller) => Padding( padding: const EdgeInsets.only( top: 12, bottom: 12, left: 16, right: 6 ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: TextFormField( controller: controller.controller, onChanged: (v) => validateDomain(v, controller.id), decoration: InputDecoration( prefixIcon: const Icon(Icons.link_rounded), border: const OutlineInputBorder( borderRadius: BorderRadius.all( Radius.circular(10) ) ), labelText: AppLocalizations.of(context)!.domain, errorText: controller.error ? AppLocalizations.of(context)!.invalidDomain : null ), ), ), const SizedBox(width: 12), Padding( padding: controller.error ? const EdgeInsets.only(bottom: 24) : const EdgeInsets.all(0), child: IconButton( onPressed: () => setState(() => _ignoredDomainsControllers = _ignoredDomainsControllers.where((e) => e.id != controller.id).toList()), icon: const Icon(Icons.remove_circle_outline_outlined), tooltip: AppLocalizations.of(context)!.removeDomain, ), ) ], ), )), if (_ignoredDomainsControllers.isEmpty) Container( padding: const EdgeInsets.symmetric(vertical: 16), child: Text( AppLocalizations.of(context)!.noIgnoredDomainsAdded, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), ], ); case LoadStatus.error: return ErrorLoadData(text: AppLocalizations.of(context)!.statisticsSettingsLoadError); default: return const SizedBox(); } }, ), ); } }