diff --git a/lib/base.dart b/lib/base.dart index c309385..1ae1264 100644 --- a/lib/base.dart +++ b/lib/base.dart @@ -22,9 +22,11 @@ class _BaseState extends State { Widget build(BuildContext context) { final serversProvider = Provider.of(context); - List screens = serversProvider.selectedServer != null - ? screensServerConnected - : screensSelectServer; + // List screens = serversProvider.selectedServer != null + // ? screensServerConnected + // : screensSelectServer; + + List screens = screensSelectServer; if (selectedScreen > screens.length-1) { setState(() => selectedScreen = 0); diff --git a/lib/classes/process_modal.dart b/lib/classes/process_modal.dart new file mode 100644 index 0000000..6febebb --- /dev/null +++ b/lib/classes/process_modal.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import 'package:adguard_home_manager/widgets/process_dialog.dart'; + +class ProcessModal { + late BuildContext context; + + ProcessModal({ + required this.context + }); + + void open(String message) async { + await Future.delayed(const Duration(seconds: 0), () => { + showDialog( + context: context, + builder: (c) { + context = c; + return ProcessDialog( + message: message, + ); + }, + barrierDismissible: false, + useSafeArea: true, + ) + }); + } + + void close() { + Navigator.pop(context); + } +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1501d43..328f917 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -26,5 +26,22 @@ "sslError": "SSL error", "unknownError": "Unknown error", "connectionNotCreated": "Connection couldn't be created", - "connecting": "Connecting..." + "connecting": "Connecting...", + "connected": "Connected", + "selectedDisconnected": "Selected but disconnected", + "connectionDefaultSuccessfully": "Connection set as default successfully.", + "connectionDefaultFailed": "Connection could not be set as default.", + "noSavedConnections": "No saved connections", + "cannotConnect": "Cannot connect to the server", + "connectionRemoved": "Connection removed successfully", + "connectionCannotBeRemoved": "Connection cannot be removed.", + "remove": "Remove", + "removeWarning": "Are you sure you want to remove the connection with this AdGuard Home server?", + "cancel": "Cancel", + "defaultConnection": "Default connection", + "setDefault": "Set default", + "edit": "Edit", + "delete": "Delete", + "save": "Save", + "connectionNotUpdated": "Connection not updated" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 6814495..bb17c28 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -26,5 +26,22 @@ "sslError": "Error de SSL", "unknownError": "Error desconocido", "connectionNotCreated": "No se pudo crear la conexión", - "connecting": "Conectando..." + "connecting": "Conectando...", + "connected": "Conectado", + "selectedDisconnected": "Seleccionado pero desconectado", + "connectionDefaultSuccessfully": "Conexión definida como por defecto.", + "connectionDefaultFailed": "No se ha podido definir como conexión por defecto.", + "noSavedConnections": "No hay conexiones guardadas", + "cannotConnect": "No se puede conectar con el servidor", + "connectionRemoved": "Conexión eliminada satisfactoriamente.", + "connectionCannotBeRemoved": "No se ha podido eliminar la conexión.", + "remove": "Eliminar", + "removeWarning": "¿Estás seguro de que deseas eliminar la conexión con este servidor?", + "cancel": "Cancelar", + "defaultConnection": "Conexión por defecto", + "setDefault": "Seleccionar por defecto", + "edit": "Editar", + "delete": "Eliminar", + "save": "Guardar", + "connectionNotUpdated": "Conexión no actualizada" } \ No newline at end of file diff --git a/lib/providers/servers_provider.dart b/lib/providers/servers_provider.dart index d1bec04..87d5fe0 100644 --- a/lib/providers/servers_provider.dart +++ b/lib/providers/servers_provider.dart @@ -9,6 +9,7 @@ class ServersProvider with ChangeNotifier { List _serversList = []; Server? _selectedServer; + bool? _isServerConnected; List get serversList { return _serversList; @@ -18,6 +19,10 @@ class ServersProvider with ChangeNotifier { return _selectedServer; } + bool? get isServerConnected { + return _isServerConnected; + } + void setDbInstance(Database db) { _dbInstance = db; } @@ -27,6 +32,16 @@ class ServersProvider with ChangeNotifier { notifyListeners(); } + void setSelectedServer(Server server) { + _selectedServer = server; + notifyListeners(); + } + + void setIsServerConnected(bool status) { + _isServerConnected = status; + notifyListeners(); + } + Future createServer(Server server) async { final saved = await saveServerIntoDb(server); if (saved == true) { @@ -74,11 +89,71 @@ class ServersProvider with ChangeNotifier { } } + Future editServer(Server server) async { + final result = await editServerDb(server); + if (result == true) { + List newServers = _serversList.map((s) { + if (s.id == server.id) { + return server; + } + else { + return s; + } + }).toList(); + _serversList = newServers; + notifyListeners(); + return true; + } + else { + return false; + } + } + + Future removeServer(Server server) async { + final result = await removeFromDb(server.id); + if (result == true) { + _selectedServer = null; + List newServers = _serversList.where((s) => s.id != server.id).toList(); + _serversList = newServers; + notifyListeners(); + return true; + } + else { + return false; + } + } + Future saveServerIntoDb(Server server) async { try { return await _dbInstance!.transaction((txn) async { await txn.rawInsert( - 'INSERT INTO servers (id, name, connectionMethod, domain, path, port, user, password, defaultServer) VALUES ("${server.id}", "${server.name}", "${server.connectionMethod}", "${server.domain}", "${server.path}", ${server.port}, "${server.user}", "${server.password}", 0)', + 'INSERT INTO servers (id, name, connectionMethod, domain, path, port, user, password, defaultServer) VALUES ("${server.id}", "${server.name}", "${server.connectionMethod}", "${server.domain}", ${server.path != null ? "${server.path}" : null}, ${server.port}, "${server.user}", "${server.password}", 0)', + ); + return true; + }); + } catch (e) { + return false; + } + } + + Future editServerDb(Server server) async { + try { + return await _dbInstance!.transaction((txn) async { + await txn.rawUpdate( + 'UPDATE servers SET name = "${server.name}", connectionMethod = "${server.connectionMethod}", domain = "${server.domain}", path = ${server.path != null ? "${server.path}" : null}, port = ${server.port}, user = "${server.user}", password = "${server.password}" WHERE id = "${server.id}"', + ); + return true; + }); + } catch (e) { + return false; + } + } + + Future removeFromDb(String id) async { + try { + return await _dbInstance!.transaction((txn) async { + await txn.rawDelete( + 'DELETE FROM servers WHERE id = "$id"', ); return true; }); diff --git a/lib/screens/connect/connect.dart b/lib/screens/connect/connect.dart index 3a9a7c6..871f50b 100644 --- a/lib/screens/connect/connect.dart +++ b/lib/screens/connect/connect.dart @@ -1,15 +1,37 @@ +import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:adguard_home_manager/widgets/servers_list/servers_list.dart'; + import 'package:adguard_home_manager/providers/servers_provider.dart'; -class Connect extends StatelessWidget { +class Connect extends StatefulWidget { const Connect({Key? key}) : super(key: key); + @override + State createState() => _ConnectState(); +} + +class _ConnectState extends State { + List expandableControllerList = []; + + void expandOrContract(int index) async { + expandableControllerList[index].expanded = !expandableControllerList[index].expanded; + } + @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); - return Container(); + for (var i = 0; i < serversProvider.serversList.length; i++) { + expandableControllerList.add(ExpandableController()); + } + + return ServersList( + context: context, + controllers: expandableControllerList, + onChange: expandOrContract + ); } } \ No newline at end of file diff --git a/lib/widgets/add_server_modal.dart b/lib/widgets/add_server_modal.dart index 8dfa88a..3c21f5b 100644 --- a/lib/widgets/add_server_modal.dart +++ b/lib/widgets/add_server_modal.dart @@ -13,7 +13,12 @@ import 'package:adguard_home_manager/models/server.dart'; import 'package:adguard_home_manager/config/system_overlay_style.dart'; class AddServerModal extends StatefulWidget { - const AddServerModal({Key? key}) : super(key: key); + final Server? server; + + const AddServerModal({ + Key? key, + this.server, + }) : super(key: key); @override State createState() => _AddServerModalState(); @@ -178,6 +183,22 @@ class _AddServerModalState extends State { checkDataValid(); } + @override + void initState() { + if (widget.server != null) { + nameController.text = widget.server!.name; + connectionType = widget.server!.connectionMethod; + 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; + } + checkDataValid(); + super.initState(); + } + @override Widget build(BuildContext context) { final serversProvider = Provider.of(context, listen: false); @@ -195,7 +216,9 @@ class _AddServerModalState extends State { password: passwordController.text, defaultServer: defaultServer ); + setState(() => isConnecting = true); final result = await login(serverObj); + setState(() => isConnecting = false); if (result['result'] == 'success') { final serverCreated = await serversProvider.createServer(serverObj); if (serverCreated == true) { @@ -252,6 +275,74 @@ class _AddServerModalState extends State { } } + void edit() async { + final Server serverObj = Server( + id: widget.server!.id, + name: nameController.text, + connectionMethod: connectionType, + domain: ipDomainController.text, + port: int.parse(portController.text), + user: userController.text, + password: passwordController.text, + defaultServer: defaultServer + ); + final result = await login(serverObj); + if (result['result'] == 'success') { + final serverSaved = await serversProvider.editServer(serverObj); + if (serverSaved == true) { + Navigator.pop(context); + } + else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.connectionNotUpdated), + backgroundColor: Colors.red, + ) + ); + } + } + else if (result['result'] == 'error' && result['message'] == 'invalid_username_password') { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.invalidUsernamePassword), + backgroundColor: Colors.red, + ) + ); + } + else if (result['result'] == 'error' && result['message'] == 'many_attempts') { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.tooManyAttempts), + backgroundColor: Colors.red, + ) + ); + } + else if (result['result'] == 'error' && result['message'] == 'no_connection') { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.cantReachServer), + backgroundColor: Colors.red, + ) + ); + } + else if (result['result'] == 'error' && result['message'] == 'ssl_error') { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.sslError), + backgroundColor: Colors.red, + ) + ); + } + else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.unknownError), + backgroundColor: Colors.red, + ) + ); + } + } + return Stack( children: [ Scaffold( @@ -264,11 +355,19 @@ class _AddServerModalState extends State { Padding( padding: const EdgeInsets.only(right: 10), child: IconButton( - tooltip: AppLocalizations.of(context)!.connect, + tooltip: widget.server == null + ? AppLocalizations.of(context)!.connect + : AppLocalizations.of(context)!.save, onPressed: allDataValid == true - ? () => connect() + ? widget.server == null + ? () => connect() + : () => edit() : null, - icon: const Icon(Icons.login_rounded) + icon: Icon( + widget.server == null + ? Icons.login_rounded + : Icons.save_rounded + ) ), ), ], @@ -349,7 +448,9 @@ class _AddServerModalState extends State { Material( color: Colors.transparent, child: InkWell( - onTap: () => setState(() => defaultServer = !defaultServer), + onTap: widget.server == null + ? () => setState(() => defaultServer = !defaultServer) + : null, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( @@ -363,7 +464,9 @@ class _AddServerModalState extends State { ), Switch( value: defaultServer, - onChanged: (value) => setState(() => defaultServer = value), + onChanged: widget.server == null + ? (value) => setState(() => defaultServer = value) + : null, activeColor: Theme.of(context).primaryColor, ) ], diff --git a/lib/widgets/process_dialog.dart b/lib/widgets/process_dialog.dart new file mode 100644 index 0000000..49fa16b --- /dev/null +++ b/lib/widgets/process_dialog.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class ProcessDialog extends StatelessWidget { + final String message; + + const ProcessDialog({ + Key? key, + required this.message, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: Dialog( + backgroundColor: Theme.of(context).dialogBackgroundColor, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 30, + horizontal: 30 + ), + child: Row( + children: [ + const CircularProgressIndicator(), + const SizedBox(width: 40), + Text(message) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/servers_list/delete_modal.dart b/lib/widgets/servers_list/delete_modal.dart new file mode 100644 index 0000000..fff1b32 --- /dev/null +++ b/lib/widgets/servers_list/delete_modal.dart @@ -0,0 +1,98 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/server.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class DeleteModal extends StatelessWidget { + final Server serverToDelete; + + const DeleteModal({ + Key? key, + required this.serverToDelete, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + + void removeServer() async { + final deleted = await serversProvider.removeServer(serverToDelete); + Navigator.pop(context); + if (deleted == true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.connectionRemoved), + backgroundColor: Colors.green, + ) + ); + } + else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.connectionCannotBeRemoved), + backgroundColor: Colors.red, + ) + ); + } + } + + return AlertDialog( + title: Column( + children: [ + const Icon( + Icons.delete, + size: 26, + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text( + AppLocalizations.of(context)!.remove, + style: const TextStyle( + fontSize: 24 + ), + ), + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Text( + AppLocalizations.of(context)!.removeWarning, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + Center( + child: Text( + "${serverToDelete.connectionMethod}://${serverToDelete.domain}${serverToDelete.path ?? ""}${serverToDelete.port != null ? ':${serverToDelete.port}' : ""}", + style: const TextStyle( + fontStyle: FontStyle.italic + ), + ), + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () => { + Navigator.pop(context) + }, + child: Text(AppLocalizations.of(context)!.cancel) + ), + TextButton( + onPressed: removeServer, + child: Text(AppLocalizations.of(context)!.remove), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/servers_list/servers_list.dart b/lib/widgets/servers_list/servers_list.dart new file mode 100644 index 0000000..b015991 --- /dev/null +++ b/lib/widgets/servers_list/servers_list.dart @@ -0,0 +1,357 @@ +// ignore_for_file: use_build_context_synchronously +import 'package:adguard_home_manager/widgets/add_server_modal.dart'; +import 'package:flutter/material.dart'; +import 'package:expandable/expandable.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/servers_list/delete_modal.dart'; + +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/models/server.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/services/http_requests.dart'; + +class ServersList extends StatelessWidget { + final BuildContext context; + final List controllers; + final Function(int) onChange; + + const ServersList({ + Key? key, + required this.context, + required this.controllers, + required this.onChange + }) : super(key: key); + + @override + // ignore: avoid_renaming_method_parameters + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + List servers = serversProvider.serversList; + + final width = MediaQuery.of(context).size.width; + + void showDeleteModal(Server server) async { + await Future.delayed(const Duration(seconds: 0), () => { + showDialog( + context: context, + builder: (context) => DeleteModal( + serverToDelete: server, + ), + barrierDismissible: false + ) + }); + } + + void openAddServerBottomSheet({Server? server}) async { + await Future.delayed(const Duration(seconds: 0), (() => { + Navigator.push(context, MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) => AddServerModal(server: server) + )) + })); + } + + void connectToServer(Server server) async { + Future connectSuccess(result) async { + serversProvider.setSelectedServer(server); + } + + final ProcessModal process = ProcessModal(context: context); + process.open(AppLocalizations.of(context)!.connecting); + + final result = await login(server); + + process.close(); + + if (result['result'] == 'success') { + await connectSuccess(result); + } + else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.cannotConnect), + backgroundColor: Colors.red, + ) + ); + } + } + + void setDefaultServer(Server server) async { + final result = await serversProvider.setDefaultServer(server); + if (result == true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.connectionDefaultSuccessfully), + backgroundColor: Colors.green, + ) + ); + } + else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.connectionDefaultFailed), + backgroundColor: Colors.red, + ) + ); + } + } + + Widget leadingIcon(Server server) { + if (server.defaultServer == true) { + return Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.storage_rounded, + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id + ? serversProvider.isServerConnected == true + ? Colors.green + : Colors.orange + : null, + ), + SizedBox( + width: 25, + height: 25, + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(20) + ), + child: const Icon( + Icons.star, + color: Colors.white, + size: 10, + ), + ), + ], + ), + ) + ], + ); + } + else { + return Icon( + Icons.storage_rounded, + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id + ? serversProvider.isServerConnected == true + ? Colors.green + : Colors.orange + : null, + ); + } + } + + Widget topRow(Server server, int index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 48, + margin: const EdgeInsets.only(right: 12), + child: leadingIcon(servers[index]), + ), + SizedBox( + width: width-168, + child: Column( + children: [ + Text( + "${server.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}", + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500 + ), + ), + Column( + children: [ + const SizedBox(height: 10), + Text( + servers[index].name, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + fontStyle: FontStyle.italic + ), + ) + ], + ) + ], + ), + ), + IconButton( + onPressed: () => onChange(index), + icon: const Icon(Icons.arrow_drop_down), + splashRadius: 20, + ), + ], + ); + } + + Widget bottomRow(Server server, int index) { + return Column( + children: [ + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PopupMenuButton( + color: Theme.of(context).dialogBackgroundColor, + itemBuilder: (context) => [ + PopupMenuItem( + enabled: server.defaultServer == false + ? true + : false, + onTap: server.defaultServer == false + ? (() => setDefaultServer(server)) + : null, + child: SizedBox( + child: Row( + children: [ + const Icon(Icons.star), + const SizedBox(width: 15), + Text( + server.defaultServer == true + ? AppLocalizations.of(context)!.defaultConnection + : AppLocalizations.of(context)!.setDefault, + ) + ], + ), + ) + ), + PopupMenuItem( + onTap: (() => openAddServerBottomSheet(server: server)), + child: Row( + children: [ + const Icon(Icons.edit), + const SizedBox(width: 15), + Text(AppLocalizations.of(context)!.edit) + ], + ) + ), + PopupMenuItem( + onTap: (() => showDeleteModal(server)), + child: Row( + children: [ + const Icon(Icons.delete), + const SizedBox(width: 15), + Text(AppLocalizations.of(context)!.delete) + ], + ) + ), + ] + ), + SizedBox( + child: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == servers[index].id + ? Container( + margin: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: BoxDecoration( + color: serversProvider.isServerConnected == true + ? Colors.green + : Colors.orange, + borderRadius: BorderRadius.circular(30) + ), + child: Row( + children: [ + Icon( + serversProvider.isServerConnected == true + ? Icons.check + : Icons.warning, + color: Colors.white, + ), + const SizedBox(width: 10), + Text( + serversProvider.isServerConnected == true + ? AppLocalizations.of(context)!.connected + : AppLocalizations.of(context)!.selectedDisconnected, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500 + ), + ) + ], + ), + ) + : Container( + margin: const EdgeInsets.only(right: 10), + child: TextButton( + onPressed: () => connectToServer(servers[index]), + child: Text(AppLocalizations.of(context)!.connect), + ), + ), + ) + ], + ) + ], + ); + } + + return servers.isNotEmpty ? + ListView.builder( + itemCount: servers.length, + itemBuilder: (context, index) => Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 1 + ) + ) + ), + child: ExpandableNotifier( + controller: controllers[index], + child: Column( + children: [ + Expandable( + collapsed: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onChange(index), + child: Padding( + padding: const EdgeInsets.all(10), + child: topRow(servers[index], index), + ), + ), + ), + expanded: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onChange(index), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + topRow(servers[index], index), + bottomRow(servers[index], index) + ], + ), + ), + ), + ) + ) + ], + ), + ), + ) + ) : SizedBox( + height: double.maxFinite, + child: Center( + child: Text( + AppLocalizations.of(context)!.noSavedConnections, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24, + color: Colors.grey, + fontWeight: FontWeight.w500 + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index f0026b2..8712c3b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -106,6 +106,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.4" + expandable: + dependency: "direct main" + description: + name: expandable + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 01d71f1..361c1e2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: animations: ^2.0.5 device_info_plus: ^4.1.2 uuid: ^3.0.6 + expandable: ^5.0.1 dev_dependencies: flutter_test: