From 76609f6cf1799823d79c886fd0f4cf14fc4c89f9 Mon Sep 17 00:00:00 2001 From: alexvasl Date: Thu, 18 May 2023 02:16:20 +0300 Subject: [PATCH 1/8] Added Login Screen --- lib/models/keys.dart | 4 + lib/pages/home_screen/home_screen_widget.dart | 2 +- lib/pages/login_screen/login_screen.dart | 153 ++++++++++++++++++ lib/pages/main_screen/main_screen_widget.dart | 6 + lib/pages/profile_screen/profile_screen.dart | 22 +-- .../widgets/key_exist_dialog.dart | 2 +- .../widgets/ok_button_widget.dart | 2 +- pubspec.yaml | 2 +- 8 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 lib/pages/login_screen/login_screen.dart diff --git a/lib/models/keys.dart b/lib/models/keys.dart index 85335ea..fd23c7f 100644 --- a/lib/models/keys.dart +++ b/lib/models/keys.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:nostr_tools/nostr_tools.dart'; class Keys { @@ -11,3 +12,6 @@ class Keys { class Relay { static final relay = RelayApi(relayUrl: 'wss://relay.damus.io'); } + +final keyController = TextEditingController(); +final formKey = GlobalKey(); diff --git a/lib/pages/home_screen/home_screen_widget.dart b/lib/pages/home_screen/home_screen_widget.dart index 175f735..e9e7a31 100644 --- a/lib/pages/home_screen/home_screen_widget.dart +++ b/lib/pages/home_screen/home_screen_widget.dart @@ -372,7 +372,7 @@ class _CreatePostState extends State { padding: const EdgeInsets.symmetric(vertical: 24), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: AppColors.mainDarkBlue, + color: AppColors.background, ), child: const Center( child: Text( diff --git a/lib/pages/login_screen/login_screen.dart b/lib/pages/login_screen/login_screen.dart new file mode 100644 index 0000000..0fc0794 --- /dev/null +++ b/lib/pages/login_screen/login_screen.dart @@ -0,0 +1,153 @@ +import 'package:dart_nostr/dart_nostr.dart'; +import 'package:drifter/models/keys.dart'; +import 'package:drifter/pages/home_screen/widgets/message_text_button_widget.dart'; + +import 'package:drifter/pages/profile_screen/widgets/keys_option_modal_bottom_sheet.dart'; +import 'package:drifter/pages/profile_screen/widgets/message_snack_bar.dart'; +import 'package:drifter/pages/profile_screen/widgets/ok_button_widget.dart'; + +import 'package:drifter/theme/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:nostr_tools/nostr_tools.dart'; + +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + final _secureStorage = const FlutterSecureStorage(); + + Future addKeyToStorage( + String privateKeyHex, + String publicKeyHex, + String nsecKey, + String npubKey, + ) async { +// Waiting for both write operations to complete + Future.wait([ + _secureStorage.write(key: 'privateKey', value: privateKeyHex), + _secureStorage.write(key: 'publicKey', value: privateKeyHex), + _secureStorage.write(key: 'nsec', value: nsecKey), + _secureStorage.write(key: 'npub', value: npubKey), + ]); + + // Updating status variables and starting widget rebuilding + +// Returns a boolean value indicating whether the keys were successfully added to the repository or not. + return Keys.keysExist; + } + + @override + Widget build(BuildContext context) { + return ListView( + children: [ + SizedBox( + height: 200, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const Text( + 'Enter your private key', + style: TextStyle( + fontSize: 20, + ), + ), + const SizedBox(height: 30), + TextFormField( + decoration: const InputDecoration( + labelText: 'Enter nsec or hex', + border: OutlineInputBorder(), + ), + maxLength: 64, + controller: keyController, + key: formKey, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your private key'; + } + try { + bool isValidHexKey = + Nostr.instance.keysService.isValidPrivateKey(value); + bool isValidNSec = value.trim().startsWith('nsec') && + Nostr.instance.keysService.isValidPrivateKey( + NostrClientUtils.hexEncode(value)['data']); + if (!(isValidHexKey || isValidNSec)) { + return 'Your private key is not valid.'; + } + } on ChecksumVerificationException catch (e) { + return e.message; + } catch (e) { + return 'Error: $e'; + } + return null; + }), + ], + ), + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(AppColors.background)), + onPressed: () { + if (formKey.currentState!.validate()) { + // Он получает значение закрытого ключа из _keyController текстового поля и присваивает его переменной privateKeyHex. + String privateKeyHex = keyController.text.trim(); + String publicKeyHex; + String nsecKey; + String npubKey; + + // Он проверяет, начинается ли строка privateKeyHex со строки « nsec », что указывает на то, что она может быть в формате NIP-19. Если это так, он декодирует метод privateKeyHex using _nip19.decode(privateKeyHex) для получения поля « данные », которое представляет фактический закрытый ключ в шестнадцатеричном формате. + if (privateKeyHex.startsWith('nsec')) { + nsecKey = privateKeyHex; + final decoded = Nostr.instance.keysService + .decodeNsecKeyToPrivateKey(privateKeyHex); + privateKeyHex = decoded; + publicKeyHex = Nostr.instance.keysService + .derivePublicKey(privateKey: 'privateKeyHex'); + npubKey = Nostr.instance.keysService + .encodePublicKeyToNpub(publicKeyHex); + } + // Если privateKeyHex не начинается с « nsec », это означает, что это обычный шестнадцатеричный закрытый ключ. + else { + publicKeyHex = Nostr.instance.keysService + .derivePublicKey(privateKey: 'privateKeyHex'); + nsecKey = Nostr.instance.keysService + .encodePrivateKeyToNsec(privateKeyHex); + npubKey = Nostr.instance.keysService + .encodePublicKeyToNpub(publicKeyHex); + } + + // Затем он вызывает _addKeysToStorage метод для добавления закрытого ключа и открытого ключа в хранилище. Он прикрепляет then() к этому методу обратный вызов для обработки случая, когда ключи успешно добавлены в хранилище. + addKeyToStorage(privateKeyHex, publicKeyHex, nsecKey, npubKey) + .then((keysAdded) { + if (keysAdded) { + keyController.clear(); + ScaffoldMessenger.of(context).showSnackBar( + MessageSnackBar(label: 'Congratulations! Keys Stored!'), + ); + setState() { + Keys.privateKey = privateKeyHex; + Keys.publicKey = publicKeyHex; + Keys.nsecKey = nsecKey; + Keys.npubKey = npubKey; + Keys.keysExist = true; + } + } + }); + } else { + formKey.currentState?.setState(() {}); + } + }, + child: Text( + 'Login', + ), + ), + ) + ], + ); + } +} diff --git a/lib/pages/main_screen/main_screen_widget.dart b/lib/pages/main_screen/main_screen_widget.dart index 06c478c..abc87dd 100644 --- a/lib/pages/main_screen/main_screen_widget.dart +++ b/lib/pages/main_screen/main_screen_widget.dart @@ -1,5 +1,6 @@ // import 'package:drifter/pages/home_screen/home_screen_widget.dart'; import 'package:drifter/pages/home_screen/home_screen_widget.dart'; +import 'package:drifter/pages/login_screen/login_screen.dart'; import 'package:drifter/pages/message_screen/message_screen_widget.dart'; import 'package:drifter/pages/profile_screen/profile_screen.dart'; import 'package:drifter/theme/app_colors.dart'; @@ -58,6 +59,7 @@ class _MainScreenWidgetState extends State { HomeScreen(), MessageScreen(), ProfileScreen(), + LoginScreen(), ], ), bottomNavigationBar: BottomNavigationBar( @@ -75,6 +77,10 @@ class _MainScreenWidgetState extends State { icon: Icon(Icons.person), label: 'Profile', ), + BottomNavigationBarItem( + icon: Icon(Icons.login), + label: 'Login', + ), ], onTap: onSelectedtap, ), diff --git a/lib/pages/profile_screen/profile_screen.dart b/lib/pages/profile_screen/profile_screen.dart index a1eaf26..3f40f70 100644 --- a/lib/pages/profile_screen/profile_screen.dart +++ b/lib/pages/profile_screen/profile_screen.dart @@ -37,8 +37,8 @@ class ProfileScreenState extends State { final nsec = Nostr.instance.keysService.encodePrivateKeyToNsec(newPrivateKey); - final nsecDecoded = - Nostr.instance.keysService.decodeNsecKeyToPrivateKey(nsec); + // final nsecDecoded = + // Nostr.instance.keysService.decodeNsecKeyToPrivateKey(nsec); // assert(nsecDecoded['type'] == 'nsec'); // assert(nsecDecoded['data'] == newPrivateKey); @@ -47,12 +47,12 @@ class ProfileScreenState extends State { // final newPublicKey = keyGenerator.getPublicKey(newPrivateKey); final npub = Nostr.instance.keysService.encodePublicKeyToNpub(newPublicKey); - final npubDecoded = - Nostr.instance.keysService.decodeNpubKeyToPublicKey(npub); + // final npubDecoded = + // Nostr.instance.keysService.decodeNpubKeyToPublicKey(npub); // assert(npubDecoded['type'] == 'npub'); // assert(npubDecoded['data'] == newPublicKey); - return await _addKeyToStorage(newPrivateKey, newPublicKey, nsec, npub); + return await addKeyToStorage(newPrivateKey, newPublicKey, nsec, npub); } Future _getKeysFromStorage() async { @@ -80,7 +80,7 @@ class ProfileScreenState extends State { // Adding a new key // Writing a private and public key to a secure vault - Future _addKeyToStorage( + Future addKeyToStorage( String privateKeyHex, String publicKeyHex, String nsecKey, @@ -89,7 +89,7 @@ class ProfileScreenState extends State { // Waiting for both write operations to complete Future.wait([ _secureStorage.write(key: 'privateKey', value: privateKeyHex), - _secureStorage.write(key: 'publicKey', value: privateKeyHex), + _secureStorage.write(key: 'publicKey', value: publicKeyHex), _secureStorage.write(key: 'nsec', value: nsecKey), _secureStorage.write(key: 'npub', value: npubKey), ]); @@ -150,8 +150,8 @@ class ProfileScreenState extends State { Keys.keysExist ? ElevatedButton( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - AppColors.mainDarkBlue)), + backgroundColor: + MaterialStateProperty.all(AppColors.background)), onPressed: () { keysExistDialog( Nostr.instance.keysService @@ -166,8 +166,8 @@ class ProfileScreenState extends State { ) : ElevatedButton( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - AppColors.mainDarkBlue)), + backgroundColor: + MaterialStateProperty.all(AppColors.background)), onPressed: () { modalBottomSheet(); }, diff --git a/lib/pages/profile_screen/widgets/key_exist_dialog.dart b/lib/pages/profile_screen/widgets/key_exist_dialog.dart index d74e34f..84230b1 100644 --- a/lib/pages/profile_screen/widgets/key_exist_dialog.dart +++ b/lib/pages/profile_screen/widgets/key_exist_dialog.dart @@ -43,7 +43,7 @@ class _KeysExistDialogState extends State { padding: const EdgeInsets.symmetric(vertical: 24), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: AppColors.mainDarkBlue, + color: AppColors.background, ), child: const Center( child: Text( diff --git a/lib/pages/profile_screen/widgets/ok_button_widget.dart b/lib/pages/profile_screen/widgets/ok_button_widget.dart index 6dbaad7..ae7412b 100644 --- a/lib/pages/profile_screen/widgets/ok_button_widget.dart +++ b/lib/pages/profile_screen/widgets/ok_button_widget.dart @@ -16,7 +16,7 @@ class OkButton extends StatelessWidget { return ElevatedButton( onPressed: onPressed, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.mainDarkBlue, + backgroundColor: AppColors.background, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), diff --git a/pubspec.yaml b/pubspec.yaml index 68dea71..6b26206 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/ + - assets/images/logo/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see -- 2.45.2 From d25e5074f65910f44cc1fdaad2afccea1b6454c3 Mon Sep 17 00:00:00 2001 From: alexvasl Date: Sat, 20 May 2023 02:52:01 +0300 Subject: [PATCH 2/8] Changes to the LoginScreen, setting up the key entry. --- lib/pages/login_screen/login_screen.dart | 105 +++++++------- lib/pages/profile_screen/profile_screen.dart | 78 ++++++----- .../delete_keys_dialog.dart | 87 ------------ .../generated_keys.dart | 128 ------------------ .../key_exist_dialog.dart | 128 ------------------ .../keys_option_modal_bottom_sheet.dart | 34 ----- .../message_snack_bar.dart | 46 ------- .../ok_button_widget.dart | 34 ----- .../user_info_widget.dart | 97 ------------- 9 files changed, 95 insertions(+), 642 deletions(-) delete mode 100644 lib/pages/profile_screen/profile_screen_widgets/delete_keys_dialog.dart delete mode 100644 lib/pages/profile_screen/profile_screen_widgets/generated_keys.dart delete mode 100644 lib/pages/profile_screen/profile_screen_widgets/key_exist_dialog.dart delete mode 100644 lib/pages/profile_screen/profile_screen_widgets/keys_option_modal_bottom_sheet.dart delete mode 100644 lib/pages/profile_screen/profile_screen_widgets/message_snack_bar.dart delete mode 100644 lib/pages/profile_screen/profile_screen_widgets/ok_button_widget.dart delete mode 100644 lib/pages/profile_screen/profile_screen_widgets/user_info_widget.dart diff --git a/lib/pages/login_screen/login_screen.dart b/lib/pages/login_screen/login_screen.dart index 0fc0794..8f13ed8 100644 --- a/lib/pages/login_screen/login_screen.dart +++ b/lib/pages/login_screen/login_screen.dart @@ -1,6 +1,7 @@ import 'package:dart_nostr/dart_nostr.dart'; import 'package:drifter/models/keys.dart'; import 'package:drifter/pages/home_screen/widgets/message_text_button_widget.dart'; +import 'package:drifter/pages/profile_screen/profile_screen.dart'; import 'package:drifter/pages/profile_screen/widgets/keys_option_modal_bottom_sheet.dart'; import 'package:drifter/pages/profile_screen/widgets/message_snack_bar.dart'; @@ -11,30 +12,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:nostr_tools/nostr_tools.dart'; -class LoginScreen extends StatelessWidget { +class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); - final _secureStorage = const FlutterSecureStorage(); + @override + State createState() => LoginScreenState(); +} - Future addKeyToStorage( - String privateKeyHex, - String publicKeyHex, - String nsecKey, - String npubKey, - ) async { -// Waiting for both write operations to complete - Future.wait([ - _secureStorage.write(key: 'privateKey', value: privateKeyHex), - _secureStorage.write(key: 'publicKey', value: privateKeyHex), - _secureStorage.write(key: 'nsec', value: nsecKey), - _secureStorage.write(key: 'npub', value: npubKey), - ]); - - // Updating status variables and starting widget rebuilding - -// Returns a boolean value indicating whether the keys were successfully added to the repository or not. - return Keys.keysExist; - } +class LoginScreenState extends State { + final keyGenerator = KeyApi(); + final nip19 = Nip19(); @override Widget build(BuildContext context) { @@ -70,8 +57,9 @@ class LoginScreen extends StatelessWidget { bool isValidHexKey = Nostr.instance.keysService.isValidPrivateKey(value); bool isValidNSec = value.trim().startsWith('nsec') && - Nostr.instance.keysService.isValidPrivateKey( - NostrClientUtils.hexEncode(value)['data']); + Nostr.instance.keysService.isValidPrivateKey(Nostr + .instance.keysService + .decodeNsecKeyToPrivateKey(value)); if (!(isValidHexKey || isValidNSec)) { return 'Your private key is not valid.'; } @@ -92,59 +80,68 @@ class LoginScreen extends StatelessWidget { style: ButtonStyle( backgroundColor: MaterialStateProperty.all(AppColors.background)), + child: const Text( + 'Login', + ), onPressed: () { if (formKey.currentState!.validate()) { - // Он получает значение закрытого ключа из _keyController текстового поля и присваивает его переменной privateKeyHex. String privateKeyHex = keyController.text.trim(); String publicKeyHex; String nsecKey; String npubKey; - // Он проверяет, начинается ли строка privateKeyHex со строки « nsec », что указывает на то, что она может быть в формате NIP-19. Если это так, он декодирует метод privateKeyHex using _nip19.decode(privateKeyHex) для получения поля « данные », которое представляет фактический закрытый ключ в шестнадцатеричном формате. + // if (privateKeyHex.startsWith('nsec')) { + // nsecKey = privateKeyHex; + // final decoded = Nostr.instance.keysService + // .decodeNsecKeyToPrivateKey(privateKeyHex); + // privateKeyHex = decoded; + // publicKeyHex = Nostr.instance.keysService + // .derivePublicKey(privateKey: 'privateKeyHex'); + // npubKey = Nostr.instance.keysService + // .encodePublicKeyToNpub(publicKeyHex); + // } else { + // publicKeyHex = Nostr.instance.keysService + // .derivePublicKey(privateKey: 'privateKeyHex'); + // nsecKey = Nostr.instance.keysService + // .encodePrivateKeyToNsec(privateKeyHex); + // npubKey = Nostr.instance.keysService + // .encodePublicKeyToNpub(publicKeyHex); + // } + if (privateKeyHex.startsWith('nsec')) { - nsecKey = privateKeyHex; - final decoded = Nostr.instance.keysService - .decodeNsecKeyToPrivateKey(privateKeyHex); - privateKeyHex = decoded; - publicKeyHex = Nostr.instance.keysService - .derivePublicKey(privateKey: 'privateKeyHex'); - npubKey = Nostr.instance.keysService - .encodePublicKeyToNpub(publicKeyHex); - } - // Если privateKeyHex не начинается с « nsec », это означает, что это обычный шестнадцатеричный закрытый ключ. - else { - publicKeyHex = Nostr.instance.keysService - .derivePublicKey(privateKey: 'privateKeyHex'); - nsecKey = Nostr.instance.keysService - .encodePrivateKeyToNsec(privateKeyHex); - npubKey = Nostr.instance.keysService - .encodePublicKeyToNpub(publicKeyHex); + final decoded = nip19.decode(privateKeyHex); + privateKeyHex = decoded['data']; + publicKeyHex = keyGenerator.getPublicKey(privateKeyHex); + nsecKey = nip19.nsecEncode(privateKeyHex); + npubKey = nip19.npubEncode(publicKeyHex); + } else { + publicKeyHex = keyGenerator.getPublicKey(privateKeyHex); + nsecKey = nip19.nsecEncode(privateKeyHex); + npubKey = nip19.npubEncode(publicKeyHex); } - // Затем он вызывает _addKeysToStorage метод для добавления закрытого ключа и открытого ключа в хранилище. Он прикрепляет then() к этому методу обратный вызов для обработки случая, когда ключи успешно добавлены в хранилище. - addKeyToStorage(privateKeyHex, publicKeyHex, nsecKey, npubKey) + ProfileScreenState() + .addKeyToStorage( + privateKeyHex, publicKeyHex, nsecKey, npubKey) .then((keysAdded) { if (keysAdded) { keyController.clear(); ScaffoldMessenger.of(context).showSnackBar( MessageSnackBar(label: 'Congratulations! Keys Stored!'), ); - setState() { - Keys.privateKey = privateKeyHex; - Keys.publicKey = publicKeyHex; - Keys.nsecKey = nsecKey; - Keys.npubKey = npubKey; - Keys.keysExist = true; - } + // setState() { + // Keys.privateKey = privateKeyHex; + // Keys.publicKey = publicKeyHex; + // Keys.nsecKey = nsecKey; + // Keys.npubKey = npubKey; + // Keys.keysExist = true; + // } } }); } else { formKey.currentState?.setState(() {}); } }, - child: Text( - 'Login', - ), ), ) ], diff --git a/lib/pages/profile_screen/profile_screen.dart b/lib/pages/profile_screen/profile_screen.dart index 3f40f70..c8dc3c1 100644 --- a/lib/pages/profile_screen/profile_screen.dart +++ b/lib/pages/profile_screen/profile_screen.dart @@ -17,7 +17,8 @@ class ProfileScreen extends StatefulWidget { } class ProfileScreenState extends State { - final _secureStorage = const FlutterSecureStorage(); + final secureStorage = const FlutterSecureStorage(); + bool _toHex = false; TextEditingController privateKeyInput = TextEditingController(); TextEditingController publicKeyInput = TextEditingController(); @@ -37,6 +38,7 @@ class ProfileScreenState extends State { final nsec = Nostr.instance.keysService.encodePrivateKeyToNsec(newPrivateKey); + // final nsecDecoded = // Nostr.instance.keysService.decodeNsecKeyToPrivateKey(nsec); // assert(nsecDecoded['type'] == 'nsec'); @@ -47,6 +49,7 @@ class ProfileScreenState extends State { // final newPublicKey = keyGenerator.getPublicKey(newPrivateKey); final npub = Nostr.instance.keysService.encodePublicKeyToNpub(newPublicKey); + // final npubDecoded = // Nostr.instance.keysService.decodeNpubKeyToPublicKey(npub); // assert(npubDecoded['type'] == 'npub'); @@ -57,11 +60,11 @@ class ProfileScreenState extends State { Future _getKeysFromStorage() async { // Reading values associated with the " privateKey " and " publicKey " keys from a secure repository - final storedPrivateKey = await _secureStorage.read(key: 'privateKey'); - final storedPublicKey = await _secureStorage.read(key: 'publicKey'); + final storedPrivateKey = await secureStorage.read(key: 'privateKey'); + final storedPublicKey = await secureStorage.read(key: 'publicKey'); - final storedNsecKey = await _secureStorage.read(key: 'nsec'); - final storedNpubKey = await _secureStorage.read(key: 'npub'); + final storedNsecKey = await secureStorage.read(key: 'nsec'); + final storedNpubKey = await secureStorage.read(key: 'npub'); // Indicates that both private and public keys are stored in a secure repository, after which, the state variables are updated if (storedPrivateKey != null && @@ -88,10 +91,10 @@ class ProfileScreenState extends State { ) async { // Waiting for both write operations to complete Future.wait([ - _secureStorage.write(key: 'privateKey', value: privateKeyHex), - _secureStorage.write(key: 'publicKey', value: publicKeyHex), - _secureStorage.write(key: 'nsec', value: nsecKey), - _secureStorage.write(key: 'npub', value: npubKey), + secureStorage.write(key: 'privateKey', value: privateKeyHex), + secureStorage.write(key: 'publicKey', value: publicKeyHex), + secureStorage.write(key: 'nsec', value: nsecKey), + secureStorage.write(key: 'npub', value: npubKey), ]); // Updating status variables and starting widget rebuilding @@ -110,10 +113,10 @@ class ProfileScreenState extends State { Future _deleteKeysStorage() async { // Calling secure storage to remove keys from storage Future.wait([ - _secureStorage.delete(key: 'privateKey'), - _secureStorage.delete(key: 'publicKey'), - _secureStorage.delete(key: 'nsec'), - _secureStorage.delete(key: 'npub'), + secureStorage.delete(key: 'privateKey'), + secureStorage.delete(key: 'publicKey'), + secureStorage.delete(key: 'nsec'), + secureStorage.delete(key: 'npub'), ]); // Updating status variables, resetting values after deleting keys from the repository @@ -128,42 +131,49 @@ class ProfileScreenState extends State { @override Widget build(BuildContext context) { - privateKeyInput.text = Keys.nsecKey; - publicKeyInput.text = Keys.npubKey; + privateKeyInput.text = _toHex ? Keys.nsecKey : Keys.privateKey; + publicKeyInput.text = _toHex ? Keys.npubKey : Keys.publicKey; return ListView( children: [ - SizedBox( + const SizedBox( height: 60, ), - UserInfo(), - SizedBox( + const UserInfo(), + const SizedBox( height: 40, ), FormKeys(), - SizedBox(height: 20), + const SizedBox(height: 20), Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Keys.keysExist - ? ElevatedButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(AppColors.background)), + ? IconButton( onPressed: () { - keysExistDialog( - Nostr.instance.keysService - .encodePublicKeyToNpub(Keys.publicKey), - Nostr.instance.keysService - .encodePrivateKeyToNsec(Keys.privateKey), - ); + setState(() { + _toHex = !_toHex; + }); }, - child: Text( - 'Keys', - ), - ) + icon: const Icon(Icons.refresh)) + // ElevatedButton( + // style: ButtonStyle( + // backgroundColor: + // MaterialStateProperty.all(AppColors.background)), + // onPressed: () { + // keysExistDialog( + // Nostr.instance.keysService + // .encodePublicKeyToNpub(Keys.publicKey), + // Nostr.instance.keysService + // .encodePrivateKeyToNsec(Keys.privateKey), + // ); + // }, + // child: const Text( + // 'Keys', + // ), + // ) : ElevatedButton( style: ButtonStyle( backgroundColor: @@ -171,7 +181,7 @@ class ProfileScreenState extends State { onPressed: () { modalBottomSheet(); }, - child: Text( + child: const Text( 'Generate Keys', ), ), diff --git a/lib/pages/profile_screen/profile_screen_widgets/delete_keys_dialog.dart b/lib/pages/profile_screen/profile_screen_widgets/delete_keys_dialog.dart deleted file mode 100644 index 8e4874f..0000000 --- a/lib/pages/profile_screen/profile_screen_widgets/delete_keys_dialog.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:drifter/theme/app_colors.dart'; - -import 'ok_button_widget.dart'; - -class DeleteKeysDialog extends StatelessWidget { - const DeleteKeysDialog({ - super.key, - required this.onNoPressed, - required this.onYesPressed, - }); - - final void Function()? onNoPressed; - final void Function()? onYesPressed; - - @override - Widget build(BuildContext context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Container( - constraints: const BoxConstraints(maxWidth: 600), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 24), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.redAccent, - ), - child: const Center( - child: Text( - 'Delete Keys!', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Do you want to delete your keys?', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[700], - ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: onNoPressed, - child: Text( - 'On', - style: TextStyle(color: AppColors.mainDarkBlue), - ), - ), - OkButton( - onPressed: onYesPressed, - label: 'YES', - ), - ], - ), - ], - ), - ) - ], - ), - ), - ); - } -} diff --git a/lib/pages/profile_screen/profile_screen_widgets/generated_keys.dart b/lib/pages/profile_screen/profile_screen_widgets/generated_keys.dart deleted file mode 100644 index 4049374..0000000 --- a/lib/pages/profile_screen/profile_screen_widgets/generated_keys.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'ok_button_widget.dart'; - -class GeneratedKeys extends StatefulWidget { - const GeneratedKeys({ - super.key, - required this.npubEncoded, - required this.nsecEncoded, - required this.hexPriv, - required this.hexPub, - }); - - final String npubEncoded; - final String nsecEncoded; - final String hexPriv; - final String hexPub; - - @override - State createState() => _GeneratedKeysState(); -} - -class _GeneratedKeysState extends State { - bool _toHex = false; - - @override - Widget build(BuildContext context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Container( - constraints: const BoxConstraints(maxWidth: 600), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 24), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.indigo, - ), - child: const Center( - child: Text( - 'Keys', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Public Key', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[700], - ), - ), - const SizedBox(height: 12), - SelectableText( - _toHex ? widget.hexPub : widget.npubEncoded, - style: TextStyle( - fontSize: 16, - color: Colors.grey[800], - ), - ), - const SizedBox(height: 24), - Text( - 'Private Key', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[700], - ), - ), - const SizedBox(height: 12), - SelectableText( - _toHex ? widget.hexPriv : widget.nsecEncoded, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.redAccent, - ), - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment - .spaceBetween, // Changed to space between to create space for icon buttons - children: [ - IconButton( - onPressed: () { - setState(() { - _toHex = !_toHex; - }); - }, - icon: const Icon(Icons.autorenew_outlined), - color: Colors.grey[700], - ), - OkButton( - onPressed: () { - Navigator.pop(context); - }, - label: 'OK', - ), - ], - ) - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/profile_screen/profile_screen_widgets/key_exist_dialog.dart b/lib/pages/profile_screen/profile_screen_widgets/key_exist_dialog.dart deleted file mode 100644 index d74e34f..0000000 --- a/lib/pages/profile_screen/profile_screen_widgets/key_exist_dialog.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:drifter/theme/app_colors.dart'; - -import 'ok_button_widget.dart'; - -class KeysExistDialog extends StatefulWidget { - const KeysExistDialog({ - super.key, - required this.npubEncoded, - required this.nsecEncoded, - required this.hexPriv, - required this.hexPub, - }); - - final String npubEncoded; - final String nsecEncoded; - final String hexPriv; - final String hexPub; - - @override - State createState() => _KeysExistDialogState(); -} - -class _KeysExistDialogState extends State { - bool _toHex = false; - - @override - Widget build(BuildContext context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Container( - constraints: const BoxConstraints(maxWidth: 600), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 24), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: AppColors.mainDarkBlue, - ), - child: const Center( - child: Text( - 'Keys', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Public Key', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[700], - ), - ), - const SizedBox(height: 12), - SelectableText( - _toHex ? widget.hexPub : widget.npubEncoded, - style: TextStyle( - fontSize: 16, - color: Colors.grey[800], - ), - ), - const SizedBox(height: 24), - Text( - 'Private Key', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[700], - ), - ), - const SizedBox(height: 12), - SelectableText( - _toHex ? widget.hexPriv : widget.nsecEncoded, - style: TextStyle( - fontSize: 16, - color: Colors.grey[800], - ), - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment - .spaceBetween, // Changed to space between to create space for icon buttons - children: [ - IconButton( - onPressed: () { - setState(() { - _toHex = !_toHex; - }); - }, - icon: const Icon(Icons.autorenew_outlined), - color: Colors.grey[700], - ), - OkButton( - onPressed: () { - Navigator.pop(context); - }, - label: 'OK', - ), - ], - ) - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/profile_screen/profile_screen_widgets/keys_option_modal_bottom_sheet.dart b/lib/pages/profile_screen/profile_screen_widgets/keys_option_modal_bottom_sheet.dart deleted file mode 100644 index 25ad2e5..0000000 --- a/lib/pages/profile_screen/profile_screen_widgets/keys_option_modal_bottom_sheet.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:drifter/theme/app_colors.dart'; - -class KeysOptionModalBottomSheet extends StatelessWidget { - const KeysOptionModalBottomSheet({ - super.key, - required this.generateNewKeyPressed, - }); - - final void Function()? generateNewKeyPressed; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration(color: AppColors.mainDarkBlue), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ElevatedButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.white)), - onPressed: generateNewKeyPressed, - child: Text( - 'Generate New Key', - style: TextStyle(color: AppColors.mainDarkBlue), - ), - ), - const SizedBox(height: 10), - ], - ), - ); - } -} diff --git a/lib/pages/profile_screen/profile_screen_widgets/message_snack_bar.dart b/lib/pages/profile_screen/profile_screen_widgets/message_snack_bar.dart deleted file mode 100644 index d5c4fd1..0000000 --- a/lib/pages/profile_screen/profile_screen_widgets/message_snack_bar.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; - -class MessageSnackBar extends SnackBar { - MessageSnackBar({Key? key, required this.label, this.isWarning = false}) - : super( - key: key, - content: _GenericErrorSnackBarMessage( - label: label, - isWarning: isWarning, - ), - backgroundColor: isWarning! ? Colors.red : Colors.white, - elevation: 6.0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - behavior: SnackBarBehavior.fixed, - ); - - final String label; - final bool? isWarning; -} - -class _GenericErrorSnackBarMessage extends StatelessWidget { - const _GenericErrorSnackBarMessage({ - Key? key, - required this.label, - this.isWarning, - }) : super(key: key); - - final String label; - final bool? isWarning; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), - child: Text( - label, - style: TextStyle( - color: isWarning! ? Colors.white : Colors.black, - fontSize: 16.0, - ), - ), - ); - } -} diff --git a/lib/pages/profile_screen/profile_screen_widgets/ok_button_widget.dart b/lib/pages/profile_screen/profile_screen_widgets/ok_button_widget.dart deleted file mode 100644 index 6dbaad7..0000000 --- a/lib/pages/profile_screen/profile_screen_widgets/ok_button_widget.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:drifter/theme/app_colors.dart'; - -class OkButton extends StatelessWidget { - const OkButton({ - super.key, - required this.onPressed, - required this.label, - }); - - final void Function()? onPressed; - final String label; - - @override - Widget build(BuildContext context) { - return ElevatedButton( - onPressed: onPressed, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.mainDarkBlue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Text( - label, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ); - } -} diff --git a/lib/pages/profile_screen/profile_screen_widgets/user_info_widget.dart b/lib/pages/profile_screen/profile_screen_widgets/user_info_widget.dart deleted file mode 100644 index 53c47f4..0000000 --- a/lib/pages/profile_screen/profile_screen_widgets/user_info_widget.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:drifter/theme/app_colors.dart'; -import 'package:flutter/material.dart'; - -class UserInfo extends StatelessWidget { - const UserInfo({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - child: Column( - children: [ - AvatarWidget(), - SizedBox( - height: 10, - ), - UserNameWidget(), - ], - ), - ); - } -} - -class AvatarWidget extends StatelessWidget { - const AvatarWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black), - borderRadius: BorderRadius.all(Radius.circular(75))), - width: 150, - height: 150, - ); - } -} - -class UserNameWidget extends StatefulWidget { - const UserNameWidget({super.key}); - - @override - State createState() => _UserNameWidgetState(); -} - -class _UserNameWidgetState extends State { - late final TextEditingController messageController; - late final FocusNode messageFocusNode; - - @override - void initState() { - messageController = TextEditingController(); - messageFocusNode = FocusNode(); - - super.initState(); - } - - @override - void dispose() { - messageController.dispose(); - messageFocusNode.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(4.0), - child: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular( - 0.0, - ), - ), - child: TextField( - controller: messageController, - focusNode: messageFocusNode, - style: const TextStyle( - fontSize: 14, - ), - decoration: InputDecoration( - hintText: 'Username', - hintStyle: const TextStyle(fontSize: 14), - suffixIcon: IconButton( - icon: const Icon( - Icons.send, - color: AppColors.mainDarkBlue, - size: 30, - ), - onPressed: () {}, - ), - ), - ), - ), - ); - } -} -- 2.45.2 From a5fc25ddec810f97eb3d0fca929b5a5e48969598 Mon Sep 17 00:00:00 2001 From: alexvasl Date: Mon, 22 May 2023 23:04:20 +0300 Subject: [PATCH 3/8] Authorization for entering a private key has been set up. --- lib/main.dart | 6 +- lib/pages/login_screen/login_screen.dart | 243 ++++++++++-------- lib/pages/main_screen/main_screen_widget.dart | 11 +- lib/pages/profile_screen/profile_screen.dart | 39 ++- 4 files changed, 171 insertions(+), 128 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index b58b6a4..6992ff1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:drifter/pages/login_screen/login_screen.dart'; import 'package:drifter/pages/main_screen/main_screen_widget.dart'; import 'package:drifter/pages/splash_screen/splash_screen.dart'; import 'package:drifter/theme/app_colors.dart'; @@ -25,7 +26,10 @@ class MyApp extends StatelessWidget { unselectedItemColor: Colors.grey, ), ), - home: const Splash(), + routes: { + '/': (context) => const Splash(), + '/login': (context) => const LoginScreen(), + }, ); } } diff --git a/lib/pages/login_screen/login_screen.dart b/lib/pages/login_screen/login_screen.dart index 8f13ed8..61342db 100644 --- a/lib/pages/login_screen/login_screen.dart +++ b/lib/pages/login_screen/login_screen.dart @@ -8,8 +8,10 @@ import 'package:drifter/pages/profile_screen/widgets/message_snack_bar.dart'; import 'package:drifter/pages/profile_screen/widgets/ok_button_widget.dart'; import 'package:drifter/theme/app_colors.dart'; +import 'package:drifter/utilities/assets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:nostr_tools/nostr_tools.dart'; class LoginScreen extends StatefulWidget { @@ -25,126 +27,143 @@ class LoginScreenState extends State { @override Widget build(BuildContext context) { - return ListView( - children: [ - SizedBox( - height: 200, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( + return Scaffold( + appBar: AppBar( + title: Row( + // mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - 'Enter your private key', - style: TextStyle( - fontSize: 20, - ), + SvgPicture.asset( + Assets.svg.drifterIcon, + height: 30, + width: 30, + alignment: Alignment.centerLeft, + ), + const SizedBox( + width: 125, + ), + const Text( + "Login", + style: TextStyle( + color: AppColors.mainAccent, + ), + // textAlign: TextAlign.center, ), - const SizedBox(height: 30), - TextFormField( - decoration: const InputDecoration( - labelText: 'Enter nsec or hex', - border: OutlineInputBorder(), - ), - maxLength: 64, - controller: keyController, - key: formKey, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your private key'; - } - try { - bool isValidHexKey = - Nostr.instance.keysService.isValidPrivateKey(value); - bool isValidNSec = value.trim().startsWith('nsec') && - Nostr.instance.keysService.isValidPrivateKey(Nostr - .instance.keysService - .decodeNsecKeyToPrivateKey(value)); - if (!(isValidHexKey || isValidNSec)) { - return 'Your private key is not valid.'; - } - } on ChecksumVerificationException catch (e) { - return e.message; - } catch (e) { - return 'Error: $e'; - } - return null; - }), ], ), + centerTitle: true, ), - SizedBox(height: 20), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(AppColors.background)), - child: const Text( - 'Login', + body: ListView( + children: [ + const SizedBox( + height: 200, ), - onPressed: () { - if (formKey.currentState!.validate()) { - String privateKeyHex = keyController.text.trim(); - String publicKeyHex; - String nsecKey; - String npubKey; + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const Text( + 'Enter your private key', + style: TextStyle( + fontSize: 20, + ), + ), + const SizedBox(height: 30), + TextFormField( + decoration: const InputDecoration( + labelText: 'Enter nsec or hex', + border: OutlineInputBorder(), + ), + maxLength: 64, + controller: keyController, + key: formKey, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your private key'; + } + try { + bool isValidHexKey = Nostr.instance.keysService + .isValidPrivateKey(value); + bool isValidNSec = value.trim().startsWith('nsec') && + Nostr.instance.keysService.isValidPrivateKey(Nostr + .instance.keysService + .decodeNsecKeyToPrivateKey(value)); + if (!(isValidHexKey || isValidNSec)) { + return 'Your private key is not valid.'; + } + } on ChecksumVerificationException catch (e) { + return e.message; + } catch (e) { + return 'Error: $e'; + } + return null; + }), + ], + ), + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(AppColors.background)), + child: const Text( + 'Login', + ), + onPressed: () { + if (formKey.currentState!.validate()) { + String privateKeyHex = keyController.text.trim(); + String publicKeyHex; + String nsecKey; + String npubKey; - // if (privateKeyHex.startsWith('nsec')) { - // nsecKey = privateKeyHex; - // final decoded = Nostr.instance.keysService - // .decodeNsecKeyToPrivateKey(privateKeyHex); - // privateKeyHex = decoded; - // publicKeyHex = Nostr.instance.keysService - // .derivePublicKey(privateKey: 'privateKeyHex'); - // npubKey = Nostr.instance.keysService - // .encodePublicKeyToNpub(publicKeyHex); - // } else { - // publicKeyHex = Nostr.instance.keysService - // .derivePublicKey(privateKey: 'privateKeyHex'); - // nsecKey = Nostr.instance.keysService - // .encodePrivateKeyToNsec(privateKeyHex); - // npubKey = Nostr.instance.keysService - // .encodePublicKeyToNpub(publicKeyHex); - // } - - if (privateKeyHex.startsWith('nsec')) { - final decoded = nip19.decode(privateKeyHex); - privateKeyHex = decoded['data']; - publicKeyHex = keyGenerator.getPublicKey(privateKeyHex); - nsecKey = nip19.nsecEncode(privateKeyHex); - npubKey = nip19.npubEncode(publicKeyHex); - } else { - publicKeyHex = keyGenerator.getPublicKey(privateKeyHex); - nsecKey = nip19.nsecEncode(privateKeyHex); - npubKey = nip19.npubEncode(publicKeyHex); - } - - ProfileScreenState() - .addKeyToStorage( - privateKeyHex, publicKeyHex, nsecKey, npubKey) - .then((keysAdded) { - if (keysAdded) { - keyController.clear(); - ScaffoldMessenger.of(context).showSnackBar( - MessageSnackBar(label: 'Congratulations! Keys Stored!'), - ); - // setState() { - // Keys.privateKey = privateKeyHex; - // Keys.publicKey = publicKeyHex; - // Keys.nsecKey = nsecKey; - // Keys.npubKey = npubKey; - // Keys.keysExist = true; + // if (privateKeyHex.startsWith('nsec')) { + // nsecKey = privateKeyHex; + // final decoded = Nostr.instance.keysService + // .decodeNsecKeyToPrivateKey(privateKeyHex); + // privateKeyHex = decoded; + // publicKeyHex = Nostr.instance.keysService + // .derivePublicKey(privateKey: 'privateKeyHex'); + // npubKey = Nostr.instance.keysService + // .encodePublicKeyToNpub(publicKeyHex); + // } else { + // publicKeyHex = Nostr.instance.keysService + // .derivePublicKey(privateKey: 'privateKeyHex'); + // nsecKey = Nostr.instance.keysService + // .encodePrivateKeyToNsec(privateKeyHex); + // npubKey = Nostr.instance.keysService + // .encodePublicKeyToNpub(publicKeyHex); // } + + if (privateKeyHex.startsWith('nsec')) { + final decoded = nip19.decode(privateKeyHex); + privateKeyHex = decoded['data']; + publicKeyHex = keyGenerator.getPublicKey(privateKeyHex); + nsecKey = nip19.nsecEncode(privateKeyHex); + npubKey = nip19.npubEncode(publicKeyHex); + } else { + publicKeyHex = keyGenerator.getPublicKey(privateKeyHex); + nsecKey = nip19.nsecEncode(privateKeyHex); + npubKey = nip19.npubEncode(publicKeyHex); + } + try { + ProfileScreenState().addKeyToStorage( + privateKeyHex, publicKeyHex, nsecKey, npubKey); + + keyController.clear(); + ScaffoldMessenger.of(context).showSnackBar( + MessageSnackBar(label: 'Congratulations! Keys Stored!'), + ); + + Navigator.of(context).pop(true); + } catch (e) {} + } else { + formKey.currentState?.setState(() {}); } - }); - } else { - formKey.currentState?.setState(() {}); - } - }, - ), - ) - ], - ); + }, + ), + ) + ], + )); } } diff --git a/lib/pages/main_screen/main_screen_widget.dart b/lib/pages/main_screen/main_screen_widget.dart index abc87dd..d7ebc7f 100644 --- a/lib/pages/main_screen/main_screen_widget.dart +++ b/lib/pages/main_screen/main_screen_widget.dart @@ -4,6 +4,7 @@ import 'package:drifter/pages/login_screen/login_screen.dart'; import 'package:drifter/pages/message_screen/message_screen_widget.dart'; import 'package:drifter/pages/profile_screen/profile_screen.dart'; import 'package:drifter/theme/app_colors.dart'; +import 'package:drifter/main.dart'; import 'package:drifter/utilities/assets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -59,7 +60,7 @@ class _MainScreenWidgetState extends State { HomeScreen(), MessageScreen(), ProfileScreen(), - LoginScreen(), + // LoginScreen(), ], ), bottomNavigationBar: BottomNavigationBar( @@ -77,10 +78,10 @@ class _MainScreenWidgetState extends State { icon: Icon(Icons.person), label: 'Profile', ), - BottomNavigationBarItem( - icon: Icon(Icons.login), - label: 'Login', - ), + // BottomNavigationBarItem( + // icon: Icon(Icons.login), + // label: 'Login', + // ), ], onTap: onSelectedtap, ), diff --git a/lib/pages/profile_screen/profile_screen.dart b/lib/pages/profile_screen/profile_screen.dart index c8dc3c1..eaf1743 100644 --- a/lib/pages/profile_screen/profile_screen.dart +++ b/lib/pages/profile_screen/profile_screen.dart @@ -1,6 +1,7 @@ import 'package:dart_nostr/dart_nostr.dart'; import 'package:drifter/models/keys.dart'; import 'package:drifter/pages/profile_screen/widgets/delete_keys_dialog.dart'; +import 'package:drifter/main.dart'; import 'package:drifter/pages/profile_screen/widgets/key_exist_dialog.dart'; import 'package:drifter/pages/profile_screen/widgets/keys_option_modal_bottom_sheet.dart'; import 'package:drifter/pages/profile_screen/widgets/message_snack_bar.dart'; @@ -174,16 +175,34 @@ class ProfileScreenState extends State { // 'Keys', // ), // ) - : ElevatedButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(AppColors.background)), - onPressed: () { - modalBottomSheet(); - }, - child: const Text( - 'Generate Keys', - ), + : Row( + children: [ + ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + AppColors.background)), + onPressed: () { + modalBottomSheet(); + }, + child: const Text( + 'Generate Keys', + ), + ), + SizedBox(width: 100), + ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + AppColors.background)), + onPressed: () { + Navigator.pushNamed(context, '/login').then((_) { + initState(); + }); + }, + child: const Text( + 'Login', + ), + ), + ], ), Keys.keysExist ? Row( -- 2.45.2 From c7cfb392bafe5511e432dfbc03bd7d8dcd7f62bb Mon Sep 17 00:00:00 2001 From: alexvasl Date: Thu, 25 May 2023 16:07:26 +0300 Subject: [PATCH 4/8] Added screens Welcome, Terms of Service, Login --- assets/images/logo/welcome.png | Bin 0 -> 3973 bytes assets/images/logo/welcome.svg | 10 + lib/main.dart | 7 +- lib/pages/login_screen/login_screen.dart | 304 ++++++++++-------- .../terms_of_service/button_continue.dart | 52 +++ .../terms_of_service/terms_of_service.dart | 90 ++++++ .../terms_of_service_text.dart | 23 ++ lib/pages/welcome_screen/welcome_screen.dart | 73 +++++ lib/theme/app_colors.dart | 35 +- lib/utilities/assets.dart | 1 + pubspec.lock | 16 + pubspec.yaml | 5 +- 12 files changed, 482 insertions(+), 134 deletions(-) create mode 100644 assets/images/logo/welcome.png create mode 100644 assets/images/logo/welcome.svg create mode 100644 lib/pages/terms_of_service/button_continue.dart create mode 100644 lib/pages/terms_of_service/terms_of_service.dart create mode 100644 lib/pages/terms_of_service/terms_of_service_text.dart create mode 100644 lib/pages/welcome_screen/welcome_screen.dart diff --git a/assets/images/logo/welcome.png b/assets/images/logo/welcome.png new file mode 100644 index 0000000000000000000000000000000000000000..612617857d065596e4c0563c9f4210aa05444b7e GIT binary patch literal 3973 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 literal 0 HcmV?d00001 diff --git a/assets/images/logo/welcome.svg b/assets/images/logo/welcome.svg new file mode 100644 index 0000000..c38bd33 --- /dev/null +++ b/assets/images/logo/welcome.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/main.dart b/lib/main.dart index 6992ff1..f081039 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:drifter/pages/login_screen/login_screen.dart'; import 'package:drifter/pages/main_screen/main_screen_widget.dart'; import 'package:drifter/pages/splash_screen/splash_screen.dart'; +import 'package:drifter/pages/terms_of_service/terms_of_service.dart'; +import 'package:drifter/pages/welcome_screen/welcome_screen.dart'; import 'package:drifter/theme/app_colors.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -27,8 +29,11 @@ class MyApp extends StatelessWidget { ), ), routes: { - '/': (context) => const Splash(), + '/': (context) => const WelcomeScreen() + // Splash() + , '/login': (context) => const LoginScreen(), + '/terms': (context) => const TermsOfServiceScreen(), }, ); } diff --git a/lib/pages/login_screen/login_screen.dart b/lib/pages/login_screen/login_screen.dart index 61342db..b884f8b 100644 --- a/lib/pages/login_screen/login_screen.dart +++ b/lib/pages/login_screen/login_screen.dart @@ -6,6 +6,7 @@ import 'package:drifter/pages/profile_screen/profile_screen.dart'; import 'package:drifter/pages/profile_screen/widgets/keys_option_modal_bottom_sheet.dart'; import 'package:drifter/pages/profile_screen/widgets/message_snack_bar.dart'; import 'package:drifter/pages/profile_screen/widgets/ok_button_widget.dart'; +import 'package:drifter/pages/terms_of_service/button_continue.dart'; import 'package:drifter/theme/app_colors.dart'; import 'package:drifter/utilities/assets.dart'; @@ -13,6 +14,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; import 'package:nostr_tools/nostr_tools.dart'; +import 'package:sliding_switch/sliding_switch.dart'; +import 'package:toggle_switch/toggle_switch.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -28,142 +31,187 @@ class LoginScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Row( - // mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.svg.drifterIcon, - height: 30, - width: 30, - alignment: Alignment.centerLeft, - ), - const SizedBox( - width: 125, - ), - const Text( - "Login", - style: TextStyle( - color: AppColors.mainAccent, - ), - // textAlign: TextAlign.center, - ), - ], + appBar: AppBar( + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: AppColors.topNavIconBack, ), - centerTitle: true, + onPressed: () => Navigator.of(context).pop(), ), - body: ListView( + elevation: 0, + backgroundColor: AppColors.mainBackground, + ), + backgroundColor: AppColors.mainBackground, + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( children: [ const SizedBox( - height: 200, + height: 56, ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const Text( - 'Enter your private key', - style: TextStyle( - fontSize: 20, - ), - ), - const SizedBox(height: 30), - TextFormField( - decoration: const InputDecoration( - labelText: 'Enter nsec or hex', - border: OutlineInputBorder(), - ), - maxLength: 64, - controller: keyController, - key: formKey, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your private key'; - } - try { - bool isValidHexKey = Nostr.instance.keysService - .isValidPrivateKey(value); - bool isValidNSec = value.trim().startsWith('nsec') && - Nostr.instance.keysService.isValidPrivateKey(Nostr - .instance.keysService - .decodeNsecKeyToPrivateKey(value)); - if (!(isValidHexKey || isValidNSec)) { - return 'Your private key is not valid.'; - } - } on ChecksumVerificationException catch (e) { - return e.message; - } catch (e) { - return 'Error: $e'; - } - return null; - }), - ], - ), - ), - SizedBox(height: 20), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(AppColors.background)), - child: const Text( - 'Login', + ToggleSwitch( + minWidth: double.infinity, + minHeight: 56, + totalSwitches: 2, + labels: ['Login', 'View only'], + activeBgColor: [AppColors.ToggleSwitchActiveBg], + activeFgColor: AppColors.ToggleSwitchTextActive, + inactiveBgColor: AppColors.ToggleSwitchBg, + inactiveFgColor: AppColors.ToggleSwitchTextInactive, + activeBorders: [ + Border.all( + color: AppColors.ToggleSwitchBg, + width: 4, ), - onPressed: () { - if (formKey.currentState!.validate()) { - String privateKeyHex = keyController.text.trim(); - String publicKeyHex; - String nsecKey; - String npubKey; - - // if (privateKeyHex.startsWith('nsec')) { - // nsecKey = privateKeyHex; - // final decoded = Nostr.instance.keysService - // .decodeNsecKeyToPrivateKey(privateKeyHex); - // privateKeyHex = decoded; - // publicKeyHex = Nostr.instance.keysService - // .derivePublicKey(privateKey: 'privateKeyHex'); - // npubKey = Nostr.instance.keysService - // .encodePublicKeyToNpub(publicKeyHex); - // } else { - // publicKeyHex = Nostr.instance.keysService - // .derivePublicKey(privateKey: 'privateKeyHex'); - // nsecKey = Nostr.instance.keysService - // .encodePrivateKeyToNsec(privateKeyHex); - // npubKey = Nostr.instance.keysService - // .encodePublicKeyToNpub(publicKeyHex); - // } - - if (privateKeyHex.startsWith('nsec')) { - final decoded = nip19.decode(privateKeyHex); - privateKeyHex = decoded['data']; - publicKeyHex = keyGenerator.getPublicKey(privateKeyHex); - nsecKey = nip19.nsecEncode(privateKeyHex); - npubKey = nip19.npubEncode(publicKeyHex); - } else { - publicKeyHex = keyGenerator.getPublicKey(privateKeyHex); - nsecKey = nip19.nsecEncode(privateKeyHex); - npubKey = nip19.npubEncode(publicKeyHex); - } - try { - ProfileScreenState().addKeyToStorage( - privateKeyHex, publicKeyHex, nsecKey, npubKey); - - keyController.clear(); - ScaffoldMessenger.of(context).showSnackBar( - MessageSnackBar(label: 'Congratulations! Keys Stored!'), - ); - - Navigator.of(context).pop(true); - } catch (e) {} - } else { - formKey.currentState?.setState(() {}); + ], + radiusStyle: true, + cornerRadius: 8, + customTextStyles: [ + TextStyle(fontSize: 14, fontWeight: FontWeight.w600) + ], + onToggle: (index) {}, + ), + SizedBox( + height: 56, + ), + const Text( + 'Login', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 13), + const Text( + 'Paste your password (AKA Private Key)', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 24), + TextFormField( + decoration: const InputDecoration( + labelText: 'nsec... / hex...', + border: OutlineInputBorder(), + ), + controller: keyController, + key: formKey, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your private key'; } - }, + try { + bool isValidHexKey = + Nostr.instance.keysService.isValidPrivateKey(value); + bool isValidNSec = value.trim().startsWith('nsec') && + Nostr.instance.keysService.isValidPrivateKey(Nostr + .instance.keysService + .decodeNsecKeyToPrivateKey(value)); + if (!(isValidHexKey || isValidNSec)) { + return 'Your private key is not valid.'; + } + } on ChecksumVerificationException catch (e) { + return e.message; + } catch (e) { + return 'Error: $e'; + } + return null; + }), + const SizedBox(height: 10), + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.white, ), - ) + height: 168, + child: Note.note, + ), + Expanded(child: SizedBox()), + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: () { + if (formKey.currentState!.validate()) { + String privateKeyHex = keyController.text.trim(); + String publicKeyHex; + String nsecKey; + String npubKey; + + // if (privateKeyHex.startsWith('nsec')) { + // nsecKey = privateKeyHex; + // final decoded = Nostr.instance.keysService + // .decodeNsecKeyToPrivateKey(privateKeyHex); + // privateKeyHex = decoded; + // publicKeyHex = Nostr.instance.keysService + // .derivePublicKey(privateKey: 'privateKeyHex'); + // npubKey = Nostr.instance.keysService + // .encodePublicKeyToNpub(publicKeyHex); + // } else { + // publicKeyHex = Nostr.instance.keysService + // .derivePublicKey(privateKey: 'privateKeyHex'); + // nsecKey = Nostr.instance.keysService + // .encodePrivateKeyToNsec(privateKeyHex); + // npubKey = Nostr.instance.keysService + // .encodePublicKeyToNpub(publicKeyHex); + // } + + if (privateKeyHex.startsWith('nsec')) { + final decoded = nip19.decode(privateKeyHex); + privateKeyHex = decoded['data']; + publicKeyHex = keyGenerator.getPublicKey(privateKeyHex); + nsecKey = nip19.nsecEncode(privateKeyHex); + npubKey = nip19.npubEncode(publicKeyHex); + } else { + publicKeyHex = keyGenerator.getPublicKey(privateKeyHex); + nsecKey = nip19.nsecEncode(privateKeyHex); + npubKey = nip19.npubEncode(publicKeyHex); + } + try { + ProfileScreenState().addKeyToStorage( + privateKeyHex, publicKeyHex, nsecKey, npubKey); + + keyController.clear(); + ScaffoldMessenger.of(context).showSnackBar( + MessageSnackBar( + label: 'Congratulations! Keys Stored!'), + ); + + Navigator.of(context).pop(true); + } catch (e) {} + } else { + formKey.currentState?.setState(() {}); + } + }, + child: Text( + 'Continue', + style: TextStyle(fontSize: 16), + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100))), + backgroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDefaultBg), + foregroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDefaultText)), + )), ], - )); + ), + ), + ); } } + +abstract class Note { + static final note = Text.rich( + TextSpan( + style: TextStyle( + height: 1.6, + fontSize: 10, + fontWeight: FontWeight.w400, + ), + children: [ + TextSpan( + text: + "Your private key starts with “nsec” or “hex” and gives your full access to your account. That means, if you log in using your private key, you will be able to make posts and send and receive private messages.\n\n" + "Your public key starts with “npub” and gives your view-only access to account. If you log in using your public key, you won’t be able to make posts or access private messages, but you will be able to view your feed. Go to “View only” tab to log in via your public key.") + ]), + ); +} diff --git a/lib/pages/terms_of_service/button_continue.dart b/lib/pages/terms_of_service/button_continue.dart new file mode 100644 index 0000000..1bda815 --- /dev/null +++ b/lib/pages/terms_of_service/button_continue.dart @@ -0,0 +1,52 @@ +import 'package:drifter/theme/app_colors.dart'; +import 'package:flutter/material.dart'; + +class DisabledElevatedButton extends StatelessWidget { + const DisabledElevatedButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: null, + child: Text( + 'Continue', + style: TextStyle(fontSize: 16), + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + backgroundColor: + const MaterialStatePropertyAll(AppColors.buttonPrimaryDisabledBg), + foregroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDisabledText)), + ); + } +} + +class ActiveElevatedButton extends StatelessWidget { + const ActiveElevatedButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: () { + Navigator.pushNamed(context, '/login'); + }, + child: Text( + 'Continue', + style: TextStyle(fontSize: 16), + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + backgroundColor: + const MaterialStatePropertyAll(AppColors.buttonPrimaryDefaultBg), + foregroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDefaultText)), + ); + } +} diff --git a/lib/pages/terms_of_service/terms_of_service.dart b/lib/pages/terms_of_service/terms_of_service.dart new file mode 100644 index 0000000..9488246 --- /dev/null +++ b/lib/pages/terms_of_service/terms_of_service.dart @@ -0,0 +1,90 @@ +import 'package:drifter/pages/terms_of_service/button_continue.dart'; +import 'package:drifter/pages/terms_of_service/terms_of_service_text.dart'; +import 'package:drifter/theme/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; + +class TermsOfServiceScreen extends StatefulWidget { + const TermsOfServiceScreen({super.key}); + + @override + State createState() => _TermsOfServiceScreenState(); +} + +class _TermsOfServiceScreenState extends State { + bool isChecked = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: AppColors.topNavIconBack, + ), + onPressed: () => Navigator.of(context).pop(), + ), + elevation: 0, + backgroundColor: AppColors.mainBackground, + ), + backgroundColor: AppColors.mainBackground, + body: Padding( + padding: const EdgeInsets.only(top: 10, right: 16, left: 16), + child: Center( + child: Column(children: [ + Text( + 'Terms of service', + style: TextStyle( + color: Color(0xFF302F38), + fontSize: 20, + fontWeight: FontWeight.w600), + ), + Padding( + padding: const EdgeInsets.only(top: 20, bottom: 14), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.white, + ), + height: 568, + child: ListView( + padding: EdgeInsets.all(16), + children: [TermsOfServiceText.termsOfService], + ), + ), + ), + Row( + children: [ + Checkbox( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4)), + checkColor: Colors.white, + activeColor: AppColors.checkboxCheckedBg, + value: isChecked, + onChanged: (bool? value) { + setState(() { + isChecked = value!; + }); + }, + ), + const Text( + 'I agree to the Terms of Service and Privacy Policy', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + ), + ], + ), + SizedBox( + width: double.infinity, + height: 56, + child: isChecked + ? const ActiveElevatedButton() + : const DisabledElevatedButton(), + ), + ]), + ), + ), + ); + } +} diff --git a/lib/pages/terms_of_service/terms_of_service_text.dart b/lib/pages/terms_of_service/terms_of_service_text.dart new file mode 100644 index 0000000..2716ccd --- /dev/null +++ b/lib/pages/terms_of_service/terms_of_service_text.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +abstract class TermsOfServiceText { + static final termsOfService = Text.rich( + TextSpan( + style: TextStyle( + fontSize: 14, + ), + children: [ + TextSpan( + text: 'We do not collect your data\n\n', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + TextSpan( + text: + "Effective as of Jan 20, 2023 for the distributed applications in the Play Store and the F-Droid Catalogue\n\n" + "The Amethyst app for Android does not collect or process any personal information from its users. The app is used to connect to third-party Nostr servers (also called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them.\n\n" + "Data from connected accounts is only stored locally on the device when it is required for functionality and performance of Amethyst. This data is strictly confidental and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app.\n\n" + "You cannot use the Amethyst app for Android to submit Objectionable Content to relays. Objectionable Content includes, but is not limited to: (i) sexually explicit materials; (ii) obscene, defamatory, libelous, slanderous, violent and/or unlawful content or profanity; (iii) content that infringes upon the rights of any third party, including copyright, trademark, privacy, publicity or other personal or proprietary right, or that is deceptive or fraudulent; (iv) content that promotes the use or sale of illegal or regulated substances, tobacco products, ammunition and/or firearms; and (v) illegal content related to gambling.\n\n" + "We reserve the right to modify this Privacy Policy at any time. Any modifications to this Privacy Policy will be effective upon our posting the new terms and/or upon implementation of the new changes on the Service (or as otherwise indicated at the time of posting). In all cases, your continued use of the app after the posting of any modified Privacy Policy indicates your acceptance of the terms of the modified Privacy Policy.\n\n" + "If you have any questions about Amethyst or this privacy policy, you can send a message to amethyst@vitorpamplona.com") + ]), + ); +} diff --git a/lib/pages/welcome_screen/welcome_screen.dart b/lib/pages/welcome_screen/welcome_screen.dart new file mode 100644 index 0000000..86d5c8c --- /dev/null +++ b/lib/pages/welcome_screen/welcome_screen.dart @@ -0,0 +1,73 @@ +import 'package:drifter/theme/app_colors.dart'; +import 'package:flutter/material.dart'; + +class WelcomeScreen extends StatelessWidget { + const WelcomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.mainBackground, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Image(image: AssetImage('assets/images/logo/welcome.png')), + SizedBox( + height: 270, + ), + Padding( + padding: const EdgeInsets.only(left: 16, right: 16), + child: SizedBox( + height: 56, + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pushNamed(context, '/terms'); + }, + child: Text( + 'Login', + style: TextStyle(fontSize: 16), + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100))), + backgroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDefaultBg), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + height: 56, + width: double.infinity, + child: OutlinedButton( + onPressed: () {}, + child: Text( + 'Create an account', + style: TextStyle(fontSize: 16), + ), + style: ButtonStyle( + side: const MaterialStatePropertyAll(BorderSide( + width: 2, + color: AppColors.buttonOutlinedDefaultBorder)), + shape: MaterialStatePropertyAll(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100))), + backgroundColor: const MaterialStatePropertyAll( + AppColors.mainBackground, + ), + foregroundColor: const MaterialStatePropertyAll( + AppColors.buttonOutlinedDefaultText), + overlayColor: const MaterialStatePropertyAll( + AppColors.buttonOutlinedPressedBg)), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart index f1b06ae..02b7df1 100644 --- a/lib/theme/app_colors.dart +++ b/lib/theme/app_colors.dart @@ -1,9 +1,38 @@ import 'package:flutter/material.dart'; abstract class AppColors { - static const background = const Color(0xFF4f46f1); - static const mainAccent = const Color(0xFFFFCC11); - static const mainBackground = const Color(0xFFF2EFFF); + static const background = Color(0xFF4f46f1); + static const mainAccent = Color(0xFFFFCC11); + static const mainBackground = Color(0xFFF2EFFF); + static const buttonPrimaryDefaultBg = Color(0xFF4F46F1); + static const buttonPrimaryDefaultText = Color(0xFFFFFFFF); + static const buttonPrimaryDisabledBg = Color(0x1A18171B); + static const buttonPrimaryDisabledText = Color(0xFFA7A7A8); + static const buttonOutlinedDefaultBorder = Color(0xFF4F46F1); + static const buttonOutlinedDefaultText = Color(0xFF4F46F1); + static const buttonOutlinedPressedBg = Color(0xFFE3E0F9); + +// TopNav + + static const topNavIconPtimary = Color(0xFF787680); + static const topNavText = Color(0xFF000000); + static const topNavIconBack = Color(0xFF4A40EC); + static const topNavIconBg = Color(0xFFE3E0F9); + +// Checkbox + static const checkboxCheckedIcon = Color(0xFFFFFFFF); + static const checkboxCheckedBg = Color(0xFF4A40EC); + static const checkboxEmptyBorder = Color(0xFF302F38); + static const checkboxTextLabel = Color(0xFF302F38); + static const checkboxDisabledBg = Color(0xFFB7B3F7); + static const checkboxDisabledIcon = Color(0xFFFFFFFF); + + // ToggleSwitch + + static const ToggleSwitchBg = Color(0xFFE2DFFF); + static const ToggleSwitchTextInactive = Color(0xFF837CA3); + static const ToggleSwitchActiveBg = Color(0xFFFCF8FF); + static const ToggleSwitchTextActive = Color(0xFF4F46F1); static const mainDarkBlue = Color.fromRGBO(3, 37, 65, 1); static const mainLightBlue = Color.fromRGBO(48, 86, 117, 1); diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 1a98636..bbd27be 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -13,4 +13,5 @@ class _SVG { const _SVG(); String get drifterIcon => "assets/images/logo/drifter_logo_circle.svg"; + String get welcomeImage => "assets/images/logo/welcome.svg"; } diff --git a/pubspec.lock b/pubspec.lock index 24ffa05..189d259 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -357,6 +357,14 @@ packages: description: flutter source: sdk version: "0.0.99" + sliding_switch: + dependency: "direct main" + description: + name: sliding_switch + sha256: "049a9582c9bc30913ce4e34eb26063b468acf5fe47a053636bbf950e5b180dc0" + url: "https://pub.dev" + source: hosted + version: "1.1.0" source_span: dependency: transitive description: @@ -405,6 +413,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.16" + toggle_switch: + dependency: "direct main" + description: + name: toggle_switch + sha256: "9e6af1f0c5a97d9de41109dc7b9e1b3bbe73417f89b10e0e44dc834fb493d4cb" + url: "https://pub.dev" + source: hosted + version: "2.1.0" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6b26206..3fe36b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,8 @@ dependencies: nostr_tools: ^1.0.7 flutter_secure_storage: ^8.0.0 flutter_svg: ^2.0.5 - + toggle_switch: ^2.1.0 + sliding_switch: ^1.1.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 @@ -63,7 +64,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/images/logo/ + - assets/images/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see -- 2.45.2 From 3a691ddda461e58ace87de9a18fd5f263bac5502 Mon Sep 17 00:00:00 2001 From: alexvasl Date: Mon, 29 May 2023 22:02:40 +0300 Subject: [PATCH 5/8] Corrections to the login screen --- lib.7z | Bin 0 -> 12761 bytes lib/pages/login_screen/login_screen.dart | 44 ++++++++++++++++++----- lib/theme/app_colors.dart | 23 +++++++++--- pubspec.yaml | 3 +- 4 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 lib.7z diff --git a/lib.7z b/lib.7z new file mode 100644 index 0000000000000000000000000000000000000000..89a64d6fe7866c36ff7a8b871ed4aff07274f0eb GIT binary patch literal 12761 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%0APNU~7r4^RS|{j`YW` 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 literal 0 HcmV?d00001 diff --git a/lib/pages/login_screen/login_screen.dart b/lib/pages/login_screen/login_screen.dart index b884f8b..47a6133 100644 --- a/lib/pages/login_screen/login_screen.dart +++ b/lib/pages/login_screen/login_screen.dart @@ -55,13 +55,13 @@ class LoginScreenState extends State { minHeight: 56, totalSwitches: 2, labels: ['Login', 'View only'], - activeBgColor: [AppColors.ToggleSwitchActiveBg], - activeFgColor: AppColors.ToggleSwitchTextActive, - inactiveBgColor: AppColors.ToggleSwitchBg, - inactiveFgColor: AppColors.ToggleSwitchTextInactive, + activeBgColor: [AppColors.toggleSwitchActiveBg], + activeFgColor: AppColors.toggleSwitchTextActive, + inactiveBgColor: AppColors.toggleSwitchBg, + inactiveFgColor: AppColors.toggleSwitchTextInactive, activeBorders: [ Border.all( - color: AppColors.ToggleSwitchBg, + color: AppColors.toggleSwitchBg, width: 4, ), ], @@ -86,10 +86,35 @@ class LoginScreenState extends State { ), const SizedBox(height: 24), TextFormField( - decoration: const InputDecoration( - labelText: 'nsec... / hex...', - border: OutlineInputBorder(), - ), + decoration: InputDecoration( + filled: true, + fillColor: AppColors.textFieldDefaultBg, + labelText: 'nsec... / hex...', + labelStyle: + TextStyle(color: AppColors.textFieldDefaultText), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.paste), + color: AppColors.textFieldActiveIconTrail, + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.qr_code_scanner), + color: AppColors.textFieldActiveIconTrail, + onPressed: () {}, + ), + ], + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), + iconColor: AppColors.textFieldDefaultIconTrail), controller: keyController, key: formKey, validator: (value) { @@ -209,6 +234,7 @@ abstract class Note { ), children: [ TextSpan( + style: TextStyle(color: AppColors.noteText), text: "Your private key starts with “nsec” or “hex” and gives your full access to your account. That means, if you log in using your private key, you will be able to make posts and send and receive private messages.\n\n" "Your public key starts with “npub” and gives your view-only access to account. If you log in using your public key, you won’t be able to make posts or access private messages, but you will be able to view your feed. Go to “View only” tab to log in via your public key.") diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart index 02b7df1..c0141ef 100644 --- a/lib/theme/app_colors.dart +++ b/lib/theme/app_colors.dart @@ -29,10 +29,25 @@ abstract class AppColors { // ToggleSwitch - static const ToggleSwitchBg = Color(0xFFE2DFFF); - static const ToggleSwitchTextInactive = Color(0xFF837CA3); - static const ToggleSwitchActiveBg = Color(0xFFFCF8FF); - static const ToggleSwitchTextActive = Color(0xFF4F46F1); + static const toggleSwitchBg = Color(0xFFE2DFFF); + static const toggleSwitchTextInactive = Color(0xFF837CA3); + static const toggleSwitchActiveBg = Color(0xFFFCF8FF); + static const toggleSwitchTextActive = Color(0xFF4F46F1); + +// TextField + + static const textFieldDefaultBg = Color(0xFFFCF8FF); + static const textFieldDefaultIconLead = Color(0xFFC9BFFF); + static const textFieldDefaultIconTrail = Color(0xFF5D55F2); + static const textFieldDefaultText = Color(0xFF837CA3); + + static const textFieldActiveBg = Color(0xFFFFFCFF); + static const textFieldActiveIconLead = Color(0xFF5D55F2); + static const textFieldActiveIconTrail = Color(0xFF5D55F2); + static const textFieldActiveText = Color(0xFF252229); + static const textFieldActiveLabel = Color(0xFF5D55F2); + + static const noteText = Color(0xFF787680); static const mainDarkBlue = Color.fromRGBO(3, 37, 65, 1); static const mainLightBlue = Color.fromRGBO(48, 86, 117, 1); diff --git a/pubspec.yaml b/pubspec.yaml index 3fe36b7..9911b8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,8 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/images/ + - assets/images/logo/welcome.png + # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see -- 2.45.2 From 6921e30ecdb68ae88e64cfa4953be4aa2e78d260 Mon Sep 17 00:00:00 2001 From: alexvasl Date: Tue, 30 May 2023 04:09:23 +0300 Subject: [PATCH 6/8] Added create an account screen --- lib/main.dart | 8 +- lib/models/models.dart | 24 +++ .../create_account_screen.dart | 164 ++++++++++++++++++ lib/pages/home_screen/home_screen_widget.dart | 16 +- lib/pages/login_screen/login_screen.dart | 15 +- lib/pages/main_screen/main_screen_widget.dart | 43 ++--- lib/pages/profile_screen/profile_screen.dart | 2 +- lib/pages/splash_screen/splash_screen.dart | 3 +- .../btn_continue_terms_of_service.dart | 55 ++++++ .../terms_of_service/terms_of_service.dart | 2 +- lib/pages/welcome_screen/welcome_screen.dart | 7 +- lib/theme/app_colors.dart | 21 +++ lib/widgets/btn_continue.dart | 52 ++++++ pubspec.yaml | 2 +- 14 files changed, 373 insertions(+), 41 deletions(-) create mode 100644 lib/models/models.dart create mode 100644 lib/pages/create_account_screen/create_account_screen.dart create mode 100644 lib/pages/terms_of_service/btn_continue_terms_of_service.dart create mode 100644 lib/widgets/btn_continue.dart diff --git a/lib/main.dart b/lib/main.dart index f081039..4e83a28 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:drifter/pages/create_account_screen/create_account_screen.dart'; +import 'package:drifter/pages/home_screen/home_screen_widget.dart'; import 'package:drifter/pages/login_screen/login_screen.dart'; import 'package:drifter/pages/main_screen/main_screen_widget.dart'; import 'package:drifter/pages/splash_screen/splash_screen.dart'; @@ -22,6 +24,8 @@ class MyApp extends StatelessWidget { title: 'Flutter Demo', theme: ThemeData( appBarTheme: const AppBarTheme(backgroundColor: AppColors.background), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom(foregroundColor: AppColors.background)), bottomNavigationBarTheme: const BottomNavigationBarThemeData( backgroundColor: AppColors.background, selectedItemColor: AppColors.mainAccent, @@ -29,11 +33,13 @@ class MyApp extends StatelessWidget { ), ), routes: { - '/': (context) => const WelcomeScreen() + '/': (context) => const Splash() // Splash() , '/login': (context) => const LoginScreen(), '/terms': (context) => const TermsOfServiceScreen(), + '/createAccount': (context) => const CreateAccountScreen(), + '/MainScreen': (context) => const MainScreenWidget(), }, ); } diff --git a/lib/models/models.dart b/lib/models/models.dart new file mode 100644 index 0000000..bda3f8b --- /dev/null +++ b/lib/models/models.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:nostr_tools/nostr_tools.dart'; + +class Keys { + static String privateKey = ''; + static String publicKey = ''; + static String nsecKey = ''; + static String npubKey = ''; + static bool keysExist = false; +} + +class Relay { + static final relay = RelayApi(relayUrl: 'wss://relay.damus.io'); +} + +final keyController = TextEditingController(); +final formKey = GlobalKey(); +final userNameController = TextEditingController(); +final nameController = TextEditingController(); +final descriptionController = TextEditingController(); + +class UserData { + static bool isLogin = true; +} diff --git a/lib/pages/create_account_screen/create_account_screen.dart b/lib/pages/create_account_screen/create_account_screen.dart new file mode 100644 index 0000000..c745baf --- /dev/null +++ b/lib/pages/create_account_screen/create_account_screen.dart @@ -0,0 +1,164 @@ +import 'package:dart_nostr/dart_nostr.dart'; +import 'package:drifter/models/models.dart'; +import 'package:drifter/pages/home_screen/widgets/message_text_button_widget.dart'; +import 'package:drifter/pages/profile_screen/profile_screen.dart'; + +import 'package:drifter/pages/profile_screen/widgets/keys_option_modal_bottom_sheet.dart'; +import 'package:drifter/pages/profile_screen/widgets/message_snack_bar.dart'; +import 'package:drifter/pages/profile_screen/widgets/ok_button_widget.dart'; +import 'package:drifter/widgets/btn_continue.dart'; +import 'package:drifter/theme/app_colors.dart'; +import 'package:drifter/utilities/assets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:nostr_tools/nostr_tools.dart'; +import 'package:sliding_switch/sliding_switch.dart'; +import 'package:toggle_switch/toggle_switch.dart'; + +class CreateAccountScreen extends StatefulWidget { + const CreateAccountScreen({super.key}); + + @override + State createState() => CreateAccountScreenState(); +} + +class CreateAccountScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: AppColors.topNavIconBack, + ), + onPressed: () => Navigator.of(context).pop(), + ), + actions: [ + TextButton( + onPressed: () {}, + child: Text('Skip'), + ) + ], + elevation: 0, + backgroundColor: AppColors.mainBackground, + ), + backgroundColor: AppColors.mainBackground, + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const Text( + 'Create an account', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 13), + const Text( + 'You can change this information later in your Profile.', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w400), + ), + const SizedBox(height: 24), + TextField( + decoration: InputDecoration( + filled: true, + fillColor: AppColors.textFieldDefaultBg, + labelText: 'Username', + labelStyle: TextStyle(color: AppColors.textFieldDefaultText), + prefixIcon: const Icon(Icons.alternate_email), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), + iconColor: AppColors.textFieldDefaultIconTrail), + controller: userNameController, + ), + const SizedBox(height: 8), + TextField( + decoration: InputDecoration( + filled: true, + fillColor: AppColors.textFieldDefaultBg, + labelText: 'Name (optional)', + labelStyle: TextStyle(color: AppColors.textFieldDefaultText), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), + iconColor: AppColors.textFieldDefaultIconTrail), + controller: nameController, + ), + const SizedBox(height: 8), + SizedBox( + height: 112, + child: TextField( + minLines: 3, + maxLines: 5, + decoration: InputDecoration( + filled: true, + fillColor: AppColors.textFieldDefaultBg, + labelText: 'Description (optional)', + alignLabelWithHint: true, + labelStyle: + TextStyle(color: AppColors.textFieldDefaultText), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), + iconColor: AppColors.textFieldDefaultIconTrail), + controller: descriptionController, + ), + ), + Expanded(child: SizedBox()), + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: () { + Navigator.pushNamedAndRemoveUntil( + context, '/MainScreen', (_) => false); + }, + child: Text( + 'Continue', + style: TextStyle(fontSize: 16), + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100))), + backgroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDefaultBg), + foregroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDefaultText)), + )), + ], + ), + ), + ); + } +} + +abstract class Note { + static final note = Text.rich( + TextSpan( + style: TextStyle( + height: 1.6, + fontSize: 10, + fontWeight: FontWeight.w400, + ), + children: [ + TextSpan( + style: TextStyle(color: AppColors.noteText), + text: + "Your private key starts with “nsec” or “hex” and gives your full access to your account. That means, if you log in using your private key, you will be able to make posts and send and receive private messages.\n\n" + "Your public key starts with “npub” and gives your view-only access to account. If you log in using your public key, you won’t be able to make posts or access private messages, but you will be able to view your feed. Go to “View only” tab to log in via your public key.") + ]), + ); +} diff --git a/lib/pages/home_screen/home_screen_widget.dart b/lib/pages/home_screen/home_screen_widget.dart index e9e7a31..6524249 100644 --- a/lib/pages/home_screen/home_screen_widget.dart +++ b/lib/pages/home_screen/home_screen_widget.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:drifter/domain_models/domain_models.dart'; -import 'package:drifter/models/keys.dart'; +import 'package:drifter/models/models.dart'; import 'package:drifter/theme/app_colors.dart'; import 'package:drifter/pages/home_screen/widgets/message_ok_button_widget.dart'; import 'package:drifter/pages/home_screen/widgets/message_text_button_widget.dart'; @@ -235,18 +235,10 @@ class DomainCard extends StatelessWidget { Widget build(BuildContext context) { final List? imageLinks = extractImage(domain.content); return Container( - margin: const EdgeInsets.all(8), + margin: const EdgeInsets.all(4), decoration: BoxDecoration( - color: AppColors.mainLightBlue, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: const Offset(0, 3), - ), - ], + color: Colors.grey, + borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/login_screen/login_screen.dart b/lib/pages/login_screen/login_screen.dart index 47a6133..11fe95c 100644 --- a/lib/pages/login_screen/login_screen.dart +++ b/lib/pages/login_screen/login_screen.dart @@ -1,12 +1,12 @@ import 'package:dart_nostr/dart_nostr.dart'; -import 'package:drifter/models/keys.dart'; +import 'package:drifter/models/models.dart'; import 'package:drifter/pages/home_screen/widgets/message_text_button_widget.dart'; import 'package:drifter/pages/profile_screen/profile_screen.dart'; import 'package:drifter/pages/profile_screen/widgets/keys_option_modal_bottom_sheet.dart'; import 'package:drifter/pages/profile_screen/widgets/message_snack_bar.dart'; import 'package:drifter/pages/profile_screen/widgets/ok_button_widget.dart'; -import 'package:drifter/pages/terms_of_service/button_continue.dart'; +import 'package:drifter/widgets/btn_continue.dart'; import 'package:drifter/theme/app_colors.dart'; import 'package:drifter/utilities/assets.dart'; @@ -27,6 +27,7 @@ class LoginScreen extends StatefulWidget { class LoginScreenState extends State { final keyGenerator = KeyApi(); final nip19 = Nip19(); + final indexToggle = 0; @override Widget build(BuildContext context) { @@ -39,6 +40,12 @@ class LoginScreenState extends State { ), onPressed: () => Navigator.of(context).pop(), ), + actions: [ + TextButton( + onPressed: () {}, + child: Text('Skip'), + ) + ], elevation: 0, backgroundColor: AppColors.mainBackground, ), @@ -70,7 +77,9 @@ class LoginScreenState extends State { customTextStyles: [ TextStyle(fontSize: 14, fontWeight: FontWeight.w600) ], - onToggle: (index) {}, + onToggle: (indexToggle) { + print(indexToggle); + }, ), SizedBox( height: 56, diff --git a/lib/pages/main_screen/main_screen_widget.dart b/lib/pages/main_screen/main_screen_widget.dart index d7ebc7f..0d43224 100644 --- a/lib/pages/main_screen/main_screen_widget.dart +++ b/lib/pages/main_screen/main_screen_widget.dart @@ -31,28 +31,28 @@ class _MainScreenWidgetState extends State { return Scaffold( backgroundColor: AppColors.mainBackground, appBar: AppBar( - title: Row( - // mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.svg.drifterIcon, - height: 30, - width: 30, - alignment: Alignment.centerLeft, - ), - const SizedBox( - width: 125, - ), - const Text( - "Drifter", - style: TextStyle( - color: AppColors.mainAccent, - ), - // textAlign: TextAlign.center, - ), - ], + leading: IconButton( + icon: Icon(Icons.settings), + color: AppColors.topNavIconPtimary, + onPressed: () {}, + ), + actions: [ + IconButton( + icon: Icon(Icons.rss_feed), + color: AppColors.topNavIconPtimary, + onPressed: () {}, + ), + ], + title: const Text( + "Drifter", + style: TextStyle( + color: AppColors.topNavText, + ), + // textAlign: TextAlign.center, ), centerTitle: true, + backgroundColor: AppColors.mainBackground, + elevation: 0, ), body: IndexedStack( index: _selectedTap, @@ -64,6 +64,9 @@ class _MainScreenWidgetState extends State { ], ), bottomNavigationBar: BottomNavigationBar( + backgroundColor: AppColors.bottomNavBackground, + selectedItemColor: AppColors.bottomNavIconActive, + unselectedItemColor: AppColors.bottomNavIconDefault, currentIndex: _selectedTap, items: const [ BottomNavigationBarItem( diff --git a/lib/pages/profile_screen/profile_screen.dart b/lib/pages/profile_screen/profile_screen.dart index eaf1743..4d656f7 100644 --- a/lib/pages/profile_screen/profile_screen.dart +++ b/lib/pages/profile_screen/profile_screen.dart @@ -1,5 +1,5 @@ import 'package:dart_nostr/dart_nostr.dart'; -import 'package:drifter/models/keys.dart'; +import 'package:drifter/models/models.dart'; import 'package:drifter/pages/profile_screen/widgets/delete_keys_dialog.dart'; import 'package:drifter/main.dart'; import 'package:drifter/pages/profile_screen/widgets/key_exist_dialog.dart'; diff --git a/lib/pages/splash_screen/splash_screen.dart b/lib/pages/splash_screen/splash_screen.dart index 1e7a69f..f05810e 100644 --- a/lib/pages/splash_screen/splash_screen.dart +++ b/lib/pages/splash_screen/splash_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:drifter/pages/main_screen/main_screen_widget.dart'; +import 'package:drifter/pages/welcome_screen/welcome_screen.dart'; import 'package:drifter/theme/app_colors.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -18,7 +19,7 @@ class _SplashState extends State { super.initState(); Future.delayed(const Duration(seconds: 3), () { Navigator.pushReplacement(context, - MaterialPageRoute(builder: (context) => const MainScreenWidget())); + MaterialPageRoute(builder: (context) => const WelcomeScreen())); }); } diff --git a/lib/pages/terms_of_service/btn_continue_terms_of_service.dart b/lib/pages/terms_of_service/btn_continue_terms_of_service.dart new file mode 100644 index 0000000..d878e1c --- /dev/null +++ b/lib/pages/terms_of_service/btn_continue_terms_of_service.dart @@ -0,0 +1,55 @@ +import 'package:drifter/models/models.dart'; +import 'package:drifter/theme/app_colors.dart'; +import 'package:flutter/material.dart'; + +class DisabledElevatedButton extends StatelessWidget { + const DisabledElevatedButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: null, + child: Text( + 'Continue', + style: TextStyle(fontSize: 16), + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + backgroundColor: + const MaterialStatePropertyAll(AppColors.buttonPrimaryDisabledBg), + foregroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDisabledText)), + ); + } +} + +class ActiveElevatedButton extends StatelessWidget { + const ActiveElevatedButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: () { + UserData.isLogin + ? Navigator.pushNamed(context, '/login') + : Navigator.pushNamed(context, '/createAccount'); + }, + child: Text( + 'Continue', + style: TextStyle(fontSize: 16), + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + backgroundColor: + const MaterialStatePropertyAll(AppColors.buttonPrimaryDefaultBg), + foregroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDefaultText)), + ); + } +} diff --git a/lib/pages/terms_of_service/terms_of_service.dart b/lib/pages/terms_of_service/terms_of_service.dart index 9488246..1843f4a 100644 --- a/lib/pages/terms_of_service/terms_of_service.dart +++ b/lib/pages/terms_of_service/terms_of_service.dart @@ -1,4 +1,4 @@ -import 'package:drifter/pages/terms_of_service/button_continue.dart'; +import 'package:drifter/pages/terms_of_service/btn_continue_terms_of_service.dart'; import 'package:drifter/pages/terms_of_service/terms_of_service_text.dart'; import 'package:drifter/theme/app_colors.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pages/welcome_screen/welcome_screen.dart b/lib/pages/welcome_screen/welcome_screen.dart index 86d5c8c..95affb3 100644 --- a/lib/pages/welcome_screen/welcome_screen.dart +++ b/lib/pages/welcome_screen/welcome_screen.dart @@ -1,3 +1,4 @@ +import 'package:drifter/models/models.dart'; import 'package:drifter/theme/app_colors.dart'; import 'package:flutter/material.dart'; @@ -23,6 +24,7 @@ class WelcomeScreen extends StatelessWidget { width: double.infinity, child: ElevatedButton( onPressed: () { + UserData.isLogin = true; Navigator.pushNamed(context, '/terms'); }, child: Text( @@ -44,7 +46,10 @@ class WelcomeScreen extends StatelessWidget { height: 56, width: double.infinity, child: OutlinedButton( - onPressed: () {}, + onPressed: () { + UserData.isLogin = false; + Navigator.pushNamed(context, '/terms'); + }, child: Text( 'Create an account', style: TextStyle(fontSize: 16), diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart index c0141ef..a727e69 100644 --- a/lib/theme/app_colors.dart +++ b/lib/theme/app_colors.dart @@ -19,6 +19,13 @@ abstract class AppColors { static const topNavIconBack = Color(0xFF4A40EC); static const topNavIconBg = Color(0xFFE3E0F9); +// BottomNav + + static const bottomNavIconDefault = Color(0xFF787680); + static const bottomNavIconActive = Color(0xFF4A40EC); + static const bottomNavBackground = Color(0xFFFFFFFF); + static const bottomNavShadow = Color(0xFFF2EFFF); + // Checkbox static const checkboxCheckedIcon = Color(0xFFFFFFFF); static const checkboxCheckedBg = Color(0xFF4A40EC); @@ -51,4 +58,18 @@ abstract class AppColors { static const mainDarkBlue = Color.fromRGBO(3, 37, 65, 1); static const mainLightBlue = Color.fromRGBO(48, 86, 117, 1); + +// Post + + static const postFullName = Color(0xFF302F38); + static const postUserName = Color(0xFFC8C5D0); + static const postBg = Color(0xFFF9F8FF); + static const postTime = Color(0xFFC8C5D0); + static const postMoreIcon = Color(0xFF323232); + static const postBodyText = Color(0xFF000000); + static const postBodyLink = Color(0xFF3A00E5); + static const postActionNumber = Color(0xFFC8C5D0); + static const postActionIconDefault = Color(0xFFC8C5D0); + static const postActionIconPressed = Color(0xFFFE82B1); + static const postBookmark = Color(0xFF8482FF); } diff --git a/lib/widgets/btn_continue.dart b/lib/widgets/btn_continue.dart new file mode 100644 index 0000000..1bda815 --- /dev/null +++ b/lib/widgets/btn_continue.dart @@ -0,0 +1,52 @@ +import 'package:drifter/theme/app_colors.dart'; +import 'package:flutter/material.dart'; + +class DisabledElevatedButton extends StatelessWidget { + const DisabledElevatedButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: null, + child: Text( + 'Continue', + style: TextStyle(fontSize: 16), + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + backgroundColor: + const MaterialStatePropertyAll(AppColors.buttonPrimaryDisabledBg), + foregroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDisabledText)), + ); + } +} + +class ActiveElevatedButton extends StatelessWidget { + const ActiveElevatedButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: () { + Navigator.pushNamed(context, '/login'); + }, + child: Text( + 'Continue', + style: TextStyle(fontSize: 16), + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + backgroundColor: + const MaterialStatePropertyAll(AppColors.buttonPrimaryDefaultBg), + foregroundColor: const MaterialStatePropertyAll( + AppColors.buttonPrimaryDefaultText)), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9911b8c..c79fc99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/images/logo/welcome.png + - assets/images/logo/ # - images/a_dot_ham.jpeg -- 2.45.2 From a7ea864c47cfb43ea99358f23e803706121dab9f Mon Sep 17 00:00:00 2001 From: alexvasl Date: Wed, 31 May 2023 00:56:26 +0300 Subject: [PATCH 7/8] Changes in the feed screen, keys are generated after the account is created --- assets/images/icons/drifter_vector.png | Bin 0 -> 7917 bytes lib/main.dart | 2 + .../create_account_screen.dart | 88 ++++++++++++++++++ lib/pages/home_screen/home_screen_widget.dart | 84 +++++++++++++---- lib/pages/main_screen/main_screen_widget.dart | 73 ++++++++++++--- .../message_screen/message_screen_widget.dart | 2 +- .../notifications_screen.dart | 18 ++++ lib/pages/search_screen/search_screen.dart | 18 ++++ .../settings_screen/settings_screen.dart | 41 ++++++++ lib/pages/splash_screen/splash_screen.dart | 2 +- pubspec.yaml | 3 +- 11 files changed, 298 insertions(+), 33 deletions(-) create mode 100644 assets/images/icons/drifter_vector.png create mode 100644 lib/pages/notifications_screen/notifications_screen.dart create mode 100644 lib/pages/search_screen/search_screen.dart create mode 100644 lib/pages/settings_screen/settings_screen.dart diff --git a/assets/images/icons/drifter_vector.png b/assets/images/icons/drifter_vector.png new file mode 100644 index 0000000000000000000000000000000000000000..14ee6b238fdc21c2f36a9296ddef9be0e79f8edb GIT binary patch literal 7917 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 Lz#R2l&n5mp2K=S< literal 0 HcmV?d00001 diff --git a/lib/main.dart b/lib/main.dart index 4e83a28..f7c5dd0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:drifter/pages/create_account_screen/create_account_screen.dart'; import 'package:drifter/pages/home_screen/home_screen_widget.dart'; import 'package:drifter/pages/login_screen/login_screen.dart'; import 'package:drifter/pages/main_screen/main_screen_widget.dart'; +import 'package:drifter/pages/settings_screen/settings_screen.dart'; import 'package:drifter/pages/splash_screen/splash_screen.dart'; import 'package:drifter/pages/terms_of_service/terms_of_service.dart'; import 'package:drifter/pages/welcome_screen/welcome_screen.dart'; @@ -40,6 +41,7 @@ class MyApp extends StatelessWidget { '/terms': (context) => const TermsOfServiceScreen(), '/createAccount': (context) => const CreateAccountScreen(), '/MainScreen': (context) => const MainScreenWidget(), + '/Settings': (context) => const SettingsScreen(), }, ); } diff --git a/lib/pages/create_account_screen/create_account_screen.dart b/lib/pages/create_account_screen/create_account_screen.dart index c745baf..b4bf74e 100644 --- a/lib/pages/create_account_screen/create_account_screen.dart +++ b/lib/pages/create_account_screen/create_account_screen.dart @@ -2,6 +2,7 @@ import 'package:dart_nostr/dart_nostr.dart'; import 'package:drifter/models/models.dart'; import 'package:drifter/pages/home_screen/widgets/message_text_button_widget.dart'; import 'package:drifter/pages/profile_screen/profile_screen.dart'; +import 'package:drifter/pages/profile_screen/profile_screen.dart'; import 'package:drifter/pages/profile_screen/widgets/keys_option_modal_bottom_sheet.dart'; import 'package:drifter/pages/profile_screen/widgets/message_snack_bar.dart'; @@ -24,6 +25,84 @@ class CreateAccountScreen extends StatefulWidget { } class CreateAccountScreenState extends State { + final secureStorage = const FlutterSecureStorage(); + + Future generateNewKeys() async { + final newPrivateKey = await Nostr.instance.keysService.generatePrivateKey(); + // final newPrivateKey = keyGenerator.generatePrivateKey(); + + final nsec = + Nostr.instance.keysService.encodePrivateKeyToNsec(newPrivateKey); + + // final nsecDecoded = + // Nostr.instance.keysService.decodeNsecKeyToPrivateKey(nsec); + // assert(nsecDecoded['type'] == 'nsec'); + // assert(nsecDecoded['data'] == newPrivateKey); + + final newPublicKey = await Nostr.instance.keysService + .derivePublicKey(privateKey: newPrivateKey); + // final newPublicKey = keyGenerator.getPublicKey(newPrivateKey); + + final npub = Nostr.instance.keysService.encodePublicKeyToNpub(newPublicKey); + + // final npubDecoded = + // Nostr.instance.keysService.decodeNpubKeyToPublicKey(npub); + // assert(npubDecoded['type'] == 'npub'); + // assert(npubDecoded['data'] == newPublicKey); + + return await addKeyToStorage(newPrivateKey, newPublicKey, nsec, npub); + } + + Future _getKeysFromStorage() async { + // Reading values associated with the " privateKey " and " publicKey " keys from a secure repository + final storedPrivateKey = await secureStorage.read(key: 'privateKey'); + final storedPublicKey = await secureStorage.read(key: 'publicKey'); + + final storedNsecKey = await secureStorage.read(key: 'nsec'); + final storedNpubKey = await secureStorage.read(key: 'npub'); + + // Indicates that both private and public keys are stored in a secure repository, after which, the state variables are updated + if (storedPrivateKey != null && + storedPublicKey != null && + storedNsecKey != null && + storedNpubKey != null) { + setState(() { + Keys.privateKey = storedPrivateKey; + Keys.publicKey = storedPublicKey; + Keys.nsecKey = storedNsecKey; + Keys.npubKey = storedNpubKey; + Keys.keysExist = true; + }); + } + } + + Future addKeyToStorage( + String privateKeyHex, + String publicKeyHex, + String nsecKey, + String npubKey, + ) async { +// Waiting for both write operations to complete + Future.wait([ + secureStorage.write(key: 'privateKey', value: privateKeyHex), + secureStorage.write(key: 'publicKey', value: publicKeyHex), + secureStorage.write(key: 'nsec', value: nsecKey), + secureStorage.write(key: 'npub', value: npubKey), + ]); + + // Updating status variables and starting widget rebuilding + setState(() { + Keys.privateKey = privateKeyHex; + Keys.publicKey = publicKeyHex; + Keys.nsecKey = nsecKey; + Keys.npubKey = npubKey; + Keys.keysExist = true; + }); + +// Returns a boolean value indicating whether the keys were successfully added to the repository or not. + return Keys.keysExist; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -123,6 +202,15 @@ class CreateAccountScreenState extends State { height: 56, child: ElevatedButton( onPressed: () { + final currentContext = context; + generateNewKeys().then( + (keysGenerated) { + if (keysGenerated) { + ScaffoldMessenger.of(currentContext).showSnackBar( + MessageSnackBar(label: 'Keys Generated!')); + } + }, + ); Navigator.pushNamedAndRemoveUntil( context, '/MainScreen', (_) => false); }, diff --git a/lib/pages/home_screen/home_screen_widget.dart b/lib/pages/home_screen/home_screen_widget.dart index 6524249..48391a0 100644 --- a/lib/pages/home_screen/home_screen_widget.dart +++ b/lib/pages/home_screen/home_screen_widget.dart @@ -90,6 +90,7 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: AppColors.mainBackground, body: RefreshIndicator( onRefresh: () async { await _resubscribeStream(); @@ -192,16 +193,16 @@ class TimeAgo { String timeAgo = ''; if (difference.inDays > 0) { - timeAgo = - '${difference.inDays} ${difference.inDays == 1 ? 'day' : 'days'} ago'; + timeAgo = '${difference.inDays}d'; + // '${difference.inDays} ${difference.inDays == 1 ? 'day' : 'days'} ago'; } else if (difference.inHours > 0) { - timeAgo = - '${difference.inHours} ${difference.inHours == 1 ? 'hour' : 'hours'} ago'; + timeAgo = '${difference.inHours}h'; + // '${difference.inHours} ${difference.inHours == 1 ? 'hour' : 'hours'} ago'; } else if (difference.inMinutes > 0) { - timeAgo = - '${difference.inMinutes} ${difference.inMinutes == 1 ? 'minute' : 'minutes'} ago'; + timeAgo = '${difference.inMinutes}m'; + // '${difference.inMinutes} ${difference.inMinutes == 1 ? 'minute' : 'minutes'} ago'; } else { - timeAgo = 'just now'; + timeAgo = '${difference.inMinutes}m'; } return timeAgo; @@ -237,34 +238,79 @@ class DomainCard extends StatelessWidget { return Container( margin: const EdgeInsets.all(4), decoration: BoxDecoration( - color: Colors.grey, + color: AppColors.postBg, borderRadius: BorderRadius.circular(8), ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Row( + // children: [ + // CircleAvatar( + // radius: 25, + // backgroundImage: FadeInImage( + // placeholder: const NetworkImage( + // 'https://i.ibb.co/mJkxDkb/satoshi.png'), + // image: NetworkImage(domain.avatarUrl), + // ).image, + // ), + // Container( + // width: 300, + // child: Row( + // children: [ + // Text(domain.name), + // SizedBox( + // width: 4, + // ), + // Text('@${domain.username.toLowerCase()}', + // style: TextStyle(color: AppColors.postUserName)), + // Expanded(child: SizedBox()), + // Text('${domain.time}', + // style: TextStyle(color: AppColors.postUserName)), + // ], + // ), + // ) + // ], + // ), ListTile( + contentPadding: EdgeInsets.only(top: 16, right: 16, left: 16), leading: CircleAvatar( + radius: 25, backgroundImage: FadeInImage( placeholder: const NetworkImage('https://i.ibb.co/mJkxDkb/satoshi.png'), image: NetworkImage(domain.avatarUrl), ).image, ), - title: - Text(domain.name, style: const TextStyle(color: Colors.white)), - subtitle: Text('@${domain.username.toLowerCase()} • ${domain.time}', - style: TextStyle(color: Colors.grey.shade400)), - trailing: const Icon(Icons.more_vert, color: Colors.grey), + title: Row( + children: [ + Text(domain.name), + SizedBox( + width: 4, + ), + Expanded( + child: Container( + child: Text('@${domain.username.toLowerCase()}', + overflow: TextOverflow.ellipsis, + style: TextStyle(color: AppColors.postUserName)), + ), + ), + Text('${domain.time}', + style: TextStyle(color: AppColors.postUserName)), + ], + ), + trailing: + const Icon(Icons.more_horiz, color: AppColors.postMoreIcon), ), - Divider(height: 1, color: Colors.grey.shade400), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: + const EdgeInsets.only(top: 12, right: 16, bottom: 14, left: 78), child: Text(domain.content, - style: const TextStyle(color: Colors.white)), + style: const TextStyle(color: AppColors.postBodyText)), ), if (imageLinks != null && imageLinks.isNotEmpty) - Center( + Padding( + padding: const EdgeInsets.only( + top: 12, right: 16, bottom: 14, left: 78), child: Stack( children: [ const Placeholder( @@ -274,7 +320,7 @@ class DomainCard extends StatelessWidget { Center( child: FadeInImage( placeholder: const NetworkImage( - 'https://i.ibb.co/D9jqXgR/58038897-167f0280-7ae6-11e9-94eb-88e880a25f0f.gif', + 'https://media.tenor.com/On7kvXhzml4AAAAj/loading-gif.gif', ), image: NetworkImage(imageLinks.first), fit: BoxFit.cover, diff --git a/lib/pages/main_screen/main_screen_widget.dart b/lib/pages/main_screen/main_screen_widget.dart index 0d43224..de04474 100644 --- a/lib/pages/main_screen/main_screen_widget.dart +++ b/lib/pages/main_screen/main_screen_widget.dart @@ -2,7 +2,9 @@ import 'package:drifter/pages/home_screen/home_screen_widget.dart'; import 'package:drifter/pages/login_screen/login_screen.dart'; import 'package:drifter/pages/message_screen/message_screen_widget.dart'; +import 'package:drifter/pages/notifications_screen/notifications_screen.dart'; import 'package:drifter/pages/profile_screen/profile_screen.dart'; +import 'package:drifter/pages/search_screen/search_screen.dart'; import 'package:drifter/theme/app_colors.dart'; import 'package:drifter/main.dart'; import 'package:drifter/utilities/assets.dart'; @@ -17,15 +19,48 @@ class MainScreenWidget extends StatefulWidget { } class _MainScreenWidgetState extends State { - int _selectedTap = 0; + int _selectedTap = 2; + late String _title; void onSelectedtap(int index) { if (_selectedTap == index) return; setState(() { _selectedTap = index; + switch (index) { + case 0: + { + _title = 'Message'; + } + break; + case 1: + { + _title = 'Search'; + } + break; + case 2: + { + _title = 'Feed'; + } + break; + case 3: + { + _title = 'Notifications'; + } + break; + case 4: + { + _title = 'Profile'; + } + break; + } }); } + @override + initState() { + _title = 'Feed'; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -34,7 +69,9 @@ class _MainScreenWidgetState extends State { leading: IconButton( icon: Icon(Icons.settings), color: AppColors.topNavIconPtimary, - onPressed: () {}, + onPressed: () { + Navigator.pushNamed(context, '/Settings'); + }, ), actions: [ IconButton( @@ -43,8 +80,8 @@ class _MainScreenWidgetState extends State { onPressed: () {}, ), ], - title: const Text( - "Drifter", + title: Text( + _title, style: TextStyle( color: AppColors.topNavText, ), @@ -57,8 +94,10 @@ class _MainScreenWidgetState extends State { body: IndexedStack( index: _selectedTap, children: const [ - HomeScreen(), MessageScreen(), + SearchScreen(), + HomeScreen(), + NotificationsScreen(), ProfileScreen(), // LoginScreen(), ], @@ -67,19 +106,31 @@ class _MainScreenWidgetState extends State { backgroundColor: AppColors.bottomNavBackground, selectedItemColor: AppColors.bottomNavIconActive, unselectedItemColor: AppColors.bottomNavIconDefault, + selectedFontSize: 0, currentIndex: _selectedTap, + type: BottomNavigationBarType.fixed, items: const [ BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Home', + icon: Icon(Icons.mail_outline), + label: '', ), BottomNavigationBarItem( - icon: Icon(Icons.message), - label: 'Message', + icon: Icon(Icons.search), + label: '', ), BottomNavigationBarItem( - icon: Icon(Icons.person), - label: 'Profile', + icon: + ImageIcon(AssetImage('assets/images/icons/drifter_vector.png')), + label: '', + ), + BottomNavigationBarItem( + icon: Icon(Icons.notifications_outlined), + label: '', + ), + + BottomNavigationBarItem( + icon: Icon(Icons.person_2_outlined), + label: '', ), // BottomNavigationBarItem( // icon: Icon(Icons.login), diff --git a/lib/pages/message_screen/message_screen_widget.dart b/lib/pages/message_screen/message_screen_widget.dart index ced3ed2..d149c5f 100644 --- a/lib/pages/message_screen/message_screen_widget.dart +++ b/lib/pages/message_screen/message_screen_widget.dart @@ -18,7 +18,7 @@ class _MessageScreenState extends State { children: const [ Center( child: Text( - "List of posts", + "Message", ), ) ], diff --git a/lib/pages/notifications_screen/notifications_screen.dart b/lib/pages/notifications_screen/notifications_screen.dart new file mode 100644 index 0000000..26a33e7 --- /dev/null +++ b/lib/pages/notifications_screen/notifications_screen.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; + +class NotificationsScreen extends StatefulWidget { + const NotificationsScreen({super.key}); + + @override + State createState() => _NotificationsScreenState(); +} + +class _NotificationsScreenState extends State { + @override + Widget build(BuildContext context) { + return Center( + child: Text('Notifications Page'), + ); + } +} diff --git a/lib/pages/search_screen/search_screen.dart b/lib/pages/search_screen/search_screen.dart new file mode 100644 index 0000000..30f920d --- /dev/null +++ b/lib/pages/search_screen/search_screen.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; + +class SearchScreen extends StatefulWidget { + const SearchScreen({super.key}); + + @override + State createState() => _SearchScreenState(); +} + +class _SearchScreenState extends State { + @override + Widget build(BuildContext context) { + return Center( + child: Text('Search Page'), + ); + } +} diff --git a/lib/pages/settings_screen/settings_screen.dart b/lib/pages/settings_screen/settings_screen.dart new file mode 100644 index 0000000..062e4ab --- /dev/null +++ b/lib/pages/settings_screen/settings_screen.dart @@ -0,0 +1,41 @@ +import 'package:drifter/theme/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.mainBackground, + appBar: AppBar( + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: AppColors.topNavIconPtimary, + ), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text( + ('Settings'), + style: TextStyle( + color: AppColors.topNavText, + ), + // textAlign: TextAlign.center, + ), + centerTitle: true, + backgroundColor: AppColors.mainBackground, + elevation: 0, + ), + body: Center( + child: Text('Settings'), + ), + ); + } +} diff --git a/lib/pages/splash_screen/splash_screen.dart b/lib/pages/splash_screen/splash_screen.dart index f05810e..4256876 100644 --- a/lib/pages/splash_screen/splash_screen.dart +++ b/lib/pages/splash_screen/splash_screen.dart @@ -35,7 +35,7 @@ class _SplashState extends State { height: 300, ), Image.asset( - 'assets/images/logo/drifter_vector.png', + 'assets/images/icons/drifter_vector.png', height: 111, width: 93, ), diff --git a/pubspec.yaml b/pubspec.yaml index c79fc99..d37d8e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,8 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/images/logo/ + - assets/images//icons/ + - assets/images//logo/ # - images/a_dot_ham.jpeg -- 2.45.2 From c7d0a114e00136e56a96f3fe91c3bb5cef799ece Mon Sep 17 00:00:00 2001 From: alexvasl Date: Fri, 2 Jun 2023 16:11:05 +0300 Subject: [PATCH 8/8] Added QRCode screen, in the development of the profile screen --- assets/images/avatar.png | Bin 0 -> 13357 bytes assets/images/banner.png | Bin 0 -> 64824 bytes assets/images/logo/drifter_logo_white.png | Bin 0 -> 8975 bytes assets/images/qr.png | Bin 0 -> 462 bytes lib/main.dart | 2 + lib/pages/home_screen/home_screen_widget.dart | 163 +++++-- lib/pages/profile_screen/profile_screen.dart | 460 ++++++++++++++---- lib/pages/qr_code_screen/qr_code_screen.dart | 166 +++++++ lib/theme/app_colors.dart | 19 + pubspec.lock | 18 +- pubspec.yaml | 6 +- 11 files changed, 710 insertions(+), 124 deletions(-) create mode 100644 assets/images/avatar.png create mode 100644 assets/images/banner.png create mode 100644 assets/images/logo/drifter_logo_white.png create mode 100644 assets/images/qr.png create mode 100644 lib/pages/qr_code_screen/qr_code_screen.dart diff --git a/assets/images/avatar.png b/assets/images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..70bcf4a43ce5702e5f625325875e47348c5f921a GIT binary patch literal 13357 zcmV+|G}6n7P){3x~g-WneLuEJxQY(g&7G+EMTw%*vDaQV2#(91tZQeHr`!~bl5$>u}v`c zaljz4i!lhZAY_6h5TmS7m^`BiJ)Lu^oWtAi{;#U1CI|&2yl0FCj_7&3rfdDK4cz3l684H&CG8|Ldr%fQi4NQ>j&Y18ma-0BA7 z%l&-Zjw_AyTDLJi4)Sq-h0niCgZ@$sn2H$(`1BFOG+wa&emaxE@YpCWOpNm}hD{bgZl?t3L4;(ff1AT26?Crp=&AZUr)(NL00E@*1O|xEp$b0#?-yhPx zhc|e8!we|irUQ5K03Shv(GH(FfybUbqzrcK^l4Q2C5vXjra1s>6*lN_+A7#F5Wi#R9wTfiH%3&Il!Vsd&CK97su=t7~Sqsmwf1w-)IEr>17pi;=dN^8O9gD>2RNXHIXZQlB6 zNzVLQAf(-e*Lizg4Va7>@8#1cQOj*d=xz7h_b+@t$pi5pU58G?=?#ai(r`AoV6&!a zv>t zfNW9rYs2&+;12d<*Uszehf^Nu^=n_l>$JTt2Bg>ZG7H^P%W*hz;sm~O-!~YpCt*68 z1`CZRUZ>ZdWizT(#xI~34gBZ#?8oLct#C2dS#`TIwB1TiGm0n`O9(f$AfGSrH)R;5 z5*C(Xh_tjL8J~j{71;AJymk@s7fcZpzWR-C;Jyd$hoMVA8tMp|W*W_+nO?YRK!e|)m%Zi2 zt$5pkO>om-ZkHS73cagZ;G;xiRcTNic886>wIY?tLa$Vj&K2MwxU_ZmGB4(E?6LbX ze(DsOTAC1|5!Y47H@|H?OUce@Mw-ujz(|L=SlZ3G~0aeCvOzo<7_@fx>U zuDh$2>u~Uyr}00(@Hu3&xmDw6dd=hb%s)|~u`I^LF*B+LB0(Q+-qnYI+lFef%mG=1 zi$=6Ew>fFFTy_x)bCdFIL|fbWo(oH{1ahe)tUfmuC(0_+HHK{W`i*pG3(BP&%GoTe zktSHK>BZu=4fx}EJbeGRaQw(K_`^T=W88e;?Lp?zdzp5Ih}Iv)YusLw0c#=qUD{+w zyzU?W`Rn-R_wQ$zYAO$jiA>|s>nv8~aaDRv#i$$BQ29$dG(=?e+pWlC;+USC!@#-$ zluAW-eSR4EG+uc2Apg#zr+1zDox#uATbhvzIUEy;v81uG2D3TJGdWG7y%*BwMKkZ2CN&8 zFMH1n591?$ei!DK7i$KB3R82_R*fiOTCE$E9*HtNw8XzH3_FxD(B(xcK?52UINW|L zEYG2w&th_T8DWNKZy$4`FUn(BkWMDmpv){UBYAoP(O@e!ZQX)gp@gymqzI`CQvqFF z=e`T^G|~m~#zYox%lPnCyBmK&vc5PM!!wIX%*@T8%(QXG?H?p1YU_P1M$}*F7aNWD z;_wB$_s)-DW`0pp)5Wc3K@%hlRA@Y@Bq}8$Cp||Frjs;#y%q$02DYq^pu1}TJ?po@ z8wwzkT*lJOIF@H85L=kWxzi^Qjgm|gNXnHws?{8vUMKeKe-lUEif5jA0*^m>kO0GD zS_=sKL-4XHsU}j0Pfd_kEh08Ljm}gNaVI%sHH*J4_;6h^hgd3)<7cPw>{CzUJKy;N z;_-Po^|}#z@d~$BL9b1O3fqSnH1%}z#6VD@)$|_Ds z$VE1j#`yRMZ0;sZo;iiqrYPdoID$Pr%!?Z7j}PDb%I8pC8bgp3O0=sVD~t1p&m|e! z78F?R^mR4k#0Uq8AhX?7#2Mz6OuTFe z6QRB!Xq!yO@sHp58~EMKRSt#;a~8DfW#85qvJBg@gl<(KZnfI@OoKDAuZh?Yl#9WtbPuvQ$+Zw*`Q*#l$MW+&l+uhl`gGMOV{GaP4D>c(*HAaMZfQqPXM_~Q z&D>UIrBERHF;6LjGMvQWOgLW6hft)MhNVH;S{xYaB-J2ioVaitd8Pw_?_?r|#f2G+ zjhsg^nL@P5gT&Gtrp8B6&XL;q!`xQ}skw33axr*`t_6~1I|pU?{PWnfVLf^_?Lc$i zAORU91!?2AgUk#vCQCtBfg3a90GL4s(D@Xsjox&il z{rJc4N_^(CcR%y04Jdg}+?X$&iS!6PAw#(Lp(zkTXQUZxx+yKKYeO${SzAkx9#=p% znOB}Cek~P;V2UAmQPsb7aIsKyDPU*6ANfL(q&Wz;*Nc)~WFGQii3sbWV9+->2%p`o ziFe)lcJyu71{6|=k=j%;3k=Umw03tRvAD$F`$(#TXkWh*j&PLwalqlU5{y<*9GOLq z`E$vxp};|Cr!*5RX*f;gC&sWY5#3iSBc7{`rDR)M6M_LRn!-KeiP!$g$M5>qXFq%Q zRYUnDb09zXJg+~P$zkX*Nqb-3(=;L{D-ekx0T$wWPw(wmE$m9NdA$6&la$K{Onp0o<_ry|5+|$Y=6c zo;iZ!kNyA}IiJ((LeH83cw07N?D;1s7(}Vsc<4CuJSQKf=Vy>FW|*(@UPJ^bjVtJ&$QIZ#5EJG)(| zLn0#rr+AT}9)hlA6{$*Jp^9RI-{V1FXD4=V-;CdQ_pNx>TeqW~wP%s2S7C?YOkWL!!c?n0m>ZX!sA~k|iIjan1tg>XS zVZ5M{uxu8VO+mj6n+HAU?QF-+ZR>Gncmj_+eTrVNLdC@{$|=9EV}5Q0`3|4JI6=dY z_{toXmX_cPM6gURF)Agv7;+X?BR-EE339@AdX}TB8w zW`>**?P5;!Bh=PLph?2NO~=9B03t;mmpStYz5Q0wp9Kz17imq{ZBg_>$sXbmyp(u6 ziL+-f5Xfvq{VCl0wZF!P{>y(C{qZiQwfiqe4dS4F9i5IW1tagbJu^QU4 z>B>trqO=1chF^Pg5M9hCK9`PJhEk4bStc#<(gBD1L!?6)mTm^QUV=2luP&mP&yjc+ z;U+*hnJQ+drl>Oe7^W>~?&zf_JA_WEQz($2H-(y+2IyJCC+SL5Zz@Hiaz>>YJ4>FU zj~<7ftYCC*89iAC!?VJY(}lBcVANs9sKpFJi_kK9VTr^)TgbCqOtV5t#hy6Aho}z^} zT|$x3bfihc@sEj1??QN!5a?pB97wp;Sr?hjds$W0>i46m4(ot>{5cp z&NF8v@$~sc%r6`wMbeo9=JA^=I==YDFQK>1Pi3ZCfzrOs188xjap1rKRQw*ey9QCr z#gQf7wDWT-jpkq)AXXs9n%D1kA*+iA8aNYmgOnC^CM&sJxPsD>l#F@tnhzg&nvZ9& zWBav1t0~{zeN_XZmivHHOx^5F&y;)e>c{JBxLZ zRsx95{N}|e65a<7oyN$-oT^&vkG^RVvS365t}6 zMLBljrjzV(Xq`Cw)Q?fI`{>CQvAIcx(ZfgZ&fDLFYjG&S`XTuK@clSLy12l6X&3d2 zE>^YZrWYvEuPHf1swkswJhQB4P#E~Z55JA~|K3L>)kzp1G_MS+GX_-Bo1S}&+%12p z&5SDzsx`DGFTQ8F;yaDct0TaF3*`#Nr)Cg1`#3_;0Fm2?v%{y+F}O{6tW!LJLtUbh zB4UGuobT8ZKf;6Ofm`-i4j1T_?uomXKzdm&sOeVQij?Pd5sF z4gO#kPE60TMz3O?+%iL?^-%V*OYLsel?TdJt#=MiWL!)xUd;~6y|xY~BL&n~`aTr^h0`|43O)TlWKF8zILP;uHwU@Q3155AA4NQ;u} z(}`uo7Z*@qyTHQl7jp^z&Vs$~ct75?*^Xx)eg^Tpoz;gEhmMVrnz-?w-t~68dG|W( zT@%D_-rSE5yn8d=eoHT6={%l0ca}8ChWY6Um1d~s6RRso=|-*GGO#qC#kL)LP+}oJ zH_Uzri~MM~8C4SFB74Gi=E5@RNqL4OTeHLC46@>B!45|NLpK}%I|NjMELa=zqRM{B zXFv6~=pEdE-qt3n-t8z!i*#KFh9_q*Im5EAXd&UQta_ZvarMv@Pt-3O(QHkK>}63@ z=j{0t^-B=Y{is&Ye^v6Oso9^sBE!_Ts~M;9y-RbTzWVx-eN#RvojZi^X{L`C#*x@h zdm~$^o0gc*mne_T zVePs-6l9Zlo?X_GP7_xwI5|8;B0qxp(McrvnS^S&WJZh#2=%<)c#idyWYf+3C_s4d zr{}0l1q6*l9D6k{k0YH@JG^-D@}H|1(WrOWu5QpqudBZ{Polo>OMjaS`B7S=5b(fp zSlU})|A*tKzOxg>O;#AZD2u4c#WeEi3}P!q*4EEL^R?l(fB*MU)bj+94jg{y$4I4F zFSoQ(5PbyK@7aSU*E!5r?5xKZk)Zw;V?M1=PaNE{i*ZMBTinErkT+Dgq zg{(S?nG-7l#I^IU;>?+o%7FYvueClSR8*JCs|xM1i+?r7YV^Lwu()DBLMgg?F?tIV z{*#9>%`@`1&{IgBZS271s1%n<%wd)yB3)6OKAOOpsTJ%T>>~IOaA@CW9()Q_5^j&z zfv_vYCR2>P76;sI8*%Lx%3p3foD4ZV5JrhIR%?3)tS*O|Yh&S{StT`4SvT0zsiQBd z7d^2~EY$QA#_WX)FV<{?AtBY_m#ysq)N@fT%760}eYJNmBK1$V$eUw02q9e;uE zv$ALkBfvI-%+k_%QA3;Bu*KR$L1GvWe)(H8tPAVdYM5A=$3WDA^?eRF+c#n~Yggx~ zQ6#m>Yy|GV$ZIQ5>avowdxJsBb#tau)HX3rJx5ywfg8@VTBkF73=h9}(SY?3&dg7# zc49u4MVVnk+ODQ*SlmvIq8A?aE9AAtQfzgn)#_!Ewt+^FxSH{BSqQ5^Fw6#g=h{K! z7RS)k)TD^ENbnv?`~g2(o#cWJyN&7%!>-4UuA?P%FH-(|Fw&YtUX3eOjElcn^}uKU z*F97$7LjMKqDVnegQ>&CvYl_v+vL0Dtlj-SIwW?R63Zp*x^e(sRN{NRkZLE$$nrVF-rE{Gl z*S5YN%^No$ym>2|I_+!-Sx|SZ#+j$92Gr_0Os#g-E**1Gg3C0_x#KV5uRiq|eC8AH zLB!7s_c&2XCh^3>-$!RxCl%pl<}nK!Qf(@2B-nsTbFeik&^mV#ZEuB(D*Ds$06TD< zYG$j!XwhgaXyyQtPePZelV+4F)%o_?O4u;v*3xRoZD^N(h}5RVLItN!pTTvzHo|3NsO9o%3W&q&N9US#IQPP{Fbe&Q z1`o-!Ux7fBB5cqVfRhwN3pf#Qxp8E21#@1uBJFO(i+RMBSy|CwPAbvTtTO1m;=vB) zM9r^b_ko+Z|31u4Ph;%D1;l3ODW%0tu#hvmL|igfHO@u`vT8tw%wf^%7hR(lOd-~yLa?G4X2@Mdk(M1gNM6@oe?59`z8T)u z7N#j{t*3A)8rP*RQKv;xFOwkK*j;;`dC1O?^)QUx%SW-cJ&H7qstlwXiY{eSF&ujM zhltOyV@Dtf)BC(MhL^ymljGT?b=kHVv)&Fx8#IfPl}7e8YA7$ti#G&`Eh2U-dwZp-@?sJ8#K($#}nepFF8tPBtGtWRwe6Ku?F zAzu?^`vG1&OD=L!3DZ(OrKbA2JZ|iH(=B-Z@yBuI@Da>REW^!|(b^J(CD?(+&*JFGS)6?CFp|?RB0oEVoB9XP>-Q@$R{9xs z8-)lO&Sue+SSz4XshWsuItj0Xr(2dZ#YU9d7i3ClXB)LmfoHX18n~XjeLjCZ6U8M@ zDg)ZRLRPJ6XJRyYRBOL7>VG#FuUh-tDxXtdE0R8pKlK<^CMMNH9VLq!=3vxLQ@tw6 zNxC9PF-$#n2X#17i>i)PE=h#sDTSHm7}}Ej%X(QEz-A>ErHrMOi|C>9Y|)G8Y;Hj& zUxI%&kL3&O=FR1hUQVed(>#G-FzUyfc5X(H<(-qso!A7MWJ6^<6}d_aWQ?l8&F0Dq zrAuO1-87`r=4Q^6`7@Q;;F}K7-@mEujLHL#%B$$WRy8U{t;)ElEdTtPw_ZC+xF*g| zV(RH9k(#4?UNz?eDk>z!I+K9iDU*=MAK7@9V-=}BOFnUts7qa%p(nfA1`*kGJrZmR zSr~#g?jscGrWX`Y%B-M7qxbE&hGB1GyE6`H(7vPO!TB>Yh$pRxw5(xso&(4ccVv^O zP7bq^7eIT+&C14DO+_WfZg2%D3C46OcBm*Mg9!h!k1f{EitutfE@%>1Qk%tJb>7=I5IGc4Y1 z0c7a0N%A+zO|m2+%$y^WD1F`_FVKoFe)=wa^V{EnCD6+qI%$lws#`TGA^`%1i@lMS z?u}S~&3?Gp>Rhl__`4MTjQKP<#>zr>GMCXH6k|(;G&-U_^mVnOm9m-BCTL+laHj31J>MUC_|Bb{)JMHo;&b&pMi+c>`FabmXa0j;CyL;mkP<4D@4xBP=U1 zUAjYgOH(k!hWav2pE{1ou?v_RJqrsjLJ=dxLh(}4Lxjwcbs9LZ{IX#y|2@H~zlKa9nxC6;OwKD*cA#+%=Ud?tyQ zvmI_X1q4d`A~{(rCdsx)W#kIubDw%Io}4jo%Hcuy%o+U3@HsphPcm&d)RKqm_uZ%# z;rM8@LQa-Lu({9S%4ACL859{@8LV$_#_$w5Cc|GMCCGD&Mxt0@BTLC#hLWb0O?K?q zTZ==pFI+d^qhi2~H{Fh>{`qfAb)Yu?ulD6tBCQZv*^U>I)3NEd-(ybEEyU?L5jDYk zZgvJEPdtVj5A2tfKx}-k#U%T&ZLC*ig8BJ#XK?=f1=a8Hk#5**9$a_bn@Ds$&`63& zUa0z>K~HBZPK?jsMz%lo;uQQG$wdN#EJ!S|9ct%5=8Xb|qG3F+XD1e(d=`V}N6@!2 zibdxJxV$zrDQz)_2m*?Z$n2K_D;*Igl6=1-97dLOXM1l84quoekY(5|4JgnO&RP}H zt(j7u$YTfg{|7WUooT>Y9*pn_HxxtyzlB8g{sZJ%ljx~X!= zb8IDsEnBwY+_@KV-+ljz+its!QkVyWR6p{pPn(&x-w&dePW##XUSJ_cIF&{Yxg4?rSg(V zBXHmM@#2SgoaR<6^L!$$K{ngd94IT1zpC7*5?cc8*tKgPYBqaxHMP~IYaNu=`+ob68i;cpiOmL!dMMDK5s7*( z8X!^1lYgx!vMN$hC>T~LU`^iu?zsJ(c>IY+nOEi+wnRmShn@VYN@b)Y+Jd%dv#J6- z9_BOxfm~oSE1CI)1-ShotXbQS(?oR#4Sd?M7M0IG2)Be`ngh)IRJ0K_y)HDf|Iz>S zPtg})UK{AdLexv=k*rSRqKK2q7<3Fp z>16sjy){REnP>hS8oH+LQ}@=VxUH6QwCESD!6zM9ReS@n6Mt9X?dNT#)w88W~2>%WCZe)0pA&o*t|rMOTzti=6d{Wa3|LM@z>Dp@8-rF1} zsc&`?#h0)c=vD$kTYDD*L5i^q)v~*!7VuQqh>~7&km7N$jA__ zPb8AMvqblouoN_}(Cc;%?Zy1u0v`PF{Y*U+2eJj8AL~*Jc^$cyAoK#oTxlMO7s|z_ z5=-1q0fPfuuwmC;4#I<&y?zHSH21OCpU22I9!2J#N!riP5WF&&K6@S$PoBi`ufGLz z*X?2%*2;R?P2kFrC)ydZ1=To`D9Ex3N^!u;Jf8SjE}vD`G%|hx+t)VpoJ^rXiTV~@ zedpG<-Hr}6zXW~#A?^Mv45-o@@CA9k^@E>y$*!AHC1hqjYs)zEi?jv>rILvR!=OMz zEKp5JF^A<;y_DgJE-od}Ke%1p)8Ho${Toh?jIx5NqRHb|zb~>XXkqB~bgib~V3jmcU-T%)^)d$*IoMSWl))-T2>lH^wjk_b>p&I^u^O?E zbBZUv=K~+9+vu)Fdt5TXvTTO&oojdR53bq0A0y8_cG2%@AR-K2kW55}5?hH*UMzv0 zVUj1g^)jDG=c}MfGx=VLNm}@ngC`ehZpWF^C)6!Xrl;-H=r$9ws{{klj%J3U7o+5i>0BI%eVg&YYu|-3fn{-i2JbBUP}+7oTH4zPSOg>j zQ$CYcHMn&2q>JYAu>HWnDVHtE8)d&X3ayI^OFTC-!M8Q{vVk8b^|C33AXSR+z`Zx# zj6K&MP~KOqfyY%nP!i*+7yjX&*R(vah<79oE$W}qtzJ-#xtKkc$)$IH*xtNVc6M}7J4iv8yj}){NPpI&b&*c;_ zN!{w;Z-OnI7+sbQVhTPBk=fD&TcAaeY*{ZXVd`Os%G_%S9%)wD`TIOk+%CeZ+oAYw zGL}{%gP^gCh+X9VW-|<70*!@ve9xwxl+gAn?+b*qJ1)1^6$@>|3z;(Rme;p27s`EG zROr^qX{FRi?}MZ15EUbpQMoTmUbVEmq^6*ym;)swWL1;&T7@RaXj)a?Tx2+j&>5n^ zBbc6m$GdUlrCWP-PG-bY)Rq+Kz$nw$>nxiD^CyrxcavF2#oTfVOwn}fUMI9u&<#rPb?Bp z5^Oq&ye@s^6h%;rZZ?{PBZ?Lg2}P;yYU<_^Gt=YbitBiiUd+r*VQP8?Sr+yInSvpy zgBK!3^mx6h9a-RSq+Q@;m`+d5Ds(JW^USMF$oK{*G6a~5B{Nr4r%+Vo0I7km;Vl*~Frg2!0Tribv_?Zp(wd!*DU~A@kMp|~hIp9OOakqsA!$*!9?x;&A6DSB|<99uR_|> z^t5Cmi@pKBYUrn#dM2McgIjj&#_oZQa*b?P;U(|t@sjI>C0X)%RVd%WP!6~Euog5| zpV#{xR(gT78k?FT2(za3(SXwVlDWv~v~aDox_Eglyf4l0k|f^D+O@l@9g(JxS`{RB zO7T;gk^zaw5-QJ4j8U}-2H80B)I?hfw(*4WYMHH&PG>MaK29$awSmjn@}2r*!bzvLx2aWC&}zdvM3z8gc|P44n;FADyzdso8G5Q2&Z~7td$Ijff2D3IBFN>XyNf|Y3+tqWBn$YQ zs{Uj`W;jZ<gaf`!c7cOwZvk;LPNlGIU3znN((ysOeG0kR0mqQXnNDR7&RU1EiEJ z(|bBQJ1IzMsxj}xGd;(|!E(?osC80@wX@n|F zIATXKUWT6vPL$5DwtY80lgV}XLf~cYDf6-`))~ro8*-CFRw%#w#qY5D7Gz(;PzNt% zQ<$4Phb(iE!|GPGh}Yv&q2O{mFD|r!IgQQBkr$LKmn$l?!~hJ!os(QJp}b8}f@}__NmB%Tq%A7kRKQ5u zrP)kwmBvV^Srl>C=3?2`OcJlNM&DL9-qFA-xFOHW8c?A!aEHKn`}%G8z`Z|(BM^nm zC(os082;CLu{d@DHkU;$e(^GCTVw`kuAmlZOP-2Gnv|R#X7YBku$T9172akc2lRRUm?CJ*u@sc@Ff0=#=_Mhij1HMGq9wKq?R~_hXm-Mg?X?q6{mPslwF2d`Ty`X4@@nm-_02`3}5-?G@ejL~;Ok zc$4h7WBpcq@T(8P;SVbwSQ?+i_EQ_VnLd}0O7Z@v}IzRlRWZ6BJL$K+(^$*b}dD(yZu zbwdLkejnz){{V3AB-X#}MpSz`5G$}wb$KaYB@qaBGIU)^l@TIbDr8mu%a^LEoD%&` zEzn`uwh(+0$+#-TL~?Q|;Hpp*$eB4%I(~#?A9lf+Z1?6qQa1gF3JWemP)2f_#TV19NE z`DOMWVv`6sE2`X5_eY?&X>HqZY~II^ZdWqALG~@2Qtq|tWL{fdSyp$goG0lXT;IsM{(dHh~s4fLC0vNxv zn~%v>JJ5EaICA&1wQF{hj26|s3xdo=YAL@MQC0yp+t4@EfeQO15hm(rmlKX5jykKV zY$1WHOzNb5;kz#CI+Syg*=>im@Vpkdd zisKPH7MX*)J3Fw%+!{;9RPRO5S4y>>o~;C{h`JV`Dm32Rul3HqulU|sSKH*idyIW%p9meMC5nD*%#J@ZUJ1I&wF@-b*063`@J6IO7 zoO88bi|_pKIM%iM)vcIv#k9&{%LFAiee->a@*wB6vCHpwOa)U;g;o3s?3^S znPpD8K%PZ8iH=@OQNFk35M}b8WWV>oCq9B4jaN>vAIm&>bS8#W)r+-VAyk6hNM?%W zWJX!`)zr*Ff0|xtMUtQ(Q;WrS=BN(_0zP$nDXEuL-lf5sD5OqLO_57RRY76y zJvr*?&4dq1srvuKEkGN$*pl&HUfU-HVC3(W_&fn3iHb<(^pZ+}Bzo*HBJ34=LOzo3 z97>)hq^9Q>hHji39>qX+H*)Y&VPR##aC3*;2+9--cIPFR`N$=+aw9SlW8r*Kg$dUj z8yi!%SMd_bCq_rHc`MtR?5+(^5O8|}>ih&KL{)hyVc@lFud4y;TOwwBl27lg8&ZVP z`IUKfV=}?J)Y2@{=bwhVFpvH{*P~>Vm_K7A*+kbeNiDfrB4@`!jxv-tiWtS!c9w=Q zHq0FY3h5Qn57Nc&a@Y>fiKoY)cI0;e7bzgkF?{fZz7*IBK z555WxGKs00000NkvXXu0mjf D7dhUA literal 0 HcmV?d00001 diff --git a/assets/images/banner.png b/assets/images/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..b83bfad81d8d07bd57a57b7a5966df835dbc87f3 GIT binary patch literal 64824 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 const CreateAccountScreen(), '/MainScreen': (context) => const MainScreenWidget(), '/Settings': (context) => const SettingsScreen(), + '/QrCodeScreen': (context) => const QrCodeScreen(), }, ); } diff --git a/lib/pages/home_screen/home_screen_widget.dart b/lib/pages/home_screen/home_screen_widget.dart index 48391a0..c1228f0 100644 --- a/lib/pages/home_screen/home_screen_widget.dart +++ b/lib/pages/home_screen/home_screen_widget.dart @@ -10,6 +10,7 @@ import 'package:drifter/pages/home_screen/widgets/message_text_button_widget.dar import 'package:drifter/pages/home_screen/widgets/message_text_form_field_widget.dart'; import 'package:drifter/pages/profile_screen/widgets/message_snack_bar.dart'; import 'package:nostr_tools/nostr_tools.dart'; +import 'package:toggle_switch/toggle_switch.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -20,6 +21,7 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { bool _isConnected = false; + List isSelected = [true, false]; final List _events = []; final Map _metaDatas = {}; @@ -95,43 +97,123 @@ class _HomeScreenState extends State { onRefresh: () async { await _resubscribeStream(); }, - child: StreamBuilder( - stream: _controller.stream, - builder: (context, snapshot) { - // Inside the builder callback, the snapshot object contains the most recent event from the thread. - // If snapshot.hasData is true, there is data to display. In this case, ListView.builder is returned, which displays a list of NoostCard widgets. - if (snapshot.hasData) { - return ListView.builder( - // The itemCount property of ListView.builder is set to _events.length, , which is the number of events in the _events list. - itemCount: _events.length, - itemBuilder: (context, index) { - final event = _events[index]; - final metadata = _metaDatas[event.pubkey]; - // For each event, a Noost object is created that encapsulates the details of the event, including id, avatarUrl, name,username, time, content и pubkey. - // _metaDatas, you can map the event public key to the author's metadata. - final domain = Domain( - noteId: event.id, - avatarUrl: metadata?.picture ?? - 'https://robohash.org/${event.pubkey}', - name: metadata?.name ?? 'Anon', - username: metadata?.displayName ?? - (metadata?.display_name ?? 'Anon'), - time: TimeAgo.format(event.created_at), - content: event.content, - pubkey: event.pubkey, - ); - return DomainCard(domain: domain); + child: Column( + children: [ + // ToggleButtons( + // isSelected: isSelected, + // renderBorder: false, + // fillColor: Color(0xFFFFFFFF), + // selectedColor: Color(0xFF4F46F1), + // disabledColor: Color(0xFF837CA3), + // children: [ + // Container( + // margin: const EdgeInsets.all(4), + // child: const Text('Following')), + // Container( + // margin: const EdgeInsets.all(4), + // child: const Text('Global')), + // ], + // onPressed: (int newIndex) { + // setState(() { + // for (int index = 0; index < isSelected.length; index++) { + // if (index == newIndex) { + // isSelected[index] = true; + // } else { + // isSelected[index] = false; + // } + // } + // }); + // }), + ToggleSwitch( + minWidth: double.infinity, + minHeight: 40, + totalSwitches: 2, + labels: ['Following', 'Global'], + activeBgColor: [AppColors.toggleSwitchActiveBg], + activeFgColor: AppColors.toggleSwitchTextActive, + inactiveBgColor: AppColors.toggleSwitchBg, + inactiveFgColor: AppColors.toggleSwitchTextInactive, + activeBorders: [ + Border.all( + color: AppColors.toggleSwitchBg, + width: 4, + ), + ], + radiusStyle: true, + cornerRadius: 100, + customTextStyles: [ + TextStyle(fontSize: 14, fontWeight: FontWeight.w600) + ], + onToggle: (indexToggle) { + print(indexToggle); + }, + ), + SizedBox( + height: 12, + ), + Flexible( + child: StreamBuilder( + stream: _controller.stream, + builder: (context, snapshot) { + // Inside the builder callback, the snapshot object contains the most recent event from the thread. + // If snapshot.hasData is true, there is data to display. In this case, ListView.builder is returned, which displays a list of NoostCard widgets. + if (snapshot.hasData) { + return ListView.builder( + // The itemCount property of ListView.builder is set to _events.length, , which is the number of events in the _events list. + itemCount: _events.length, + itemBuilder: (context, index) { + final event = _events[index]; + final metadata = _metaDatas[event.pubkey]; + // For each event, a Noost object is created that encapsulates the details of the event, including id, avatarUrl, name,username, time, content и pubkey. + // _metaDatas, you can map the event public key to the author's metadata. + final domain = Domain( + noteId: event.id, + avatarUrl: metadata?.picture ?? + 'https://robohash.org/${event.pubkey}', + name: metadata?.name ?? 'Anon', + username: metadata?.displayName ?? + (metadata?.display_name ?? 'Anon'), + time: TimeAgo.format(event.created_at), + content: event.content, + pubkey: event.pubkey, + ); + return DomainCard(domain: domain); + }, + ); + } else if (snapshot.connectionState == + ConnectionState.waiting) { + return const Center(child: Text('Loading....')); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } + return const CenteredCircularProgressIndicator(); }, - ); - } else if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: Text('Loading....')); - } else if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}')); - } - return const CenteredCircularProgressIndicator(); - }, + ), + ), + ], ), ), + + // ToggleButtons( + // isSelected: isSelected, + // children: [ + // SizedBox(width: 300, child: Text('Following')), + // SizedBox(child: Text('Global')), + // ], + // onPressed: (int newIndex) { + // setState(() { + // for (int index = 0; + // index < isSelected.length; + // index++) { + // if (index == newIndex) { + // isSelected[index] = true; + // } else { + // isSelected[index] = false; + // } + // } + // }); + // }), + floatingActionButton: Keys.keysExist ? CreatePost( // The publishNote function is called when the user launches the "Noost!" button in the dialog box. @@ -318,12 +400,15 @@ class DomainCard extends StatelessWidget { color: Colors.transparent, ), Center( - child: FadeInImage( - placeholder: const NetworkImage( - 'https://media.tenor.com/On7kvXhzml4AAAAj/loading-gif.gif', + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: FadeInImage( + placeholder: const NetworkImage( + 'https://media.tenor.com/On7kvXhzml4AAAAj/loading-gif.gif', + ), + image: NetworkImage(imageLinks.first), + fit: BoxFit.cover, ), - image: NetworkImage(imageLinks.first), - fit: BoxFit.cover, ), ), ], diff --git a/lib/pages/profile_screen/profile_screen.dart b/lib/pages/profile_screen/profile_screen.dart index 4d656f7..330e025 100644 --- a/lib/pages/profile_screen/profile_screen.dart +++ b/lib/pages/profile_screen/profile_screen.dart @@ -132,94 +132,390 @@ class ProfileScreenState extends State { @override Widget build(BuildContext context) { - privateKeyInput.text = _toHex ? Keys.nsecKey : Keys.privateKey; - publicKeyInput.text = _toHex ? Keys.npubKey : Keys.publicKey; + publicKeyInput.text = Keys.npubKey; - return ListView( - children: [ - const SizedBox( - height: 60, - ), - const UserInfo(), - const SizedBox( - height: 40, - ), - FormKeys(), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + return Container( + color: AppColors.profileWrapperBg, + child: Column( + children: [ + Stack( children: [ - Keys.keysExist - ? IconButton( - onPressed: () { - setState(() { - _toHex = !_toHex; - }); - }, - icon: const Icon(Icons.refresh)) - // ElevatedButton( - // style: ButtonStyle( - // backgroundColor: - // MaterialStateProperty.all(AppColors.background)), - // onPressed: () { - // keysExistDialog( - // Nostr.instance.keysService - // .encodePublicKeyToNpub(Keys.publicKey), - // Nostr.instance.keysService - // .encodePrivateKeyToNsec(Keys.privateKey), - // ); - // }, - // child: const Text( - // 'Keys', - // ), - // ) - : Row( - children: [ - ElevatedButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - AppColors.background)), - onPressed: () { - modalBottomSheet(); - }, - child: const Text( - 'Generate Keys', - ), - ), - SizedBox(width: 100), - ElevatedButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - AppColors.background)), - onPressed: () { - Navigator.pushNamed(context, '/login').then((_) { - initState(); - }); - }, - child: const Text( - 'Login', - ), - ), - ], + Container( + width: double.infinity, + height: 70, + child: FittedBox( + child: Image.asset('assets/images/banner.png'), + fit: BoxFit.fill, + ), + ), + Row( + children: [ + Padding( + padding: + const EdgeInsets.only(top: 30, left: 16, right: 58), + child: ClipRRect( + borderRadius: BorderRadius.circular(200), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + border: Border.all( + width: 2, color: Color(0xFFF2EFFF))), + child: Image.asset('assets/images/avatar.png')), ), - Keys.keysExist - ? Row( - children: [ - IconButton( - onPressed: () { - deleteKeysDialog(); - }, - icon: const Icon(Icons.delete)), - ], - ) - : Container(), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 74, right: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + width: 32, + height: 32, + child: FloatingActionButton( + backgroundColor: + AppColors.buttonSecondaryDefaultBg, + onPressed: () {}, + elevation: 0, + child: Icon( + Icons.edit, + size: 18, + color: AppColors.buttonSecondaryIcon, + ), + ), + ), + SizedBox( + width: 8, + ), + SizedBox( + width: 32, + height: 32, + child: FloatingActionButton( + backgroundColor: + AppColors.buttonSecondaryDefaultBg, + onPressed: () { + Navigator.pushNamed(context, '/QrCodeScreen'); + }, + elevation: 0, + child: Icon( + Icons.qr_code, + size: 18, + color: AppColors.buttonSecondaryIcon, + ), + ), + ), + SizedBox( + width: 8, + ), + SizedBox( + width: 32, + height: 32, + child: FloatingActionButton( + backgroundColor: + AppColors.buttonSecondaryDefaultBg, + onPressed: () {}, + elevation: 0, + child: Icon( + Icons.mail_outlined, + size: 18, + color: AppColors.buttonSecondaryIcon, + ), + ), + ), + SizedBox( + width: 8, + ), + SizedBox( + width: 100, + height: 32, + child: ElevatedButton( + onPressed: () {}, + child: Text( + 'Follow', + style: TextStyle( + color: + AppColors.buttonPrimaryDefaultText), + ), + style: ElevatedButton.styleFrom( + backgroundColor: + AppColors.buttonPrimaryDefaultBg, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(100)))), + ) + ], + ), + ), + ) + ], + ) ], ), - ) - ], + Container( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Cameron Williamson', + style: TextStyle( + color: AppColors.profileFullName, + fontWeight: FontWeight.w600), + ), + SizedBox( + height: 5, + ), + Text('@cameron', + style: TextStyle( + color: AppColors.profileUserName, + )), + SizedBox( + height: 5, + ), + Text( + 'This is the description of my profile. I am a nostrich and I love cats. Follow me for fun pictures and bad jokes. I also want to be a politician.', + style: TextStyle( + color: AppColors.profileSummary, + )), + SizedBox( + height: 12, + ), + Container( + child: TextField( + style: TextStyle(color: AppColors.profileKeyText), + readOnly: true, + decoration: InputDecoration( + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(Icons.copy), + color: AppColors.buttonSecondaryIcon, + onPressed: () {}, + ), + IconButton( + icon: Icon(Icons.qr_code), + color: AppColors.buttonSecondaryIcon, + onPressed: () {}, + ), + ], + ), + filled: true, + fillColor: AppColors.profileKeyField, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: + BorderSide(width: 0, style: BorderStyle.none), + )), + minLines: 2, + maxLines: 2, + controller: publicKeyInput, + ), + ), + Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton.icon( + // <-- TextButton + onPressed: () {}, + icon: Icon( + Icons.check_circle, + size: 15.0, + color: AppColors.profileSocialIcons, + ), + label: Text( + 'nip-05-name', + style: TextStyle( + color: AppColors.profileSocialLinks, + fontWeight: FontWeight.w500), + ), + ), + SizedBox( + height: 6, + ), + TextButton.icon( + // <-- TextButton + onPressed: () {}, + icon: Icon( + Icons.check_circle, + size: 15.0, + color: AppColors.profileSocialIcons, + ), + label: Text( + 'www.cameronforpresident.com', + style: TextStyle( + color: AppColors.profileSocialLinks, + fontWeight: FontWeight.w500), + ), + ), + SizedBox( + height: 6, + ), + TextButton.icon( + // <-- TextButton + onPressed: () {}, + icon: Icon( + Icons.score, + size: 15.0, + color: AppColors.profileSocialIcons, + ), + label: Text( + '@cameron_official', + style: TextStyle( + color: AppColors.profileSocialLinks, + fontWeight: FontWeight.w500), + ), + ), + SizedBox( + height: 6, + ), + TextButton.icon( + // <-- TextButton + onPressed: () {}, + icon: Icon( + Icons.check_circle, + size: 15.0, + color: AppColors.profileSocialIcons, + ), + label: Text( + 'cameron.williamson.com/@cameron_official', + style: TextStyle( + color: AppColors.profileSocialLinks, + fontWeight: FontWeight.w500), + ), + ), + SizedBox( + height: 6, + ), + TextButton.icon( + // <-- TextButton + onPressed: () {}, + icon: Icon( + Icons.flash_on, + size: 15.0, + color: AppColors.profileSocialIcons, + ), + label: Text( + 'whoknowswhatgoeshere', + style: TextStyle( + color: AppColors.profileSocialLinks, + fontWeight: FontWeight.w500), + ), + ), + SizedBox( + height: 6, + ), + TextButton.icon( + // <-- TextButton + onPressed: () {}, + icon: Icon( + Icons.flash_on, + size: 15.0, + color: AppColors.profileSocialIcons, + ), + label: Text( + 'cameron@satoshiwallet.com', + style: TextStyle( + color: AppColors.profileSocialLinks, + fontWeight: FontWeight.w500), + ), + ), + ]), + ) + ]), + ), + ), + ], + ), ); + // return ListView( + // children: [ + // const SizedBox( + // height: 60, + // ), + // const UserInfo(), + // const SizedBox( + // height: 40, + // ), + // FormKeys(), + // const SizedBox(height: 20), + // Padding( + // padding: const EdgeInsets.all(16.0), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceAround, + // children: [ + // Keys.keysExist + // ? IconButton( + // onPressed: () { + // setState(() { + // _toHex = !_toHex; + // }); + // }, + // icon: const Icon(Icons.refresh)) + // // ElevatedButton( + // // style: ButtonStyle( + // // backgroundColor: + // // MaterialStateProperty.all(AppColors.background)), + // // onPressed: () { + // // keysExistDialog( + // // Nostr.instance.keysService + // // .encodePublicKeyToNpub(Keys.publicKey), + // // Nostr.instance.keysService + // // .encodePrivateKeyToNsec(Keys.privateKey), + // // ); + // // }, + // // child: const Text( + // // 'Keys', + // // ), + // // ) + // : Row( + // children: [ + // ElevatedButton( + // style: ButtonStyle( + // backgroundColor: MaterialStateProperty.all( + // AppColors.background)), + // onPressed: () { + // modalBottomSheet(); + // }, + // child: const Text( + // 'Generate Keys', + // ), + // ), + // SizedBox(width: 100), + // ElevatedButton( + // style: ButtonStyle( + // backgroundColor: MaterialStateProperty.all( + // AppColors.background)), + // onPressed: () { + // Navigator.pushNamed(context, '/login').then((_) { + // initState(); + // }); + // }, + // child: const Text( + // 'Login', + // ), + // ), + // ], + // ), + // Keys.keysExist + // ? Row( + // children: [ + // IconButton( + // onPressed: () { + // deleteKeysDialog(); + // }, + // icon: const Icon(Icons.delete)), + // ], + // ) + // : Container(), + // ], + // ), + // ) + // ], + // ); } Form FormKeys() { diff --git a/lib/pages/qr_code_screen/qr_code_screen.dart b/lib/pages/qr_code_screen/qr_code_screen.dart new file mode 100644 index 0000000..06525c2 --- /dev/null +++ b/lib/pages/qr_code_screen/qr_code_screen.dart @@ -0,0 +1,166 @@ +import 'package:drifter/models/models.dart'; +import 'package:drifter/pages/terms_of_service/btn_continue_terms_of_service.dart'; +import 'package:drifter/pages/terms_of_service/terms_of_service_text.dart'; +import 'package:drifter/theme/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +class QrCodeScreen extends StatefulWidget { + const QrCodeScreen({super.key}); + + @override + State createState() => _QrCodeScreenState(); +} + +class _QrCodeScreenState extends State { + String _publicKey = Keys.npubKey; + bool isCopied = false; + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: AppColors.topNavIconBack, + ), + onPressed: () => Navigator.of(context).pop(), + ), + elevation: 0, + backgroundColor: AppColors.mainBackground, + ), + backgroundColor: AppColors.mainBackground, + body: Padding( + padding: + const EdgeInsets.only(top: 10, right: 16, left: 16, bottom: 16), + child: Center( + child: Column(children: [ + Text( + 'Follow me on Nostr', + style: TextStyle( + color: Color(0xFF302F38), + fontSize: 20, + fontWeight: FontWeight.w600), + ), + Padding( + padding: const EdgeInsets.only( + top: 24, bottom: 83, right: 48, left: 48), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.white, + ), + height: 406, + width: double.infinity, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 32.0, bottom: 10), + child: ClipRRect( + borderRadius: BorderRadius.circular(200), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + border: Border.all( + width: 2, color: Color(0xFFF2EFFF))), + child: Image.asset('assets/images/avatar.png')), + ), + ), + Text( + 'Cameron Williams on', + style: TextStyle( + color: Color(0xFF302F38), + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 4), + Text( + '@cameron', + style: TextStyle( + fontSize: 12, color: AppColors.profileUserName), + ), + SizedBox(height: 20), + Container( + width: 230, + child: Text(_publicKey, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.profileSocialLinks))), + SizedBox(height: 20), + Container( + width: 143, + height: 143, + child: QrImageView( + eyeStyle: QrEyeStyle( + color: AppColors.qrCodeBody, + eyeShape: QrEyeShape.square), + dataModuleStyle: + QrDataModuleStyle(color: AppColors.qrCodeBody), + embeddedImage: AssetImage( + 'assets/images/logo/drifter_logo_white.png'), + data: _publicKey, + version: QrVersions.auto, + ), + ), + ], + )), + ), + Expanded(child: SizedBox()), + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100)), + backgroundColor: AppColors.buttonSecondaryDefaultBg), + onPressed: () { + Clipboard.setData(ClipboardData(text: _publicKey)) + .then((value) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Copied to clipboard'), + )); + isCopied = true; + setState(() {}); + }); + }, + child: isCopied + ? Text( + 'Copied', + style: TextStyle(color: AppColors.buttonSecondaryText), + ) + : Text( + 'Copy user ID', + style: TextStyle(color: AppColors.buttonSecondaryText), + ), + ), + ), + SizedBox(height: 12), + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100)), + backgroundColor: AppColors.buttonPrimaryDefaultBg), + onPressed: () {}, + child: Text( + 'Share QR code', + style: TextStyle(color: AppColors.buttonPrimaryDefaultText), + ), + ), + ), + ]), + ), + ), + ); + } +} diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart index a727e69..aa09de9 100644 --- a/lib/theme/app_colors.dart +++ b/lib/theme/app_colors.dart @@ -4,6 +4,8 @@ abstract class AppColors { static const background = Color(0xFF4f46f1); static const mainAccent = Color(0xFFFFCC11); static const mainBackground = Color(0xFFF2EFFF); + + // Button static const buttonPrimaryDefaultBg = Color(0xFF4F46F1); static const buttonPrimaryDefaultText = Color(0xFFFFFFFF); static const buttonPrimaryDisabledBg = Color(0x1A18171B); @@ -11,6 +13,9 @@ abstract class AppColors { static const buttonOutlinedDefaultBorder = Color(0xFF4F46F1); static const buttonOutlinedDefaultText = Color(0xFF4F46F1); static const buttonOutlinedPressedBg = Color(0xFFE3E0F9); + static const buttonSecondaryDefaultBg = Color(0xFFDCDAFC); + static const buttonSecondaryIcon = Color(0xFF2D25BC); + static const buttonSecondaryText = Color(0xFF2D25BC); // TopNav @@ -72,4 +77,18 @@ abstract class AppColors { static const postActionIconDefault = Color(0xFFC8C5D0); static const postActionIconPressed = Color(0xFFFE82B1); static const postBookmark = Color(0xFF8482FF); + +// Profile + + static const profileWrapperBg = Color(0xFFF9F8FF); + static const profileFullName = Color(0xFF302F38); + static const profileUserName = Color(0xFF787680); + static const profileSummary = Color(0xFF302F38); + static const profileKeyField = Color(0xFFF2EFFF); + static const profileKeyText = Color(0xFF6E61A0); + static const profileSocialLinks = Color(0xFF4F46F1); + static const profileSocialIcons = Color(0xFF837CA3); + +// QR code + static const qrCodeBody = Color(0xFF25207A); } diff --git a/pubspec.lock b/pubspec.lock index 189d259..7592d65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -352,6 +352,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.3" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" sky_engine: dependency: transitive description: flutter @@ -479,4 +495,4 @@ packages: version: "6.2.2" sdks: dart: ">=2.19.6 <3.0.0" - flutter: ">=3.7.0-0" + flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index d37d8e9..8fc49f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 another_flushbar: ^1.12.29 + qr_flutter: ^4.1.0 dev_dependencies: flutter_test: @@ -64,8 +65,9 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/images//icons/ - - assets/images//logo/ + - assets/images/ + - assets/images/icons/ + - assets/images/logo/ # - images/a_dot_ham.jpeg -- 2.45.2