2020-07-27 20:43:58 +00:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
2022-11-17 21:43:16 +00:00
|
|
|
import 'package:flutter_svg/svg.dart';
|
2020-07-27 20:43:58 +00:00
|
|
|
import 'package:mobile_nebula/components/SimplePage.dart';
|
|
|
|
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
|
|
|
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
|
|
|
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
|
|
|
import 'package:mobile_nebula/models/HostInfo.dart';
|
|
|
|
import 'package:mobile_nebula/models/Site.dart';
|
|
|
|
import 'package:mobile_nebula/screens/SiteLogsScreen.dart';
|
|
|
|
import 'package:mobile_nebula/screens/SiteTunnelsScreen.dart';
|
|
|
|
import 'package:mobile_nebula/screens/siteConfig/SiteConfigScreen.dart';
|
|
|
|
import 'package:mobile_nebula/services/utils.dart';
|
|
|
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
|
|
|
|
|
|
|
//TODO: If the site isn't active, don't respond to reloads on hostmaps
|
|
|
|
//TODO: ios is now the problem with connecting screwing our ability to query the hostmap (its a race)
|
|
|
|
|
|
|
|
class SiteDetailScreen extends StatefulWidget {
|
2022-11-22 21:50:11 +00:00
|
|
|
const SiteDetailScreen({
|
|
|
|
Key? key,
|
|
|
|
required this.site,
|
|
|
|
this.onChanged,
|
|
|
|
required this.supportsQRScanning,
|
|
|
|
}) : super(key: key);
|
2020-07-27 20:43:58 +00:00
|
|
|
|
|
|
|
final Site site;
|
2022-09-21 20:27:35 +00:00
|
|
|
final Function? onChanged;
|
2022-11-22 21:50:11 +00:00
|
|
|
final bool supportsQRScanning;
|
2020-07-27 20:43:58 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
_SiteDetailScreenState createState() => _SiteDetailScreenState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
2022-09-21 20:27:35 +00:00
|
|
|
late Site site;
|
|
|
|
late StreamSubscription onChange;
|
2020-07-27 20:43:58 +00:00
|
|
|
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
|
|
|
|
bool changed = false;
|
2022-09-21 20:27:35 +00:00
|
|
|
List<HostInfo>? activeHosts;
|
|
|
|
List<HostInfo>? pendingHosts;
|
2020-07-27 20:43:58 +00:00
|
|
|
RefreshController refreshController = RefreshController(initialRefresh: false);
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
site = widget.site;
|
|
|
|
if (site.connected) {
|
|
|
|
_listHostmap();
|
|
|
|
}
|
|
|
|
|
|
|
|
onChange = site.onChange().listen((_) {
|
2022-11-17 21:43:16 +00:00
|
|
|
// TODO: Gross hack... we get site.connected = true to trigger the toggle before the VPN service has started.
|
|
|
|
// If we fetch the hostmap now we'll never get a response. Wait until Nebula is running.
|
|
|
|
if (site.status == 'Connected') {
|
|
|
|
_listHostmap();
|
|
|
|
} else {
|
|
|
|
activeHosts = null;
|
|
|
|
pendingHosts = null;
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
2022-11-17 21:43:16 +00:00
|
|
|
|
2021-04-27 15:29:28 +00:00
|
|
|
setState(() {});
|
2020-07-27 20:43:58 +00:00
|
|
|
}, onError: (err) {
|
|
|
|
setState(() {});
|
|
|
|
Utils.popError(context, "Error", err);
|
|
|
|
});
|
|
|
|
|
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
onChange.cancel();
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2024-08-28 18:46:03 +00:00
|
|
|
final dnIcon =
|
|
|
|
Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
|
2022-11-17 21:43:16 +00:00
|
|
|
final title = Row(children: [
|
2024-08-28 18:46:03 +00:00
|
|
|
site.managed
|
|
|
|
? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
|
|
|
|
: Container(),
|
2022-11-17 21:43:16 +00:00
|
|
|
Expanded(child: Text(site.name, style: TextStyle(fontWeight: FontWeight.bold)))
|
|
|
|
]);
|
|
|
|
|
2020-07-27 20:43:58 +00:00
|
|
|
return SimplePage(
|
2022-11-17 21:43:16 +00:00
|
|
|
title: title,
|
2020-07-27 20:43:58 +00:00
|
|
|
leadingAction: Utils.leadingBackWidget(context, onPressed: () {
|
|
|
|
if (changed && widget.onChanged != null) {
|
2022-09-21 20:27:35 +00:00
|
|
|
widget.onChanged!();
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
Navigator.pop(context);
|
|
|
|
}),
|
|
|
|
refreshController: refreshController,
|
|
|
|
onRefresh: () async {
|
2021-04-27 15:29:28 +00:00
|
|
|
if (site.connected && site.status == "Connected") {
|
2020-07-27 20:43:58 +00:00
|
|
|
await _listHostmap();
|
|
|
|
}
|
|
|
|
refreshController.refreshCompleted();
|
|
|
|
},
|
|
|
|
child: Column(children: [
|
|
|
|
_buildErrors(),
|
|
|
|
_buildConfig(),
|
|
|
|
site.connected ? _buildHosts() : Container(),
|
|
|
|
_buildSiteDetails(),
|
|
|
|
_buildDelete(),
|
|
|
|
]));
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildErrors() {
|
|
|
|
if (site.errors.length == 0) {
|
|
|
|
return Container();
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Widget> items = [];
|
|
|
|
site.errors.forEach((error) {
|
|
|
|
items.add(ConfigItem(
|
2022-07-27 16:38:02 +00:00
|
|
|
labelWidth: 0, content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error))));
|
2020-07-27 20:43:58 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return ConfigSection(
|
|
|
|
label: 'ERRORS',
|
|
|
|
borderColor: CupertinoColors.systemRed.resolveFrom(context),
|
|
|
|
labelColor: CupertinoColors.systemRed.resolveFrom(context),
|
|
|
|
children: items,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildConfig() {
|
|
|
|
return ConfigSection(children: <Widget>[
|
|
|
|
ConfigItem(
|
|
|
|
label: Text('Status'),
|
|
|
|
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[
|
|
|
|
Padding(
|
|
|
|
padding: EdgeInsets.only(right: 5),
|
|
|
|
child: Text(widget.site.status,
|
|
|
|
style: TextStyle(color: CupertinoColors.secondaryLabel.resolveFrom(context)))),
|
|
|
|
Switch.adaptive(
|
|
|
|
value: widget.site.connected,
|
|
|
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
|
|
onChanged: (v) async {
|
|
|
|
try {
|
|
|
|
if (v) {
|
|
|
|
await widget.site.start();
|
|
|
|
} else {
|
|
|
|
await widget.site.stop();
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
var action = v ? 'start' : 'stop';
|
|
|
|
Utils.popError(context, 'Failed to $action the site', error.toString());
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
])),
|
|
|
|
ConfigPageItem(
|
|
|
|
label: Text('Logs'),
|
|
|
|
onPressed: () {
|
|
|
|
Utils.openPage(context, (context) {
|
|
|
|
return SiteLogsScreen(site: widget.site);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildHosts() {
|
|
|
|
Widget active, pending;
|
|
|
|
|
|
|
|
if (activeHosts == null) {
|
|
|
|
active = SizedBox(height: 20, width: 20, child: PlatformCircularProgressIndicator());
|
|
|
|
} else {
|
2022-09-21 20:27:35 +00:00
|
|
|
active = Text(Utils.itemCountFormat(activeHosts!.length, singleSuffix: "tunnel", multiSuffix: "tunnels"));
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (pendingHosts == null) {
|
|
|
|
pending = SizedBox(height: 20, width: 20, child: PlatformCircularProgressIndicator());
|
|
|
|
} else {
|
2022-09-21 20:27:35 +00:00
|
|
|
pending = Text(Utils.itemCountFormat(pendingHosts!.length, singleSuffix: "tunnel", multiSuffix: "tunnels"));
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return ConfigSection(
|
|
|
|
label: "TUNNELS",
|
|
|
|
children: <Widget>[
|
|
|
|
ConfigPageItem(
|
|
|
|
onPressed: () {
|
2022-09-21 20:27:35 +00:00
|
|
|
if (activeHosts == null) return;
|
|
|
|
|
2020-07-27 20:43:58 +00:00
|
|
|
Utils.openPage(
|
|
|
|
context,
|
|
|
|
(context) => SiteTunnelsScreen(
|
2024-08-28 18:46:03 +00:00
|
|
|
pending: false,
|
|
|
|
tunnels: activeHosts!,
|
|
|
|
site: site,
|
|
|
|
onChanged: (hosts) {
|
|
|
|
setState(() {
|
|
|
|
activeHosts = hosts;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
supportsQRScanning: widget.supportsQRScanning,
|
|
|
|
));
|
2020-07-27 20:43:58 +00:00
|
|
|
},
|
|
|
|
label: Text("Active"),
|
|
|
|
content: Container(alignment: Alignment.centerRight, child: active)),
|
|
|
|
ConfigPageItem(
|
|
|
|
onPressed: () {
|
2022-09-21 20:27:35 +00:00
|
|
|
if (pendingHosts == null) return;
|
|
|
|
|
2020-07-27 20:43:58 +00:00
|
|
|
Utils.openPage(
|
|
|
|
context,
|
|
|
|
(context) => SiteTunnelsScreen(
|
2024-08-28 18:46:03 +00:00
|
|
|
pending: true,
|
|
|
|
tunnels: pendingHosts!,
|
|
|
|
site: site,
|
|
|
|
onChanged: (hosts) {
|
|
|
|
setState(() {
|
|
|
|
pendingHosts = hosts;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
supportsQRScanning: widget.supportsQRScanning,
|
|
|
|
));
|
2020-07-27 20:43:58 +00:00
|
|
|
},
|
|
|
|
label: Text("Pending"),
|
|
|
|
content: Container(alignment: Alignment.centerRight, child: pending))
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildSiteDetails() {
|
|
|
|
return ConfigSection(children: <Widget>[
|
|
|
|
ConfigPageItem(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
content: Text('Configuration'),
|
|
|
|
onPressed: () {
|
|
|
|
Utils.openPage(context, (context) {
|
|
|
|
return SiteConfigScreen(
|
2024-08-28 18:46:03 +00:00
|
|
|
site: widget.site,
|
|
|
|
onSave: (site) async {
|
|
|
|
changed = true;
|
|
|
|
setState(() {});
|
|
|
|
},
|
2022-11-22 21:50:11 +00:00
|
|
|
supportsQRScanning: widget.supportsQRScanning,
|
|
|
|
);
|
2020-07-27 20:43:58 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildDelete() {
|
|
|
|
return Padding(
|
|
|
|
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
|
|
|
child: SizedBox(
|
|
|
|
width: double.infinity,
|
2022-07-27 16:38:02 +00:00
|
|
|
child: PlatformElevatedButton(
|
2020-07-27 20:43:58 +00:00
|
|
|
child: Text('Delete'),
|
|
|
|
color: CupertinoColors.systemRed.resolveFrom(context),
|
|
|
|
onPressed: () => Utils.confirmDelete(context, 'Delete Site?', () async {
|
|
|
|
if (await _deleteSite()) {
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
}
|
|
|
|
}))));
|
|
|
|
}
|
|
|
|
|
|
|
|
_listHostmap() async {
|
|
|
|
try {
|
|
|
|
var maps = await site.listAllHostmaps();
|
|
|
|
activeHosts = maps["active"];
|
|
|
|
pendingHosts = maps["pending"];
|
|
|
|
setState(() {});
|
|
|
|
} catch (err) {
|
2022-09-21 20:27:35 +00:00
|
|
|
Utils.popError(context, 'Error while fetching hostmaps', err.toString());
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> _deleteSite() async {
|
|
|
|
try {
|
|
|
|
var err = await platform.invokeMethod("deleteSite", widget.site.id);
|
|
|
|
if (err != null) {
|
|
|
|
Utils.popError(context, 'Failed to delete the site', err);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
Utils.popError(context, 'Failed to delete the site', err.toString());
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (widget.onChanged != null) {
|
2022-09-21 20:27:35 +00:00
|
|
|
widget.onChanged!();
|
2020-07-27 20:43:58 +00:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|