some code for remote signing

This commit is contained in:
DASHU
2024-09-10 17:18:51 +08:00
parent 49c18ccab6
commit 85f9385624
14 changed files with 933 additions and 117 deletions

View File

@@ -0,0 +1,65 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import '../util/router_util.dart';
class QRScanner extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _QRScanner();
}
static Future<String> show(BuildContext context) async {
return await RouterUtil.push(context, MaterialPageRoute(builder: (context) {
return QRScanner();
}));
}
}
class _QRScanner extends State<QRScanner> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
Barcode? result;
QRViewController? controller;
// In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS.
@override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
controller!.pauseCamera();
} else if (Platform.isIOS) {
controller!.resumeCamera();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
),
);
}
bool scanComplete = false;
void _onQRViewCreated(QRViewController controller) {
this.controller = controller;
controller.scannedDataStream.listen((scanData) {
if (!scanComplete) {
scanComplete = true;
RouterUtil.back(context, scanData.code);
}
});
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
}

View File

@@ -4,4 +4,6 @@ class RouterPath {
static const String WEB_URL_INPUT = "/webUrlInput";
static const String ME = "/me";
static const String KEYS = "/keys";
static const String APPS = "/apps";
static const String ADD_REMOTE_APP = "/addRemoteApp";
}

View File

