Added servers list and edit server

This commit is contained in:
Juan Gilsanz Polo 2022-09-26 22:43:30 +02:00
parent 3acfc7c5a5
commit 59e9917a4b
12 changed files with 777 additions and 14 deletions

View file

@ -22,9 +22,11 @@ class _BaseState extends State<Base> {
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
List<AppScreen> screens = serversProvider.selectedServer != null
? screensServerConnected
: screensSelectServer;
// List<AppScreen> screens = serversProvider.selectedServer != null
// ? screensServerConnected
// : screensSelectServer;
List<AppScreen> screens = screensSelectServer;
if (selectedScreen > screens.length-1) {
setState(() => selectedScreen = 0);

View file

@ -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);
}
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -9,6 +9,7 @@ class ServersProvider with ChangeNotifier {
List<Server> _serversList = [];
Server? _selectedServer;
bool? _isServerConnected;
List<Server> 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<bool> createServer(Server server) async {
final saved = await saveServerIntoDb(server);
if (saved == true) {
@ -74,11 +89,71 @@ class ServersProvider with ChangeNotifier {
}
}
Future<bool> editServer(Server server) async {
final result = await editServerDb(server);
if (result == true) {
List<Server> newServers = _serversList.map((s) {
if (s.id == server.id) {
return server;
}
else {
return s;
}
}).toList();
_serversList = newServers;
notifyListeners();
return true;
}
else {
return false;
}
}
Future<bool> removeServer(Server server) async {
final result = await removeFromDb(server.id);
if (result == true) {
_selectedServer = null;
List<Server> newServers = _serversList.where((s) => s.id != server.id).toList();
_serversList = newServers;
notifyListeners();
return true;
}
else {
return false;
}
}
Future<bool> 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<bool> 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<bool> removeFromDb(String id) async {
try {
return await _dbInstance!.transaction((txn) async {
await txn.rawDelete(
'DELETE FROM servers WHERE id = "$id"',
);
return true;
});

View file

@ -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<Connect> createState() => _ConnectState();
}
class _ConnectState extends State<Connect> {
List<ExpandableController> expandableControllerList = [];
void expandOrContract(int index) async {
expandableControllerList[index].expanded = !expandableControllerList[index].expanded;
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
return Container();
for (var i = 0; i < serversProvider.serversList.length; i++) {
expandableControllerList.add(ExpandableController());
}
return ServersList(
context: context,
controllers: expandableControllerList,
onChange: expandOrContract
);
}
}

View file

@ -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<AddServerModal> createState() => _AddServerModalState();
@ -178,6 +183,22 @@ class _AddServerModalState extends State<AddServerModal> {
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<ServersProvider>(context, listen: false);
@ -195,7 +216,9 @@ class _AddServerModalState extends State<AddServerModal> {
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<AddServerModal> {
}
}
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<AddServerModal> {
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<AddServerModal> {
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<AddServerModal> {
),
Switch(
value: defaultServer,
onChanged: (value) => setState(() => defaultServer = value),
onChanged: widget.server == null
? (value) => setState(() => defaultServer = value)
: null,
activeColor: Theme.of(context).primaryColor,
)
],

View file

@ -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)
],
),
),
),
);
}
}

View file

@ -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<ServersProvider>(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),
),
],
);
}
}

View file

@ -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<ExpandableController> 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<ServersProvider>(context);
List<Server> 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
),
),
),
);
}
}

View file

@ -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:

View file

@ -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: