diff --git a/lib/main.dart b/lib/main.dart index 3402e90..b58b6a4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,9 @@ +import 'dart:io'; + 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'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; void main() { @@ -21,7 +25,7 @@ class MyApp extends StatelessWidget { unselectedItemColor: Colors.grey, ), ), - home: const MainScreenWidget(), + home: const Splash(), ); } } diff --git a/lib/models/keys.dart b/lib/models/keys.dart index 6a51293..85335ea 100644 --- a/lib/models/keys.dart +++ b/lib/models/keys.dart @@ -1,5 +1,4 @@ -import 'package:dart_nostr/dart_nostr.dart'; -import 'package:flutter/material.dart'; +import 'package:nostr_tools/nostr_tools.dart'; class Keys { static String privateKey = ''; @@ -8,3 +7,7 @@ class Keys { static String npubKey = ''; static bool keysExist = false; } + +class Relay { + static final relay = RelayApi(relayUrl: 'wss://relay.damus.io'); +} diff --git a/lib/pages/home_screen/home_screen_widget.dart b/lib/pages/home_screen/home_screen_widget.dart index d23e7ce..175f735 100644 --- a/lib/pages/home_screen/home_screen_widget.dart +++ b/lib/pages/home_screen/home_screen_widget.dart @@ -1,457 +1,440 @@ -// import 'dart:async'; -// import 'dart:convert'; -// import 'dart:html'; -// -// import 'package:flutter/material.dart'; -// import 'package:drifter/domain_models/domain_models.dart'; -// import 'package:drifter/models/keys.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'; -// import 'package:drifter/pages/home_screen/widgets/message_text_form_field_widget.dart'; -// import 'package:drifter/pages/profile_screen/profile_screen_widgets/message_snack_bar.dart'; -// -// import 'package:drifter/pages/profile_screen/profile_screen.dart'; -// import 'package:dart_nostr/dart_nostr.dart'; -// -// class HomeScreen extends StatefulWidget { -// const HomeScreen({super.key}); -// -// @override -// State createState() => _HomeScreenState(); -// } -// -// class _HomeScreenState extends State { -// bool _isConnected = false; -// -// final relay = Nostr.instance.relaysService.init( -// relaysUrl: ['wss://relay.damus.io'], -// onRelayListening: (String relayUrl, receivedData) {}, -// onRelayError: (String relayUrl, Object? error) {}, -// onRelayDone: (String relayUrl) {}, -// lazyListeningToRelays: false, -// ); -// final List _events = []; -// final Map _metaDatas = {}; -// late Stream _stream; -// final _controller = StreamController(); -// -// bool _isNotePublishing = false; -// -// @override -// void initState() { -// _initStream(); -// super.initState(); -// } -// -// Future> _connectToRelay() async { -// // final stream = await relay.connect(); -// -// // relay.on((event) { -// // if (event == RelayEvent.connect) { -// // setState(() => _isConnected = true); -// // } else if (event == RelayEvent.error) { -// // setState(() => _isConnected = false); -// // } -// // }); -// -// NostrRequest req = NostrRequest( -// filters: [ -// NostrFilter( -// kinds: [1], -// t: ["p", "..."], -// authors: ["..."], -// ), -// ], -// ); -// -// final stream = -// Nostr.instance.relaysService.startEventsSubscription(request: req); -// -// // listening to the stream. -// stream.listen((event) { -// print(event); -// }); -// return stream.listen((event) { -// print(event); -// }); -// } -// -// void _initStream() async { -// _stream = (await _connectToRelay()) as Stream; -// _stream.listen((message) { -// final event = message; -// if (event.kind == 1) { -// setState(() => _events.add(event)); -// relay.sub([ -// NostrFilter(kinds: [0], authors: [event.pubkey]) -// ]); -// } else if (event.kind == 0) { -// final metadata = Metadata.fromJson(jsonDecode(event.content)); -// setState(() => _metaDatas[event.pubkey] = metadata); -// } -// _controller.add(event); -// }); -// } -// -// // The _resubscribeStream() method clears the _events and _metaData scollection after a 1 second delay -// Future _resubscribeStream() async { -// await Future.delayed(const Duration(seconds: 1), () { -// setState(() { -// _events.clear(); -// _metaDatas.clear(); -// }); -// // _initStream() method, responsible for initializing and subscribing to the stream, to reconnect and re-subscribe to the filter. -// _initStream(); -// }); -// } -// -// @override -// Widget build(BuildContext context) { -// return Scaffold( -// body: RefreshIndicator( -// 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 pages. -// 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(); -// }, -// ), -// ), -// floatingActionButton: Keys.keysExist -// ? CreatePost( -// // The publishNote function is called when the user launches the "Noost!" button in the dialog box. -// publishNote: (note) { -// setState(() => _isNotePublishing = true); -// -// // EventApi Creates an instance of the class defined in the nostr_tools package. -// final eventApi = EventApi(); -// // The finishEvent method of the EventApi class is called with the Event object and the _privateKey variable. -// // finishEvent will set the event id with the event hash and sign the event with the given _privateKey. -// final event = NostrEvent( -// pubkey: '', -// kind: 0, -// content: 'This is a test event content', -// createdAt: DateTime.now(), -// id: '', // you will need to generate and set the id of the event manually by hashing other event fields, please refer to the official Nostr protocol documentation to learn how to do it yourself. -// tags: [], -// sig: -// '', // you will need to generate and set the signature of the event manually by signing the event's id, please refer to the official Nostr protocol documentation to learn how to do it yourself. -// ); -// if (eventApi.verifySignature(event)) { -// try { -// // If the signature is verified, the _relay method is called for the object to publish the event. -// relay.publish(event); -// // After the _resubscribeStream event is published, a method is called that will probably update the stream or subscription to reflect the recently published event. -// _resubscribeStream(); -// // Show SnackBar to display a message that the note has been successfully published. -// ScaffoldMessenger.of(context).showSnackBar( -// MessageSnackBar( -// label: 'Congratulations! Noost Published!'), -// ); -// } catch (_) { -// // If an error occurs during the publishing process (e.g., an exception is caught), SnackBar displays a warning instead. -// ScaffoldMessenger.of(context).showSnackBar(MessageSnackBar( -// label: 'Oops! Something went wrong!', -// isWarning: true, -// )); -// } -// } -// setState(() => _isNotePublishing = false); -// Navigator.pop(context); -// }, -// isNotePublishing: _isNotePublishing, -// ) -// : -// // If _keysExist is false, then an empty widget is displayed, which means that the FAB will not be visible. Container() -// Container(), -// ); -// } -// } -// -// class TimeAgo { -// static String format(int timestamp) { -// DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); -// Duration difference = DateTime.now().difference(dateTime); -// -// String timeAgo = ''; -// -// if (difference.inDays > 0) { -// timeAgo = -// '${difference.inDays} ${difference.inDays == 1 ? 'day' : 'days'} ago'; -// } else if (difference.inHours > 0) { -// timeAgo = -// '${difference.inHours} ${difference.inHours == 1 ? 'hour' : 'hours'} ago'; -// } else if (difference.inMinutes > 0) { -// timeAgo = -// '${difference.inMinutes} ${difference.inMinutes == 1 ? 'minute' : 'minutes'} ago'; -// } else { -// timeAgo = 'just now'; -// } -// -// return timeAgo; -// } -// } -// -// class DomainCard extends StatelessWidget { -// const DomainCard({ -// super.key, -// required this.domain, -// }); -// -// final Domain domain; -// -// List? extractImage(String text) { -// final RegExp exp = RegExp( -// r"(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png|jpeg)", -// caseSensitive: false, -// multiLine: true, -// ); -// -// final Iterable matches = exp.allMatches(text); -// -// final List imageLinks = -// matches.map((match) => match.group(0)!).toList(); -// -// return imageLinks.isNotEmpty ? imageLinks : null; -// } -// -// @override -// Widget build(BuildContext context) { -// final List? imageLinks = extractImage(domain.content); -// return Container( -// margin: const EdgeInsets.all(8), -// 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), -// ), -// ], -// ), -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// ListTile( -// leading: CircleAvatar( -// 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), -// ), -// Divider(height: 1, color: Colors.grey.shade400), -// Padding( -// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), -// child: Text(domain.content, -// style: const TextStyle(color: Colors.white)), -// ), -// if (imageLinks != null && imageLinks.isNotEmpty) -// Center( -// child: Stack( -// children: [ -// const Placeholder( -// fallbackHeight: 200, -// color: Colors.transparent, -// ), -// Center( -// child: FadeInImage( -// placeholder: const NetworkImage( -// 'https://i.ibb.co/D9jqXgR/58038897-167f0280-7ae6-11e9-94eb-88e880a25f0f.gif', -// ), -// image: NetworkImage(imageLinks.first), -// fit: BoxFit.cover, -// ), -// ), -// ], -// ), -// ), -// ], -// ), -// ); -// } -// } -// -// class CenteredCircularProgressIndicator extends StatelessWidget { -// const CenteredCircularProgressIndicator({Key? key}) : super(key: key); -// -// @override -// Widget build(BuildContext context) { -// return const Center( -// child: CircularProgressIndicator(), -// ); -// } -// } -// -// class CreatePost extends StatefulWidget { -// const CreatePost({ -// Key? key, -// required this.publishNote, -// required this.isNotePublishing, -// }) : super(key: key); -// -// final Function(String?) publishNote; -// final bool isNotePublishing; -// -// @override -// State createState() => _CreatePostState(); -// } -// -// class _CreatePostState extends State { -// final _noteController = TextEditingController(); -// final GlobalKey _formKey = GlobalKey(); -// -// @override -// Widget build(BuildContext context) { -// return FloatingActionButton( -// backgroundColor: Colors.deepPurpleAccent, -// tooltip: 'Create a new post', -// elevation: 2, -// highlightElevation: 4, -// foregroundColor: Colors.white, -// child: Stack( -// alignment: Alignment.center, -// children: [ -// Container( -// width: 60, -// height: 60, -// decoration: const BoxDecoration( -// shape: BoxShape.circle, color: AppColors.mainDarkBlue), -// ), -// const Icon( -// Icons.add, -// color: Colors.white, -// ), -// ], -// ), -// onPressed: () async { -// _noteController.clear(); -// await showDialog( -// barrierDismissible: false, -// context: context, -// builder: ((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( -// 'Create a Noost', -// style: TextStyle( -// fontSize: 24, -// fontWeight: FontWeight.bold, -// color: Colors.white, -// ), -// ), -// ), -// ), -// const SizedBox(height: 24), -// Padding( -// padding: const EdgeInsets.symmetric(horizontal: 24), -// child: MessageTextFormField( -// maxLines: 5, -// hintText: 'Type your Noost here...', -// controller: _noteController, -// formKey: _formKey, -// validator: (value) { -// if (value == null || value.trim().isEmpty) { -// return 'Please enter your note.'; -// } -// return null; -// }, -// ), -// ), -// const SizedBox(height: 24), -// widget.isNotePublishing -// ? const CenteredCircularProgressIndicator() -// : Row( -// mainAxisAlignment: MainAxisAlignment.end, -// children: [ -// MessageTextButton( -// onPressed: () { -// Navigator.pop(context); -// }, -// label: 'Cancel', -// ), -// const SizedBox(width: 16), -// MessageOkButton( -// onPressed: () { -// if (_formKey.currentState!.validate()) { -// widget.publishNote( -// _noteController.text.trim()); -// } else { -// _formKey.currentState?.setState(() {}); -// } -// }, -// label: 'Noost!', -// ), -// const SizedBox(width: 24), -// ], -// ) -// ], -// ), -// ), -// ); -// }), -// ); -// }, -// ); -// } -// } +import 'dart:async'; +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/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'; +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'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + bool _isConnected = false; + + final List _events = []; + final Map _metaDatas = {}; + late Stream _stream; + final _controller = StreamController(); + + bool _isNotePublishing = false; + + @override + void initState() { + _initStream(); + super.initState(); + } + + Future> _connectToRelay() async { + final stream = await Relay.relay.connect(); + + Relay.relay.on((event) { + if (event == RelayEvent.connect) { + setState(() => _isConnected = true); + } else if (event == RelayEvent.error) { + setState(() => _isConnected = false); + } + }); + + Relay.relay.sub([ + Filter( + kinds: [1], + limit: 100, + t: ['nostr'], + ) + ]); + + return stream + .where((message) => message.type == 'EVENT') + .map((message) => message.message); + } + + void _initStream() async { + _stream = await _connectToRelay(); + _stream.listen((message) { + final event = message; + if (event.kind == 1) { + setState(() => _events.add(event)); + Relay.relay.sub([ + Filter(kinds: [0], authors: [event.pubkey]) + ]); + } else if (event.kind == 0) { + final metadata = Metadata.fromJson(jsonDecode(event.content)); + setState(() => _metaDatas[event.pubkey] = metadata); + } + _controller.add(event); + }); + } + +// The _resubscribeStream() method clears the _events and _metaData scollection after a 1 second delay + Future _resubscribeStream() async { + await Future.delayed(const Duration(seconds: 1), () { + setState(() { + _events.clear(); + _metaDatas.clear(); + }); + // _initStream() method, responsible for initializing and subscribing to the stream, to reconnect and re-subscribe to the filter. + _initStream(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: RefreshIndicator( + 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); + }, + ); + } 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(); + }, + ), + ), + floatingActionButton: Keys.keysExist + ? CreatePost( + // The publishNote function is called when the user launches the "Noost!" button in the dialog box. + publishNote: (note) { + setState(() => _isNotePublishing = true); + + // EventApi Creates an instance of the class defined in the nostr_tools package. + final eventApi = EventApi(); + // The finishEvent method of the EventApi class is called with the Event object and the _privateKey variable. + // finishEvent will set the event id with the event hash and sign the event with the given _privateKey. + final event = eventApi.finishEvent( + // This creates a new instance of the Event class with certain properties, such as: + Event( + kind: 1, + tags: [ + ['t', 'nostr'] + ], + content: note!, + created_at: DateTime.now().millisecondsSinceEpoch ~/ 1000, + ), + Keys.publicKey, + ); + if (eventApi.verifySignature(event)) { + try { + // If the signature is verified, the _relay method is called for the object to publish the event. + Relay.relay.publish(event); + // After the _resubscribeStream event is published, a method is called that will probably update the stream or subscription to reflect the recently published event. + _resubscribeStream(); + // Show SnackBar to display a message that the note has been successfully published. + ScaffoldMessenger.of(context).showSnackBar( + MessageSnackBar( + label: 'Congratulations! Noost Published!'), + ); + } catch (_) { + // If an error occurs during the publishing process (e.g., an exception is caught), SnackBar displays a warning instead. + ScaffoldMessenger.of(context).showSnackBar(MessageSnackBar( + label: 'Oops! Something went wrong!', + isWarning: true, + )); + } + } + setState(() => _isNotePublishing = false); + Navigator.pop(context); + }, + isNotePublishing: _isNotePublishing, + ) + : + // If _keysExist is false, then an empty widget is displayed, which means that the FAB will not be visible. Container() + Container(), + ); + } +} + +class TimeAgo { + static String format(int timestamp) { + DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + Duration difference = DateTime.now().difference(dateTime); + + String timeAgo = ''; + + if (difference.inDays > 0) { + timeAgo = + '${difference.inDays} ${difference.inDays == 1 ? 'day' : 'days'} ago'; + } else if (difference.inHours > 0) { + timeAgo = + '${difference.inHours} ${difference.inHours == 1 ? 'hour' : 'hours'} ago'; + } else if (difference.inMinutes > 0) { + timeAgo = + '${difference.inMinutes} ${difference.inMinutes == 1 ? 'minute' : 'minutes'} ago'; + } else { + timeAgo = 'just now'; + } + + return timeAgo; + } +} + +class DomainCard extends StatelessWidget { + const DomainCard({ + super.key, + required this.domain, + }); + + final Domain domain; + + List? extractImage(String text) { + final RegExp exp = RegExp( + r"(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png|jpeg)", + caseSensitive: false, + multiLine: true, + ); + + final Iterable matches = exp.allMatches(text); + + final List imageLinks = + matches.map((match) => match.group(0)!).toList(); + + return imageLinks.isNotEmpty ? imageLinks : null; + } + + @override + Widget build(BuildContext context) { + final List? imageLinks = extractImage(domain.content); + return Container( + margin: const EdgeInsets.all(8), + 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), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + leading: CircleAvatar( + 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), + ), + Divider(height: 1, color: Colors.grey.shade400), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text(domain.content, + style: const TextStyle(color: Colors.white)), + ), + if (imageLinks != null && imageLinks.isNotEmpty) + Center( + child: Stack( + children: [ + const Placeholder( + fallbackHeight: 200, + color: Colors.transparent, + ), + Center( + child: FadeInImage( + placeholder: const NetworkImage( + 'https://i.ibb.co/D9jqXgR/58038897-167f0280-7ae6-11e9-94eb-88e880a25f0f.gif', + ), + image: NetworkImage(imageLinks.first), + fit: BoxFit.cover, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class CenteredCircularProgressIndicator extends StatelessWidget { + const CenteredCircularProgressIndicator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Center( + child: CircularProgressIndicator(), + ); + } +} + +class CreatePost extends StatefulWidget { + const CreatePost({ + Key? key, + required this.publishNote, + required this.isNotePublishing, + }) : super(key: key); + + final Function(String?) publishNote; + final bool isNotePublishing; + + @override + State createState() => _CreatePostState(); +} + +class _CreatePostState extends State { + final _noteController = TextEditingController(); + final GlobalKey _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return FloatingActionButton( + backgroundColor: Colors.deepPurpleAccent, + tooltip: 'Create a new post', + elevation: 2, + highlightElevation: 4, + foregroundColor: Colors.white, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, color: AppColors.mainDarkBlue), + ), + const Icon( + Icons.add, + color: Colors.white, + ), + ], + ), + onPressed: () async { + _noteController.clear(); + await showDialog( + barrierDismissible: false, + context: context, + builder: ((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( + 'Create a Noost', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: MessageTextFormField( + maxLines: 5, + hintText: 'Type your Noost here...', + controller: _noteController, + formKey: _formKey, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter your note.'; + } + return null; + }, + ), + ), + const SizedBox(height: 24), + widget.isNotePublishing + ? const CenteredCircularProgressIndicator() + : Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MessageTextButton( + onPressed: () { + Navigator.pop(context); + }, + label: 'Cancel', + ), + const SizedBox(width: 16), + MessageOkButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + widget.publishNote( + _noteController.text.trim()); + } else { + _formKey.currentState?.setState(() {}); + } + }, + label: 'Noost!', + ), + const SizedBox(width: 24), + ], + ) + ], + ), + ), + ); + }), + ); + }, + ); + } +} diff --git a/lib/pages/main_screen/main_screen_widget.dart b/lib/pages/main_screen/main_screen_widget.dart index aefe6c9..06c478c 100644 --- a/lib/pages/main_screen/main_screen_widget.dart +++ b/lib/pages/main_screen/main_screen_widget.dart @@ -1,4 +1,5 @@ // import 'package:drifter/pages/home_screen/home_screen_widget.dart'; +import 'package:drifter/pages/home_screen/home_screen_widget.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'; @@ -26,6 +27,7 @@ class _MainScreenWidgetState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: AppColors.mainBackground, appBar: AppBar( title: Row( // mainAxisAlignment: MainAxisAlignment.center, @@ -53,7 +55,7 @@ class _MainScreenWidgetState extends State { body: IndexedStack( index: _selectedTap, children: const [ - // HomeScreen(), + HomeScreen(), MessageScreen(), ProfileScreen(), ], @@ -61,10 +63,10 @@ class _MainScreenWidgetState extends State { bottomNavigationBar: BottomNavigationBar( currentIndex: _selectedTap, items: const [ - // BottomNavigationBarItem( - // icon: Icon(Icons.home), - // label: 'Home', - // ), + BottomNavigationBarItem( + icon: Icon(Icons.home), + label: 'Home', + ), BottomNavigationBarItem( icon: Icon(Icons.message), label: 'Message', diff --git a/lib/pages/profile_screen/profile_screen.dart b/lib/pages/profile_screen/profile_screen.dart index 994ca46..a1eaf26 100644 --- a/lib/pages/profile_screen/profile_screen.dart +++ b/lib/pages/profile_screen/profile_screen.dart @@ -1,10 +1,10 @@ import 'package:dart_nostr/dart_nostr.dart'; import 'package:drifter/models/keys.dart'; -import 'package:drifter/pages/profile_screen/profile_screen_widgets/delete_keys_dialog.dart'; -import 'package:drifter/pages/profile_screen/profile_screen_widgets/key_exist_dialog.dart'; -import 'package:drifter/pages/profile_screen/profile_screen_widgets/keys_option_modal_bottom_sheet.dart'; -import 'package:drifter/pages/profile_screen/profile_screen_widgets/message_snack_bar.dart'; -import 'package:drifter/pages/profile_screen/profile_screen_widgets/user_info_widget.dart'; +import 'package:drifter/pages/profile_screen/widgets/delete_keys_dialog.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'; +import 'package:drifter/pages/profile_screen/widgets/user_info_widget.dart'; import 'package:drifter/theme/app_colors.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; diff --git a/lib/pages/profile_screen/widgets/delete_keys_dialog.dart b/lib/pages/profile_screen/widgets/delete_keys_dialog.dart new file mode 100644 index 0000000..8e4874f --- /dev/null +++ b/lib/pages/profile_screen/widgets/delete_keys_dialog.dart @@ -0,0 +1,87 @@ +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/widgets/generated_keys.dart b/lib/pages/profile_screen/widgets/generated_keys.dart new file mode 100644 index 0000000..4049374 --- /dev/null +++ b/lib/pages/profile_screen/widgets/generated_keys.dart @@ -0,0 +1,128 @@ +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/widgets/key_exist_dialog.dart b/lib/pages/profile_screen/widgets/key_exist_dialog.dart new file mode 100644 index 0000000..d74e34f --- /dev/null +++ b/lib/pages/profile_screen/widgets/key_exist_dialog.dart @@ -0,0 +1,128 @@ +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/widgets/keys_option_modal_bottom_sheet.dart b/lib/pages/profile_screen/widgets/keys_option_modal_bottom_sheet.dart new file mode 100644 index 0000000..25ad2e5 --- /dev/null +++ b/lib/pages/profile_screen/widgets/keys_option_modal_bottom_sheet.dart @@ -0,0 +1,34 @@ +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/widgets/message_snack_bar.dart b/lib/pages/profile_screen/widgets/message_snack_bar.dart new file mode 100644 index 0000000..d5c4fd1 --- /dev/null +++ b/lib/pages/profile_screen/widgets/message_snack_bar.dart @@ -0,0 +1,46 @@ +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/widgets/ok_button_widget.dart b/lib/pages/profile_screen/widgets/ok_button_widget.dart new file mode 100644 index 0000000..6dbaad7 --- /dev/null +++ b/lib/pages/profile_screen/widgets/ok_button_widget.dart @@ -0,0 +1,34 @@ +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/widgets/user_info_widget.dart b/lib/pages/profile_screen/widgets/user_info_widget.dart new file mode 100644 index 0000000..53c47f4 --- /dev/null +++ b/lib/pages/profile_screen/widgets/user_info_widget.dart @@ -0,0 +1,97 @@ +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: () {}, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/splash_screen/splash_screen.dart b/lib/pages/splash_screen/splash_screen.dart new file mode 100644 index 0000000..1e7a69f --- /dev/null +++ b/lib/pages/splash_screen/splash_screen.dart @@ -0,0 +1,56 @@ +import 'dart:io'; + +import 'package:drifter/pages/main_screen/main_screen_widget.dart'; +import 'package:drifter/theme/app_colors.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class Splash extends StatefulWidget { + const Splash({super.key}); + + @override + State createState() => _SplashState(); +} + +class _SplashState extends State { + @override + void initState() { + super.initState(); + Future.delayed(const Duration(seconds: 3), () { + Navigator.pushReplacement(context, + MaterialPageRoute(builder: (context) => const MainScreenWidget())); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 300, + ), + Image.asset( + 'assets/images/logo/drifter_vector.png', + height: 111, + width: 93, + ), + const SizedBox( + height: 250, + ), + if (Platform.isAndroid) + const CircularProgressIndicator( + color: AppColors.mainAccent, + ) + else + const CupertinoActivityIndicator( + radius: 20, + ) + ], + )), + ); + } +} diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart index b6c5691..f1b06ae 100644 --- a/lib/theme/app_colors.dart +++ b/lib/theme/app_colors.dart @@ -3,6 +3,7 @@ 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 mainDarkBlue = Color.fromRGBO(3, 37, 65, 1); static const mainLightBlue = Color.fromRGBO(48, 86, 117, 1); diff --git a/pubspec.lock b/pubspec.lock index dea5788..24ffa05 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.10.0" + base58check: + dependency: transitive + description: + name: base58check + sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5" + url: "https://pub.dev" + source: hosted + version: "2.0.0" bech32: dependency: transitive description: @@ -33,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + bip32: + dependency: transitive + description: + name: bip32 + sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" + url: "https://pub.dev" + source: hosted + version: "2.0.0" bip340: dependency: transitive description: @@ -41,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0" + bip39: + dependency: transitive + description: + name: bip39 + sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc + url: "https://pub.dev" + source: hosted + version: "1.0.6" boolean_selector: dependency: transitive description: @@ -49,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + bs58check: + dependency: transitive + description: + name: bs58check + sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9 + url: "https://pub.dev" + source: hosted + version: "1.0.2" characters: dependency: transitive description: @@ -232,6 +264,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.5" + kepler: + dependency: transitive + description: + name: kepler + sha256: "8cf9f7df525bd4e5b192d91e52f1c75832b1fefb27fb4f4a09b1412b0f4f23d0" + url: "https://pub.dev" + source: hosted + version: "1.0.3" lints: dependency: transitive description: @@ -264,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" + nostr_tools: + dependency: "direct main" + description: + name: nostr_tools + sha256: "5536c4017419bcef7777f44f61884a22f864a7a937a3b10254e889ed284757d3" + url: "https://pub.dev" + source: hosted + version: "1.0.7" path: dependency: transitive description: @@ -397,6 +445,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 84c6b1a..68dea71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: flutter: sdk: flutter dart_nostr: ^1.5.0 - # nostr_tools: ^1.0.7 + nostr_tools: ^1.0.7 flutter_secure_storage: ^8.0.0 flutter_svg: ^2.0.5 @@ -63,8 +63,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/images/logo/drifter_vector.png - - assets/images/logo/drifter_logo_circle.svg + - assets/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see