@@ -0,0 +1,60 @@
class RemoteSigningInfo {
int? id;
int? appId;
String? localPubkey;
String? remoteSignerKey;
String? relays;
String? secret;
int? createdAt;
int? updatedAt;
RemoteSigningInfo(
{this.id,
this.appId,
this.localPubkey,
this.remoteSignerKey,
this.relays,
this.secret,
this.createdAt,
this.updatedAt});
RemoteSigningInfo.fromJson(Map<String, dynamic> json) {
id = json['id'];
appId = json['app_id'];
localPubkey = json['local_pubkey'];
remoteSignerKey = json['remote_signer_key'];
relays = json['relays'];
secret = json['secret'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['app_id'] = this.appId;
data['local_pubkey'] = this.localPubkey;
data['remote_signer_key'] = this.remoteSignerKey;
data['relays'] = this.relays;
data['secret'] = this.secret;
data['created_at'] = this.createdAt;
data['updated_at'] = this.updatedAt;
return data;
}
static RemoteSigningInfo? parseNostrConnectUrl(String nostrconnectUrlText) {
var uri = Uri.parse(nostrconnectUrlText);
var pars = uri.queryParametersAll;
var localPubkey = uri.host;
var relays = pars["relay"];
if (relays == null || relays.isEmpty) {
return null;
}
return RemoteSigningInfo(
localPubkey: localPubkey,
relays: relays.join(","),
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:nowser/data/remote_signing_info.dart';
import 'package:sqflite/sqflite.dart';
import 'db.dart';
class RemoteSigningInfoDB {
static Future<int> insert(RemoteSigningInfo o, {DatabaseExecutor? db}) async {
db = await DB.getDB(db);
return await db.insert("remote_signing_info", o.toJson());
}
static Future<RemoteSigningInfo?> getByAppId(int appId,
{DatabaseExecutor? db}) async {
db = await DB.getDB(db);
List<Map<String, dynamic>> list = await db.rawQuery(
"select * from remote_signing_info where app_id = ?", [appId]);
if (list.isNotEmpty) {
return RemoteSigningInfo.fromJson(list.first);
}
return null;
}
}

View File

@@ -13,6 +13,8 @@ import 'package:nowser/provider/app_provider.dart';
import 'package:nowser/provider/key_provider.dart';
import 'package:nowser/provider/permission_check_mixin.dart';
import 'package:nowser/provider/web_provider.dart';
import 'package:nowser/router/apps/add_remote_app_router.dart';
import 'package:nowser/router/apps/apps_router.dart';
import 'package:nowser/router/index/index_router.dart';
import 'package:nowser/router/keys/keys_router.dart';
import 'package:nowser/router/me/me_router.dart';
@@ -26,6 +28,7 @@ import 'const/colors.dart';
import 'const/router_path.dart';
import 'generated/l10n.dart';
import 'provider/data_util.dart';
import 'provider/remote_signing_provider.dart';
import 'provider/setting_provider.dart';
import 'util/colors_util.dart';
@@ -37,6 +40,8 @@ late KeyProvider keyProvider;
late AppProvider appProvider;
late RemoteSigningProvider remoteSigningProvider;
late Map<String, WidgetBuilder> routes;
Future<void> main() async {
@@ -55,6 +60,7 @@ Future<void> main() async {
var futureResultList = await Future.wait([settingTask, appTask]);
settingProvider = futureResultList[0] as SettingProvider;
webProvider = WebProvider();
remoteSigningProvider = RemoteSigningProvider();
runApp(MyApp());
}
@@ -92,6 +98,8 @@ class _MyApp extends State<MyApp> {
RouterPath.WEB_URL_INPUT: (context) => WebUrlInputRouter(),
RouterPath.ME: (context) => MeRouter(),
RouterPath.KEYS: (context) => KeysRouter(),
RouterPath.APPS: (context) => AppsRouter(),
RouterPath.ADD_REMOTE_APP: (context) => AddRemoteAppRouter(),
};
return MultiProvider(
@@ -105,6 +113,9 @@ class _MyApp extends State<MyApp> {
ListenableProvider<AppProvider>.value(
value: appProvider,
),
ListenableProvider<RemoteSigningProvider>.value(
value: remoteSigningProvider,
),
],
child: MaterialApp(
builder: BotToastInit(),
@@ -283,114 +294,3 @@ Color _getMainColor() {
}
return color500;
}
// class MyApp extends StatelessWidget {
// const MyApp({super.key});
// @override
// Widget build(BuildContext context) {
// return MaterialApp(
// title: 'Flutter Demo',
// theme: ThemeData(
// colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
// useMaterial3: true,
// ),
// home: const MyHomePage(title: 'Flutter Demo Home Page'),
// );
// }
// }
// class MyHomePage extends StatefulWidget {
// const MyHomePage({super.key, required this.title});
// final String title;
// @override
// State<MyHomePage> createState() => _MyHomePageState();
// }
// class _MyHomePageState extends State<MyHomePage> {
// void _incrementCounter() {
// // setState(() {
// // _counter++;
// // });
// index++;
// index %= 3;
// setState(() {});
// }
// StreamSubscription? _sub;
// @override
// void initState() {
// super.initState();
// _sub = receiveIntent.ReceiveIntent.receivedIntentStream.listen(
// (receiveIntent.Intent? intent) {
// log("receive intent!!!!!");
// log(intent.toString());
// }, onError: (err) {
// print("listen error ");
// print(err);
// });
// print("listen begin!");
// test();
// }
// Future<void> test() async {
// try {
// final receivedIntent =
// await receiveIntent.ReceiveIntent.getInitialIntent();
// print(receivedIntent);
// // Validate receivedIntent and warn the user, if it is not correct,
// // but keep in mind it could be `null` or "empty"(`receivedIntent.isNull`).
// } catch (e) {
// // Handle exception
// }
// }
// int index = 0;
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// appBar: AppBar(
// backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// title: Text(widget.title),
// ),
// body: IndexedStack(
// index: index,
// children: [
// InAppWebView(
// initialUrlRequest: URLRequest(
// url: WebUri(
// "https://xueqiu.com/",
// ),
// ),
// ),
// InAppWebView(
// initialUrlRequest: URLRequest(
// url: WebUri(
// "https://inappwebview.dev/",
// ),
// ),
// ),
// InAppWebView(
// initialUrlRequest: URLRequest(
// url: WebUri(
// "https://github.com/",
// ),
// ),
// )
// ],
// ),
// floatingActionButton: FloatingActionButton(
// onPressed: _incrementCounter,
// tooltip: 'Increment',
// child: const Icon(Icons.add),
// ),
// );
// }
// }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/const/app_type.dart';
import 'package:nowser/const/connect_type.dart';
import 'package:nowser/data/app_db.dart';
@@ -11,6 +12,16 @@ class AppProvider extends ChangeNotifier {
List<App> get appList => _list;
List<App> remoteAppList() {
List<App> apps = [];
for (var app in _list) {
if (app.appType == AppType.REMOTE) {
apps.add(app);
}
}
return apps;
}
Map<int, App> _appMap = {};
Map<String, Map<String, int>> appPermissions = {};

View File

@@ -0,0 +1,316 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:nostr_sdk/client_utils/keys.dart';
import 'package:nostr_sdk/event.dart';
import 'package:nostr_sdk/event_kind.dart';
import 'package:nostr_sdk/filter.dart';
import 'package:nostr_sdk/nip46/nostr_remote_request.dart';
import 'package:nostr_sdk/nip46/nostr_remote_response.dart';
import 'package:nostr_sdk/relay/relay.dart';
import 'package:nostr_sdk/relay/relay_isolate.dart';
import 'package:nostr_sdk/relay/relay_status.dart';
import 'package:nostr_sdk/signer/local_nostr_signer.dart';
import 'package:nostr_sdk/signer/nostr_signer.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/const/auth_type.dart';
import 'package:nowser/data/app.dart';
import 'package:nowser/provider/permission_check_mixin.dart';
import '../data/remote_signing_info.dart';
import '../data/remote_signing_info_db.dart';
import '../main.dart';
class RemoteSigningProvider extends ChangeNotifier with PermissionCheckMixin {
BuildContext? context;
void updateContext(BuildContext _context) {
context = _context;
}
// k - v ==> localPubkey - Relay
Map<String, List<Relay>> relayMap = {};
// localPubkey - RemoteSigningInfo
Map<String, RemoteSigningInfo> remoteSigningInfoMap = {};
// localPubkey - App
Map<String, App> appMap = {};
Future<void> reload() async {
relayMap = {};
remoteSigningInfoMap = {};
appMap = {};
}
Future<void> load() async {
var remoteAppList = appProvider.remoteAppList();
for (var remoteApp in remoteAppList) {
await add(remoteApp);
}
}
Future<void> add(App remoteApp) async {
var remoteSigningInfo = await RemoteSigningInfoDB.getByAppId(remoteApp.id!);
if (remoteSigningInfo != null &&
StringUtil.isNotBlank(remoteSigningInfo.remoteSignerKey) &&
StringUtil.isNotBlank(remoteSigningInfo.localPubkey) &&
StringUtil.isNotBlank(remoteSigningInfo.relays)) {
var localPubkey = remoteSigningInfo.localPubkey!;
var pubkey = getPublicKey(remoteSigningInfo.remoteSignerKey!);
var relayAddrs = remoteSigningInfo.relays!.split(",");
List<Relay> relays = [];
for (var relayAddr in relayAddrs) {
// use pubkey relace with
var relay = RelayIsolate(relayAddr, RelayStatus(localPubkey));
var filter = Filter(p: [pubkey]);
relay.pendingAuthedMessages
.add(["REQ", StringUtil.rndNameStr(10), filter.toJson()]);
relay.onMessage = _onEvent;
relay.connect();
relays.add(relay);
}
relayMap[localPubkey] = relays;
remoteSigningInfoMap[localPubkey] = remoteSigningInfo;
}
}
Future<void> onRequest(
Relay relay,
NostrRemoteRequest request,
RemoteSigningInfo remoteSigningInfo,
App app,
) async {
String localPubkey = remoteSigningInfo.localPubkey!;
NostrSigner signer = LocalNostrSigner(remoteSigningInfo.remoteSignerKey!);
var remoteSignerPubkey = await signer.getPublicKey();
var appType = app.appType!;
var code = app.code!;
NostrRemoteResponse? response;
if (request.method == "ping") {
response = NostrRemoteResponse(request.id, "pong");
sendResponse(relay, response, signer, localPubkey, remoteSignerPubkey!);
} else {
int? eventKind;
String? authDetail;
int authType = AuthType.GET_PUBLIC_KEY;
dynamic eventObj;
if (request.method == "sign_event") {
authType = AuthType.SIGN_EVENT;
authDetail = request.params[0];
eventObj = jsonDecode(authDetail);
eventKind = eventObj["kind"];
} else if (request.method == "get_relays") {
authType = AuthType.GET_RELAYS;
} else if (request.method == "get_public_key") {
authType = AuthType.GET_PUBLIC_KEY;
} else if (request.method == "nip04_encrypt") {
authType = AuthType.NIP04_ENCRYPT;
authDetail = request.params[1];
} else if (request.method == "nip04_decrypt") {
authType = AuthType.NIP04_DECRYPT;
authDetail = request.params[1];
} else if (request.method == "nip44_encrypt") {
authType = AuthType.NIP44_ENCRYPT;
authDetail = request.params[1];
} else if (request.method == "nip44_decrypt") {
authType = AuthType.NIP44_DECRYPT;
authDetail = request.params[1];
}
checkPermission(context!, appType, code, authType,
eventKind: eventKind, authDetail: authDetail, (app) {
response = NostrRemoteResponse(request.id, "", error: "forbid");
sendResponse(relay, response, signer, localPubkey, remoteSignerPubkey!);
}, (app, signer) async {
if (request.method == "sign_event") {
var tags = eventObj["tags"];
Event? event = Event(
app.pubkey!, eventObj["kind"], tags ?? [], eventObj["content"],
createdAt: eventObj["created_at"]);
log(jsonEncode(event.toJson()));
event = await signer.signEvent(event);
if (event == null) {
log("sign event fail");
return;
}
response =
NostrRemoteResponse(request.id, jsonEncode(event.toJson()));
} else if (request.method == "get_relays") {
// TODO
} else if (request.method == "get_public_key") {
var pubkey = await signer.getPublicKey();
response = NostrRemoteResponse(request.id, pubkey!);
} else if (request.method == "nip04_encrypt") {
var text = await signer.encrypt(localPubkey, authDetail);
response = NostrRemoteResponse(request.id, text!);
} else if (request.method == "nip04_decrypt") {
var text = await signer.decrypt(localPubkey, authDetail);
response = NostrRemoteResponse(request.id, text!);
} else if (request.method == "nip44_encrypt") {
var text = await signer.nip44Encrypt(localPubkey, authDetail);
response = NostrRemoteResponse(request.id, text!);
} else if (request.method == "nip44_decrypt") {
var text = await signer.nip44Decrypt(localPubkey, authDetail);
response = NostrRemoteResponse(request.id, text!);
}
sendResponse(relay, response, signer, localPubkey, remoteSignerPubkey!);
});
}
}
Future<void> sendResponse(Relay relay, NostrRemoteResponse? response,
NostrSigner signer, String localPubkey, String remoteSignerPubkey) async {
if (response != null) {
var result = await response.encrypt(signer, localPubkey);
var event = Event(
remoteSignerPubkey!,
EventKind.NOSTR_REMOTE_SIGNING,
[
["p", localPubkey]
],
result!);
relay.send(["event", event.toJson()]);
}
}
Future<void> _onEvent(Relay relay, List<dynamic> json) async {
var localPubkey = relay.relayStatus.addr;
var remoteSigningInfo = remoteSigningInfoMap[localPubkey];
if (remoteSigningInfo == null) {
print("remoteSigningInfo is null");
return;
}
var remotePubkey = getPublicKey(remoteSigningInfo.remoteSignerKey!);
var nostrSigner = LocalNostrSigner(remoteSigningInfo.remoteSignerKey!);
final messageType = json[0];
if (messageType == 'EVENT') {
try {
final event = Event.fromJson(json[2]);
// add some statistics
relay.relayStatus.noteReceive();
event.sources.add(relay.url);
if (event.kind == EventKind.NOSTR_REMOTE_SIGNING) {
var request = await NostrRemoteRequest.decrypt(
event.content, nostrSigner, localPubkey);
if (request != null) {}
}
} catch (err) {
log(err.toString());
}
} else if (messageType == 'EOSE') {
} else if (messageType == "NOTICE") {
} else if (messageType == "AUTH") {
// auth needed
if (json.length < 2) {
log("AUTH result not right.");
return;
}
final challenge = json[1] as String;
var tags = [
["relay", relay.url],
["challenge", challenge]
];
Event? event = Event(remotePubkey, EventKind.AUTHENTICATION, tags, "");
event = await nostrSigner.signEvent(event);
if (event != null) {
relay.send(["AUTH", event.toJson()], forceSend: true);
relay.relayStatus.authed = true;
if (relay.pendingAuthedMessages.isNotEmpty) {
Future.delayed(const Duration(seconds: 1), () {
for (var message in relay.pendingAuthedMessages) {
relay.send(message);
}
relay.pendingAuthedMessages.clear();
});
}
}
}
}
/**
* The below code is design for relay n - remoteSignerKey n
*/
// Map<String, RelayStatus> relayStatusMap = {};
// // relayAddr - Relay
// Map<String, Relay> relayMap = {};
// // remoteSignerPubkey - RemoteSigningInfo
// Map<String, RemoteSigningInfo> remoteSigningInfoMap = {};
// Map<String, List<String>> relayToRemoteSignerPubkeys = {};
// Map<String, List<String>> remoteSignerPubkeyToRelays = {};
// /// relay - remoteSignerKey > n - n
// /// ==>
// /// relay - remoteSignerKey > 1 - n
// /// remoteSigner - relay > 1 - n
// Future<void> load() async {
// var remoteAppList = appProvider.remoteAppList();
// for (var remoteApp in remoteAppList) {
// var remoteSigningInfo =
// await RemoteSigningInfoDB.getByAppId(remoteApp.id!);
// if (remoteSigningInfo != null &&
// StringUtil.isNotBlank(remoteSigningInfo.remoteSignerKey) &&
// StringUtil.isNotBlank(remoteSigningInfo.localPubkey) &&
// StringUtil.isNotBlank(remoteSigningInfo.relays)) {
// var pubkey = getPublicKey(remoteSigningInfo.secret!);
// var relays = remoteSigningInfo.relays!.split(",");
// remoteSignerPubkeyToRelays[pubkey] = relays;
// remoteSigningInfoMap[pubkey] = remoteSigningInfo;
// for (var relay in relays) {
// relay = RelayAddrUtil.handle(relay);
// var pubkeys = relayToRemoteSignerPubkeys[relay];
// if (pubkeys == null) {
// pubkeys = [];
// relayToRemoteSignerPubkeys[relay] = pubkeys;
// }
// if (!pubkeys.contains(pubkey)) {
// pubkeys.add(pubkey);
// }
// }
// }
// }
// // data handle complete, begin to init relays
// for (var entry in relayToRemoteSignerPubkeys.entries) {
// var relayAddr = entry.key;
// var pubkeys = entry.value;
// var relayStatus = RelayStatus(relayAddr);
// relayStatusMap[relayAddr] = relayStatus;
// var relay = RelayIsolate(relayAddr, relayStatus);
// relayMap[relayAddr] = relay;
// // var filter = Filter(
// // p: pubkeys, since: DateTime.now().millisecondsSinceEpoch ~/ 1000);
// }
// }
}

View File

@@ -0,0 +1,326 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:nostr_sdk/client_utils/keys.dart';
import 'package:nostr_sdk/nip19/nip19.dart';
import 'package:nostr_sdk/nip46/nostr_remote_signer_info.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/component/qrscanner.dart';
import 'package:nowser/data/remote_signing_info.dart';
import 'package:nowser/data/remote_signing_info_db.dart';
import 'package:nowser/util/router_util.dart';
import 'package:provider/provider.dart';
import '../../component/appbar_back_btn_component.dart';
import '../../const/base.dart';
import '../../provider/key_provider.dart';
class AddRemoteAppRouter extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AddRemoteAppRouter();
}
}
class _AddRemoteAppRouter extends State<AddRemoteAppRouter> {
TextEditingController nostrconnectConn = TextEditingController();
TextEditingController bunkerConn = TextEditingController();
bool editBunker = false;
@override
void initState() {
super.initState();
relayAddrController.addListener(() {
reloadBunker();
});
secretController.addListener(() {
reloadBunker();
});
}
@override
Widget build(BuildContext context) {
var themeData = Theme.of(context);
var textColor = themeData.textTheme.bodyMedium!.color;
var mainColor = themeData.primaryColor;
if (StringUtil.isBlank(bunkerConn.text)) {
refreshBunkerUrl();
}
Widget bunkerShowmoreBtn = Container(
alignment: Alignment.center,
child: GestureDetector(
onTap: () {
setState(() {
editBunker = true;
});
},
behavior: HitTestBehavior.translucent,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.expand_more),
Text("Edit"),
],
),
),
);
if (editBunker) {
bunkerShowmoreBtn = Container(
alignment: Alignment.center,
child: GestureDetector(
onTap: () {
setState(() {
editBunker = false;
});
},
behavior: HitTestBehavior.translucent,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.expand_less),
Text("Close Edit"),
],
),
),
);
}
Widget bunkerWidget = Container();
if (editBunker) {
bunkerWidget = Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
child: TextField(
decoration: InputDecoration(
labelText: "Relay",
),
controller: relayAddrController,
),
),
Container(
child: TextField(
decoration: InputDecoration(
labelText: "Secret",
),
controller: secretController,
),
),
],
);
}
var main = DefaultTabController(
length: 2,
initialIndex: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 60,
child: TabBar(
labelColor: textColor,
unselectedLabelColor: textColor,
indicatorColor: mainColor,
tabs: [
Text(
"Connect by\nnostrconnect:// url",
textAlign: TextAlign.center,
),
Text(
"Connect by\nbunker:// url",
textAlign: TextAlign.center,
),
],
),
),
Expanded(
child: Container(
child: TabBarView(
children: [
Container(
padding: EdgeInsets.all(Base.BASE_PADDING),
child: ListView(
children: [
Container(
margin: EdgeInsets.only(top: Base.BASE_PADDING_HALF),
child: TextField(
controller: nostrconnectConn,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
maxLines: 4,
),
),
Container(
margin: EdgeInsets.only(top: Base.BASE_PADDING_HALF),
child: Row(
children: [
Expanded(child: Container()),
IconButton(
onPressed: () {
scanNostrConnectQRCode();
},
icon: Icon(Icons.qr_code_scanner),
),
],
),
),
Container(
margin: EdgeInsets.only(top: Base.BASE_PADDING),
width: double.infinity,
child: FilledButton(
onPressed: () {},
child: Text("Confirm"),
),
),
],
),
),
Container(
padding: EdgeInsets.all(Base.BASE_PADDING),
child: ListView(
children: [
Container(
margin: EdgeInsets.only(top: Base.BASE_PADDING_HALF),
child: TextField(
controller: bunkerConn,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
readOnly: true,
maxLines: 4,
),
),
Container(
margin: EdgeInsets.only(top: Base.BASE_PADDING_HALF),
child: Row(
children: [
Expanded(child: Container()),
IconButton(
onPressed: refreshBunkerUrl,
icon: Icon(Icons.refresh)),
// IconButton(
// onPressed: showBunkerUrlQRCode,
// icon: Icon(Icons.qr_code)),
IconButton(
onPressed: copyBunkerUrl,
icon: Icon(Icons.copy)),
],
),
),
bunkerShowmoreBtn,
bunkerWidget,
Container(
margin: EdgeInsets.only(top: Base.BASE_PADDING),
width: double.infinity,
child: FilledButton(
onPressed: confirmBunkerUrl,
child: Text("Confirm"),
),
),
],
),
),
],
),
)),
],
),
);
return Scaffold(
appBar: AppBar(
leading: AppbarBackBtnComponent(),
title: Text(
"Add Remote App",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: themeData.textTheme.bodyLarge!.fontSize,
),
),
),
body: Container(
padding: const EdgeInsets.all(Base.BASE_PADDING),
child: main,
),
);
}
Future<void> scanNostrConnectQRCode() async {
var value = await QRScanner.show(context);
if (StringUtil.isNotBlank(value)) {
nostrconnectConn.text = value;
}
}
void confirmNostrConnect() {
var remoteSigningInfo =
RemoteSigningInfo.parseNostrConnectUrl(nostrconnectConn.text);
if (remoteSigningInfo == null) {
// TODO
return;
}
remoteSigningInfo.remoteSignerKey = generatePrivateKey();
remoteSigningInfo.createdAt = DateTime.now().millisecondsSinceEpoch ~/ 10;
remoteSigningInfo.updatedAt = remoteSigningInfo.createdAt;
// TODO
}
TextEditingController remoteSignerKeyController = TextEditingController();
TextEditingController relayAddrController = TextEditingController();
TextEditingController secretController = TextEditingController();
void refreshBunkerUrl() {
remoteSignerKeyController.text = generatePrivateKey();
secretController.text = StringUtil.rndNameStr(20);
relayAddrController.text = "wss://relay.nsec.app";
reloadBunker();
}
// void showBunkerUrlQRCode() {}
void copyBunkerUrl() {
Clipboard.setData(ClipboardData(text: bunkerConn.text)).then((_) {
// TODO
// BotToast.showText(text: S.of(context).Copy_success);
});
}
void confirmBunkerUrl() {
var remoteSignerKey = remoteSignerKeyController.text;
var relays = [relayAddrController.text];
var remoteSigningInfo = RemoteSigningInfo(
remoteSignerKey: remoteSignerKey,
relays: relays.join(","),
secret: secretController.text,
createdAt: DateTime.now().millisecondsSinceEpoch ~/ 10,
);
remoteSigningInfo.updatedAt = remoteSigningInfo.createdAt;
RemoteSigningInfoDB.insert(remoteSigningInfo);
RouterUtil.back(context);
}
void reloadBunker() {
var remoteSignerKey = remoteSignerKeyController.text;
var remoteSignerPubkey = getPublicKey(remoteSignerKey);
var nostrRemoteSignerInfo = NostrRemoteSignerInfo(
remoteUserPubkey: remoteSignerPubkey,
relays: [relayAddrController.text],
optionalSecret: secretController.text,
);
bunkerConn.text = nostrRemoteSignerInfo.toString();
}
}

