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: