513 lines
16 KiB
Dart
513 lines
16 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
import 'package:basic_utils/basic_utils.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:server_pinger/server.dart';
|
|
|
|
import 'server.dart';
|
|
|
|
class ServerList extends StatefulWidget {
|
|
ServerList({Key key, this.title}) : super(key: key);
|
|
|
|
final String title;
|
|
|
|
@override
|
|
_ServerListState createState() => _ServerListState();
|
|
}
|
|
|
|
class _ServerListState extends State<ServerList> {
|
|
// Currently expanded server list entry, -1 = no field is expanded
|
|
int selectedServer = -1;
|
|
List<Server> servers = [
|
|
new Server(
|
|
"example.com",
|
|
"http://example.com",
|
|
),
|
|
new Server(
|
|
"youtube.com",
|
|
"https://youtube.com",
|
|
),
|
|
new Server(
|
|
"My VPS",
|
|
"http://kescher.at",
|
|
),
|
|
new Server(
|
|
"Raspberry Pi",
|
|
"https://pi.kescher.at/isup",
|
|
),
|
|
];
|
|
|
|
Timer timer;
|
|
Random rnd = new Random();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_updateAllServers();
|
|
timer = new Timer.periodic(
|
|
new Duration(seconds: 30), (_) => _updateAllServers());
|
|
}
|
|
|
|
void _updateAllServers() async {
|
|
servers.forEach(_updateServerStatus);
|
|
}
|
|
|
|
void _updateServerStatus(Server s) {
|
|
_checkStatus(s.uri).then((value) => setState(() => s.status = value));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// This method is rerun every time setState is called, for instance as done
|
|
// by the _incrementCounter method above.
|
|
//
|
|
// The Flutter framework has been optimized to make rerunning build methods
|
|
// fast, so that you can just rebuild anything that needs updating rather
|
|
// than having to individually change instances of widgets.
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
// Here we take the value from the MyHomePage object that was created by
|
|
// the App.build method, and use it to set our appbar title.
|
|
title: Text(widget.title),
|
|
),
|
|
body: SingleChildScrollView(
|
|
// Center is a layout widget. It takes a single child and positions it
|
|
// in the middle of the parent.
|
|
child: Column(
|
|
// Column is also a layout widget. It takes a list of children and
|
|
// arranges them vertically. By default, it sizes itself to fit its
|
|
// children horizontally, and tries to be as tall as its parent.
|
|
//
|
|
// Invoke "debug painting" (press "p" in the console, choose the
|
|
// "Toggle Debug Paint" action from the Flutter Inspector in Android
|
|
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
|
|
// to see the wireframe for each widget.
|
|
//
|
|
// Column has various properties to control how it sizes itself and
|
|
// how it positions its children. Here we use mainAxisAlignment to
|
|
// center the children vertically; the main axis here is the vertical
|
|
// axis because Columns are vertical (the cross axis would be
|
|
// horizontal).
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: _createChildren(),
|
|
),
|
|
),
|
|
floatingActionButton: FloatingActionButton(
|
|
shape: CircleBorder(),
|
|
child: Icon(Icons.settings),
|
|
)
|
|
/*SpeedDial(
|
|
animatedIcon: AnimatedIcons.menu_close,
|
|
animatedIconTheme: IconThemeData(size: 20.0),
|
|
animationSpeed: 50,
|
|
tooltip: 'Menu',
|
|
backgroundColor: Colors.red.shade900,
|
|
foregroundColor: Colors.white,
|
|
shape: CircleBorder(),
|
|
children: [
|
|
SpeedDialChild(
|
|
child: Icon(Icons.settings),
|
|
label: 'Settings',
|
|
backgroundColor: Colors.red.shade600,
|
|
foregroundColor: Colors.white,
|
|
labelStyle: TextStyle(fontSize: 18.0),
|
|
onTap: _settings,
|
|
),
|
|
SpeedDialChild(
|
|
child: Icon(Icons.add),
|
|
label: 'Add server',
|
|
backgroundColor: Colors.red.shade600,
|
|
foregroundColor: Colors.white,
|
|
labelStyle: TextStyle(fontSize: 18.0),
|
|
onTap: _addServer,
|
|
),
|
|
],*/
|
|
// This trailing comma makes auto-formatting nicer for build methods.
|
|
);
|
|
}
|
|
|
|
void _settings() {}
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
void _addServerForm() {
|
|
TextEditingController servername = TextEditingController();
|
|
TextEditingController uri = TextEditingController();
|
|
showDialog<String>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Adding a server'),
|
|
content: SingleChildScrollView(
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
TextFormField(
|
|
decoration: InputDecoration(
|
|
labelText: 'Custom Name',
|
|
hintText: 'Optional',
|
|
),
|
|
controller: servername,
|
|
),
|
|
TextFormField(
|
|
controller: uri,
|
|
decoration: InputDecoration(
|
|
labelText: 'URI',
|
|
hintText: 'https://example.com',
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: RaisedButton(
|
|
child: Text("Add"),
|
|
onPressed: () {
|
|
if (_formKey.currentState.validate() &&
|
|
uri.text.isNotEmpty) {
|
|
_formKey.currentState.save();
|
|
_addServer(
|
|
servername.text.isEmpty
|
|
? uri.text
|
|
: servername.text,
|
|
uri.text);
|
|
}
|
|
},
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _addServer(String customName, String uri) {
|
|
Server server = Server(customName, uri);
|
|
servers.add(server);
|
|
_updateServerStatus(server);
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
void _removeServer(int index) {
|
|
setState(() {
|
|
servers.removeAt(index);
|
|
});
|
|
}
|
|
|
|
void _modifyServerForm(Server server){
|
|
TextEditingController servername = TextEditingController(text: server.displayName);
|
|
TextEditingController uri = TextEditingController(text: server.uri);
|
|
showDialog<String>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Adding a server'),
|
|
content: SingleChildScrollView(
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
TextFormField(
|
|
decoration: InputDecoration(
|
|
labelText: 'Custom Name',
|
|
),
|
|
controller: servername,
|
|
),
|
|
TextFormField(
|
|
controller: uri,
|
|
decoration: InputDecoration(
|
|
labelText: 'URI',
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Row(children: [
|
|
Expanded(
|
|
child: RaisedButton(
|
|
child: Text("Confirm"),
|
|
onPressed: () {
|
|
if (_formKey.currentState.validate()) {
|
|
_formKey.currentState.save();
|
|
Navigator.of(context).pop();
|
|
}
|
|
},
|
|
),
|
|
),
|
|
RaisedButton(
|
|
child: Text("Confirm"),
|
|
onPressed: () {
|
|
if (_formKey.currentState.validate()) {
|
|
_formKey.currentState.save();
|
|
setState(() {
|
|
server.displayName=servername.text;
|
|
server.uri = uri.text;
|
|
});
|
|
Navigator.of(context).pop();
|
|
}
|
|
},
|
|
)
|
|
]),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
List<Widget> _createChildren() {
|
|
List<Widget> list = new List<Widget>.generate(servers.length, (int index) {
|
|
Widget info = Row(children: [
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
servers[index].displayName,
|
|
style: TextStyle(fontSize: 24),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
width: 50,
|
|
height: 50,
|
|
child: Align(
|
|
alignment: Alignment.center,
|
|
child: _generateIcon(servers[index]),
|
|
),
|
|
),
|
|
]);
|
|
if (index == selectedServer) {
|
|
info = Column(
|
|
children: [
|
|
info,
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
border: Border(top: BorderSide(color: Colors.grey))),
|
|
padding: EdgeInsets.fromLTRB(0, 0, 0, 5),
|
|
),
|
|
Column(children: [
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
servers[index].uri,
|
|
textAlign: TextAlign.left,
|
|
),
|
|
),
|
|
Row(children: [
|
|
Expanded(
|
|
child: IconButton(
|
|
alignment: Alignment.centerLeft,
|
|
icon: Icon(
|
|
Icons.delete_forever,
|
|
color: Colors.red,
|
|
),
|
|
onPressed: () {
|
|
showDialog<String>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Are you sure you want to delete ' +
|
|
servers[index].displayName +
|
|
'?'),
|
|
content: Form(
|
|
key: _formKey,
|
|
child: Row(
|
|
children: <Widget>[
|
|
Expanded(
|
|
child: FlatButton(
|
|
child: Text(
|
|
'Yes',
|
|
style: TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 24,
|
|
),
|
|
),
|
|
onPressed: () {
|
|
_removeServer(index);
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
),
|
|
FlatButton(
|
|
child: Text(
|
|
'No',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 24,
|
|
),
|
|
),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.edit,
|
|
color: Colors.grey,
|
|
),
|
|
onPressed: () {
|
|
_removeServer(index);
|
|
},
|
|
),
|
|
]),
|
|
]),
|
|
],
|
|
);
|
|
}
|
|
return GestureDetector(
|
|
onTap: () {
|
|
setState(() => selectedServer = index);
|
|
print(index.toString() + " clicked");
|
|
},
|
|
child: Container(
|
|
width: MediaQuery.of(context).size.width,
|
|
margin: EdgeInsets.all(2.0),
|
|
padding: EdgeInsets.all(10.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white70,
|
|
border: Border.all(
|
|
color: Colors.grey,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: info),
|
|
);
|
|
});
|
|
list.add(
|
|
Container(
|
|
height: 75,
|
|
width: MediaQuery.of(context).size.width,
|
|
margin: EdgeInsets.all(2.0),
|
|
padding: EdgeInsets.all(10.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white70,
|
|
border: Border.all(
|
|
color: Colors.grey,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: IconButton(
|
|
icon: Icon(
|
|
Icons.add,
|
|
size: 42,
|
|
color: Colors.black,
|
|
),
|
|
onPressed: _addServerForm,
|
|
),
|
|
),
|
|
);
|
|
return list;
|
|
}
|
|
|
|
static Future<ServerStatus> _checkStatus(String uri) async {
|
|
Uri parsedUri;
|
|
|
|
try {
|
|
parsedUri = Uri.parse(uri);
|
|
} catch (FormatException) {
|
|
return ServerStatus.dnserror;
|
|
}
|
|
|
|
try {
|
|
// Check if host is IP address
|
|
InternetAddress(parsedUri.host);
|
|
} catch (ArgumentError) /* Host is a DNS name */ {
|
|
try {
|
|
var v4recs = await DnsUtils.lookupRecord(parsedUri.host, RRecordType.A);
|
|
var v6recs =
|
|
await DnsUtils.lookupRecord(parsedUri.host, RRecordType.AAAA);
|
|
var v4 = await _resolveHostname(v4recs, RRecordType.A);
|
|
var v6 = await _resolveHostname(v6recs, RRecordType.AAAA);
|
|
if (v4 != "" && v6 != "") {
|
|
return ServerStatus.dnserror;
|
|
}
|
|
} catch (SocketException) {
|
|
return ServerStatus.dnserror;
|
|
}
|
|
}
|
|
try {
|
|
return await _checkResponse(parsedUri);
|
|
} catch (TimeoutException) {
|
|
return ServerStatus.timeout;
|
|
}
|
|
}
|
|
|
|
Icon _generateIcon(Server server) {
|
|
var icon, color;
|
|
switch (server.status) {
|
|
case ServerStatus.unknown:
|
|
icon = Icons.timer;
|
|
color = Colors.blueGrey.shade400;
|
|
break;
|
|
case ServerStatus.ok:
|
|
icon = Icons.check_box;
|
|
color = Colors.green.shade400;
|
|
break;
|
|
case ServerStatus.notok:
|
|
icon = Icons.indeterminate_check_box;
|
|
color = Colors.red;
|
|
break;
|
|
case ServerStatus.timeout:
|
|
icon = Icons.timer_off;
|
|
color = Colors.yellowAccent.shade700;
|
|
break;
|
|
case ServerStatus.dnserror:
|
|
icon = Icons.dns;
|
|
color = Colors.red.shade400;
|
|
break;
|
|
default:
|
|
icon = Icons.indeterminate_check_box;
|
|
color = Colors.red;
|
|
break;
|
|
}
|
|
return new Icon(icon, color: color);
|
|
}
|
|
|
|
static _resolveHostname(List<RRecord> records, RRecordType type) {
|
|
String ipAddress;
|
|
if (records == null) return ServerStatus.dnserror;
|
|
|
|
records.forEach((record) {
|
|
if (ipAddress != null) {
|
|
return;
|
|
}
|
|
|
|
if (ipAddress == null) {
|
|
// ignore: unrelated_type_equality_checks
|
|
if (record.rType == type) {
|
|
ipAddress = record.data;
|
|
}
|
|
}
|
|
});
|
|
if (ipAddress == null) ipAddress = "";
|
|
return ipAddress;
|
|
}
|
|
|
|
static Future<ServerStatus> _checkResponse(Uri uri) async {
|
|
var response = await HttpUtils.getForFullResponse(uri.toString())
|
|
.timeout(new Duration(seconds: 3));
|
|
|
|
if (response.statusCode > 199 && response.statusCode < 300) {
|
|
return ServerStatus.ok;
|
|
} else {
|
|
return ServerStatus.notok;
|
|
}
|
|
}
|
|
}
|