mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
Compare commits
1 Commits
26a5b7b7a7
...
feat-creat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67182d2641 |
@@ -47,6 +47,7 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 发起投票
|
||||
- [x] 发布动态/评论支持`富文本编辑`/`表情显示`/`@用户`
|
||||
- [x] 修改消息设置
|
||||
- [x] 修改聊天设置
|
||||
|
||||
@@ -26,7 +26,7 @@ import 'package:flutter/services.dart';
|
||||
/// created by bggRGjQaUbCoE on 2025/6/27
|
||||
///
|
||||
|
||||
enum RichTextType { text, composing, at, emoji }
|
||||
enum RichTextType { text, composing, at, emoji, vote }
|
||||
|
||||
class Emote {
|
||||
late String url;
|
||||
@@ -43,20 +43,20 @@ class Emote {
|
||||
mixin RichTextTypeMixin {
|
||||
RichTextType get type;
|
||||
Emote? get emote;
|
||||
String? get uid;
|
||||
String? get id;
|
||||
String? get rawText;
|
||||
}
|
||||
|
||||
extension TextEditingDeltaExt on TextEditingDelta {
|
||||
({RichTextType type, String? rawText, Emote? emote, String? uid}) get config {
|
||||
({RichTextType type, String? rawText, Emote? emote, String? id}) get config {
|
||||
if (this case RichTextTypeMixin e) {
|
||||
return (type: e.type, rawText: e.rawText, emote: e.emote, uid: e.uid);
|
||||
return (type: e.type, rawText: e.rawText, emote: e.emote, id: e.id);
|
||||
}
|
||||
return (
|
||||
type: composing.isValid ? RichTextType.composing : RichTextType.text,
|
||||
rawText: null,
|
||||
emote: null,
|
||||
uid: null
|
||||
id: null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class RichTextEditingDeltaInsertion extends TextEditingDeltaInsertion
|
||||
required super.composing,
|
||||
RichTextType? type,
|
||||
this.emote,
|
||||
this.uid,
|
||||
this.id,
|
||||
this.rawText,
|
||||
}) {
|
||||
this.type = type ??
|
||||
@@ -96,7 +96,7 @@ class RichTextEditingDeltaInsertion extends TextEditingDeltaInsertion
|
||||
final Emote? emote;
|
||||
|
||||
@override
|
||||
final String? uid;
|
||||
final String? id;
|
||||
|
||||
@override
|
||||
final String? rawText;
|
||||
@@ -112,7 +112,7 @@ class RichTextEditingDeltaReplacement extends TextEditingDeltaReplacement
|
||||
required super.composing,
|
||||
RichTextType? type,
|
||||
this.emote,
|
||||
this.uid,
|
||||
this.id,
|
||||
this.rawText,
|
||||
}) {
|
||||
this.type = type ??
|
||||
@@ -126,7 +126,7 @@ class RichTextEditingDeltaReplacement extends TextEditingDeltaReplacement
|
||||
final Emote? emote;
|
||||
|
||||
@override
|
||||
final String? uid;
|
||||
final String? id;
|
||||
|
||||
@override
|
||||
final String? rawText;
|
||||
@@ -138,7 +138,7 @@ class RichTextItem {
|
||||
String? _rawText;
|
||||
late TextRange range;
|
||||
Emote? emote;
|
||||
String? uid;
|
||||
String? id;
|
||||
|
||||
String get rawText => _rawText ?? text;
|
||||
|
||||
@@ -146,7 +146,7 @@ class RichTextItem {
|
||||
|
||||
bool get isComposing => type == RichTextType.composing;
|
||||
|
||||
bool get isRich => type == RichTextType.at || type == RichTextType.emoji;
|
||||
bool get isRich => !isText && !isComposing;
|
||||
|
||||
RichTextItem({
|
||||
this.type = RichTextType.text,
|
||||
@@ -154,7 +154,7 @@ class RichTextItem {
|
||||
String? rawText,
|
||||
required this.range,
|
||||
this.emote,
|
||||
this.uid,
|
||||
this.id,
|
||||
}) {
|
||||
_rawText = rawText;
|
||||
}
|
||||
@@ -164,7 +164,7 @@ class RichTextItem {
|
||||
String? rawText,
|
||||
this.type = RichTextType.text,
|
||||
this.emote,
|
||||
this.uid,
|
||||
this.id,
|
||||
}) {
|
||||
range = TextRange(start: 0, end: text.length);
|
||||
_rawText = rawText;
|
||||
@@ -198,7 +198,7 @@ class RichTextItem {
|
||||
rawText: config.rawText,
|
||||
type: config.type,
|
||||
emote: config.emote,
|
||||
uid: config.uid,
|
||||
id: config.id,
|
||||
);
|
||||
return [insertedItem];
|
||||
}
|
||||
@@ -224,7 +224,7 @@ class RichTextItem {
|
||||
final insertedItem = RichTextItem(
|
||||
type: config.type,
|
||||
emote: config.emote,
|
||||
uid: config.uid,
|
||||
id: config.id,
|
||||
text: delta.textInserted,
|
||||
rawText: config.rawText,
|
||||
range: TextRange(start: insertionOffset, end: end),
|
||||
@@ -251,7 +251,7 @@ class RichTextItem {
|
||||
final insertedItem = RichTextItem(
|
||||
type: config.type,
|
||||
emote: config.emote,
|
||||
uid: config.uid,
|
||||
id: config.id,
|
||||
text: delta.textInserted,
|
||||
rawText: config.rawText,
|
||||
range: TextRange(start: insertionOffset, end: insertEnd),
|
||||
@@ -397,7 +397,7 @@ class RichTextItem {
|
||||
final insertedItem = RichTextItem(
|
||||
type: config.type,
|
||||
emote: config.emote,
|
||||
uid: config.uid,
|
||||
id: config.id,
|
||||
text: delta.replacementText,
|
||||
rawText: config.rawText,
|
||||
range: TextRange(
|
||||
@@ -427,7 +427,7 @@ class RichTextItem {
|
||||
text = delta.replacementText;
|
||||
type = config.type;
|
||||
emote = config.emote;
|
||||
uid = config.uid;
|
||||
id = config.id;
|
||||
final end = range.start + text.length;
|
||||
range = TextRange(start: range.start, end: end);
|
||||
controller.newSelection = TextSelection.collapsed(offset: end);
|
||||
@@ -441,7 +441,7 @@ class RichTextItem {
|
||||
_rawText = config.rawText;
|
||||
type = config.type;
|
||||
emote = config.emote;
|
||||
uid = config.uid;
|
||||
id = config.id;
|
||||
final end = range.start + text.length;
|
||||
range = TextRange(start: range.start, end: end);
|
||||
controller.newSelection = TextSelection.collapsed(offset: end);
|
||||
@@ -476,7 +476,7 @@ class RichTextItem {
|
||||
rawText: config.rawText,
|
||||
type: config.type,
|
||||
emote: config.emote,
|
||||
uid: config.uid,
|
||||
id: config.id,
|
||||
range: TextRange(start: replacedRange.start, end: end),
|
||||
);
|
||||
controller.newSelection = TextSelection.collapsed(offset: end);
|
||||
@@ -487,7 +487,7 @@ class RichTextItem {
|
||||
final config = delta.config;
|
||||
type = config.type;
|
||||
emote = config.emote;
|
||||
uid = config.uid;
|
||||
id = config.id;
|
||||
final end = range.start + text.length;
|
||||
range = TextRange(start: range.start, end: end);
|
||||
controller.newSelection = TextSelection.collapsed(offset: end);
|
||||
@@ -523,7 +523,7 @@ class RichTextItem {
|
||||
rawText: config.rawText,
|
||||
type: config.type,
|
||||
emote: config.emote,
|
||||
uid: config.uid,
|
||||
id: config.id,
|
||||
range: TextRange(start: range.start, end: end),
|
||||
);
|
||||
controller.newSelection = TextSelection.collapsed(offset: end);
|
||||
@@ -536,7 +536,7 @@ class RichTextItem {
|
||||
final config = delta.config;
|
||||
type = config.type;
|
||||
emote = config.emote;
|
||||
uid = config.uid;
|
||||
id = config.id;
|
||||
final end = range.start + text.length;
|
||||
range = TextRange(start: range.start, end: end);
|
||||
controller.newSelection = TextSelection.collapsed(offset: end);
|
||||
@@ -622,7 +622,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
rawText: config.rawText,
|
||||
type: config.type,
|
||||
emote: config.emote,
|
||||
uid: config.uid,
|
||||
id: config.id,
|
||||
),
|
||||
);
|
||||
newSelection =
|
||||
@@ -732,7 +732,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// debugPrint('isValid: $isValid');
|
||||
// debugPrint('isValid: $isValid,,${text.length},,${plainText.length}');
|
||||
// debugPrint('$items\n$selection');
|
||||
|
||||
return TextSpan(
|
||||
@@ -777,6 +777,25 @@ class RichTextEditingController extends TextEditingController {
|
||||
);
|
||||
}
|
||||
return TextSpan(text: e.text);
|
||||
case RichTextType.vote:
|
||||
richStyle ??= (style ?? const TextStyle())
|
||||
.copyWith(color: Theme.of(context).colorScheme.primary);
|
||||
return TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.bar_chart_rounded,
|
||||
size: 22,
|
||||
color: richStyle!.color,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '${e.rawText} ',
|
||||
style: richStyle,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}).toList(),
|
||||
);
|
||||
@@ -917,7 +936,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
}
|
||||
if (offset > range.start && offset < range.end) {
|
||||
if (e.isRich) {
|
||||
if (offset < value.selection.baseOffset) {
|
||||
if (offset < selection.baseOffset) {
|
||||
return newSelection.copyWith(
|
||||
baseOffset: range.start, extentOffset: range.start);
|
||||
} else {
|
||||
|
||||
@@ -917,4 +917,8 @@ class Api {
|
||||
static const String spaceAudio = '/audio/music-service/web/song/upper';
|
||||
|
||||
static const String dynMention = '/x/polymer/web-dynamic/v1/mention/search';
|
||||
|
||||
static const String createVote = '/x/vote/create';
|
||||
|
||||
static const String updateVote = '/x/vote/update';
|
||||
}
|
||||
|
||||
@@ -510,4 +510,30 @@ class DynamicsHttp {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<int?>> createVote(VoteInfo voteInfo) async {
|
||||
final res = await Request().post(
|
||||
Api.createVote,
|
||||
queryParameters: {'csrf': Accounts.main.csrf},
|
||||
data: {'vote_info': voteInfo.toJson()},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return Success(res.data['data']?['vote_id']);
|
||||
} else {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<int?>> updateVote(VoteInfo voteInfo) async {
|
||||
final res = await Request().post(
|
||||
Api.updateVote,
|
||||
queryParameters: {'csrf': Accounts.main.csrf},
|
||||
data: {'vote_info': voteInfo.toJson()},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return Success(res.data['data']?['vote_id']);
|
||||
} else {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ class MsgHttp {
|
||||
}
|
||||
|
||||
static Future uploadBfs({
|
||||
dynamic path,
|
||||
required String path,
|
||||
String? category,
|
||||
String? biz,
|
||||
CancelToken? cancelToken,
|
||||
|
||||
@@ -8,6 +8,17 @@ class SimpleVoteInfo {
|
||||
int? voteId;
|
||||
late int joinNum;
|
||||
|
||||
SimpleVoteInfo({
|
||||
this.choiceCnt,
|
||||
this.defaultShare,
|
||||
this.desc,
|
||||
this.endTime,
|
||||
this.status,
|
||||
this.uid,
|
||||
this.voteId,
|
||||
this.joinNum = 0,
|
||||
});
|
||||
|
||||
SimpleVoteInfo.fromJson(Map<String, dynamic> json) {
|
||||
choiceCnt = json['choice_cnt'];
|
||||
defaultShare = json['default_share'];
|
||||
@@ -29,6 +40,34 @@ class VoteInfo extends SimpleVoteInfo {
|
||||
int? voterLevel;
|
||||
String? face;
|
||||
String? name;
|
||||
// 0 文字, 1 图片
|
||||
int? type;
|
||||
int? votePublisher;
|
||||
int? duration;
|
||||
int? onlyFansLevel;
|
||||
|
||||
VoteInfo({
|
||||
super.choiceCnt,
|
||||
super.defaultShare,
|
||||
super.desc,
|
||||
super.endTime,
|
||||
super.status,
|
||||
super.uid,
|
||||
super.voteId,
|
||||
super.joinNum = 0,
|
||||
this.title,
|
||||
this.ctime,
|
||||
this.myVotes,
|
||||
List<Option>? options,
|
||||
this.optionsCnt,
|
||||
this.voterLevel,
|
||||
this.face,
|
||||
this.name,
|
||||
this.type,
|
||||
this.votePublisher,
|
||||
this.duration,
|
||||
this.onlyFansLevel,
|
||||
}) : options = options ?? <Option>[];
|
||||
|
||||
VoteInfo.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
|
||||
title = json['title'];
|
||||
@@ -42,22 +81,48 @@ class VoteInfo extends SimpleVoteInfo {
|
||||
voterLevel = json['voter_level'];
|
||||
face = json['face'];
|
||||
name = json['name'];
|
||||
type = json['type'];
|
||||
votePublisher = json['vote_publisher'];
|
||||
}
|
||||
|
||||
factory VoteInfo.fromSeparatedJson(Map<String, dynamic> json) {
|
||||
return VoteInfo.fromJson(json['vote_info'])
|
||||
..myVotes = (json['my_votes'] as List?)?.cast(); // voteInfo
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'title': title,
|
||||
'desc': desc,
|
||||
'type': type,
|
||||
'choice_cnt': choiceCnt,
|
||||
'duration': duration,
|
||||
'options': options.map((e) => e.toJson()).toList(),
|
||||
'only_fans_level': onlyFansLevel,
|
||||
'vote_publisher': votePublisher,
|
||||
if (voteId != null) 'vote_id': voteId,
|
||||
};
|
||||
}
|
||||
|
||||
class Option {
|
||||
int? optidx;
|
||||
String? optdesc;
|
||||
int? optIdx;
|
||||
String? optDesc;
|
||||
late int cnt;
|
||||
String? imgUrl;
|
||||
|
||||
Option({
|
||||
this.optDesc,
|
||||
this.imgUrl,
|
||||
});
|
||||
|
||||
Option.fromJson(Map<String, dynamic> json) {
|
||||
optidx = json['opt_idx'];
|
||||
optdesc = json['opt_desc'];
|
||||
optIdx = json['opt_idx'];
|
||||
optDesc = json['opt_desc'];
|
||||
cnt = json['cnt'] ?? 0;
|
||||
imgUrl = json['img_url'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'opt_desc': optDesc,
|
||||
'img_url': imgUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
if (Platform.isAndroid) {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
initPubState();
|
||||
|
||||
@@ -68,7 +70,9 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
}
|
||||
focusNode.dispose();
|
||||
editController.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
if (Platform.isAndroid) {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -83,6 +87,7 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
if (mounted &&
|
||||
widget.autofocus &&
|
||||
panelType.value == PanelType.keyboard) {
|
||||
controller.restoreChatPanel();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
|
||||
@@ -66,9 +66,9 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
controller.keepChatPanel();
|
||||
context.imageView(
|
||||
await context.imageView(
|
||||
imgList: pathList
|
||||
.map((path) => SourceModel(
|
||||
url: path,
|
||||
@@ -77,6 +77,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
.toList(),
|
||||
initialPage: index,
|
||||
);
|
||||
controller.restoreChatPanel();
|
||||
},
|
||||
onLongPress: onClear,
|
||||
child: ClipRRect(
|
||||
@@ -193,31 +194,47 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
|
||||
List<Map<String, dynamic>>? getRichContent() {
|
||||
if (editController.items.isEmpty) return null;
|
||||
return editController.items.map((e) {
|
||||
return switch (e.type) {
|
||||
RichTextType.text || RichTextType.composing => <String, dynamic>{
|
||||
final list = <Map<String, dynamic>>[];
|
||||
for (var e in editController.items) {
|
||||
switch (e.type) {
|
||||
case RichTextType.text || RichTextType.composing:
|
||||
list.add({
|
||||
"raw_text": e.text,
|
||||
"type": 1,
|
||||
"biz_id": "",
|
||||
},
|
||||
RichTextType.at => <String, dynamic>{
|
||||
});
|
||||
case RichTextType.at:
|
||||
list.add({
|
||||
"raw_text": '@${e.rawText}',
|
||||
"type": 2,
|
||||
"biz_id": e.uid,
|
||||
},
|
||||
RichTextType.emoji => <String, dynamic>{
|
||||
"biz_id": e.id,
|
||||
});
|
||||
case RichTextType.emoji:
|
||||
list.add({
|
||||
"raw_text": e.rawText,
|
||||
"type": 9,
|
||||
"biz_id": "",
|
||||
},
|
||||
};
|
||||
}).toList();
|
||||
});
|
||||
case RichTextType.vote:
|
||||
list.add({
|
||||
"raw_text": e.rawText,
|
||||
"type": 4,
|
||||
"biz_id": e.id,
|
||||
});
|
||||
list.add({
|
||||
"raw_text": ' ',
|
||||
"type": 1,
|
||||
"biz_id": "",
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
double _mentionOffset = 0;
|
||||
void onMention([bool fromClick = false]) {
|
||||
Future<void> onMention([bool fromClick = false]) async {
|
||||
controller.keepChatPanel();
|
||||
DynMentionPanel.onDynMention(
|
||||
await DynMentionPanel.onDynMention(
|
||||
context,
|
||||
offset: _mentionOffset,
|
||||
callback: (offset) => _mentionOffset = offset,
|
||||
@@ -227,11 +244,12 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
'@${res.name} ',
|
||||
RichTextType.at,
|
||||
rawText: res.name,
|
||||
uid: res.uid,
|
||||
id: res.uid,
|
||||
fromClick: fromClick,
|
||||
);
|
||||
}
|
||||
});
|
||||
controller.restoreChatPanel();
|
||||
}
|
||||
|
||||
void onInsertText(
|
||||
@@ -239,7 +257,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
RichTextType type, {
|
||||
String? rawText,
|
||||
Emote? emote,
|
||||
String? uid,
|
||||
String? id,
|
||||
bool? fromClick,
|
||||
}) {
|
||||
if (text.isEmpty) {
|
||||
@@ -268,7 +286,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
rawText: rawText,
|
||||
type: type,
|
||||
emote: emote,
|
||||
uid: uid,
|
||||
id: id,
|
||||
);
|
||||
} else {
|
||||
delta = RichTextEditingDeltaInsertion(
|
||||
@@ -282,7 +300,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
rawText: rawText,
|
||||
type: type,
|
||||
emote: emote,
|
||||
uid: uid,
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -297,7 +315,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
rawText: rawText,
|
||||
type: type,
|
||||
emote: emote,
|
||||
uid: uid,
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -308,8 +326,8 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
}
|
||||
|
||||
editController
|
||||
..value = newValue
|
||||
..syncRichText(delta);
|
||||
..syncRichText(delta)
|
||||
..value = newValue.copyWith(selection: editController.newSelection);
|
||||
} else {
|
||||
editController.value = TextEditingValue(
|
||||
text: text,
|
||||
@@ -327,7 +345,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
end: text.length,
|
||||
),
|
||||
emote: emote,
|
||||
uid: uid,
|
||||
id: id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,28 +91,30 @@ class _VotePanelState extends State<VotePanel> {
|
||||
if (_enabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: OutlinedButton(
|
||||
onPressed: groupValue.isNotEmpty
|
||||
? () async {
|
||||
final res = await widget.callback(
|
||||
groupValue,
|
||||
anonymity,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
_showPercentage = true;
|
||||
_voteInfo = res.data;
|
||||
_percentage = _cnt2Percentage(_voteInfo.options);
|
||||
});
|
||||
child: Obx(
|
||||
() => OutlinedButton(
|
||||
onPressed: groupValue.isNotEmpty
|
||||
? () async {
|
||||
final res = await widget.callback(
|
||||
groupValue,
|
||||
anonymity,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
_showPercentage = true;
|
||||
_voteInfo = res.data;
|
||||
_percentage = _cnt2Percentage(_voteInfo.options);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: const Center(child: Text('投票')),
|
||||
: null,
|
||||
child: const Center(child: Text('投票')),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -144,14 +146,14 @@ class _VotePanelState extends State<VotePanel> {
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final opt = _voteInfo.options[index];
|
||||
final selected = groupValue.contains(opt.optidx);
|
||||
final selected = groupValue.contains(opt.optIdx);
|
||||
return PercentageChip(
|
||||
label: opt.optdesc!,
|
||||
label: opt.optDesc!,
|
||||
percentage: _showPercentage ? _percentage[index] : null,
|
||||
selected: selected,
|
||||
onSelected: !_enabled || (groupValue.length >= _maxCnt && !selected)
|
||||
? null
|
||||
: (value) => _onSelected(context, value, opt.optidx!),
|
||||
: (value) => _onSelected(context, value, opt.optIdx!),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,12 +5,15 @@ import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||
import 'package:PiliPlus/common/widgets/draggable_sheet/draggable_scrollable_sheet_dyn.dart'
|
||||
as dyn_sheet;
|
||||
import 'package:PiliPlus/common/widgets/pair.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/text_field.dart';
|
||||
import 'package:PiliPlus/http/dynamics.dart';
|
||||
import 'package:PiliPlus/models/common/publish_panel_type.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_option_type.dart';
|
||||
import 'package:PiliPlus/models/dynamics/vote_model.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
|
||||
import 'package:PiliPlus/pages/common/publish/common_rich_text_pub_page.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_create_vote/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_mention/controller.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_select_topic/view.dart';
|
||||
@@ -341,7 +344,6 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
return PopupMenuButton<bool>(
|
||||
requestFocus: false,
|
||||
initialValue: _isPrivate.value,
|
||||
onOpened: controller.keepChatPanel,
|
||||
onSelected: (value) => _isPrivate.value = value,
|
||||
itemBuilder: (context) => List.generate(
|
||||
2,
|
||||
@@ -398,7 +400,6 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
return PopupMenuButton<ReplyOptionType>(
|
||||
requestFocus: false,
|
||||
initialValue: _replyOption.value,
|
||||
onOpened: controller.keepChatPanel,
|
||||
onSelected: (item) => _replyOption.value = item,
|
||||
itemBuilder: (context) => ReplyOptionType.values
|
||||
.map(
|
||||
@@ -549,6 +550,55 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
tooltip: '@',
|
||||
selected: false,
|
||||
),
|
||||
ToolbarIconButton(
|
||||
onPressed: () async {
|
||||
controller.keepChatPanel();
|
||||
RichTextItem? voteItem = editController.items
|
||||
.firstWhereOrNull((e) => e.type == RichTextType.vote);
|
||||
VoteInfo? voteInfo = await Navigator.of(context).push(
|
||||
GetPageRoute(
|
||||
page: () => CreateVotePage(
|
||||
voteId: voteItem?.id == null
|
||||
? null
|
||||
: int.parse(voteItem!.id!))),
|
||||
);
|
||||
if (voteInfo != null) {
|
||||
if (voteItem != null) {
|
||||
final delta = RichTextEditingDeltaReplacement(
|
||||
oldText: editController.text,
|
||||
replacementText: ' ${voteInfo.title} ',
|
||||
replacedRange: voteItem.range,
|
||||
selection: editController.selection,
|
||||
composing: TextRange.empty,
|
||||
type: RichTextType.vote,
|
||||
id: voteInfo.voteId.toString(),
|
||||
rawText: voteInfo.title,
|
||||
);
|
||||
final newValue = delta.apply(editController.value);
|
||||
editController
|
||||
..syncRichText(delta)
|
||||
..value = newValue.copyWith(
|
||||
selection: editController.newSelection,
|
||||
);
|
||||
} else {
|
||||
onInsertText(
|
||||
'我发起了一个投票',
|
||||
RichTextType.text,
|
||||
);
|
||||
onInsertText(
|
||||
' ${voteInfo.title} ',
|
||||
RichTextType.vote,
|
||||
rawText: voteInfo.title,
|
||||
id: voteInfo.voteId.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
controller.restoreChatPanel();
|
||||
},
|
||||
icon: const Icon(Icons.bar_chart_rounded, size: 22),
|
||||
tooltip: '投票',
|
||||
selected: false,
|
||||
),
|
||||
// if (kDebugMode)
|
||||
// ToolbarIconButton(
|
||||
// onPressed: editController.clear,
|
||||
|
||||
126
lib/pages/dynamics_create_vote/controller.dart
Normal file
126
lib/pages/dynamics_create_vote/controller.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
||||
import 'package:PiliPlus/http/dynamics.dart';
|
||||
import 'package:PiliPlus/http/msg.dart';
|
||||
import 'package:PiliPlus/models/dynamics/vote_model.dart';
|
||||
import 'package:PiliPlus/models_new/upload_bfs/data.dart';
|
||||
import 'package:PiliPlus/utils/accounts.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class CreateVoteController extends GetxController {
|
||||
CreateVoteController(this.voteId);
|
||||
final int? voteId;
|
||||
|
||||
String key = Utils.generateRandomString(6);
|
||||
final RxString title = ''.obs;
|
||||
final RxString desc = ''.obs;
|
||||
final RxInt type = 0.obs;
|
||||
final RxList<Option> options = <Option>[
|
||||
Option(optDesc: '', imgUrl: ''),
|
||||
Option(optDesc: '', imgUrl: ''),
|
||||
].obs;
|
||||
final RxInt choiceCnt = 1.obs;
|
||||
final now = DateTime.now();
|
||||
late final end = now.copyWith(day: now.day + 90);
|
||||
late Rx<DateTime> endtime;
|
||||
final RxBool canCreate = false.obs;
|
||||
|
||||
void updateCanCreate() {
|
||||
if (type.value == 0) {
|
||||
canCreate.value = title.value.isNotEmpty &&
|
||||
options.every((e) => e.optDesc?.isNotEmpty == true);
|
||||
} else {
|
||||
canCreate.value = title.value.isNotEmpty &&
|
||||
options.every(
|
||||
(e) =>
|
||||
e.optDesc?.isNotEmpty == true && e.imgUrl?.isNotEmpty == true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
endtime = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day + 1,
|
||||
now.hour,
|
||||
now.minute,
|
||||
).obs;
|
||||
if (voteId != null) {
|
||||
queryData();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> queryData() async {
|
||||
var res = await DynamicsHttp.voteInfo(voteId);
|
||||
if (res.isSuccess) {
|
||||
key = Utils.generateRandomString(6);
|
||||
final VoteInfo data = res.data;
|
||||
title.value = data.title!;
|
||||
desc.value = data.desc ?? '';
|
||||
type.value = data.options.first.imgUrl?.isNotEmpty == true ? 1 : 0;
|
||||
options.value = data.options;
|
||||
choiceCnt.value = data.choiceCnt!;
|
||||
endtime.value = DateTime.fromMillisecondsSinceEpoch(data.endTime! * 1000);
|
||||
canCreate.value = true;
|
||||
} else {
|
||||
showConfirmDialog(
|
||||
context: Get.context!,
|
||||
title: res.toString(),
|
||||
onConfirm: Get.back,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onDel(int i) {
|
||||
options.removeAt(i);
|
||||
updateCanCreate();
|
||||
if (choiceCnt.value > options.length) {
|
||||
choiceCnt.value = options.length;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onCreate() async {
|
||||
final voteInfo = VoteInfo(
|
||||
title: title.value,
|
||||
desc: desc.value,
|
||||
type: type.value,
|
||||
duration: endtime.value.difference(now).inSeconds,
|
||||
options: options,
|
||||
onlyFansLevel: 0,
|
||||
choiceCnt: choiceCnt.value,
|
||||
votePublisher: Accounts.main.mid,
|
||||
voteId: voteId,
|
||||
);
|
||||
var res = voteId == null
|
||||
? await DynamicsHttp.createVote(voteInfo)
|
||||
: await DynamicsHttp.updateVote(voteInfo);
|
||||
if (res.isSuccess) {
|
||||
voteInfo.voteId = res.data;
|
||||
Get.back(result: voteInfo);
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onUpload(int index, XFile pickedFile) async {
|
||||
var res = await MsgHttp.uploadBfs(
|
||||
path: pickedFile.path,
|
||||
category: 'daily',
|
||||
biz: 'vote',
|
||||
);
|
||||
if (res['status']) {
|
||||
UploadBfsResData data = res['data'];
|
||||
options
|
||||
..[index].imgUrl = data.imageUrl
|
||||
..refresh();
|
||||
updateCanCreate();
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
}
|
||||
}
|
||||
412
lib/pages/dynamics_create_vote/view.dart
Normal file
412
lib/pages/dynamics_create_vote/view.dart
Normal file
@@ -0,0 +1,412 @@
|
||||
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/dynamics/vote_model.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_create_vote/controller.dart';
|
||||
import 'package:PiliPlus/utils/date_util.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class CreateVotePage extends StatefulWidget {
|
||||
const CreateVotePage({super.key, this.voteId});
|
||||
|
||||
final int? voteId;
|
||||
|
||||
@override
|
||||
State<CreateVotePage> createState() => _CreateVotePageState();
|
||||
}
|
||||
|
||||
class _CreateVotePageState extends State<CreateVotePage> {
|
||||
late final _controller = Get.put(CreateVoteController(widget.voteId),
|
||||
tag: Utils.generateRandomString(8));
|
||||
late final imagePicker = ImagePicker();
|
||||
|
||||
late Divider _divider;
|
||||
late TextStyle _leadingStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
_divider = Divider(
|
||||
height: 1,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.1),
|
||||
);
|
||||
_leadingStyle = TextStyle(
|
||||
fontSize: 15,
|
||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.9),
|
||||
);
|
||||
final padding = MediaQuery.paddingOf(context);
|
||||
final divider = [
|
||||
const SizedBox(height: 10),
|
||||
_divider,
|
||||
const SizedBox(height: 10),
|
||||
];
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('${_controller.voteId != null ? '' : '发起'}投票'),
|
||||
),
|
||||
body: ListView(
|
||||
padding: EdgeInsets.only(
|
||||
left: padding.left + 16,
|
||||
right: padding.right + 16,
|
||||
bottom: padding.bottom + 80,
|
||||
),
|
||||
children: [
|
||||
const Text(
|
||||
'投票类型',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildType(theme),
|
||||
const SizedBox(height: 40),
|
||||
Obx(
|
||||
() => _buildInput(
|
||||
theme,
|
||||
key: ValueKey('${_controller.key}title'),
|
||||
initialValue: _controller.title.value,
|
||||
onChanged: (value) => _controller
|
||||
..title.value = value
|
||||
..updateCanCreate(),
|
||||
desc: '投票标题',
|
||||
hintText: '请填写标题',
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(32)],
|
||||
),
|
||||
),
|
||||
...divider,
|
||||
Obx(
|
||||
() => _buildInput(
|
||||
theme,
|
||||
key: ValueKey('${_controller.key}desc'),
|
||||
initialValue: _controller.desc.value,
|
||||
onChanged: (value) => _controller.desc.value = value,
|
||||
desc: '投票说明',
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(100)],
|
||||
),
|
||||
),
|
||||
...divider,
|
||||
const SizedBox(height: 40),
|
||||
Obx(
|
||||
() {
|
||||
final showImg = _controller.type.value == 1;
|
||||
final showDel = _controller.options.length > 2;
|
||||
List<Widget> children = [];
|
||||
for (int i = 0; i < _controller.options.length; i++) {
|
||||
final e = _controller.options[i];
|
||||
children
|
||||
..add(_buildInput(
|
||||
theme,
|
||||
key: ValueKey(e.hashCode),
|
||||
showDel: showDel,
|
||||
onDel: () {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
_controller.onDel(i);
|
||||
},
|
||||
showImg: showImg,
|
||||
imgUrl: e.imgUrl,
|
||||
onPickImg: () => EasyThrottle.throttle(
|
||||
'picImg',
|
||||
const Duration(milliseconds: 500),
|
||||
() => _onPickImg(i),
|
||||
),
|
||||
initialValue: e.optDesc,
|
||||
onChanged: (value) => _controller
|
||||
..options[i].optDesc = value
|
||||
..updateCanCreate(),
|
||||
desc: '选项${i + 1}',
|
||||
hintText: '选项内容,最多20字',
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(20)],
|
||||
))
|
||||
..addAll(divider);
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...children,
|
||||
if (_controller.options.length < 20)
|
||||
FilledButton(
|
||||
onPressed: () => _controller
|
||||
..options.add(Option(optDesc: '', imgUrl: ''))
|
||||
..updateCanCreate(),
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: Size.zero,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10, right: 14, top: 4, bottom: 4),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
foregroundColor: theme.colorScheme.onSurfaceVariant,
|
||||
backgroundColor: theme.colorScheme.onInverseSurface,
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add, size: 16),
|
||||
Text(
|
||||
' 添加选项',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text('单选/多选', style: _leadingStyle),
|
||||
),
|
||||
Obx(() {
|
||||
final choiceCnt = _controller.choiceCnt.value;
|
||||
final choices =
|
||||
List.generate(_controller.options.length, (i) => i + 1);
|
||||
return Listener(
|
||||
onPointerDown: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
child: PopupMenuButton<int>(
|
||||
initialValue: choiceCnt,
|
||||
requestFocus: false,
|
||||
child:
|
||||
Text(choiceCnt == 1 ? '单选 ' : '最多选$choiceCnt项'),
|
||||
onSelected: (value) => _controller.choiceCnt.value = value,
|
||||
itemBuilder: (context) {
|
||||
return choices
|
||||
.map((e) => PopupMenuItem(
|
||||
value: e,
|
||||
child: Text(e == 1 ? '单选' : '最多选$e项'),
|
||||
))
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
...divider,
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text('投票截止时间', style: _leadingStyle),
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
DateTime? newDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _controller.endtime.value,
|
||||
firstDate: _controller.now,
|
||||
lastDate: _controller.end,
|
||||
);
|
||||
if (newDate != null && context.mounted) {
|
||||
TimeOfDay? newTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime:
|
||||
TimeOfDay.fromDateTime(_controller.endtime.value),
|
||||
);
|
||||
if (newTime != null) {
|
||||
final newEndtime = DateTime(
|
||||
newDate.year,
|
||||
newDate.month,
|
||||
newDate.day,
|
||||
newTime.hour,
|
||||
newTime.minute,
|
||||
);
|
||||
if (newEndtime.difference(DateTime.now()) >=
|
||||
const Duration(minutes: 5)) {
|
||||
_controller.endtime.value = newEndtime;
|
||||
} else {
|
||||
SmartDialog.showToast('至少选择5分钟之后');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Obx(() => Text(
|
||||
DateUtil.longFormatD.format(_controller.endtime.value),
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
...divider,
|
||||
const SizedBox(height: 40),
|
||||
Obx(() {
|
||||
final canCreate = _controller.canCreate.value;
|
||||
return FilledButton.tonal(
|
||||
onPressed: canCreate ? _controller.onCreate : null,
|
||||
child: const Text('发起投票'),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInput(
|
||||
ThemeData theme, {
|
||||
Key? key,
|
||||
String? initialValue,
|
||||
required ValueChanged<String> onChanged,
|
||||
required String desc,
|
||||
String? hintText,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
bool showDel = false,
|
||||
bool showImg = false,
|
||||
String? imgUrl,
|
||||
VoidCallback? onDel,
|
||||
VoidCallback? onPickImg,
|
||||
}) {
|
||||
return Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 65,
|
||||
child: Text(
|
||||
desc,
|
||||
style: _leadingStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
key: key,
|
||||
initialValue: initialValue,
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: hintText ?? desc,
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 15,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
inputFormatters: inputFormatters,
|
||||
),
|
||||
),
|
||||
if (showImg)
|
||||
GestureDetector(
|
||||
onTap: onPickImg,
|
||||
child: NetworkImgLayer(
|
||||
src: imgUrl,
|
||||
width: 40,
|
||||
height: 40,
|
||||
radius: 6,
|
||||
),
|
||||
),
|
||||
if (showDel)
|
||||
iconButton(
|
||||
size: 26,
|
||||
iconSize: 18,
|
||||
tooltip: '移除',
|
||||
context: context,
|
||||
icon: Icons.clear,
|
||||
onPressed: onDel,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildType(ThemeData theme) => Obx(
|
||||
() {
|
||||
return Row(
|
||||
spacing: 16,
|
||||
children: List.generate(
|
||||
2,
|
||||
(index) {
|
||||
final isEnable = index == _controller.type.value;
|
||||
final style = TextButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 17),
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: isEnable
|
||||
? theme.colorScheme.secondary
|
||||
: theme.colorScheme.outline,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
backgroundColor: isEnable
|
||||
? theme.colorScheme.secondaryContainer
|
||||
: Colors.transparent,
|
||||
foregroundColor: isEnable
|
||||
? theme.colorScheme.onSecondaryContainer
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
);
|
||||
Widget child = TextButton(
|
||||
style: style,
|
||||
onPressed: () => _controller
|
||||
..type.value = index
|
||||
..updateCanCreate(),
|
||||
child: Text(
|
||||
'${const ['文字', '图片'][index]}投票',
|
||||
strutStyle: const StrutStyle(forceStrutHeight: true),
|
||||
),
|
||||
);
|
||||
if (isEnable) {
|
||||
child = Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
child,
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(1),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
bottomRight: Radius.circular(6),
|
||||
),
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
child: Icon(
|
||||
size: 10,
|
||||
Icons.check,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
void _onPickImg(int index) {
|
||||
EasyThrottle.throttle('imagePicker', const Duration(milliseconds: 500),
|
||||
() async {
|
||||
try {
|
||||
XFile? pickedFile = await imagePicker.pickImage(
|
||||
imageQuality: 100,
|
||||
source: ImageSource.gallery,
|
||||
);
|
||||
if (pickedFile != null) {
|
||||
_controller.onUpload(index, pickedFile);
|
||||
}
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -200,5 +200,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<LiveSendDmPanel> {
|
||||
}
|
||||
|
||||
@override
|
||||
void onMention([bool fromClick = false]) {}
|
||||
Future<void> onMention([bool fromClick = false]) {
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
|
||||
Future<void> onCustomPublish({List? pictures}) async {
|
||||
Map<String, int> atNameToMid = {
|
||||
for (var e in editController.items)
|
||||
if (e.type == RichTextType.at) e.rawText: int.parse(e.uid!),
|
||||
if (e.type == RichTextType.at) e.rawText: int.parse(e.id!),
|
||||
};
|
||||
String message = editController.rawText;
|
||||
var result = await VideoHttp.replyAdd(
|
||||
|
||||
@@ -426,9 +426,9 @@ class _SendDanmakuPanelState extends CommonTextPubPageState<SendDanmakuPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showColorPicker() {
|
||||
Future<void> _showColorPicker() async {
|
||||
controller.keepChatPanel();
|
||||
showDialog(
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
@@ -445,6 +445,7 @@ class _SendDanmakuPanelState extends CommonTextPubPageState<SendDanmakuPanel> {
|
||||
),
|
||||
),
|
||||
);
|
||||
controller.restoreChatPanel();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -335,7 +335,9 @@ class _WhisperDetailPageState
|
||||
}
|
||||
|
||||
@override
|
||||
void onMention([bool fromClick = false]) {}
|
||||
Future<void> onMention([bool fromClick = false]) {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
void onSave() {}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:PiliPlus/pages/article_list/view.dart';
|
||||
import 'package:PiliPlus/pages/blacklist/view.dart';
|
||||
import 'package:PiliPlus/pages/danmaku_block/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_create_vote/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_detail/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_topic/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_topic_rcmd/view.dart';
|
||||
@@ -186,6 +187,7 @@ class Routes {
|
||||
CustomGetPage(name: '/msgLikeDetail', page: () => const LikeDetailPage()),
|
||||
CustomGetPage(
|
||||
name: '/liveDmBlockPage', page: () => const LiveDmBlockPage()),
|
||||
CustomGetPage(name: '/createVote', page: () => const CreateVotePage()),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -85,14 +85,14 @@ extension BuildContextExt on BuildContext {
|
||||
: const Color(0xFFD44E7D);
|
||||
}
|
||||
|
||||
void imageView({
|
||||
Future<void> imageView({
|
||||
int initialPage = 0,
|
||||
required List<SourceModel> imgList,
|
||||
ValueChanged<int>? onDismissed,
|
||||
int? quality,
|
||||
}) {
|
||||
bool isMemberPage = Get.currentRoute.startsWith('/member?');
|
||||
Navigator.of(this).push(
|
||||
return Navigator.of(this).push(
|
||||
HeroDialogRoute(
|
||||
builder: (context) => InteractiveviewerGallery(
|
||||
sources: imgList,
|
||||
|
||||
Reference in New Issue
Block a user