View File

@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:nowser/const/router_path.dart';
import 'package:nowser/provider/app_provider.dart';
import 'package:nowser/util/router_util.dart';
import 'package:provider/provider.dart';
import '../../component/appbar_back_btn_component.dart';
import '../../const/base.dart';
import '../me/me_router_app_item_component.dart';
class AppsRouter extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AppsRouter();
}
}
class _AppsRouter extends State<AppsRouter> {
@override
Widget build(BuildContext context) {
var themeData = Theme.of(context);
var _appProvider = Provider.of<AppProvider>(context);
var appList = _appProvider.appList;
var main = ListView.builder(
itemBuilder: (context, index) {
if (index >= appList.length) {
return null;
}
var app = appList[index];
return Container(
child: MeRouterAppItemComponent(app),
);
},
itemCount: appList.length,
);
return Scaffold(
appBar: AppBar(
leading: AppbarBackBtnComponent(),
title: Text(
"Apps Manager",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: themeData.textTheme.bodyLarge!.fontSize,
),
),
actions: [
GestureDetector(
onTap: () {
RouterUtil.router(context, RouterPath.ADD_REMOTE_APP);
},
child: Container(
padding: const EdgeInsets.all(Base.BASE_PADDING),
child: Icon(Icons.add),
),
)
],
),
);
}
}

