From 991837676ab64074a4abf0f7c8323c612bfc47d2 Mon Sep 17 00:00:00 2001 From: Ian VanSchooten Date: Thu, 16 Jan 2025 08:16:23 -0500 Subject: [PATCH] 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| |---|---|---|---| |Android Studio 2025-01-15 14 16 36|Android Studio 2025-01-15 14 16 47|Android Studio 2025-01-15 14 15 59|Android Studio 2025-01-15 14 16 15| iOS: |Before Light|Before Dark|After Light|After Dark| |---|---|---|---| |Simulator 2025-01-15 15 56 26|Simulator 2025-01-15 15 56 36|Simulator 2025-01-15 16 05 23|Simulator 2025-01-15 16 05 37| --- lib/components/DangerButton.dart | 28 +++++++++++++++++++ lib/main.dart | 18 ++++++++---- lib/screens/HostInfoScreen.dart | 4 +-- lib/screens/SiteDetailScreen.dart | 18 ++++++------ .../siteConfig/CertificateDetailsScreen.dart | 7 ++--- .../siteConfig/StaticHostmapScreen.dart | 15 +++++----- lib/screens/siteConfig/UnsafeRouteScreen.dart | 4 +-- 7 files changed, 64 insertions(+), 30 deletions(-) create mode 100644 lib/components/DangerButton.dart diff --git a/lib/components/DangerButton.dart b/lib/components/DangerButton.dart new file mode 100644 index 00000000..4279c5ea --- /dev/null +++ b/lib/components/DangerButton.dart @@ -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))); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index a81f1a6f..ad222c69 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,8 @@ 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' - show BottomSheetThemeData, Colors, DefaultMaterialLocalizations, ThemeData, ThemeMode; + show BottomSheetThemeData, ColorScheme, Colors, DefaultMaterialLocalizations, ThemeData, ThemeMode; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -87,8 +87,11 @@ class _AppState extends State { Widget build(BuildContext context) { final ThemeData lightTheme = ThemeData( useMaterial3: false, - brightness: Brightness.light, - primarySwatch: Colors.blueGrey, + colorScheme: ColorScheme.fromSwatch( + brightness: Brightness.light, + primarySwatch: Colors.blueGrey, + errorColor: CupertinoColors.systemRed.resolveFrom(context), + ), primaryColor: Colors.blueGrey[900], fontFamily: 'PublicSans', //scaffoldBackgroundColor: Colors.grey[100], @@ -100,8 +103,11 @@ class _AppState extends State { final ThemeData darkTheme = ThemeData( useMaterial3: false, - brightness: Brightness.dark, - primarySwatch: Colors.grey, + colorScheme: ColorScheme.fromSwatch( + brightness: Brightness.dark, + primarySwatch: Colors.grey, + errorColor: CupertinoColors.systemRed.resolveFrom(context), + ), primaryColor: Colors.grey[900], fontFamily: 'PublicSans', scaffoldBackgroundColor: Colors.grey[800], diff --git a/lib/screens/HostInfoScreen.dart b/lib/screens/HostInfoScreen.dart index 169a22c3..1c850b83 100644 --- a/lib/screens/HostInfoScreen.dart +++ b/lib/screens/HostInfoScreen.dart @@ -1,6 +1,7 @@ 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/SimplePage.dart'; import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart'; @@ -161,9 +162,8 @@ class _HostInfoScreenState extends State { padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), child: SizedBox( width: double.infinity, - child: PlatformElevatedButton( + child: DangerButton( child: Text('Close Tunnel'), - color: CupertinoColors.systemRed.resolveFrom(context), onPressed: () => Utils.confirmDelete(context, 'Close Tunnel?', () async { try { await widget.site.closeTunnel(hostInfo.vpnIp); diff --git a/lib/screens/SiteDetailScreen.dart b/lib/screens/SiteDetailScreen.dart index 6bf2c195..07b1ba06 100644 --- a/lib/screens/SiteDetailScreen.dart +++ b/lib/screens/SiteDetailScreen.dart @@ -17,6 +17,8 @@ import 'package:mobile_nebula/screens/siteConfig/SiteConfigScreen.dart'; import 'package:mobile_nebula/services/utils.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: 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 { 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(); - } - })))); + child: DangerButton( + child: Text('Delete'), + onPressed: () => Utils.confirmDelete(context, 'Delete Site?', () async { + if (await _deleteSite()) { + Navigator.of(context).pop(); + } + }), + ))); } _listHostmap() async { diff --git a/lib/screens/siteConfig/CertificateDetailsScreen.dart b/lib/screens/siteConfig/CertificateDetailsScreen.dart index f6fb76c5..6b421a5c 100644 --- a/lib/screens/siteConfig/CertificateDetailsScreen.dart +++ b/lib/screens/siteConfig/CertificateDetailsScreen.dart @@ -1,6 +1,7 @@ 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/config/ConfigItem.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart'; @@ -164,9 +165,8 @@ class _CertificateDetailsScreenState extends State { padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), child: SizedBox( width: double.infinity, - child: PlatformElevatedButton( + child: DangerButton( child: Text('Replace certificate'), - color: CupertinoColors.systemRed.resolveFrom(context), onPressed: () { Utils.openPage(context, (context) { return AddCertificateScreen( @@ -199,9 +199,8 @@ class _CertificateDetailsScreenState extends State { padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), child: SizedBox( width: double.infinity, - child: PlatformElevatedButton( + child: DangerButton( child: Text('Delete'), - color: CupertinoColors.systemRed.resolveFrom(context), onPressed: () => Utils.confirmDelete(context, title, () async { Navigator.pop(context); widget.onDelete!(); diff --git a/lib/screens/siteConfig/StaticHostmapScreen.dart b/lib/screens/siteConfig/StaticHostmapScreen.dart index f07e0287..3f922097 100644 --- a/lib/screens/siteConfig/StaticHostmapScreen.dart +++ b/lib/screens/siteConfig/StaticHostmapScreen.dart @@ -1,6 +1,7 @@ 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'; @@ -119,14 +120,12 @@ class _StaticHostmapScreenState extends State { 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 host map?', () { - Navigator.of(context).pop(); - widget.onDelete!(); - }), - ))) + child: DangerButton( + child: Text('Delete'), + onPressed: () => Utils.confirmDelete(context, 'Delete host map?', () { + Navigator.of(context).pop(); + widget.onDelete!(); + })))) : Container() ])); } diff --git a/lib/screens/siteConfig/UnsafeRouteScreen.dart b/lib/screens/siteConfig/UnsafeRouteScreen.dart index a7d3771d..ab168b03 100644 --- a/lib/screens/siteConfig/UnsafeRouteScreen.dart +++ b/lib/screens/siteConfig/UnsafeRouteScreen.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.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/IPFormField.dart'; import 'package:mobile_nebula/components/config/ConfigItem.dart'; @@ -96,9 +97,8 @@ class _UnsafeRouteScreenState extends State { padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10), child: SizedBox( width: double.infinity, - child: PlatformElevatedButton( + child: DangerButton( child: Text('Delete'), - color: CupertinoColors.systemRed.resolveFrom(context), onPressed: () => Utils.confirmDelete(context, 'Delete unsafe route?', () { Navigator.of(context).pop(); widget.onDelete!();