mobile_nebula/lib/screens/siteConfig/StaticHostmapScreen.dart
Ian VanSchooten 991837676a
Add DangerButton component (#219)
This pulls out a component that we can use for "dangerous" operations like deleting, and styles it in one place.

It also starts to move us slowly towards Material 3, with the rounded corners on these buttons in Android.  

Android:

|Before Light|Before Dark|After Light|After Dark|
|---|---|---|---|
|<img width="425" alt="Android Studio 2025-01-15 14 16 36" src="https://github.com/user-attachments/assets/4823e551-6a40-48dd-9bc1-3004699b90ea" />|<img width="417" alt="Android Studio 2025-01-15 14 16 47" src="https://github.com/user-attachments/assets/df5461fd-586e-47bb-99b9-0212e63f0454" />|<img width="413" alt="Android Studio 2025-01-15 14 15 59" src="https://github.com/user-attachments/assets/d88a6225-b71a-4886-8387-e35811a3a6ec" />|<img width="418" alt="Android Studio 2025-01-15 14 16 15" src="https://github.com/user-attachments/assets/d4f23b1c-7003-4a00-b865-4a123d8fe3e9" />|


iOS:

|Before Light|Before Dark|After Light|After Dark|
|---|---|---|---|
|<img width="437" alt="Simulator 2025-01-15 15 56 26" src="https://github.com/user-attachments/assets/87c4eed3-6d07-4858-8ad8-d8c011538154" />|<img width="445" alt="Simulator 2025-01-15 15 56 36" src="https://github.com/user-attachments/assets/9dc5b174-7bc7-48ec-a3c0-61633168c31a" />|<img width="439" alt="Simulator 2025-01-15 16 05 23" src="https://github.com/user-attachments/assets/31dc9ab6-8a3c-49c7-892d-627f16e2a8cd" />|<img width="444" alt="Simulator 2025-01-15 16 05 37" src="https://github.com/user-attachments/assets/979280d6-e1f4-4d57-a86a-10bb4def729a" />|
2025-01-16 08:16:23 -05:00

223 lines
7.2 KiB
Dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/DangerButton.dart';
import 'package:mobile_nebula/components/FormPage.dart';
import 'package:mobile_nebula/components/IPAndPortFormField.dart';
import 'package:mobile_nebula/components/IPFormField.dart';
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart';
import 'package:mobile_nebula/models/Hostmap.dart';
import 'package:mobile_nebula/models/IPAndPort.dart';
import 'package:mobile_nebula/services/utils.dart';
class _IPAndPort {
final FocusNode focusNode;
IPAndPort destination;
_IPAndPort({required this.focusNode, required this.destination});
}
class StaticHostmapScreen extends StatefulWidget {
StaticHostmapScreen({
Key? key,
this.nebulaIp = '',
destinations,
this.lighthouse = false,
this.onDelete,
required this.onSave,
}) : this.destinations = destinations ?? [],
super(key: key);
final List<IPAndPort> destinations;
final String nebulaIp;
final bool lighthouse;
final ValueChanged<Hostmap>? onSave;
final Function? onDelete;
@override
_StaticHostmapScreenState createState() => _StaticHostmapScreenState();
}
class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
late Map<Key, _IPAndPort> _destinations;
late String _nebulaIp;
late bool _lighthouse;
bool changed = false;
@override
void initState() {
_nebulaIp = widget.nebulaIp;
_lighthouse = widget.lighthouse;
_destinations = {};
widget.destinations.forEach((dest) {
_destinations[UniqueKey()] = _IPAndPort(focusNode: FocusNode(), destination: dest);
});
if (_destinations.length == 0) {
_addDestination();
}
// _addDestination() above sets us to changed, set it back to false since we are at the default state
changed = false;
super.initState();
}
@override
Widget build(BuildContext context) {
return FormPage(
title: widget.onDelete == null
? widget.onSave == null
? 'View Static Host'
: 'New Static Host'
: 'Edit Static Host',
changed: changed,
onSave: _onSave,
child: Column(children: [
ConfigSection(label: 'Maps a nebula ip address to multiple real world addresses', children: <Widget>[
ConfigItem(
label: Text('Nebula IP'),
labelWidth: 200,
content: widget.onSave == null
? Text(_nebulaIp, textAlign: TextAlign.end)
: IPFormField(
help: "Required",
initialValue: _nebulaIp,
ipOnly: true,
textAlign: TextAlign.end,
crossAxisAlignment: CrossAxisAlignment.end,
textInputAction: TextInputAction.next,
onSaved: (v) {
if (v != null) {
_nebulaIp = v;
}
})),
ConfigItem(
label: Text('Lighthouse'),
labelWidth: 200,
content: Container(
alignment: Alignment.centerRight,
child: Switch.adaptive(
value: _lighthouse,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: widget.onSave == null
? null
: (v) {
setState(() {
changed = true;
_lighthouse = v;
});
})),
),
]),
ConfigSection(
label: 'List of public ips or dns names where for this host',
children: _buildHosts(),
),
widget.onDelete != null
? Padding(
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
child: SizedBox(
width: double.infinity,
child: DangerButton(
child: Text('Delete'),
onPressed: () => Utils.confirmDelete(context, 'Delete host map?', () {
Navigator.of(context).pop();
widget.onDelete!();
}))))
: Container()
]));
}
_onSave() {
Navigator.pop(context);
if (widget.onSave != null) {
var map = Hostmap(nebulaIp: _nebulaIp, destinations: [], lighthouse: _lighthouse);
_destinations.forEach((_, dest) {
map.destinations.add(dest.destination);
});
widget.onSave!(map);
}
}
List<Widget> _buildHosts() {
List<Widget> items = [];
_destinations.forEach((key, dest) {
items.add(ConfigItem(
key: key,
label: Align(
alignment: Alignment.centerLeft,
child: widget.onSave == null
? Container()
: PlatformIconButton(
padding: EdgeInsets.zero,
icon: Icon(Icons.remove_circle, color: CupertinoColors.systemRed.resolveFrom(context)),
onPressed: () => setState(() {
_removeDestination(key);
_dismissKeyboard();
}))),
labelWidth: 70,
content: Row(children: <Widget>[
Expanded(
child: widget.onSave == null
? Text(dest.destination.toString(), textAlign: TextAlign.end)
: IPAndPortFormField(
ipHelp: 'public ip or name',
ipTextAlign: TextAlign.end,
enableIPV6: true,
noBorder: true,
initialValue: dest.destination,
onSaved: (v) {
if (v != null) {
dest.destination = v;
}
},
)),
]),
));
});
if (widget.onSave != null) {
items.add(ConfigButtonItem(
content: Text('Add another'),
onPressed: () => setState(() {
_addDestination();
_dismissKeyboard();
})));
}
return items;
}
_addDestination() {
changed = true;
_destinations[UniqueKey()] = _IPAndPort(focusNode: FocusNode(), destination: IPAndPort());
// We can't onChanged here because it causes rendering issues on first build due to ensuring there is a single destination
}
_removeDestination(Key key) {
changed = true;
_destinations.remove(key);
}
_dismissKeyboard() {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
}
@override
void dispose() {
_destinations.forEach((key, dest) {
dest.focusNode.dispose();
});
super.dispose();
}
}