View File

@@ -24,7 +24,7 @@ class _IndexRouter extends State<IndexRouter>
WidgetsBinding.instance.addPostFrameCallback((_) {
handleInitialIntent(context);
});
remoteSigningProvider.updateContext(context);
webProvider.checkBlank();
var main = Selector<WebProvider, WebNumInfo>(

View File

@@ -170,12 +170,30 @@ class _MeRouter extends CustState<MeRouter> {
));
appWidgetList.add(Divider());
}
if (appWidgetList.isEmpty) {
appWidgetList.add(GestureDetector(
onTap: () {
RouterUtil.router(context, RouterPath.APPS);
},
behavior: HitTestBehavior.translucent,
child: Container(
child: Text("no apps now"),
),
));
appWidgetList.add(Divider());
}
appWidgetList.add(Container(
alignment: Alignment.center,
child: Text(
"Show more apps",
style: TextStyle(
decoration: TextDecoration.underline,
child: GestureDetector(
onTap: () {
RouterUtil.router(context, RouterPath.APPS);
},
behavior: HitTestBehavior.translucent,
child: Text(
"Show more apps",
style: TextStyle(
decoration: TextDecoration.underline,
),
),
),
));
@@ -203,6 +221,12 @@ class _MeRouter extends CustState<MeRouter> {
));
logList.add(Divider());
}
if (logList.isEmpty) {
logList.add(Container(
child: Text("no logs now"),
));
logList.add(Divider());
}
logList.add(Container(
alignment: Alignment.center,
child: Text(

View File

@@ -628,6 +628,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
pretty_qr_code:
dependency: "direct main"
description:
name: pretty_qr_code
sha256: cbdb4af29da1c1fa21dd76f809646c591320ab9e435d3b0eab867492d43607d5
url: "https://pub.dev"
source: hosted
version: "3.3.0"
process_run:
dependency: transitive
description:
@@ -652,6 +660,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_code_scanner:
dependency: "direct main"
description:
name: qr_code_scanner
sha256: f23b68d893505a424f0bd2e324ebea71ed88465d572d26bb8d2e78a4749591fd
url: "https://pub.dev"
source: hosted
version: "1.0.1"
receive_intent:
dependency: "direct main"
description:

View File

@@ -46,6 +46,8 @@ dependencies:
google_fonts: ^6.2.1
flutter_font_picker: ^1.4.0
flutter_secure_storage: ^9.2.2
pretty_qr_code: ^3.3.0
qr_code_scanner: ^1.0.1
dev_dependencies:
flutter_test: