diff --git a/example/lib/main.dart b/example/lib/main.dart index d35d35b..0cca731 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,10 +1,11 @@ // Copyright (c) 2020 // Author: Hugo Pointcheval -import 'dart:developer'; -import 'dart:typed_data'; - import 'package:flutter/material.dart'; -import 'package:native_crypto/native_crypto.dart'; +import 'package:native_crypto_example/pages/kemPage.dart'; + +import 'pages/benchmarkPage.dart'; +import 'pages/cipherPage.dart'; +import 'pages/hashKeyDerivationPage.dart'; void main() => runApp(MyApp()); @@ -14,163 +15,20 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - final textController = TextEditingController(); - final pwdController = TextEditingController(); - - String _output = 'none'; - String _bench; - - AESCipher aes; - CipherText cipherText; - Uint8List plainText; - SecretKey key; - - void _generateKey() async { - var output; - try { - aes = await AESCipher.generate( - AESKeySize.bits256, - CipherParameters( - BlockCipherMode.CBC, - PlainTextPadding.PKCS5, - ), - ); - output = 'Key generated. Length: ${aes.secretKey.encoded.length}'; - } catch (e) { - print(e); - output = e.message; - } + int _currentIndex = 0; + final List _children = [ + HashKeyDerivationPage(), + CipherPage(), + KemPage(), + BenchmarkPage(), + ]; + void onTabTapped(int index) { setState(() { - _output = output; + _currentIndex = index; }); } - void _pbkdf2() async { - final password = pwdController.text.trim(); - - var output; - if (password.isEmpty) { - output = 'Password is empty'; - } else { - PBKDF2 _pbkdf2 = - PBKDF2(keyLength: 32, iteration: 1000, hash: HashAlgorithm.SHA512); - await _pbkdf2.derive(password: password, salt: 'salty'); - key = _pbkdf2.key; - output = 'Key successfully derived.'; - } - setState(() { - _output = output; - }); - } - - void _encrypt() async { - final plainText = textController.text.trim(); - - var output; - if (plainText.isEmpty) { - output = 'Entry is empty'; - } else { - var stringToBytes = TypeHelper().stringToBytes(plainText); - cipherText = await aes.encrypt(stringToBytes); - output = 'String successfully encrypted.'; - } - setState(() { - _output = output; - }); - } - - void _alter() async { - var output; - if (cipherText == null || cipherText.bytes.isEmpty) { - output = 'Encrypt before altering payload!'; - } else { - // Add 1 to the first byte - Uint8List _altered = cipherText.bytes; - _altered[0] += 1; - // Recreate cipher text with altered data - cipherText = AESCipherText(_altered, cipherText.iv); - output = 'Payload altered.'; - } - setState(() { - _output = output; - }); - } - - void _decrypt() async { - var output; - if (cipherText == null || cipherText.bytes.isEmpty) { - output = 'Encrypt before decrypting!'; - } else { - try { - plainText = await aes.decrypt(cipherText); - var bytesToString = TypeHelper().bytesToString(plainText); - output = 'String successfully decrypted:\n\n$bytesToString'; - } on DecryptionException catch (e) { - output = e.message; - } - } - setState(() { - _output = output; - }); - } - - Future _benchmark(int megabytes) async { - String output; - var bigFile = Uint8List(megabytes * 1000000); - - var before = DateTime.now(); - var encryptedBigFile = await aes.encrypt(bigFile); - var after = DateTime.now(); - - var benchmark = - after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; - - output = '$megabytes MB\nAES Encryption: $benchmark ms\n'; - - before = DateTime.now(); - await aes.decrypt(encryptedBigFile); - after = DateTime.now(); - - benchmark = after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; - - output += 'AES Decryption: $benchmark ms\n\n'; - - return output; - } - - void _testPerf({int megabytes}) async { - var output = ''; - if (megabytes != null) { - output = await _benchmark(megabytes); - setState(() { - _output = output; - }); - } else { - setState(() { - _bench = 'Open the logcat!'; - }); - for (int i = 1; i <= 50; i += 10) { - var benchmark = await _benchmark(i); - log(benchmark, name: 'fr.pointcheval.native_crypto'); - } - } - } - - @override - void initState() { - // Generate AES instance on init. - _generateKey(); - super.initState(); - } - - void dispose() { - // Clean up the controller when the widget is disposed. - textController.dispose(); - pwdController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return MaterialApp( @@ -179,126 +37,33 @@ class _MyAppState extends State { centerTitle: true, title: const Text('Native Crypto'), ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 10, 10, 20), - child: Center( - child: Column( - children: [ - TextField( - controller: pwdController, - decoration: InputDecoration( - hintText: 'Test password', - ), - ), - SizedBox(height: 20), - FlatButton( - onPressed: _pbkdf2, - color: Colors.blue, - child: Text( - 'Pbkdf2', - style: TextStyle(color: Colors.white), - )), - SizedBox(height: 30), - TextField( - controller: textController, - decoration: InputDecoration( - hintText: 'Text to encrypt.', - ), - ), - SizedBox(height: 20), - FlatButton( - onPressed: _encrypt, - color: Colors.blue, - child: Text( - 'Encrypt String', - style: TextStyle(color: Colors.white), - )), - FlatButton( - onPressed: _alter, - color: Colors.blue, - child: Text( - 'Alter encrypted payload', - style: TextStyle(color: Colors.white), - )), - FlatButton( - onPressed: _decrypt, - color: Colors.blue, - child: Text( - 'Decrypt String', - style: TextStyle(color: Colors.white), - )), - SizedBox(height: 20), - // Output - Text( - _output, - textAlign: TextAlign.center, - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - SizedBox(height: 20), - FlatButton( - onPressed: () { - _testPerf(megabytes: 1); - }, - color: Colors.blue, - child: Text( - 'Benchmark 1 MB', - style: TextStyle(color: Colors.white), - )), - FlatButton( - onPressed: () { - _testPerf(megabytes: 10); - }, - color: Colors.blue, - child: Text( - 'Benchmark 10 MB', - style: TextStyle(color: Colors.white), - )), - FlatButton( - onPressed: () { - _testPerf(megabytes: 50); - }, - color: Colors.blue, - child: Text( - 'Benchmark 50 MB', - style: TextStyle(color: Colors.white), - )), - SizedBox(height: 20), - FlatButton( - onPressed: () { - _testPerf(); - }, - color: Colors.blue, - child: Text( - 'Full benchmark', - style: TextStyle(color: Colors.white), - )), - - (_bench != null && _bench.isNotEmpty) - ? Text(_bench) - : Container(), - ], - ), + body: _children[_currentIndex], + bottomNavigationBar: BottomNavigationBar( + selectedItemColor: Colors.blue, + unselectedItemColor: Colors.black, + showUnselectedLabels: true, + onTap: onTabTapped, // new + currentIndex: _currentIndex, // new + items: [ + BottomNavigationBarItem( + icon: Icon(Icons.vpn_key), + label: 'Key', ), - ), + BottomNavigationBarItem( + icon: Icon(Icons.lock), + label: 'Encryption', + ), + BottomNavigationBarItem( + icon: Icon(Icons.connect_without_contact), + label: 'KEM', + ), + BottomNavigationBarItem( + icon: Icon(Icons.timer), + label: 'Benchmark', + ), + ], ), ), ); } } - -/// Contains some useful functions. -class TypeHelper { - /// Returns bytes `Uint8List` from a `String`. - Uint8List stringToBytes(String source) { - var list = source.runes.toList(); - var bytes = Uint8List.fromList(list); - return bytes; - } - - /// Returns a `String` from bytes `Uint8List`. - String bytesToString(Uint8List bytes) { - var string = String.fromCharCodes(bytes); - return string; - } -} diff --git a/example/lib/pages/benchmarkPage.dart b/example/lib/pages/benchmarkPage.dart new file mode 100644 index 0000000..29341d7 --- /dev/null +++ b/example/lib/pages/benchmarkPage.dart @@ -0,0 +1,115 @@ +// Copyright (c) 2020 +// Author: Hugo Pointcheval +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:native_crypto/native_crypto.dart'; + +import '../session.dart'; +import '../widgets/button.dart'; +import '../widgets/output.dart'; + +class BenchmarkPage extends StatefulWidget { + const BenchmarkPage({key}) : super(key: key); + + @override + _BenchmarkPageState createState() => _BenchmarkPageState(); +} + +class _BenchmarkPageState extends State { + final Output keyContent = Output( + textEditingController: TextEditingController(), + ); + final Output benchmarkStatus = Output( + textEditingController: TextEditingController(), + large: true, + ); + + Future _benchmark() async { + if (Session.secretKey == null || Session.secretKey.isEmpty) { + benchmarkStatus + .print('No SecretKey!\nGo in Key tab and generate or derive one.'); + return; + } else if (!Session.aesCipher.isInitialized) { + benchmarkStatus.print( + 'Cipher not initialized!\nGo in Key tab and generate or derive one.'); + return; + } + + benchmarkStatus.print("Benchmark 1/5/10/25/50MB\n"); + List testedSizes = [1, 5, 10, 25, 50]; + + var beforeBench = DateTime.now(); + for (int size in testedSizes) { + var bigFile = Uint8List(size * 1000000); + var before = DateTime.now(); + var encryptedBigFile = await Session.aesCipher.encrypt(bigFile); + var after = DateTime.now(); + var benchmark = + after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; + benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n'); + before = DateTime.now(); + await Session.aesCipher.decrypt(encryptedBigFile); + after = DateTime.now(); + benchmark = after.millisecondsSinceEpoch - before.millisecondsSinceEpoch; + benchmarkStatus.append('[$size MB] Decryption took $benchmark ms\n\n'); + } + var afterBench = DateTime.now(); + var benchmark = + afterBench.millisecondsSinceEpoch - beforeBench.millisecondsSinceEpoch; + var sum = testedSizes.reduce((a, b) => a + b); + benchmarkStatus.append( + 'Benchmark finished.\nGenerated, encrypted and decrypted $sum MB in $benchmark ms'); + } + + void _clear() { + benchmarkStatus.clear(); + } + + @override + void initState() { + super.initState(); + if (Session.secretKey != null) { + keyContent.print(Session.secretKey.encoded.toString()); + Session.aesCipher = AESCipher( + Session.secretKey, + CipherParameters( + BlockCipherMode.CBC, + PlainTextPadding.PKCS5, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Align( + child: Text("Secret Key"), + alignment: Alignment.centerLeft, + ), + keyContent, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Button( + onPressed: _benchmark, + label: "Launch benchmark", + ), + Button( + onPressed: _clear, + label: "Clear", + ), + ], + ), + benchmarkStatus, + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/cipherPage.dart b/example/lib/pages/cipherPage.dart new file mode 100644 index 0000000..60fb8a1 --- /dev/null +++ b/example/lib/pages/cipherPage.dart @@ -0,0 +1,205 @@ +// Copyright (c) 2020 +// Author: Hugo Pointcheval +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:native_crypto/native_crypto.dart'; + +import '../session.dart'; +import '../utils.dart'; +import '../widgets/button.dart'; +import '../widgets/output.dart'; + +class CipherPage extends StatefulWidget { + const CipherPage({key}) : super(key: key); + + @override + _CipherPageState createState() => _CipherPageState(); +} + +class _CipherPageState extends State { + final Output keyContent = Output( + textEditingController: TextEditingController(), + ); + final Output encryptionStatus = Output( + textEditingController: TextEditingController(), + ); + final Output decryptionStatus = Output( + textEditingController: TextEditingController(), + ); + final Output cipherExport = Output( + textEditingController: TextEditingController(), + large: true, + editable: true, + ); + + final TextEditingController _plainTextController = TextEditingController(); + CipherText cipherText; + + void _encrypt() async { + final plainText = _plainTextController.text.trim(); + + if (Session.secretKey == null || Session.secretKey.isEmpty) { + encryptionStatus + .print('No SecretKey!\nGo in Key tab and generate or derive one.'); + } else if (!Session.aesCipher.isInitialized) { + encryptionStatus.print( + 'Cipher not initialized!\nGo in Key tab and generate or derive one.'); + } else if (plainText.isEmpty) { + encryptionStatus.print('Entry is empty'); + } else { + var stringToBytes = TypeHelper.stringToBytes(plainText); + cipherText = await Session.aesCipher.encrypt(stringToBytes); + encryptionStatus.print('String successfully encrypted.\n'); + encryptionStatus.append("IV: " + + cipherText.iv.toString() + + "\nCipherText: " + + cipherText.bytes.toString()); + } + } + + void _alter() async { + if (cipherText == null || cipherText.bytes.isEmpty) { + decryptionStatus.print('Encrypt before altering CipherText!'); + } else { + // Add 1 to the first byte + Uint8List _altered = cipherText.bytes; + _altered[0] += 1; + // Recreate cipher text with altered data + cipherText = AESCipherText(_altered, cipherText.iv); + encryptionStatus.print('String successfully encrypted.\n'); + encryptionStatus.append("IV: " + + cipherText.iv.toString() + + "\nCipherText: " + + cipherText.bytes.toString()); + decryptionStatus.print('CipherText altered!\nDecryption will fail.'); + } + } + + void _decrypt() async { + if (Session.secretKey == null || Session.secretKey.isEmpty) { + decryptionStatus + .print('No SecretKey!\nGo in Key tab and generate or derive one.'); + } else if (!Session.aesCipher.isInitialized) { + decryptionStatus.print( + 'Cipher not initialized!\nGo in Key tab and generate or derive one.'); + } else if (cipherText == null || cipherText.bytes.isEmpty) { + decryptionStatus.print('Encrypt before decrypting!'); + } else { + try { + Uint8List plainText = await Session.aesCipher.decrypt(cipherText); + var bytesToString = TypeHelper.bytesToString(plainText); + decryptionStatus + .print('String successfully decrypted:\n\n$bytesToString'); + } on DecryptionException catch (e) { + decryptionStatus.print(e.message); + } + } + } + + void _export() async { + if (cipherText == null) { + decryptionStatus.print('Encrypt data before export!'); + } else { + Uint8List payload = Uint8List.fromList(cipherText.iv + cipherText.bytes); + String data = TypeHelper.bytesToBase64(payload); + decryptionStatus.print('CipherText successfully exported'); + cipherExport.print(data); + } + } + + void _import() async { + final String data = cipherExport.read(); + if (data.isEmpty) { + encryptionStatus.print('CipherText import failed'); + } else { + Uint8List payload = TypeHelper.base64ToBytes(data); + Uint8List iv = payload.sublist(0, 16); + Uint8List bytes = payload.sublist(16); + cipherText = AESCipherText(bytes, iv); + encryptionStatus.print('CipherText successfully imported\n'); + encryptionStatus.append("IV: " + + cipherText.iv.toString() + + "\nCipherText: " + + cipherText.bytes.toString()); + } + } + + @override + void initState() { + super.initState(); + if (Session.secretKey != null) { + keyContent.print(Session.secretKey.encoded.toString()); + Session.aesCipher = AESCipher( + Session.secretKey, + CipherParameters( + BlockCipherMode.CBC, + PlainTextPadding.PKCS5, + ), + ); + } + } + + @override + void dispose() { + super.dispose(); + _plainTextController.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Align( + child: Text("Secret Key"), + alignment: Alignment.centerLeft, + ), + keyContent, + TextField( + controller: _plainTextController, + decoration: InputDecoration( + hintText: 'Plain text', + ), + ), + Button( + onPressed: _encrypt, + label: "Encrypt", + ), + encryptionStatus, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Button( + onPressed: _alter, + label: "Alter cipher", + ), + Button( + onPressed: _decrypt, + label: "Decrypt", + ), + ], + ), + decryptionStatus, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Button( + onPressed: _export, + label: "Export cipher", + ), + Button( + onPressed: _import, + label: "Import cipher", + ), + ], + ), + cipherExport + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/hashKeyDerivationPage.dart b/example/lib/pages/hashKeyDerivationPage.dart new file mode 100644 index 0000000..5219dc0 --- /dev/null +++ b/example/lib/pages/hashKeyDerivationPage.dart @@ -0,0 +1,180 @@ +// Copyright (c) 2020 +// Author: Hugo Pointcheval +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:native_crypto/native_crypto.dart'; + +import '../session.dart'; +import '../utils.dart'; +import '../widgets/button.dart'; +import '../widgets/output.dart'; + +class HashKeyDerivationPage extends StatefulWidget { + const HashKeyDerivationPage({key}) : super(key: key); + + @override + _HashKeyDerivationPageState createState() => _HashKeyDerivationPageState(); +} + +class _HashKeyDerivationPageState extends State { + final Output keyContent = Output( + textEditingController: TextEditingController(), + ); + final Output keyStatus = Output( + textEditingController: TextEditingController(), + ); + final Output keyExport = Output( + textEditingController: TextEditingController(), + large: true, + editable: true, + ); + final Output pbkdf2Status = Output( + textEditingController: TextEditingController(), + ); + final Output hashStatus = Output( + textEditingController: TextEditingController(), + ); + + final TextEditingController _pwdTextController = TextEditingController(); + final TextEditingController _messageTextController = TextEditingController(); + final TextEditingController _keyTextController = TextEditingController(); + + void _generate() async { + try { + Session.secretKey = await SecretKey.generate(256, CipherAlgorithm.AES); + keyContent.print(Session.secretKey.encoded.toString()); + keyStatus.print( + "Secret Key successfully generated.\nLength: ${Session.secretKey.encoded.length} bytes"); + } catch (e) { + keyStatus.print(e.message); + } + } + + void _pbkdf2() async { + final password = _pwdTextController.text.trim(); + + if (password.isEmpty) { + pbkdf2Status.print('Password is empty'); + } else { + PBKDF2 _pbkdf2 = + PBKDF2(keyLength: 32, iteration: 1000, hash: HashAlgorithm.SHA512); + await _pbkdf2.derive(password: password, salt: 'salty'); + SecretKey key = _pbkdf2.key; + pbkdf2Status.print('Key successfully derived.'); + Session.secretKey = key; + keyContent.print(Session.secretKey.encoded.toString()); + } + } + + void _export() async { + if (Session.secretKey == null || Session.secretKey.isEmpty) { + keyStatus + .print('No SecretKey!\nGenerate or derive one before exporting!'); + } else { + String key = TypeHelper.bytesToBase64(Session.secretKey.encoded); + keyStatus.print('Key successfully exported'); + keyExport.print(key); + } + } + + void _import() async { + final String key = keyExport.read(); + if (key.isEmpty) { + keyStatus.print('Key import failed'); + } else { + Uint8List keyBytes = TypeHelper.base64ToBytes(key); + Session.secretKey = + SecretKey.fromBytes(keyBytes, algorithm: CipherAlgorithm.AES); + keyStatus.print('Key successfully imported'); + keyContent.print(Session.secretKey.encoded.toString()); + } + } + + void _hash() async { + final message = _messageTextController.text.trim(); + + if (message.isEmpty) { + hashStatus.print('Message is empty'); + } else { + MessageDigest md = MessageDigest.getInstance("sha256"); + Uint8List hash = await md.digest(TypeHelper.stringToBytes(message)); + hashStatus.print('Message successfully hashed.\n' + hash.toString()); + } + } + + @override + void initState() { + super.initState(); + if (Session.secretKey != null) { + keyContent.print(Session.secretKey.encoded.toString()); + } + } + + @override + void dispose() { + super.dispose(); + _pwdTextController.dispose(); + _messageTextController.dispose(); + _keyTextController.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Align( + child: Text("Secret Key"), + alignment: Alignment.centerLeft, + ), + keyContent, + Button( + onPressed: _generate, + label: "Generate key", + ), + keyStatus, + TextField( + controller: _pwdTextController, + decoration: InputDecoration( + hintText: 'Password', + ), + ), + Button( + onPressed: _pbkdf2, + label: "Apply PBKDF2", + ), + pbkdf2Status, + TextField( + controller: _messageTextController, + decoration: InputDecoration( + hintText: 'Message', + ), + ), + Button( + onPressed: _hash, + label: "Hash", + ), + hashStatus, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Button( + onPressed: _export, + label: "Export key", + ), + Button( + onPressed: _import, + label: "Import key", + ), + ], + ), + keyExport + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/kemPage.dart b/example/lib/pages/kemPage.dart new file mode 100644 index 0000000..d5d728e --- /dev/null +++ b/example/lib/pages/kemPage.dart @@ -0,0 +1,21 @@ +// Copyright (c) 2020 +// Author: Hugo Pointcheval +import 'package:flutter/material.dart'; + +class KemPage extends StatefulWidget { + KemPage({Key key}) : super(key: key); + + @override + _KemPageState createState() => _KemPageState(); +} + +class _KemPageState extends State { + @override + Widget build(BuildContext context) { + return Container( + child: Center( + child: Text("Not implemented."), + ), + ); + } +} diff --git a/example/lib/session.dart b/example/lib/session.dart new file mode 100644 index 0000000..4f68f87 --- /dev/null +++ b/example/lib/session.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2020 +// Author: Hugo Pointcheval + +import 'package:native_crypto/native_crypto.dart'; + +class Session { + static SecretKey secretKey; + static AESCipher aesCipher; +} diff --git a/example/lib/utils.dart b/example/lib/utils.dart new file mode 100644 index 0000000..3f03f32 --- /dev/null +++ b/example/lib/utils.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2020 +// Author: Hugo Pointcheval +import 'dart:typed_data'; +import 'dart:convert'; + +/// Contains some useful functions. +class TypeHelper { + /// Returns bytes [Uint8List] from a [String]. + static Uint8List stringToBytes(String source) { + var list = source.runes.toList(); + var bytes = Uint8List.fromList(list); + return bytes; + } + + /// Returns a [String] from bytes [Uint8List]. + static String bytesToString(Uint8List bytes) { + var string = String.fromCharCodes(bytes); + return string; + } + + /// Returns a `base64` [String] from bytes [Uint8List]. + static String bytesToBase64(Uint8List bytes) { + return base64.encode(bytes); + } + + /// Returns a [Uint8List] from a `base64` [String]. + static Uint8List base64ToBytes(String encoded) { + return base64.decode(encoded); + } +} diff --git a/example/lib/widgets/button.dart b/example/lib/widgets/button.dart new file mode 100644 index 0000000..27c6db7 --- /dev/null +++ b/example/lib/widgets/button.dart @@ -0,0 +1,24 @@ +// Copyright (c) 2020 +// Author: Hugo Pointcheval +import 'package:flutter/material.dart'; + +class Button extends StatelessWidget { + const Button({Key key, this.onPressed, this.label}) : super(key: key); + + final void Function() onPressed; + final String label; + + @override + Widget build(BuildContext context) { + return Container( + child: FlatButton( + onPressed: onPressed, + color: Colors.blue, + child: Text( + label, + style: TextStyle(color: Colors.white), + ), + ), + ); + } +} diff --git a/example/lib/widgets/output.dart b/example/lib/widgets/output.dart new file mode 100644 index 0000000..a36123e --- /dev/null +++ b/example/lib/widgets/output.dart @@ -0,0 +1,50 @@ +// Copyright (c) 2020 +// Author: Hugo Pointcheval +import 'package:flutter/material.dart'; + +class Output extends StatelessWidget { + const Output( + {Key key, + this.textEditingController, + this.large: false, + this.editable: false}) + : super(key: key); + + final TextEditingController textEditingController; + final bool large; + final bool editable; + + void print(String message) { + textEditingController.text = message; + } + + void append(String message) { + textEditingController.text += message; + } + + void appendln(String message) { + textEditingController.text += message + "\n"; + } + + void clear() { + textEditingController.clear(); + } + + String read() { + return textEditingController.text; + } + + @override + Widget build(BuildContext context) { + return Container( + child: TextField( + enableInteractiveSelection: true, + readOnly: editable ? false : true, + minLines: large ? 3 : 1, + maxLines: large ? 50 : 5, + decoration: InputDecoration(border: OutlineInputBorder()), + controller: textEditingController, + ), + ); + } +}