diff --git a/lib/main.dart b/lib/main.dart index 6304b68..90bc2b1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,9 +12,11 @@ import 'package:zap_stream_flutter/pages/login.dart'; import 'package:zap_stream_flutter/pages/login_input.dart'; import 'package:zap_stream_flutter/pages/new_account.dart'; import 'package:zap_stream_flutter/pages/profile.dart'; +import 'package:zap_stream_flutter/pages/settings_profile.dart'; import 'package:zap_stream_flutter/pages/stream.dart'; import 'package:zap_stream_flutter/theme.dart'; import 'package:zap_stream_flutter/utils.dart'; +import 'package:zap_stream_flutter/widgets/header.dart'; import 'login.dart'; import 'pages/home.dart'; @@ -168,6 +170,23 @@ Future main() async { ); }, ), + ShellRoute( + builder: + (context, state, child) => + Column(children: [HeaderWidget(), child]), + routes: [ + GoRoute( + path: "/settings", + builder: (context, state) => SizedBox(), + routes: [ + GoRoute( + path: "profile", + builder: (context, state) => SettingsProfilePage(), + ), + ], + ), + ], + ), ], ), ], diff --git a/lib/pages/hashtag.dart b/lib/pages/hashtag.dart index 496a514..bdb632d 100644 --- a/lib/pages/hashtag.dart +++ b/lib/pages/hashtag.dart @@ -27,7 +27,7 @@ class HashtagPage extends StatelessWidget { RxFilter( Key("tags-page:$tag"), filters: [ - Filter(kinds: [30_311], limit: 50, tTags: [tag.toLowerCase()]), + Filter(kinds: [30_311], limit: 100, tTags: [tag.toLowerCase()]), ], builder: (ctx, state) { if (state == null) { diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 81a934e..2807c10 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -19,7 +19,7 @@ class HomePage extends StatelessWidget { RxFilter( Key("home-page"), filters: [ - Filter(kinds: [30_311], limit: 50), + Filter(kinds: [30_311], limit: 100), ], builder: (ctx, state) { if (state == null) { diff --git a/lib/pages/new_account.dart b/lib/pages/new_account.dart index c6afb59..7baf9f1 100644 --- a/lib/pages/new_account.dart +++ b/lib/pages/new_account.dart @@ -1,13 +1,12 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:ndk/ndk.dart'; import 'package:ndk/shared/nips/nip01/bip340.dart'; import 'package:ndk/shared/nips/nip01/key_pair.dart'; import 'package:zap_stream_flutter/login.dart'; import 'package:zap_stream_flutter/main.dart'; import 'package:zap_stream_flutter/theme.dart'; +import 'package:zap_stream_flutter/widgets/avatar_upload.dart'; import 'package:zap_stream_flutter/widgets/button.dart'; class NewAccountPage extends StatefulWidget { @@ -19,28 +18,12 @@ class NewAccountPage extends StatefulWidget { class _NewAccountPage extends State { final TextEditingController _name = TextEditingController(); + final FocusNode _nameFocus = FocusNode(); String? _avatar; String? _error; + bool _loading = false; final KeyPair _privateKey = Bip340.generatePrivateKey(); - Future _uploadAvatar() async { - ndk.accounts.loginPrivateKey( - pubkey: _privateKey.publicKey, - privkey: _privateKey.privateKey!, - ); - - final file = await ImagePicker().pickImage(source: ImageSource.gallery); - if (file != null) { - final upload = await ndk.blossom.uploadBlob( - serverUrls: ["https://nostr.download"], - data: await file.readAsBytes(), - ); - setState(() { - _avatar = upload.first.descriptor!.url; - }); - } - } - Future _login() async { if (ndk.accounts.isNotLoggedIn) { ndk.accounts.loginPrivateKey( @@ -63,55 +46,58 @@ class _NewAccountPage extends State { return Column( spacing: 20, children: [ - GestureDetector( - onTap: () { - _uploadAvatar().catchError((e) { - setState(() { - if (e is String) { - _error = e; - } - }); + AvatarUpload( + onUploadStart: () async { + if (ndk.accounts.isNotLoggedIn) { + ndk.accounts.loginPrivateKey( + pubkey: _privateKey.publicKey, + privkey: _privateKey.privateKey!, + ); + } + }, + onUpload: (i) { + setState(() { + _avatar = i; }); }, - child: Container( - width: 200, - height: 200, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(200)), - color: Color.fromARGB(100, 50, 50, 50), - ), - child: - _avatar == null - ? Center(child: Text("Upload Avatar")) - : CachedNetworkImage(imageUrl: _avatar!), - ), ), TextField( controller: _name, + readOnly: _loading, + focusNode: _nameFocus, decoration: InputDecoration(labelText: "Username"), ), - BasicButton.text( - "Login", - onTap: () { - _login() - .then((_) { - loginData.value = LoginAccount.privateKeyHex( - _privateKey.privateKey!, - ); - if (context.mounted) { - context.go("/"); - } - }) - .catchError((e) { - setState(() { - if (e is String) { - _error = e; - } - }); + ValueListenableBuilder( + valueListenable: _name, + builder: (context, value, child) { + return BasicButton.text( + "Login", + disabled: _loading || value.text.isEmpty, + onTap: () { + setState(() { + _loading = true; + _nameFocus.unfocus(); }); + _login() + .then((_) { + loginData.value = LoginAccount.privateKeyHex( + _privateKey.privateKey!, + ); + if (context.mounted) { + context.go("/"); + } + }) + .catchError((e) { + setState(() { + _loading = false; + _error = e is String ? e : e.toString(); + }); + }); + }, + ); }, ), + if (_loading) CircularProgressIndicator(), if (_error != null) Text( _error!, diff --git a/lib/pages/profile.dart b/lib/pages/profile.dart index d21f065..2837e16 100644 --- a/lib/pages/profile.dart +++ b/lib/pages/profile.dart @@ -22,6 +22,8 @@ class ProfilePage extends StatelessWidget { @override Widget build(BuildContext context) { final hexPubkey = bech32ToHex(pubkey); + final isMe = ndk.accounts.getPublicKey() == hexPubkey; + return ProfileLoaderWidget(hexPubkey, (ctx, state) { final profile = state.data ?? Metadata(pubKey: hexPubkey); return SingleChildScrollView( @@ -70,16 +72,24 @@ class ProfilePage extends StatelessWidget { ], ), - if (ndk.accounts.getPublicKey() == hexPubkey) + if (isMe) Row( + spacing: 8, children: [ BasicButton.text( "Logout", onTap: () { loginData.logout(); + ndk.accounts.logout(); context.go("/"); }, ), + BasicButton.text( + "Edit Profile", + onTap: () { + context.push("/settings/profile"); + }, + ), ], ), Text( @@ -89,10 +99,9 @@ class ProfilePage extends StatelessWidget { RxFilter( Key("profile-streams:$hexPubkey"), - relays: defaultRelays, filters: [ - Filter(kinds: [30_311], limit: 200, pTags: [hexPubkey]), - Filter(kinds: [30_311], limit: 200, authors: [hexPubkey]), + Filter(kinds: [30_311], limit: 100, pTags: [hexPubkey]), + Filter(kinds: [30_311], limit: 100, authors: [hexPubkey]), ], builder: (ctx, state) { return StreamGrid( diff --git a/lib/pages/settings_profile.dart b/lib/pages/settings_profile.dart new file mode 100644 index 0000000..776eb2a --- /dev/null +++ b/lib/pages/settings_profile.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:ndk/ndk.dart'; +import 'package:zap_stream_flutter/main.dart'; +import 'package:zap_stream_flutter/theme.dart'; +import 'package:zap_stream_flutter/widgets/avatar_upload.dart'; +import 'package:zap_stream_flutter/widgets/button.dart'; + +class SettingsProfilePage extends StatelessWidget { + final TextEditingController _picture = TextEditingController(); + final TextEditingController _name = TextEditingController(); + final TextEditingController _about = TextEditingController(); + final TextEditingController _nip5 = TextEditingController(); + final TextEditingController _lud16 = TextEditingController(); + final ValueNotifier _loading = ValueNotifier(false); + + SettingsProfilePage({super.key}); + + @override + Widget build(BuildContext context) { + final pubkey = ndk.accounts.getPublicKey(); + if (pubkey == null) return Text("Cant edit profile when logged out"); + + return FutureBuilder( + future: ndk.metadata.loadMetadata(pubkey), + builder: (context, state) { + if (state.hasData) { + _name.text = state.data!.name ?? ""; + _about.text = state.data!.about ?? ""; + _nip5.text = state.data!.nip05 ?? ""; + _lud16.text = state.data!.lud16 ?? ""; + _picture.text = state.data!.picture ?? ""; + } + return ValueListenableBuilder( + valueListenable: _loading, + builder: (context, v, _) { + return Column( + spacing: 16, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: AvatarUpload( + key: Key("avatar:${_picture.text}"), + avatar: _picture.text.isEmpty ? null : _picture.text, + onUpload: (i) { + _picture.text = i; + }, + ), + ), + TextField( + controller: _name, + readOnly: v, + decoration: InputDecoration( + labelText: "Display Name", + fillColor: LAYER_1, + filled: true, + ), + ), + TextField( + controller: _about, + readOnly: v, + decoration: InputDecoration( + labelText: "About", + fillColor: LAYER_1, + filled: true, + ), + ), + TextField( + controller: _nip5, + readOnly: v, + decoration: InputDecoration( + labelText: "Nostr Address", + fillColor: LAYER_1, + filled: true, + ), + ), + TextField( + controller: _lud16, + readOnly: v, + decoration: InputDecoration( + labelText: "Lightning Address", + fillColor: LAYER_1, + filled: true, + ), + ), + BasicButton.text( + "Save", + disabled: v, + onTap: () async { + _loading.value = true; + try { + final newMeta = Metadata( + pubKey: pubkey, + name: _name.text.isEmpty ? null : _name.text, + about: _about.text.isEmpty ? null : _about.text, + picture: _picture.text.isEmpty ? null : _picture.text, + nip05: _nip5.text.isEmpty ? null : _nip5.text, + lud16: _lud16.text.isEmpty ? null : _lud16.text, + ); + await ndk.metadata.broadcastMetadata(newMeta); + if (context.mounted) { + context.pop(); + } + } finally { + _loading.value = false; + } + }, + ), + if (v) Center(child: CircularProgressIndicator()), + ], + ); + }, + ); + }, + ); + } +} diff --git a/lib/widgets/avatar_upload.dart b/lib/widgets/avatar_upload.dart new file mode 100644 index 0000000..38b82ed --- /dev/null +++ b/lib/widgets/avatar_upload.dart @@ -0,0 +1,106 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:zap_stream_flutter/main.dart'; +import 'package:zap_stream_flutter/theme.dart'; + +class AvatarUpload extends StatefulWidget { + final String? avatar; + final Future Function()? onUploadStart; + final void Function(String)? onUpload; + + const AvatarUpload({ + super.key, + this.onUpload, + this.onUploadStart, + this.avatar, + }); + + @override + State createState() => _AvatarUpload(); +} + +class _AvatarUpload extends State { + String? _avatar; + String? _error; + bool _loading = false; + + @override + void initState() { + _avatar = widget.avatar; + super.initState(); + } + + Future _uploadAvatar() async { + if (widget.onUploadStart != null) { + await widget.onUploadStart!(); + } + final file = await ImagePicker().pickImage(source: ImageSource.gallery); + if (file != null) { + final upload = await ndk.blossom.uploadBlob( + serverUrls: ["https://nostr.download"], + data: await file.readAsBytes(), + ); + final imgUrl = upload.first.descriptor!.url; + setState(() { + _avatar = imgUrl; + }); + return imgUrl; + } + return null; + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + setState(() { + _loading = true; + }); + _uploadAvatar() + .then((u) { + setState(() { + _loading = false; + }); + if (widget.onUpload != null && u != null) { + widget.onUpload!(u); + } + }) + .catchError((e) { + setState(() { + _error = e is String ? e : e.toString(); + _loading = false; + }); + }); + }, + child: Column( + spacing: 8, + children: [ + Container( + width: 200, + height: 200, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(200)), + color: Color.fromARGB(100, 50, 50, 50), + ), + child: + _avatar == null + ? Center( + child: + _loading + ? CircularProgressIndicator() + : Text("Upload Avatar"), + ) + : CachedNetworkImage(imageUrl: _avatar!), + ), + if (_error != null) + Text( + _error!, + style: TextStyle(color: WARNING, fontWeight: FontWeight.bold), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart index d88ca1a..07543ca 100644 --- a/lib/widgets/button.dart +++ b/lib/widgets/button.dart @@ -61,7 +61,7 @@ class BasicButton extends StatelessWidget { onTap!(); } }, - child: (disabled ?? false) ? Opacity(opacity: 0.3, child: inner) : inner, + child: (disabled ?? false) ? Opacity(opacity: 0.5, child: inner) : inner, ); } }