diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 1b37d95..faa3b88 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -154,9 +154,11 @@ "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm", "writePostTypeStory": "Post a story", "writePostTypeArticle": "Write an article", + "writePostTypeQuestion": "Ask a question", "fieldPostPublisher": "Post publisher", "fieldPostContent": "What happened?!", "fieldPostTitle": "Title", + "fieldPostQuestionReward": "Answer Rewards (Source Points)", "fieldPostDescription": "Description", "fieldPostTags": "Tags", "fieldPostCategories": "Categories", @@ -610,5 +612,8 @@ }, "aiThinkingProcess": "AI Thinking Process", "accountSettingsApplied": "Account settings have been applied.", - "trayMenuExit": "Exit" + "trayMenuExit": "Exit", + "postQuestionUnanswered": "Unanswered Question", + "postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}", + "postQuestionAnswered": "Answered Question" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 3303756..591230d 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -138,9 +138,11 @@ "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域", "writePostTypeStory": "发动态", "writePostTypeArticle": "写文章", + "writePostTypeQuestion": "提问题", "fieldPostPublisher": "帖子发布者", "fieldPostContent": "发生什么事了?!", "fieldPostTitle": "标题", + "fieldPostQuestionReward": "回答奖励源点", "fieldPostDescription": "描述", "fieldPostTags": "标签", "fieldPostCategories": "分类", @@ -608,5 +610,8 @@ }, "aiThinkingProcess": "AI 思考过程", "accountSettingsApplied": "帐号设置已应用。", - "trayMenuExit": "退出" + "trayMenuExit": "退出", + "postQuestionUnanswered": "未解答的问题", + "postQuestionUnansweredWithReward": "未解答的问题,悬赏源点 {}", + "postQuestionAnswered": "已解答的问题" } diff --git a/lib/controllers/post_write_controller.dart b/lib/controllers/post_write_controller.dart index 0c04441..4ed024c 100644 --- a/lib/controllers/post_write_controller.dart +++ b/lib/controllers/post_write_controller.dart @@ -144,6 +144,7 @@ class PostWriteController extends ChangeNotifier { static const Map kTitleMap = { 'stories': 'writePostTypeStory', 'articles': 'writePostTypeArticle', + 'questions': 'writePostTypeQuestion', }; static const kAttachmentProgressWeight = 0.9; @@ -153,6 +154,7 @@ class PostWriteController extends ChangeNotifier { final TextEditingController titleController = TextEditingController(); final TextEditingController descriptionController = TextEditingController(); final TextEditingController aliasController = TextEditingController(); + final TextEditingController rewardController = TextEditingController(); bool _temporarySaveActive = false; @@ -215,6 +217,7 @@ class PostWriteController extends ChangeNotifier { descriptionController.text = post.body['description'] ?? ''; contentController.text = post.body['content'] ?? ''; aliasController.text = post.alias ?? ''; + rewardController.text = post.body['reward']?.toString() ?? ''; publishedAt = post.publishedAt; publishedUntil = post.publishedUntil; visibleUsers = List.from(post.visibleUsersList ?? [], growable: true); @@ -348,6 +351,7 @@ class PostWriteController extends ChangeNotifier { if (aliasController.text.isNotEmpty) 'alias': aliasController.text, if (titleController.text.isNotEmpty) 'title': titleController.text, if (descriptionController.text.isNotEmpty) 'description': descriptionController.text, + if (rewardController.text.isNotEmpty) 'reward': rewardController.text, if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(), 'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true), @@ -376,6 +380,7 @@ class PostWriteController extends ChangeNotifier { aliasController.text = data['alias'] ?? ''; titleController.text = data['title'] ?? ''; descriptionController.text = data['description'] ?? ''; + rewardController.text = data['reward']?.toString() ?? ''; if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail'])); attachments .addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast()); @@ -474,6 +479,8 @@ class PostWriteController extends ChangeNotifier { progress = kAttachmentProgressWeight; notifyListeners(); + final reward = double.tryParse(rewardController.text); + // Posting the content try { final baseProgressVal = progress!; @@ -499,6 +506,7 @@ class PostWriteController extends ChangeNotifier { if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(), if (replyingPost != null) 'reply_to': replyingPost!.id, if (repostingPost != null) 'repost_to': repostingPost!.id, + if (reward != null) 'reward': reward, }, onSendProgress: (count, total) { progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2); diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 5d4c2b2..a3c29bc 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -166,6 +166,27 @@ class _ExploreScreenState extends State { ), ], ), + Row( + children: [ + Text('writePostTypeQuestion').tr(), + const Gap(20), + FloatingActionButton( + heroTag: null, + tooltip: 'writePostTypeQuestion'.tr(), + onPressed: () { + GoRouter.of(context).pushNamed('postEditor', pathParameters: { + 'mode': 'questions', + }).then((value) { + if (value == true) { + _refreshPosts(); + } + }); + _fabKey.currentState!.toggle(); + }, + child: const Icon(Symbols.question_answer), + ), + ], + ), ], ), body: RefreshIndicator( diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 6e57a2b..ede439f 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -242,6 +242,10 @@ class _PostEditorScreenState extends State { controller: _writeController, onTapPublisher: _showPublisherPopup, ), + 'questions' => _PostQuestionEditor( + controller: _writeController, + onTapPublisher: _showPublisherPopup, + ), _ => const Placeholder(), }, ), @@ -438,12 +442,13 @@ class _PostStoryEditor extends StatelessWidget { @override Widget build(BuildContext context) { return Container( + padding: const EdgeInsets.symmetric(horizontal: 12), constraints: const BoxConstraints(maxWidth: 640), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Material( - elevation: 1, + elevation: 2, borderRadius: const BorderRadius.all(Radius.circular(24)), child: GestureDetector( onTap: () { @@ -455,23 +460,38 @@ class _PostStoryEditor extends StatelessWidget { ), ), Expanded( - child: TextField( - controller: controller.contentController, - maxLines: null, - decoration: InputDecoration( - hintText: 'fieldPostContent'.tr(), - hintStyle: TextStyle(fontSize: 14), - isCollapsed: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, + child: Column( + children: [ + const Gap(6), + TextField( + controller: controller.titleController, + decoration: InputDecoration.collapsed( + hintText: 'fieldPostTitle'.tr(), + border: InputBorder.none, + ), + style: Theme.of(context).textTheme.titleLarge, + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ).padding(horizontal: 16), + const Gap(8), + TextField( + controller: controller.contentController, + maxLines: null, + decoration: InputDecoration( + hintText: 'fieldPostContent'.tr(), + hintStyle: TextStyle(fontSize: 14), + isCollapsed: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + ), + border: InputBorder.none, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), - border: InputBorder.none, - ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), - ).padding(top: 4), + ], + ), ), ], - ), + ).padding(bottom: 8), ); } } @@ -508,21 +528,21 @@ class _PostArticleEditor extends StatelessWidget { }, ), ), - const Gap(4), + const Gap(16), TextField( controller: controller.titleController, - decoration: InputDecoration( - labelText: 'fieldPostTitle'.tr(), + decoration: InputDecoration.collapsed( + hintText: 'fieldPostTitle'.tr(), border: InputBorder.none, ), style: Theme.of(context).textTheme.titleLarge, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ).padding(horizontal: 16), - const Gap(4), + const Gap(8), TextField( controller: controller.descriptionController, - decoration: InputDecoration( - labelText: 'fieldPostDescription'.tr(), + decoration: InputDecoration.collapsed( + hintText: 'fieldPostDescription'.tr(), border: InputBorder.none, ), maxLines: null, @@ -530,7 +550,7 @@ class _PostArticleEditor extends StatelessWidget { style: Theme.of(context).textTheme.bodyLarge, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ).padding(horizontal: 16), - const Gap(8), + const Gap(4), ]; if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) { @@ -596,3 +616,77 @@ class _PostArticleEditor extends StatelessWidget { ); } } + +class _PostQuestionEditor extends StatelessWidget { + final PostWriteController controller; + final Function? onTapPublisher; + + const _PostQuestionEditor({required this.controller, this.onTapPublisher}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + constraints: const BoxConstraints(maxWidth: 640), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Material( + elevation: 1, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: GestureDetector( + onTap: () { + onTapPublisher?.call(); + }, + child: AccountImage( + content: controller.publisher?.avatar, + ), + ), + ), + Expanded( + child: Column( + children: [ + const Gap(6), + TextField( + controller: controller.titleController, + decoration: InputDecoration.collapsed( + hintText: 'fieldPostTitle'.tr(), + border: InputBorder.none, + ), + style: Theme.of(context).textTheme.titleLarge, + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ).padding(horizontal: 16), + const Gap(8), + TextField( + controller: controller.rewardController, + decoration: InputDecoration( + hintText: 'fieldPostQuestionReward'.tr(), + suffixText: 'walletCurrencyShort'.tr(), + border: InputBorder.none, + isCollapsed: true, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ).padding(horizontal: 16), + const Gap(8), + TextField( + controller: controller.contentController, + maxLines: null, + decoration: InputDecoration( + hintText: 'fieldPostContent'.tr(), + hintStyle: TextStyle(fontSize: 14), + isCollapsed: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + ), + border: InputBorder.none, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + ], + ), + ), + ], + ).padding(top: 8), + ); + } +} diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index a3ccfb0..4c67ff0 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -228,6 +228,7 @@ class PostItem extends StatelessWidget { if (onDeleted != null) onDeleted!(); }, ).padding(horizontal: 12, vertical: 8), + if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), if (data.body['title'] != null || data.body['description'] != null) _PostHeadline( data: data, @@ -333,6 +334,7 @@ class PostShareImageWidget extends StatelessWidget { showMenu: false, isRelativeDate: false, ).padding(horizontal: 16, bottom: 8), + if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), _PostHeadline( data: data, isEnlarge: data.type == 'article', @@ -438,6 +440,30 @@ class PostShareImageWidget extends StatelessWidget { } } +class _PostQuestionHint extends StatelessWidget { + final SnPost data; + + const _PostQuestionHint({required this.data}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20), + const Gap(4), + if (data.body['answer'] == null && data.body['reward']?.toDouble() != null) + Text('postQuestionUnansweredWithReward'.tr(args: [ + '${data.body['reward']}', + ])).opacity(0.75) + else if (data.body['answer'] == null) + Text('postQuestionUnanswered').opacity(0.75) + else + Text('postQuestionAnswered').opacity(0.75), + ], + ).opacity(0.75); + } +} + class _PostBottomAction extends StatelessWidget { final SnPost data; final bool showComments; diff --git a/lib/widgets/post/post_meta_editor.dart b/lib/widgets/post/post_meta_editor.dart index aba1da2..25a3269 100644 --- a/lib/widgets/post/post_meta_editor.dart +++ b/lib/widgets/post/post_meta_editor.dart @@ -88,16 +88,6 @@ class PostMetaEditor extends StatelessWidget { padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8), child: Column( children: [ - if (controller.mode == 'stories') - TextField( - controller: controller.titleController, - decoration: InputDecoration( - labelText: 'fieldPostTitle'.tr(), - border: UnderlineInputBorder(), - ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), - ).padding(horizontal: 24), - const Gap(4), PostTagsField( initialTags: controller.tags, labelText: 'fieldPostTags'.tr(),