From b70764b51d31fcc972a1168fc398bb33792d6ef8 Mon Sep 17 00:00:00 2001 From: realth000 Date: Sat, 12 Oct 2024 04:19:46 +0800 Subject: [PATCH] feat(post): support show and edit thread price Note that free area in editor is not supported. --- CHANGELOG.md | 1 + lib/features/post/bloc/post_edit_bloc.dart | 1 + .../post/models/post_edit_content.dart | 24 ++++ .../post/models/thread_publish_info.dart | 1 + .../post/repository/post_edit_repository.dart | 2 + lib/features/post/view/post_edit_page.dart | 25 +++++ .../post/widgets/input_price_dialog.dart | 103 ++++++++++++++++++ lib/i18n/strings.i18n.json | 6 + lib/i18n/strings_zh-CN.i18n.json | 6 + lib/i18n/strings_zh-TW.i18n.json | 6 + 10 files changed, 175 insertions(+) create mode 100644 lib/features/post/widgets/input_price_dialog.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index be2fc4ad..1edfe07d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - 支持在编辑帖子时导出和导入编辑内容,支持BBCode和Quill Data两种格式。 - BBCode格式导入后暂不支持渲染,会以纯文本形式显示,后续会支持此功能。如要保存帖子,现在更推荐以Quill Data的格式保存。 - 编辑:现在在编辑帖子页中的阅读权限图标上现实当前设定的阅读权限数值(如果有的话)。 +- 编辑:支持设置售价。 - 用户:用户页面向下滚动到用户名隐藏时,在顶部显示用户名。 - 用户:支持解析14周年坛庆积分。 - 分区:支持显示在帖子中最后回复的用户。 diff --git a/lib/features/post/bloc/post_edit_bloc.dart b/lib/features/post/bloc/post_edit_bloc.dart index e4ac3b08..e09094a9 100644 --- a/lib/features/post/bloc/post_edit_bloc.dart +++ b/lib/features/post/bloc/post_edit_bloc.dart @@ -79,6 +79,7 @@ final class PostEditBloc extends Bloc data: event.data, save: event.save, perm: event.perm, + price: event.price, options: Map.fromEntries( event.options .where((e) => !e.disabled && e.checked) diff --git a/lib/features/post/models/post_edit_content.dart b/lib/features/post/models/post_edit_content.dart index a514c210..0392d8d2 100644 --- a/lib/features/post/models/post_edit_content.dart +++ b/lib/features/post/models/post_edit_content.dart @@ -94,6 +94,7 @@ final class PostEditContent with PostEditContentMappable { required this.data, required this.options, required this.permList, + required this.price, }); /// Build a instance of [PostEditContent] from [document]. @@ -238,6 +239,18 @@ final class PostEditContent with PostEditContentMappable { permList = ThreadPerm.buildListFromSelect(permListNode); } + // A `div class="extra_price_c"` is the row to set price on current thread + // if current post is an thread (the 1st floor). + final int? price; + final priceNode = rootNode?.querySelector('div#extra_price_c > input'); + if (priceNode != null) { + // Price node exists means user can definitely set a price. + // the "value" attr may not have an value if is in an entire new thread. + price = priceNode.attributes['value']?.parseToInt() ?? 0; + } else { + price = null; + } + return PostEditContent( threadType: threadType, threadTypeList: threadTypeList, @@ -254,6 +267,7 @@ final class PostEditContent with PostEditContentMappable { data: data, options: options, permList: permList, + price: price, ); } @@ -327,4 +341,14 @@ final class PostEditContent with PostEditContentMappable { /// /// Only used in editing thread. final List? permList; + + /// Whether can set price in this edit. + /// + /// Only post in the first floor (exactly is a thread, not a post) can set + /// price, this field indicates can do that or not. + /// + /// A null value indicates this post is unable to set a price. + /// + /// A 0 value will be here if it can but not set yet. + final int? price; } diff --git a/lib/features/post/models/thread_publish_info.dart b/lib/features/post/models/thread_publish_info.dart index 628a7b39..95a536cb 100644 --- a/lib/features/post/models/thread_publish_info.dart +++ b/lib/features/post/models/thread_publish_info.dart @@ -37,6 +37,7 @@ final class ThreadPublishInfo with ThreadPublishInfoMappable { 'message': message, 'save': save, 'mastertid': '', + 'price': '${price ?? ""}', }; for (final entry in options) { body[entry.name] = entry.checked ? '1' : ''; diff --git a/lib/features/post/repository/post_edit_repository.dart b/lib/features/post/repository/post_edit_repository.dart index 0242e9a2..1c8b7493 100644 --- a/lib/features/post/repository/post_edit_repository.dart +++ b/lib/features/post/repository/post_edit_repository.dart @@ -59,6 +59,7 @@ final class PostEditRepository with LoggerMixin { required Map options, required String save, required String? perm, + required int? price, }) => AsyncVoidEither(() async { final body = { @@ -75,6 +76,7 @@ final class PostEditRepository with LoggerMixin { 'message': data, 'editsubmit': 'true', 'save': save, + 'price': '${price ?? ""}', }; if (threadType != null) { body['typeid'] = threadType; diff --git a/lib/features/post/view/post_edit_page.dart b/lib/features/post/view/post_edit_page.dart index 0cce961b..d5a1c63d 100644 --- a/lib/features/post/view/post_edit_page.dart +++ b/lib/features/post/view/post_edit_page.dart @@ -13,6 +13,7 @@ import 'package:tsdm_client/features/editor/widgets/toolbar.dart'; import 'package:tsdm_client/features/post/bloc/post_edit_bloc.dart'; import 'package:tsdm_client/features/post/models/models.dart'; import 'package:tsdm_client/features/post/repository/post_edit_repository.dart'; +import 'package:tsdm_client/features/post/widgets/input_price_dialog.dart'; import 'package:tsdm_client/features/post/widgets/select_perm_dialog.dart'; import 'package:tsdm_client/i18n/strings.g.dart'; import 'package:tsdm_client/routes/screen_paths.dart'; @@ -576,6 +577,28 @@ class _PostEditPageState extends State with LoggerMixin { }, ), ), + if (price != null) + Badge( + label: Text('$price'), + offset: const Offset(-1, -1), + isLabelVisible: price != null && price != 0, + child: IconButton( + icon: const Icon(Icons.money_off_outlined), + tooltip: context.t.postEditPage.priceDialog.entryTooltip, + selectedIcon: const Icon(Icons.attach_money_outlined), + isSelected: price != null && price != 0, + onPressed: () async { + // TODO: show a dialog to set price. + final inputPrice = + await showInputPriceDialog(context, price); + if (inputPrice != null) { + setState(() { + price = inputPrice; + }); + } + }, + ), + ), ], ), ), @@ -797,6 +820,7 @@ class _PostEditPageState extends State with LoggerMixin { threadTypeController.text = threadType?.name ?? ''; perm = state.content?.permList?.where((e) => e.selected).lastOrNull?.perm; + price = state.content?.price; }); init = true; @@ -804,6 +828,7 @@ class _PostEditPageState extends State with LoggerMixin { setState(() { perm = state.content?.permList?.where((e) => e.selected).lastOrNull?.perm; + price = state.content?.price; }); } } diff --git a/lib/features/post/widgets/input_price_dialog.dart b/lib/features/post/widgets/input_price_dialog.dart new file mode 100644 index 00000000..92b23799 --- /dev/null +++ b/lib/features/post/widgets/input_price_dialog.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tsdm_client/i18n/strings.g.dart'; + +/// Show a dialog to let user input a price for current thread. +/// +/// Only use this when setting price for thread. +Future showInputPriceDialog( + BuildContext context, + int? initialPrice, +) async => + showDialog( + context: context, + builder: (_) => _InputPriceDialog(initialPrice), + ); + +class _InputPriceDialog extends StatefulWidget { + const _InputPriceDialog(this.initialPrice); + + /// Initial value of price. + /// + /// Maybe some price is set before editing current thread. + final int? initialPrice; + + @override + State<_InputPriceDialog> createState() => _InputPriceDialogState(); +} + +class _InputPriceDialogState extends State<_InputPriceDialog> { + /// Current price value, initial value from widget or user input value. + int? currentPrice; + + final formKey = GlobalKey(); + + late TextEditingController priceController; + + @override + void initState() { + super.initState(); + currentPrice = widget.initialPrice; + priceController = TextEditingController( + text: currentPrice != null && currentPrice != 0 ? '$currentPrice' : '', + ); + } + + @override + void dispose() { + priceController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final tr = context.t.postEditPage.priceDialog; + return AlertDialog( + title: Text(tr.title), + content: Form( + key: formKey, + child: TextFormField( + controller: priceController, + autofocus: true, + decoration: InputDecoration( + helperText: tr.maximum, + ), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: (v) { + if (v == null) { + return tr.invalidPrice; + } + if (v.isEmpty) { + // An empty value means clear the price. + currentPrice = 0; + return null; + } + final iv = int.tryParse(v); + // 65535 is the maximum price value. + if (iv == null || iv < 0 || iv >= 65535) { + return tr.invalidPrice; + } + currentPrice = iv; + return null; + }, + ), + ), + actions: [ + TextButton( + child: Text(context.t.general.cancel), + onPressed: () => context.pop(), + ), + TextButton( + child: Text(context.t.general.ok), + onPressed: () { + if (formKey.currentState?.validate() ?? false) { + context.pop(currentPrice); + return; + } + return; + }, + ), + ], + ); + } +} diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json index fff8db77..6286374e 100644 --- a/lib/i18n/strings.i18n.json +++ b/lib/i18n/strings.i18n.json @@ -442,6 +442,12 @@ }, "permDialog": { "title": "Set read perm" + }, + "priceDialog": { + "title": "Set price", + "entryTooltip": "Set thread price", + "maximum": "Maximum value 65535", + "invalidPrice": "invalid price" } }, "chatHistoryPage": { diff --git a/lib/i18n/strings_zh-CN.i18n.json b/lib/i18n/strings_zh-CN.i18n.json index c3b1459f..0d64c03c 100644 --- a/lib/i18n/strings_zh-CN.i18n.json +++ b/lib/i18n/strings_zh-CN.i18n.json @@ -442,6 +442,12 @@ }, "permDialog": { "title": "设置阅读权限" + }, + "priceDialog": { + "title": "设置售价", + "entryTooltip": "设置帖子售价", + "maximum": "最高价格65535", + "invalidPrice": "价格无效" } }, "chatHistoryPage": { diff --git a/lib/i18n/strings_zh-TW.i18n.json b/lib/i18n/strings_zh-TW.i18n.json index 8a25b186..59cd60e0 100644 --- a/lib/i18n/strings_zh-TW.i18n.json +++ b/lib/i18n/strings_zh-TW.i18n.json @@ -442,6 +442,12 @@ }, "permDialog": { "title": "設定讀取權限" + }, + "priceDialog": { + "title": "設定售價", + "entryTooltip": "設定貼文售價", + "maximum": "最高價65535", + "invalidPrice": "價格無效" } }, "chatHistoryPage": {