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" />|
This commit is contained in:
Ian VanSchooten 2025-01-16 08:16:23 -05:00 committed by GitHub
parent 46f029a91d
commit 991837676a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 64 additions and 30 deletions

View file

@ -0,0 +1,28 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class DangerButton extends StatelessWidget {
const DangerButton({Key? key, required this.child, this.onPressed}) : super(key: key);
final Widget child;
final GestureTapCallback? onPressed;
@override
Widget build(BuildContext context) {
if (Platform.isAndroid) {
return FilledButton(
onPressed: onPressed,
child: child,
style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error));
} else {
// Workaround for https://github.com/flutter/flutter/issues/161590
final themeData = CupertinoTheme.of(context);
return CupertinoTheme(
data: themeData.copyWith(primaryColor: CupertinoColors.white),
child: CupertinoButton(
child: child, onPressed: onPressed, color: CupertinoColors.systemRed.resolveFrom(context)));
}
}
}

View file

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/cupertino.dart' show CupertinoThemeData, DefaultCupertinoLocalizations; import 'package:flutter/cupertino.dart' show CupertinoThemeData, DefaultCupertinoLocalizations, CupertinoColors;
import 'package:flutter/material.dart' import 'package:flutter/material.dart'
show BottomSheetThemeData, Colors, DefaultMaterialLocalizations, ThemeData, ThemeMode; show BottomSheetThemeData, ColorScheme, Colors, DefaultMaterialLocalizations, ThemeData, ThemeMode;
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -87,8 +87,11 @@ class _AppState extends State<App> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData lightTheme = ThemeData( final ThemeData lightTheme = ThemeData(
useMaterial3: false, useMaterial3: false,
brightness: Brightness.light, colorScheme: ColorScheme.fromSwatch(
primarySwatch: Colors.blueGrey, brightness: Brightness.light,
primarySwatch: Colors.blueGrey,
errorColor: CupertinoColors.systemRed.resolveFrom(context),
),
primaryColor: Colors.blueGrey[900], primaryColor: Colors.blueGrey[900],
fontFamily: 'PublicSans', fontFamily: 'PublicSans',
//scaffoldBackgroundColor: Colors.grey[100], //scaffoldBackgroundColor: Colors.grey[100],
@ -100,8 +103,11 @@ class _AppState extends State<App> {
final ThemeData darkTheme = ThemeData( final ThemeData darkTheme = ThemeData(
useMaterial3: false, useMaterial3: false,
brightness: Brightness.dark, colorScheme: ColorScheme.fromSwatch(
primarySwatch: Colors.grey, brightness: Brightness.dark,
primarySwatch: Colors.grey,
errorColor: CupertinoColors.systemRed.resolveFrom(context),
),
primaryColor: Colors.grey[900], primaryColor: Colors.grey[900],
fontFamily: 'PublicSans', fontFamily: 'PublicSans',
scaffoldBackgroundColor: Colors.grey[800], scaffoldBackgroundColor: Colors.grey[800],

View file

@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/DangerButton.dart';
import 'package:mobile_nebula/components/SimplePage.dart'; import 'package:mobile_nebula/components/SimplePage.dart';
import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart'; import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart';
@ -161,9 +162,8 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: PlatformElevatedButton( child: DangerButton(
child: Text('Close Tunnel'), child: Text('Close Tunnel'),
color: CupertinoColors.systemRed.resolveFrom(context),
onPressed: () => Utils.confirmDelete(context, 'Close Tunnel?', () async { onPressed: () => Utils.confirmDelete(context, 'Close Tunnel?', () async {
try { try {
await widget.site.closeTunnel(hostInfo.vpnIp); await widget.site.closeTunnel(hostInfo.vpnIp);

View file

@ -17,6 +17,8 @@ import 'package:mobile_nebula/screens/siteConfig/SiteConfigScreen.dart';
import 'package:mobile_nebula/services/utils.dart'; import 'package:mobile_nebula/services/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../components/DangerButton.dart';
//TODO: If the site isn't active, don't respond to reloads on hostmaps //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) //TODO: ios is now the problem with connecting screwing our ability to query the hostmap (its a race)
@ -256,14 +258,14 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: PlatformElevatedButton( child: DangerButton(
child: Text('Delete'), child: Text('Delete'),
color: CupertinoColors.systemRed.resolveFrom(context), onPressed: () => Utils.confirmDelete(context, 'Delete Site?', () async {
onPressed: () => Utils.confirmDelete(context, 'Delete Site?', () async { if (await _deleteSite()) {
if (await _deleteSite()) { Navigator.of(context).pop();
Navigator.of(context).pop(); }
} }),
})))); )));
} }
_listHostmap() async { _listHostmap() async {

View file

@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.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/FormPage.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart';
@ -164,9 +165,8 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: PlatformElevatedButton( child: DangerButton(
child: Text('Replace certificate'), child: Text('Replace certificate'),
color: CupertinoColors.systemRed.resolveFrom(context),
onPressed: () { onPressed: () {
Utils.openPage(context, (context) { Utils.openPage(context, (context) {
return AddCertificateScreen( return AddCertificateScreen(
@ -199,9 +199,8 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: PlatformElevatedButton( child: DangerButton(
child: Text('Delete'), child: Text('Delete'),
color: CupertinoColors.systemRed.resolveFrom(context),
onPressed: () => Utils.confirmDelete(context, title, () async { onPressed: () => Utils.confirmDelete(context, title, () async {
Navigator.pop(context); Navigator.pop(context);
widget.onDelete!(); widget.onDelete!();

View file

@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.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/FormPage.dart';
import 'package:mobile_nebula/components/IPAndPortFormField.dart'; import 'package:mobile_nebula/components/IPAndPortFormField.dart';
import 'package:mobile_nebula/components/IPFormField.dart'; import 'package:mobile_nebula/components/IPFormField.dart';
@ -119,14 +120,12 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: PlatformElevatedButton( child: DangerButton(
child: Text('Delete'), child: Text('Delete'),
color: CupertinoColors.systemRed.resolveFrom(context), onPressed: () => Utils.confirmDelete(context, 'Delete host map?', () {
onPressed: () => Utils.confirmDelete(context, 'Delete host map?', () { Navigator.of(context).pop();
Navigator.of(context).pop(); widget.onDelete!();
widget.onDelete!(); }))))
}),
)))
: Container() : Container()
])); ]));
} }

View file

@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/CIDRFormField.dart'; import 'package:mobile_nebula/components/CIDRFormField.dart';
import 'package:mobile_nebula/components/DangerButton.dart';
import 'package:mobile_nebula/components/FormPage.dart'; import 'package:mobile_nebula/components/FormPage.dart';
import 'package:mobile_nebula/components/IPFormField.dart'; import 'package:mobile_nebula/components/IPFormField.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart';
@ -96,9 +97,8 @@ class _UnsafeRouteScreenState extends State<UnsafeRouteScreen> {
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: PlatformElevatedButton( child: DangerButton(
child: Text('Delete'), child: Text('Delete'),
color: CupertinoColors.systemRed.resolveFrom(context),
onPressed: () => Utils.confirmDelete(context, 'Delete unsafe route?', () { onPressed: () => Utils.confirmDelete(context, 'Delete unsafe route?', () {
Navigator.of(context).pop(); Navigator.of(context).pop();
widget.onDelete!(); widget.onDelete!();