mobile_nebula/lib/screens/SiteDetailScreen.dart
Ian VanSchooten 64d45f66c7
Update Flutter, target android SDK 34 (#160)
This updates flutter to 3.24.1, the latest stable version, and also updates our flutter dependencies to latest.

It targets the latest android sdk, 34, which is required if we want to publish a new version to the Google Play store.

I also needed to make a few adjustments to handle deprecations. The biggest change is that I needed to wrap the main widget in MaterialApp to avoid problems with AdaptiveSwitch in iOS.
2024-09-20 14:19:23 -04:00

297 lines
9.4 KiB
Dart

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';
import 'package:flutter_svg/svg.dart';
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 {
const SiteDetailScreen({
Key? key,
required this.site,
this.onChanged,
required this.supportsQRScanning,
}) : super(key: key);
final Site site;
final Function? onChanged;
final bool supportsQRScanning;
@override
_SiteDetailScreenState createState() => _SiteDetailScreenState();
}
class _SiteDetailScreenState extends State<SiteDetailScreen> {
late Site site;
late StreamSubscription onChange;
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
bool changed = false;
List<HostInfo>? activeHosts;
List<HostInfo>? pendingHosts;
RefreshController refreshController = RefreshController(initialRefresh: false);
@override
void initState() {
site = widget.site;
if (site.connected) {
_listHostmap();
}
onChange = site.onChange().listen((_) {
// 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;
}
setState(() {});
}, onError: (err) {
setState(() {});
Utils.popError(context, "Error", err);
});
super.initState();
}
@override
void dispose() {
onChange.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final dnIcon =
Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
final title = Row(children: [
site.managed
? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
: Container(),
Expanded(child: Text(site.name, style: TextStyle(fontWeight: FontWeight.bold)))
]);
return SimplePage(
title: title,
leadingAction: Utils.leadingBackWidget(context, onPressed: () {
if (changed && widget.onChanged != null) {
widget.onChanged!();
}
Navigator.pop(context);
}),
refreshController: refreshController,
onRefresh: () async {
if (site.connected && site.status == "Connected") {
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(
labelWidth: 0, content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error))));
});
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 {
active = Text(Utils.itemCountFormat(activeHosts!.length, singleSuffix: "tunnel", multiSuffix: "tunnels"));
}
if (pendingHosts == null) {
pending = SizedBox(height: 20, width: 20, child: PlatformCircularProgressIndicator());
} else {
pending = Text(Utils.itemCountFormat(pendingHosts!.length, singleSuffix: "tunnel", multiSuffix: "tunnels"));
}
return ConfigSection(
label: "TUNNELS",
children: <Widget>[
ConfigPageItem(
onPressed: () {
if (activeHosts == null) return;
Utils.openPage(
context,
(context) => SiteTunnelsScreen(
pending: false,
tunnels: activeHosts!,
site: site,
onChanged: (hosts) {
setState(() {
activeHosts = hosts;
});
},
supportsQRScanning: widget.supportsQRScanning,
));
},
label: Text("Active"),
content: Container(alignment: Alignment.centerRight, child: active)),
ConfigPageItem(
onPressed: () {
if (pendingHosts == null) return;
Utils.openPage(
context,
(context) => SiteTunnelsScreen(
pending: true,
tunnels: pendingHosts!,
site: site,
onChanged: (hosts) {
setState(() {
pendingHosts = hosts;
});
},
supportsQRScanning: widget.supportsQRScanning,
));
},
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(
site: widget.site,
onSave: (site) async {
changed = true;
setState(() {});
},
supportsQRScanning: widget.supportsQRScanning,
);
});
},
),
]);
}
Widget _buildDelete() {
return Padding(
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
child: SizedBox(
width: double.infinity,
child: PlatformElevatedButton(
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) {
Utils.popError(context, 'Error while fetching hostmaps', err.toString());
}
}
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) {
widget.onChanged!();
}
return true;
}
}