diff --git a/lib/const/router_path.dart b/lib/const/router_path.dart index 9839142..cc2ff15 100644 --- a/lib/const/router_path.dart +++ b/lib/const/router_path.dart @@ -6,4 +6,5 @@ class RouterPath { static const String KEYS = "/keys"; static const String APPS = "/apps"; static const String ADD_REMOTE_APP = "/addRemoteApp"; + static const String APP_DETAIL = "/appDetail"; } diff --git a/lib/data/app_db.dart b/lib/data/app_db.dart index 29a065d..a3fb185 100644 --- a/lib/data/app_db.dart +++ b/lib/data/app_db.dart @@ -32,7 +32,7 @@ class AppDB { static Future update(App o, {DatabaseExecutor? db}) async { db = await DB.getDB(db); - await db.update("app", o.toJson(), where: "id = ?", whereArgs: [o.pubkey]); + await db.update("app", o.toJson(), where: "id = ?", whereArgs: [o.id]); } static Future delete(int id, {DatabaseExecutor? db}) async { diff --git a/lib/main.dart b/lib/main.dart index b105495..eae1942 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,7 @@ 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/app_detail/app_detail_router.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'; @@ -100,6 +101,7 @@ class _MyApp extends State { RouterPath.KEYS: (context) => KeysRouter(), RouterPath.APPS: (context) => AppsRouter(), RouterPath.ADD_REMOTE_APP: (context) => AddRemoteAppRouter(), + RouterPath.APP_DETAIL: (context) => AppDetailRouter(), }; return MultiProvider( diff --git a/lib/provider/app_provider.dart b/lib/provider/app_provider.dart index 13f3144..13c782c 100644 --- a/lib/provider/app_provider.dart +++ b/lib/provider/app_provider.dart @@ -39,6 +39,13 @@ class AppProvider extends ChangeNotifier { notifyListeners(); } + Future update(App app) async { + app.updatedAt = DateTime.now().millisecondsSinceEpoch ~/ 1000; + AppDB.update(app); + reload(); + notifyListeners(); + } + Future add(App app) async { if (await AppDB.insert(app) > 0) { _list.add(app); diff --git a/lib/router/app_detail/app_detail_permission_item_component.dart b/lib/router/app_detail/app_detail_permission_item_component.dart new file mode 100644 index 0000000..73afd6d --- /dev/null +++ b/lib/router/app_detail/app_detail_permission_item_component.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:nowser/component/tag_component.dart'; +import 'package:nowser/const/auth_type.dart'; + +class AppDetailPermissionItemComponent extends StatefulWidget { + bool allow; + + int authType; + + int? eventKind; + + Function(bool, int, int?)? onDelete; + + AppDetailPermissionItemComponent(this.allow, this.authType, + {this.eventKind, this.onDelete}); + + @override + State createState() { + return _AppDetailPermissionItemComponent(); + } +} + +class _AppDetailPermissionItemComponent + extends State { + bool tapFirst = false; + + @override + Widget build(BuildContext context) { + var permissionText = AuthType.getAuthName(context, widget.authType); + if (widget.eventKind != null) { + permissionText += " (EventKind ${widget.eventKind})"; + } + var main = TagComponent(permissionText); + + if (!tapFirst) { + return GestureDetector( + onTap: () { + setState(() { + tapFirst = true; + }); + }, + child: main, + ); + } + + return GestureDetector( + onTap: () { + if (widget.onDelete != null) { + widget.onDelete!(widget.allow, widget.authType, widget.eventKind); + } + }, + child: Container( + child: Stack( + alignment: Alignment.center, + children: [ + main, + Icon(Icons.delete), + ], + ), + ), + ); + } +} diff --git a/lib/router/app_detail/app_detail_router.dart b/lib/router/app_detail/app_detail_router.dart new file mode 100644 index 0000000..3cb2a2e --- /dev/null +++ b/lib/router/app_detail/app_detail_router.dart @@ -0,0 +1,350 @@ +import 'package:flutter/material.dart'; +import 'package:nostr_sdk/nip19/nip19.dart'; +import 'package:nostr_sdk/utils/string_util.dart'; +import 'package:nowser/component/app/app_type_component.dart'; +import 'package:nowser/component/image_component.dart'; +import 'package:nowser/const/connect_type.dart'; +import 'package:nowser/data/app.dart'; +import 'package:nowser/main.dart'; +import 'package:nowser/router/app_detail/app_detail_permission_item_component.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 AppDetailRouter extends StatefulWidget { + @override + State createState() { + return _AppDetailRouter(); + } +} + +class _AppDetailRouter extends State { + late TextEditingController nameController; + + @override + void initState() { + super.initState(); + nameController = TextEditingController(); + nameController.addListener(() { + if (app != null && !changed && nameController.text != app!.name) { + setState(() { + changed = true; + }); + } + }); + } + + App? app; + + bool changed = false; + + @override + Widget build(BuildContext context) { + var themeData = Theme.of(context); + var arg = RouterUtil.routerArgs(context); + if (arg != null && arg is App) { + if (app == null || app!.id != arg.id) { + app = App.fromJson(arg.toJson()); + print(app!.name); + nameController.text = app!.name ?? ""; + } + } + + if (app == null) { + RouterUtil.back(context); + return Container(); + } + + List list = []; + + var baseMargin = EdgeInsets.only(bottom: Base.BASE_PADDING); + + Widget imageWidget = Icon( + Icons.image, + size: 80, + ); + if (StringUtil.isNotBlank(app!.image)) { + imageWidget = ImageComponent( + imageUrl: app!.image!, + width: 80, + height: 80, + ); + } + + list.add(Container( + margin: baseMargin, + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: Base.BASE_PADDING * 2), + child: Container( + width: 80, + height: 80, + child: imageWidget, + ), + ), + Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppTypeComponent(app!.appType!), + Text( + app!.code!, + style: TextStyle(color: themeData.hintColor), + ), + ], + ), + ), + ], + ), + )); + + list.add(Container( + margin: baseMargin, + child: TextField( + controller: nameController, + decoration: InputDecoration(hintText: "Name"), + ), + )); + + var keyWidget = + Selector>(builder: (context, pubkeys, child) { + List> items = []; + for (var pubkey in pubkeys) { + items.add(DropdownMenuItem( + value: pubkey, + child: Text( + Nip19.encodePubKey(pubkey), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + )); + } + return DropdownButton( + items: items, + isExpanded: true, + onChanged: null, + value: app!.pubkey, + ); + }, selector: (context, provider) { + return provider.pubkeys; + }); + list.add(Container( + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: Base.BASE_PADDING), + child: Text("Pubkey:"), + ), + Expanded(child: keyWidget), + ], + ), + )); + + List> connectTypeItems = []; + connectTypeItems.add(DropdownMenuItem( + child: Text("Fully trust"), value: ConnectType.FULLY_TRUST)); + connectTypeItems.add(DropdownMenuItem( + child: Text("Reasonable"), value: ConnectType.REASONABLE)); + connectTypeItems.add(DropdownMenuItem( + child: Text("Alway reject"), value: ConnectType.ALWAY_REJECT)); + list.add(Container( + margin: baseMargin, + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: Base.BASE_PADDING), + child: Text("ConnectType:"), + ), + Expanded( + child: DropdownButton( + items: connectTypeItems, + isExpanded: true, + onChanged: (value) { + if (value != null) { + setState(() { + changed = true; + app!.connectType = value; + }); + } + }, + value: app!.connectType, + )), + ], + ), + )); + + if (app!.connectType == ConnectType.REASONABLE) { + if (StringUtil.isNotBlank(app!.alwaysAllow)) { + var permissionItems = + getPermissionItems(context, app!.alwaysAllow!, true); + + if (permissionItems.isNotEmpty) { + list.add(Container( + margin: baseMargin, + alignment: Alignment.centerLeft, + child: Text( + "Always Allow:", + ), + )); + list.add(Container( + width: double.infinity, + child: Wrap( + spacing: Base.BASE_PADDING, + runSpacing: Base.BASE_PADDING, + children: permissionItems, + ), + )); + } + } + + if (StringUtil.isNotBlank(app!.alwaysReject)) { + var permissionItems = + getPermissionItems(context, app!.alwaysReject!, false); + + if (permissionItems.isNotEmpty) { + list.add(Container( + margin: baseMargin, + alignment: Alignment.centerLeft, + child: Text( + "Always Reject:", + ), + )); + list.add(Container( + width: double.infinity, + child: Wrap( + spacing: Base.BASE_PADDING, + runSpacing: Base.BASE_PADDING, + children: permissionItems, + ), + )); + } + } + } + + List actions = []; + if (changed == true) { + actions.add(GestureDetector( + onTap: appUpdate, + child: Container( + padding: const EdgeInsets.all(Base.BASE_PADDING), + child: Icon(Icons.done), + ), + )); + } + + return Scaffold( + appBar: AppBar( + leading: AppbarBackBtnComponent(), + title: Text( + "App Detail", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: themeData.textTheme.bodyLarge!.fontSize, + ), + ), + actions: actions, + ), + body: SingleChildScrollView( + child: Container( + padding: EdgeInsets.all(Base.BASE_PADDING), + child: Column( + children: list, + ), + ), + ), + ); + } + + List getPermissionItems( + BuildContext context, String permissionText, bool allow) { + var permissionTexts = permissionText.split(";"); + List permissionItems = []; + for (var permissionText in permissionTexts) { + var strs = permissionText.split("-"); + var authType = int.tryParse(strs[0]); + if (authType != null) { + if (strs.length > 1) { + var eventKindStrs = strs[1].split(","); + for (var eventKindStr in eventKindStrs) { + var eventKind = int.tryParse(eventKindStr); + if (eventKind != null) { + permissionItems.add(AppDetailPermissionItemComponent( + allow, + authType, + eventKind: eventKind, + onDelete: onPermissionDelete, + )); + } + } + } else { + permissionItems.add(AppDetailPermissionItemComponent( + allow, + authType, + onDelete: onPermissionDelete, + )); + } + } + } + + return permissionItems; + } + + onPermissionDelete(bool allow, int authType, int? eventKind) { + var sourceText = app!.alwaysAllow; + if (!allow) { + sourceText = app!.alwaysReject; + } + + List permissions = []; + if (StringUtil.isNotBlank(sourceText)) { + var permissionTexts = sourceText!.split(";"); + for (var permissionText in permissionTexts) { + var strs = permissionText.split("-"); + var _authType = int.tryParse(strs[0]); + if (_authType != authType) { + permissions.add(permissionText); + } else { + // authType same, check eventKind + if (eventKind == null || strs.length <= 1) { + continue; + } else { + // should check eventKind + List checkedEventKindStrs = []; + if (strs.length > 1) { + var eventKindStrs = strs[1].split(","); + for (var eventKindStr in eventKindStrs) { + if (eventKindStr == "$eventKind") { + continue; + } else { + checkedEventKindStrs.add(eventKindStr); + } + } + } + + if (checkedEventKindStrs.isNotEmpty) { + permissions.add("$_authType-${checkedEventKindStrs.join(",")}"); + } + } + } + } + } + + if (allow) { + app!.alwaysAllow = permissions.join(";"); + } else { + app!.alwaysReject = permissions.join(";"); + } + changed = true; + setState(() {}); + } + + void appUpdate() { + app!.name = nameController.text; + appProvider.update(app!); + RouterUtil.back(context); + } +} diff --git a/lib/router/me/me_router_app_item_component.dart b/lib/router/me/me_router_app_item_component.dart index 2437851..5ee9d15 100644 --- a/lib/router/me/me_router_app_item_component.dart +++ b/lib/router/me/me_router_app_item_component.dart @@ -3,7 +3,9 @@ import 'package:nostr_sdk/utils/string_util.dart'; import 'package:nowser/component/app/app_type_component.dart'; import 'package:nowser/component/image_component.dart'; import 'package:nowser/const/app_type.dart'; +import 'package:nowser/const/router_path.dart'; import 'package:nowser/data/app.dart'; +import 'package:nowser/util/router_util.dart'; import '../../const/base.dart'; @@ -57,14 +59,20 @@ class _MeRouterAppItemComponent extends State { child: Icon(Icons.chevron_right), ); - return Container( - child: Row( - children: [ - imageWidget, - Expanded(child: titleWidget), - typeWidget, - rightIconWidget, - ], + return GestureDetector( + onTap: () { + RouterUtil.router(context, RouterPath.APP_DETAIL, widget.app); + }, + behavior: HitTestBehavior.translucent, + child: Container( + child: Row( + children: [ + imageWidget, + Expanded(child: titleWidget), + typeWidget, + rightIconWidget, + ], + ), ), ); }