Skip to content

Commit

Permalink
feat(post): support show and edit thread price
Browse files Browse the repository at this point in the history
Note that free area in editor is not supported.
  • Loading branch information
realth000 committed Oct 11, 2024
1 parent 285424e commit b70764b
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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周年坛庆积分。
- 分区:支持显示在帖子中最后回复的用户。
Expand Down
1 change: 1 addition & 0 deletions lib/features/post/bloc/post_edit_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ final class PostEditBloc extends Bloc<PostEditEvent, PostEditState>
data: event.data,
save: event.save,
perm: event.perm,
price: event.price,
options: Map.fromEntries(
event.options
.where((e) => !e.disabled && e.checked)
Expand Down
24 changes: 24 additions & 0 deletions lib/features/post/models/post_edit_content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down Expand Up @@ -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,
Expand All @@ -254,6 +267,7 @@ final class PostEditContent with PostEditContentMappable {
data: data,
options: options,
permList: permList,
price: price,
);
}

Expand Down Expand Up @@ -327,4 +341,14 @@ final class PostEditContent with PostEditContentMappable {
///
/// Only used in editing thread.
final List<ThreadPerm>? 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;
}
1 change: 1 addition & 0 deletions lib/features/post/models/thread_publish_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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' : '';
Expand Down
2 changes: 2 additions & 0 deletions lib/features/post/repository/post_edit_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ final class PostEditRepository with LoggerMixin {
required Map<String, String> options,
required String save,
required String? perm,
required int? price,
}) =>
AsyncVoidEither(() async {
final body = <String, String>{
Expand All @@ -75,6 +76,7 @@ final class PostEditRepository with LoggerMixin {
'message': data,
'editsubmit': 'true',
'save': save,
'price': '${price ?? ""}',
};
if (threadType != null) {
body['typeid'] = threadType;
Expand Down
25 changes: 25 additions & 0 deletions lib/features/post/view/post_edit_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -576,6 +577,28 @@ class _PostEditPageState extends State<PostEditPage> 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;
});
}
},
),
),
],
),
),
Expand Down Expand Up @@ -797,13 +820,15 @@ class _PostEditPageState extends State<PostEditPage> with LoggerMixin {
threadTypeController.text = threadType?.name ?? '';
perm =
state.content?.permList?.where((e) => e.selected).lastOrNull?.perm;
price = state.content?.price;
});

init = true;
} else if (state.status == PostEditStatus.editing) {
setState(() {
perm =
state.content?.permList?.where((e) => e.selected).lastOrNull?.perm;
price = state.content?.price;
});
}
}
Expand Down
103 changes: 103 additions & 0 deletions lib/features/post/widgets/input_price_dialog.dart
Original file line number Diff line number Diff line change
@@ -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<int?> showInputPriceDialog(
BuildContext context,
int? initialPrice,
) async =>
showDialog<int>(
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<FormState>();

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;
},
),
],
);
}
}
6 changes: 6 additions & 0 deletions lib/i18n/strings.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
6 changes: 6 additions & 0 deletions lib/i18n/strings_zh-CN.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,12 @@
},
"permDialog": {
"title": "设置阅读权限"
},
"priceDialog": {
"title": "设置售价",
"entryTooltip": "设置帖子售价",
"maximum": "最高价格65535",
"invalidPrice": "价格无效"
}
},
"chatHistoryPage": {
Expand Down
6 changes: 6 additions & 0 deletions lib/i18n/strings_zh-TW.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,12 @@
},
"permDialog": {
"title": "設定讀取權限"
},
"priceDialog": {
"title": "設定售價",
"entryTooltip": "設定貼文售價",
"maximum": "最高價65535",
"invalidPrice": "價格無效"
}
},
"chatHistoryPage": {
Expand Down

0 comments on commit b70764b

Please sign in to comment.