Start to update stuff

This commit is contained in:
zontreck 2024-05-22 16:09:36 -07:00
parent 110b2d150c
commit 399d884681
18 changed files with 217 additions and 240 deletions

View file

@ -1,16 +1,8 @@
# servermanager # servermanager
A new Flutter project. A Conan Exiles server manager
## Getting Started ## Client
__________
This project is a starting point for a Flutter application. Client is the software used to configure the server. It connects to, and provides an interface for managing mods, and settings.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -0,0 +1,5 @@
import 'dart:ui';
class Constants {
static const TITLEBAR_COLOR = Color.fromARGB(255, 97, 0, 0);
}

View file

@ -1,25 +1,13 @@
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:libac_flutter/nbt/NbtUtils.dart';
import 'package:hive/hive.dart'; import 'package:libac_flutter/nbt/impl/CompoundTag.dart';
import 'package:libac_flutter/nbt/impl/IntTag.dart';
import 'package:servermanager/settings.dart'; import 'package:servermanager/settings.dart';
part 'autorestart.g.dart';
@HiveType(typeId: 3)
class AutomaticRestartInfo { class AutomaticRestartInfo {
@HiveField(0, defaultValue: 0)
final int hours; final int hours;
@HiveField(1, defaultValue: 0)
final int minutes; final int minutes;
@HiveField(2, defaultValue: 0)
final int seconds; final int seconds;
@HiveField(3, defaultValue: false)
final bool enabled; final bool enabled;
const AutomaticRestartInfo( const AutomaticRestartInfo(
@ -27,6 +15,30 @@ class AutomaticRestartInfo {
this.minutes = 0, this.minutes = 0,
this.seconds = 0, this.seconds = 0,
this.enabled = false}); this.enabled = false});
static AutomaticRestartInfo deserialize(CompoundTag tag) {
return AutomaticRestartInfo(
hours: tag.get(TAG_HOURS)?.asInt() ?? 12,
minutes: tag.get(TAG_MINUTES)?.asInt() ?? 0,
seconds: tag.get(TAG_SECONDS)?.asInt() ?? 0,
enabled: NbtUtils.readBoolean(tag, TAG_ENABLED));
}
CompoundTag serialize() {
CompoundTag tag = CompoundTag();
tag.put(TAG_HOURS, IntTag.valueOf(hours));
tag.put(TAG_MINUTES, IntTag.valueOf(minutes));
tag.put(TAG_SECONDS, IntTag.valueOf(seconds));
NbtUtils.writeBoolean(tag, TAG_ENABLED, enabled);
return tag;
}
static const TAG_NAME = "ari";
static const TAG_HOURS = "hours";
static const TAG_MINUTES = "minutes";
static const TAG_SECONDS = "seconds";
static const TAG_ENABLED = "enabled";
} }
class AutoRestartPage extends StatefulWidget { class AutoRestartPage extends StatefulWidget {
@ -62,8 +74,8 @@ class AutoRestartState extends State<AutoRestartPage> {
title: Text("Automatic Restart"), title: Text("Automatic Restart"),
backgroundColor: Color.fromARGB(255, 100, 0, 0), backgroundColor: Color.fromARGB(255, 100, 0, 0),
), ),
body: WillPopScope( body: PopScope(
onWillPop: () async { onPopInvoked: (v) async {
Navigator.pop( Navigator.pop(
context, context,
AutomaticRestartInfo( AutomaticRestartInfo(
@ -71,8 +83,6 @@ class AutoRestartState extends State<AutoRestartPage> {
hours: hours, hours: hours,
minutes: minutes, minutes: minutes,
seconds: seconds)); seconds: seconds));
return true;
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(

View file

@ -1,19 +1,11 @@
import 'package:hive/hive.dart'; import 'package:libac_flutter/nbt/NbtUtils.dart';
import 'package:libac_flutter/nbt/impl/CompoundTag.dart';
import 'package:libac_flutter/nbt/impl/StringTag.dart';
part 'credentials.g.dart';
@HiveType(typeId: 1)
class Credentials { class Credentials {
@HiveField(0, defaultValue: "")
String username; String username;
@HiveField(1, defaultValue: "")
String password; String password;
@HiveField(2, defaultValue: "")
String secret; String secret;
@HiveField(3, defaultValue: false)
bool has_2fa = false; bool has_2fa = false;
Credentials( Credentials(
@ -21,4 +13,28 @@ class Credentials {
required this.password, required this.password,
required this.secret, required this.secret,
required this.has_2fa}); required this.has_2fa});
CompoundTag save() {
CompoundTag tag = CompoundTag();
tag.put(TAG_USERNAME, StringTag.valueOf(username));
tag.put(TAG_PASSWORD, StringTag.valueOf(password));
tag.put(TAG_SECRET, StringTag.valueOf(secret));
NbtUtils.writeBoolean(tag, TAG_2FA, has_2fa);
return tag;
}
static Credentials deserialize(CompoundTag tag) {
return Credentials(
username: tag.get(TAG_USERNAME)?.asString() ?? "",
password: tag.get(TAG_PASSWORD)?.asString() ?? "",
secret: tag.get(TAG_SECRET)?.asString() ?? "",
has_2fa: NbtUtils.readBoolean(tag, TAG_2FA));
}
static const TAG_NAME = "credentials";
static const TAG_USERNAME = "username";
static const TAG_PASSWORD = "password";
static const TAG_SECRET = "secret";
static const TAG_2FA = "2fa";
} }

View file

@ -1,5 +1,3 @@
// ignore_for_file: non_constant_identifier_names, prefer_typing_uninitialized_variables, prefer_const_constructors
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class InputBox extends StatelessWidget { class InputBox extends StatelessWidget {

View file

@ -3,9 +3,9 @@ import 'dart:io';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:file_selector/file_selector.dart'; import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:libac_flutter/utils/IOTools.dart';
import 'package:servermanager/autorestart.dart'; import 'package:servermanager/autorestart.dart';
import 'package:servermanager/mod.dart'; import 'package:servermanager/mod.dart';
import 'package:servermanager/pathtools.dart';
import 'package:servermanager/serversettings.dart'; import 'package:servermanager/serversettings.dart';
import 'package:servermanager/settings.dart'; import 'package:servermanager/settings.dart';
import 'package:servermanager/statemachine.dart'; import 'package:servermanager/statemachine.dart';

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:file_selector/file_selector.dart'; import 'package:file_selector/file_selector.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:servermanager/Constants.dart';
import 'package:servermanager/settings.dart'; import 'package:servermanager/settings.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
@ -20,13 +21,10 @@ class HomePageState extends State<HomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
settings.Read();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text("Conan Exiles Server Manager"), title: Text("Conan Exiles Server Manager"),
backgroundColor: Color.fromARGB(255, 100, 0, 0), backgroundColor: Constants.TITLEBAR_COLOR),
),
drawer: Drawer( drawer: Drawer(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column(children: [ child: Column(children: [

View file

@ -1,23 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:libac_flutter/packets/packets.dart';
import 'package:servermanager/Constants.dart';
import 'package:servermanager/autorestart.dart'; import 'package:servermanager/autorestart.dart';
import 'package:servermanager/credentials.dart';
import 'package:servermanager/game.dart'; import 'package:servermanager/game.dart';
import 'package:servermanager/home.dart'; import 'package:servermanager/home.dart';
import 'package:servermanager/mod.dart';
import 'package:servermanager/proton.dart'; import 'package:servermanager/proton.dart';
import 'package:servermanager/serversettings.dart'; import 'package:servermanager/serversettings.dart';
import 'package:servermanager/settings.dart'; import 'package:servermanager/settings.dart';
import 'package:servermanager/settingsEntry.dart';
import 'package:servermanager/steamcmd.dart'; import 'package:servermanager/steamcmd.dart';
Future<void> main() async { Future<void> main() async {
await Hive.initFlutter();
Hive.registerAdapter(CredentialsAdapter());
Hive.registerAdapter(ModAdapter());
Hive.registerAdapter(SettingsEntryAdapter());
Hive.registerAdapter(AutomaticRestartInfoAdapter());
Hive.registerAdapter(ServerSettingsAdapter());
runApp(MyApp()); runApp(MyApp());
} }
@ -30,10 +22,8 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: 'Server Manager', title: 'Server Manager',
theme: ThemeData.dark(useMaterial3: true), theme: ThemeData.dark(useMaterial3: true),
home: HomePage(
settings: appSettings,
),
routes: { routes: {
"/": (context) => ServerPage(),
"/home": (context) => HomePage(settings: appSettings), "/home": (context) => HomePage(settings: appSettings),
"/proton": (context) => Proton(settings: appSettings), "/proton": (context) => Proton(settings: appSettings),
"/steamcmd": (context) => SteamCMD( "/steamcmd": (context) => SteamCMD(
@ -48,3 +38,54 @@ class MyApp extends StatelessWidget {
}); });
} }
} }
class ServerPage extends StatelessWidget {
TextEditingController serverIP = TextEditingController();
TextEditingController username = TextEditingController();
TextEditingController password = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Conan Exiles Server Manager - Login"),
backgroundColor: Constants.TITLEBAR_COLOR,
),
floatingActionButton: ElevatedButton(
onPressed: () {
// Send login packet to server
Settings settings = Settings();
settings.client = PacketClient(serverIP.text);
},
child: Text("Login"),
),
body: SingleChildScrollView(
child: Column(
children: [
ListTile(
title: Text("Server IP/FQDN"),
subtitle: TextField(
controller: serverIP,
decoration: InputDecoration(hintText: "ex. 192.168.1.100"),
),
),
ListTile(
title: Text("Username"),
subtitle: TextField(
controller: username,
decoration: InputDecoration(hintText: "the_user"),
),
),
ListTile(
title: Text("Password"),
subtitle: TextField(
controller: password,
decoration: InputDecoration(hintText: "pass"),
obscureText: true,
),
)
],
),
));
}
}

View file

@ -1,30 +1,20 @@
import 'package:hive/hive.dart'; import 'package:libac_flutter/utils/uuid/NbtUUID.dart';
import 'package:uuid/v4.dart'; import 'package:libac_flutter/utils/uuid/UUID.dart';
part 'mod.g.dart';
@HiveType(typeId: 2)
class Mod { class Mod {
@HiveField(0, defaultValue: "")
String mod_name = ""; String mod_name = "";
@HiveField(1, defaultValue: 0)
int mod_id = 0; int mod_id = 0;
@HiveField(2, defaultValue: "")
String mod_pak = ""; String mod_pak = "";
@HiveField(3, defaultValue: "")
String mod_hash = ""; String mod_hash = "";
bool newMod = false; bool newMod = false;
String _id = ""; NbtUUID _id = NbtUUID.ZERO;
String mod_instance_id() { String mod_instance_id() {
if (_id.isEmpty) { if (_id.toString() == NbtUUID.ZERO) {
_id = UuidV4().generate(); _id = NbtUUID.fromUUID(UUID.generate(4));
} }
return _id; return _id.toString();
} }
Mod( Mod(

View file

View file

@ -1,67 +0,0 @@
import 'dart:io';
class PathHelper {
String pth = "";
PathHelper({required this.pth});
static String combine(String path1, String path2) {
return path1 + Platform.pathSeparator + path2;
}
static PathHelper builder(String startPath) {
return PathHelper(pth: startPath);
}
PathHelper resolve(String path2) {
pth += Platform.pathSeparator + path2;
return this;
}
PathHelper conditionalResolve(bool flag, String path) {
if (flag) pth += Platform.pathSeparator + path;
return this;
}
bool deleteDirectory({bool recursive = false}) {
Directory dir = new Directory(build());
try {
dir.deleteSync(recursive: recursive);
return true;
} catch (E) {
return false;
}
}
bool deleteFile() {
File file = new File(build());
try {
file.deleteSync(recursive: true);
return true;
} catch (E) {
return false;
}
}
PathHelper removeDir() {
deleteDirectory(recursive: true);
return this;
}
PathHelper removeFile() {
deleteFile();
return this;
}
PathHelper mkdir() {
Directory dir = new Directory(build());
dir.createSync(recursive: true);
return this;
}
String build() {
return pth;
}
}

View file

@ -1,23 +1,12 @@
import 'dart:ffi';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:libac_flutter/nbt/impl/CompoundTag.dart';
import 'package:libac_flutter/nbt/impl/IntTag.dart';
import 'package:libac_flutter/nbt/impl/StringTag.dart';
part 'serversettings.g.dart';
@HiveType(typeId: 5)
class ServerSettings { class ServerSettings {
@HiveField(0, defaultValue: "Password01234")
final String RconPassword; final String RconPassword;
@HiveField(1, defaultValue: 7779)
final int RconPort; final int RconPort;
@HiveField(2, defaultValue: 7780)
final int GamePort; final int GamePort;
@HiveField(3, defaultValue: 7782)
final int QueryPort; final int QueryPort;
const ServerSettings( const ServerSettings(
@ -25,6 +14,30 @@ class ServerSettings {
required this.RconPort, required this.RconPort,
required this.GamePort, required this.GamePort,
required this.QueryPort}); required this.QueryPort});
static ServerSettings deserialize(CompoundTag tag) {
return ServerSettings(
RconPassword: tag.get(TAG_PASSWORD)?.asString() ?? "",
RconPort: tag.get(TAG_RCON_PORT)?.asInt() ?? 25565,
GamePort: tag.get(TAG_GAME_PORT)?.asInt() ?? 0,
QueryPort: tag.get(TAG_QUERY_PORT)?.asInt() ?? 0);
}
CompoundTag serialize() {
CompoundTag tag = CompoundTag();
tag.put(TAG_PASSWORD, StringTag.valueOf(RconPassword));
tag.put(TAG_RCON_PORT, IntTag.valueOf(RconPort));
tag.put(TAG_GAME_PORT, IntTag.valueOf(GamePort));
tag.put(TAG_QUERY_PORT, IntTag.valueOf(QueryPort));
return tag;
}
static const TAG_NAME = "serverSettings";
static const TAG_PASSWORD = "password";
static const TAG_RCON_PORT = "rcon";
static const TAG_GAME_PORT = "game";
static const TAG_QUERY_PORT = "query";
} }
class ServerSettingsPage extends StatefulWidget { class ServerSettingsPage extends StatefulWidget {

View file

@ -1,8 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:hive/hive.dart'; import 'package:libac_flutter/nbt/NbtIo.dart';
import 'package:libac_flutter/nbt/impl/CompoundTag.dart';
import 'package:libac_flutter/packets/packets.dart';
import 'package:libac_flutter/utils/IOTools.dart';
import 'package:servermanager/mod.dart'; import 'package:servermanager/mod.dart';
import 'package:servermanager/pathtools.dart';
import 'package:servermanager/settingsEntry.dart'; import 'package:servermanager/settingsEntry.dart';
import 'package:servermanager/statemachine.dart'; import 'package:servermanager/statemachine.dart';
@ -12,6 +14,7 @@ class Settings {
String steamcmd_path = ""; String steamcmd_path = "";
String game_path = ""; String game_path = "";
PacketClient? client;
StateMachine subsys = StateMachine(); StateMachine subsys = StateMachine();
@ -21,45 +24,43 @@ class Settings {
SettingsEntry? inst; SettingsEntry? inst;
void Read() { Future<void> Read() async {
if (!isValid()) return; try {
var box = Hive.box("settings"); var tag = await NbtIo.read(
PathHelper.builder(game_path).resolve("settings.dat").build());
inst = box.get("entry", defaultValue: SettingsEntry()) as SettingsEntry; inst = SettingsEntry.deserialize(tag.get("entry") as CompoundTag);
} catch (E) {
inst = SettingsEntry();
}
} }
void Write() { void Write() {
if (!isValid()) return; if (!isValid()) return;
var box = Hive.box("settings"); CompoundTag tag = CompoundTag();
box.put("entry", inst); tag.put("entry", inst!.serialize());
box.compact(); NbtIo.write(
PathHelper.builder(game_path).resolve("settings.dat").build(), tag);
} }
bool isValid() { bool isValid() {
if (!Hive.isBoxOpen("settings")) { if (inst == null) {
return false; return false;
} else { } else {
return true; return true;
} }
} }
Future<Box<E>> Open<E>() { Future<void> Open() async {
Close(); Close();
return Hive.openBox( Instance.Read();
"settings",
path: game_path,
compactionStrategy: (entries, deletedEntries) {
return deletedEntries > 1;
},
);
} }
static void Close() { static void Close() async {
if (Hive.isBoxOpen("settings")) { if (Instance.isValid()) {
var box = Hive.box("settings"); Instance.Write();
box.compact(); Instance.inst = null;
box.close();
} }
} }

View file

@ -1,37 +1,50 @@
import 'package:hive/hive.dart'; import 'package:libac_flutter/nbt/NbtUtils.dart';
import 'package:libac_flutter/nbt/impl/CompoundTag.dart';
import 'package:libac_flutter/nbt/impl/StringTag.dart';
import 'package:servermanager/autorestart.dart'; import 'package:servermanager/autorestart.dart';
import 'package:servermanager/credentials.dart'; import 'package:servermanager/credentials.dart';
import 'package:servermanager/mod.dart'; import 'package:servermanager/mod.dart';
import 'package:servermanager/serversettings.dart'; import 'package:servermanager/serversettings.dart';
part 'settingsEntry.g.dart';
@HiveType(typeId: 0)
class SettingsEntry { class SettingsEntry {
@HiveField(0, defaultValue: [])
List<Mod> mods = []; List<Mod> mods = [];
@HiveField(3)
Credentials? steam_creds; Credentials? steam_creds;
@HiveField(4, defaultValue: AutomaticRestartInfo())
AutomaticRestartInfo timer = AutomaticRestartInfo(); AutomaticRestartInfo timer = AutomaticRestartInfo();
@HiveField(5,
defaultValue: ServerSettings(
RconPassword: "Password01234",
RconPort: 7779,
GamePort: 7780,
QueryPort: 7782))
ServerSettings serverSettings = ServerSettings( ServerSettings serverSettings = ServerSettings(
RconPassword: "Password01234", RconPassword: "Password01234",
RconPort: 7779, RconPort: 7779,
GamePort: 7780, GamePort: 7780,
QueryPort: 7782); QueryPort: 7782);
@HiveField(6, defaultValue: true)
bool downloadMods = true; bool downloadMods = true;
@HiveField(7, defaultValue: "")
String conanExilesLibraryPath = ""; String conanExilesLibraryPath = "";
static SettingsEntry deserialize(CompoundTag tag) {
SettingsEntry st = SettingsEntry();
if (tag.containsKey(Credentials.TAG_NAME))
st.steam_creds =
Credentials.deserialize(tag.get(Credentials.TAG_NAME) as CompoundTag);
st.timer = AutomaticRestartInfo.deserialize(
tag.get(AutomaticRestartInfo.TAG_NAME) as CompoundTag);
st.serverSettings = ServerSettings.deserialize(
tag.get(ServerSettings.TAG_NAME) as CompoundTag);
st.downloadMods = NbtUtils.readBoolean(tag, "download");
st.conanExilesLibraryPath = tag.get("libpath")?.asString() ?? "";
return st;
}
CompoundTag serialize() {
CompoundTag tag = CompoundTag();
if (steam_creds != null) tag.put(Credentials.TAG_NAME, steam_creds!.save());
tag.put(AutomaticRestartInfo.TAG_NAME, timer.serialize());
tag.put(ServerSettings.TAG_NAME, serverSettings.serialize());
NbtUtils.writeBoolean(tag, "download", downloadMods);
tag.put("libpath", StringTag.valueOf(conanExilesLibraryPath));
return tag;
}
} }

View file

@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:libac_flutter/utils/IOTools.dart';
import 'package:mc_rcon_dart/mc_rcon_dart.dart';
import 'package:servermanager/game.dart'; import 'package:servermanager/game.dart';
import 'package:servermanager/pathtools.dart';
import 'package:servermanager/proton.dart'; import 'package:servermanager/proton.dart';
import 'package:servermanager/settings.dart'; import 'package:servermanager/settings.dart';
import 'package:mc_rcon_dart/mc_rcon_dart.dart';
enum States { enum States {
Idle, // For when the state machine is waiting for a state change Idle, // For when the state machine is waiting for a state change

View file

@ -6,9 +6,7 @@ import FlutterMacOS
import Foundation import Foundation
import file_selector_macos import file_selector_macos
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
} }

View file

@ -35,14 +35,14 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
hive:
hive_flutter:
file_selector: file_selector:
archive: archive:
dio: dio:
mc_rcon_dart: mc_rcon_dart:
uuid: ^4.1.0
crypto: crypto:
libac_flutter:
hosted: https://git.zontreck.com/api/packages/AriasCreations/pub/
version: 1.0.8
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -54,7 +54,6 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^3.0.0 flutter_lints: ^3.0.0
hive_generator:
build_runner: build_runner:
msix: ^3.16.6 msix: ^3.16.6

View file

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:servermanager/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}