// ignore_for_file: use_build_context_synchronously import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; import 'package:segmented_button_slide/segmented_button_slide.dart'; import 'package:uuid/uuid.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/add_server/unsupported_version_modal.dart'; import 'package:adguard_home_manager/widgets/add_server/form_text_field.dart'; import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart'; import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart'; import 'package:adguard_home_manager/config/minimum_server_version.dart'; import 'package:adguard_home_manager/models/server_status.dart'; import 'package:adguard_home_manager/functions/compare_versions.dart'; import 'package:adguard_home_manager/services/auth.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/services/api_client.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/constants/urls.dart'; import 'package:adguard_home_manager/functions/open_url.dart'; import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/functions/base64.dart'; import 'package:adguard_home_manager/models/app_log.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/models/server.dart'; enum ConnectionType { http, https} class AddServerModal extends StatefulWidget { final Server? server; final bool fullScreen; final void Function(String version) onUnsupportedVersion; const AddServerModal({ super.key, this.server, required this.fullScreen, required this.onUnsupportedVersion }); @override State createState() => _AddServerModalState(); } class _AddServerModalState extends State { final uuid = const Uuid(); final TextEditingController nameController = TextEditingController(); String? nameError; ConnectionType connectionType = ConnectionType.http; final TextEditingController ipDomainController = TextEditingController(); String? ipDomainError; final TextEditingController pathController = TextEditingController(); String? pathError; final TextEditingController portController = TextEditingController(); String? portError; final TextEditingController userController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); bool defaultServer = false; bool homeAssistant = false; bool allDataValid = false; bool isConnecting = false; @override void initState() { if (widget.server != null) { nameController.text = widget.server!.name; connectionType = widget.server!.connectionMethod == 'https' ? ConnectionType.https : ConnectionType.http; ipDomainController.text = widget.server!.domain; pathController.text = widget.server!.path ?? ''; portController.text = widget.server!.port != null ? widget.server!.port.toString() : ""; userController.text = widget.server!.user ?? ""; passwordController.text = widget.server!.password ?? ""; defaultServer = widget.server!.defaultServer; homeAssistant = widget.server!.runningOnHa; } setState(() => allDataValid = checkDataValid( ipDomainController: ipDomainController, nameController: nameController, ipDomainError: ipDomainError, pathError: pathError, portError: portError )); super.initState(); } @override Widget build(BuildContext context) { final serversProvider = Provider.of(context, listen: false); final statusProvider = Provider.of(context, listen: false); final appConfigProvider = Provider.of(context, listen: false); void cancelConnecting() { if (mounted) { setState(() => isConnecting = false); } else { isConnecting = false; } } void validateData() { setState(() => allDataValid = checkDataValid( ipDomainController: ipDomainController, nameController: nameController, ipDomainError: ipDomainError, pathError: pathError, portError: portError )); } String getErrorMessage(AuthStatus status) { if (status == AuthStatus.invalidCredentials) return AppLocalizations.of(context)!.invalidUsernamePassword; if (status == AuthStatus.manyAttepts) return AppLocalizations.of(context)!.tooManyAttempts; if (status == AuthStatus.socketException || status == AuthStatus.timeoutException) return AppLocalizations.of(context)!.cantReachServer; if (status == AuthStatus.serverError) return AppLocalizations.of(context)!.serverError; if (status == AuthStatus.handshakeException) return AppLocalizations.of(context)!.sslError; return AppLocalizations.of(context)!.unknownError; } void connect() async { setState(() => isConnecting = true); Server serverObj = Server( id: uuid.v4(), name: nameController.text, connectionMethod: connectionType.name, domain: ipDomainController.text, port: portController.text != '' ? int.parse(portController.text) : null, user: userController.text != "" ? userController.text : null, password: passwordController.text != "" ? passwordController.text : null, path: pathController.text != "" ? pathController.text : null, defaultServer: defaultServer, authToken: homeAssistant == true ? encodeBase64UserPass(userController.text, passwordController.text) : null, runningOnHa: homeAssistant ); final result = homeAssistant == true ? await ServerAuth.loginHA(serverObj) : await ServerAuth.login(serverObj); // If something goes wrong with the connection if (result != AuthStatus.success) { cancelConnecting(); if (mounted) { showSnackbar( appConfigProvider: appConfigProvider, label: getErrorMessage(result), color: Colors.red ); } return; } if (serverObj.user != null && serverObj.password != null) { serverObj.authToken = encodeBase64UserPass(serverObj.user!, serverObj.password!); } statusProvider.setServerStatusLoad(LoadStatus.loading); final ApiClientV2 apiClient2 = ApiClientV2(server: serverObj); final serverStatus = await apiClient2.getServerStatus(); // If something goes wrong when fetching server status if (serverStatus.successful == false) { statusProvider.setServerStatusLoad(LoadStatus.error); Navigator.pop(context); return; } final status = serverStatus.content as ServerStatus; // Check if ths server version is compatible final validVersion = serverVersionIsAhead( currentVersion: status.serverVersion, referenceVersion: MinimumServerVersion.stable, referenceVersionBeta: MinimumServerVersion.beta ); if (validVersion == false) { showDialog( context: context, builder: (ctx) => UnsupportedVersionModal( serverVersion: status.serverVersion, onClose: () => Navigator.pop(context) ) ); return; } final serverCreated = await serversProvider.createServer(serverObj); // If something goes wrong when saving the connection on the db if (serverCreated != null) { if (mounted) setState(() => isConnecting = false); if (mounted) { showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.connectionNotCreated, color: Colors.red ); } return; } // If everything is successful statusProvider.setServerStatusData( data: status ); serversProvider.setApiClient2(apiClient2); statusProvider.setServerStatusLoad(LoadStatus.loaded); if (status.serverVersion.contains('a') || status.serverVersion.contains('b')) { Navigator.pop(context); widget.onUnsupportedVersion(status.serverVersion); } else { Navigator.pop(context); } } void edit() async { setState(() => isConnecting = true); final Server serverObj = Server( id: widget.server!.id, name: nameController.text, connectionMethod: connectionType.name, domain: ipDomainController.text, port: portController.text != '' ? int.parse(portController.text) : null, user: userController.text != "" ? userController.text : null, password: passwordController.text != "" ? passwordController.text : null, defaultServer: defaultServer, authToken: homeAssistant == true ? encodeBase64UserPass(userController.text, passwordController.text) : null, runningOnHa: homeAssistant ); final result = homeAssistant == true ? await ServerAuth.loginHA(serverObj) : await ServerAuth.login(serverObj); // If something goes wrong with the connection if (result != AuthStatus.success) { cancelConnecting(); if (mounted) { showSnackbar( appConfigProvider: appConfigProvider, label: getErrorMessage(result), color: Colors.red ); } return; } if (serverObj.user != null && serverObj.password != null) { serverObj.authToken = encodeBase64UserPass(serverObj.user!, serverObj.password!); } final ApiClientV2 apiClient2 = ApiClientV2(server: serverObj); final version = await apiClient2.getServerVersion(); if (version.successful == false) { if (mounted) setState(() => isConnecting = false); return; } // Check if ths server version is compatible final validVersion = serverVersionIsAhead( currentVersion: version.content, referenceVersion: MinimumServerVersion.stable, referenceVersionBeta: MinimumServerVersion.beta ); if (validVersion == false) { showDialog( context: context, builder: (ctx) => UnsupportedVersionModal( serverVersion: version.content, onClose: () => Navigator.pop(context) ) ); return; } final serverSaved = await serversProvider.editServer(serverObj); // If something goes wrong when saving the connection on the db if (serverSaved != null) { if (mounted) setState(() => isConnecting = false); appConfigProvider.addLog( AppLog( type: 'save_connection_db', dateTime: DateTime.now(), message: serverSaved.toString() ) ); if (mounted) { showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.connectionNotCreated, color: Colors.red ); } return; } // If everything is successful if ( version.successful == true && (version.content.contains('a') || version.content.contains('b')) // alpha or beta ) { Navigator.pop(context); widget.onUnsupportedVersion(version.content); } else { Navigator.pop(context); } } Widget actions() { return Row( children: [ IconButton( onPressed: () => openUrl(Urls.connectionInstructions), icon: const Icon(Icons.help_outline_outlined) ), IconButton( tooltip: widget.server == null ? AppLocalizations.of(context)!.connect : AppLocalizations.of(context)!.save, onPressed: allDataValid == true && isConnecting == false ? widget.server == null ? () => connect() : () => edit() : null, icon: isConnecting ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator() ) : Icon( widget.server == null ? Icons.login_rounded : Icons.save_rounded ) ), ], ); } List form() { return [ Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), margin: const EdgeInsets.only( top: 24, left: 24, right: 24 ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withOpacity(0.05), borderRadius: BorderRadius.circular(30), border: Border.all( color: Theme.of(context).colorScheme.primary ) ), child: Text( "${connectionType.name}://${ipDomainController.text}${portController.text != '' ? ':${portController.text}' : ""}${pathController.text}", textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w500 ), ), ), SectionLabel( label: AppLocalizations.of(context)!.general, padding: const EdgeInsets.all(24), ), FormTextField( label: AppLocalizations.of(context)!.name, controller: nameController, icon: Icons.badge_rounded, error: nameError, onChanged: (value) { if (value != '') { setState(() => nameError = null); } else { setState(() => nameError = AppLocalizations.of(context)!.nameNotEmpty); } validateData(); }, isConnecting: isConnecting, ), SectionLabel( label: AppLocalizations.of(context)!.connection, padding: const EdgeInsets.all(24), ), SegmentedButtonSlide( entries: const [ SegmentedButtonSlideEntry(label: "HTTP"), SegmentedButtonSlideEntry(label: "HTTPS"), ], selectedEntry: connectionType.index, onChange: (v) => setState(() => connectionType = ConnectionType.values[v]), colors: SegmentedButtonSlideColors( barColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), backgroundSelectedColor: Theme.of(context).colorScheme.primary, ), textOverflow: TextOverflow.ellipsis, height: 40, margin: const EdgeInsets.symmetric( horizontal: 24, ), selectedTextStyle: TextStyle( color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.w700 ), unselectedTextStyle: TextStyle( color: Theme.of(context).colorScheme.onSurface, ), hoverTextStyle: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), Card( margin: const EdgeInsets.only( top: 16, left: 24, right: 24 ), child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon( Icons.warning_rounded, color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(width: 16), Flexible(child: Text(AppLocalizations.of(context)!.redirectHttpsWarning)) ], ), ), ), if (connectionType == ConnectionType.https) Card( margin: const EdgeInsets.only( top: 16, left: 24, right: 24 ), child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon( Icons.info_rounded, color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(width: 16), Flexible(child: Text(AppLocalizations.of(context)!.sslWarning)) ], ), ), ), const SizedBox(height: 30), FormTextField( label: AppLocalizations.of(context)!.ipDomain, controller: ipDomainController, icon: Icons.link_rounded, error: ipDomainError, keyboardType: TextInputType.url, onChanged: (v) { setState(() => ipDomainError = validateAddress(context: context, value: v)); validateData(); }, isConnecting: isConnecting, ), const SizedBox(height: 20), FormTextField( label: AppLocalizations.of(context)!.path, controller: pathController, icon: Icons.route_rounded, error: pathError, onChanged: (v) { setState(() => pathError = validateSubroute(context: context, value: v)); validateData(); }, hintText: AppLocalizations.of(context)!.examplePath, helperText: AppLocalizations.of(context)!.helperPath, isConnecting: isConnecting, ), const SizedBox(height: 20), FormTextField( label: AppLocalizations.of(context)!.port, controller: portController, icon: Icons.numbers_rounded, error: portError, keyboardType: TextInputType.number, onChanged: (v) { setState(() => portError = validatePort(context: context, value: v)); validateData(); }, isConnecting: isConnecting, ), SectionLabel( label: AppLocalizations.of(context)!.authentication, padding: const EdgeInsets.all(24), ), FormTextField( label: AppLocalizations.of(context)!.username, controller: userController, icon: Icons.person_rounded, isConnecting: isConnecting, ), const SizedBox(height: 20), FormTextField( label: AppLocalizations.of(context)!.password, controller: passwordController, icon: Icons.lock_rounded, keyboardType: TextInputType.visiblePassword, obscureText: true, isConnecting: isConnecting, ), SectionLabel( label: AppLocalizations.of(context)!.other, padding: const EdgeInsets.only( top: 32, left: 24, bottom: 12 ), ), CustomSwitchListTile( value: defaultServer, onChanged: (value) => setState(() => defaultServer = value), title: AppLocalizations.of(context)!.defaultServer, disabled: widget.server != null || isConnecting, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 4 ), ), CustomSwitchListTile( value: homeAssistant, onChanged: (value) => setState(() => homeAssistant = value), title: AppLocalizations.of(context)!.runningHomeAssistant, disabled: widget.server != null || isConnecting, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 4 ), ), const SizedBox(height: 20), ]; } if (widget.fullScreen == true) { return Dialog.fullscreen( child: Scaffold( appBar: AppBar( leading: CloseButton( onPressed: () => Navigator.pop(context), ), title: widget.server == null ? Text(AppLocalizations.of(context)!.createConnection) : Text(AppLocalizations.of(context)!.editConnection), actions: [ actions(), const SizedBox(width: 8) ], ), body: SafeArea( child: ListView( children: form() ), ), ), ); } else { return Dialog( child: SizedBox( width: 400, child: Column( children: [ Padding( padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.clear_rounded) ), const SizedBox(width: 8), Text( widget.server == null ? AppLocalizations.of(context)!.createConnection : AppLocalizations.of(context)!.editConnection, style: const TextStyle( fontSize: 20 ), ), ], ), actions() ], ), ), Expanded( child: ListView( children: form() ), ) ], ), ), ); } } }