Files
PiliPlus/lib/pages/common/publish/common_rich_text_pub_page.dart
bggRGjQaUbCoE 67182d2641 feat: create vote
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-04 22:08:35 +08:00

359 lines
9.5 KiB
Dart

import 'dart:io';
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_mention/item.dart';
import 'package:PiliPlus/models_new/emote/emote.dart' as e;
import 'package:PiliPlus/models_new/live/live_emote/emoticon.dart';
import 'package:PiliPlus/pages/common/publish/common_publish_page.dart';
import 'package:PiliPlus/pages/dynamics_mention/view.dart';
import 'package:PiliPlus/utils/extension.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:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
abstract class CommonRichTextPubPage
extends CommonPublishPage<List<RichTextItem>> {
const CommonRichTextPubPage({
super.key,
this.items,
super.onSave,
super.autofocus,
super.imageLengthLimit,
});
final List<RichTextItem>? items;
}
abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
extends CommonPublishPageState<T> {
@override
late final RichTextEditingController editController =
RichTextEditingController(
items: widget.items,
onMention: onMention,
);
@override
void initPubState() {
if (editController.rawText.trim().isNotEmpty) {
enablePublish.value = true;
}
}
@override
void didChangeDependencies() {
editController.richStyle = null;
super.didChangeDependencies();
}
Widget buildImage(int index, double height) {
final color =
Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.5);
void onClear() {
pathList.removeAt(index);
if (pathList.isEmpty && editController.rawText.trim().isEmpty) {
enablePublish.value = false;
}
}
return Stack(
clipBehavior: Clip.none,
children: [
GestureDetector(
onTap: () async {
controller.keepChatPanel();
await context.imageView(
imgList: pathList
.map((path) => SourceModel(
url: path,
sourceType: SourceType.fileImage,
))
.toList(),
initialPage: index,
);
controller.restoreChatPanel();
},
onLongPress: onClear,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: Image(
height: height,
fit: BoxFit.fitHeight,
filterQuality: FilterQuality.low,
image: FileImage(File(pathList[index])),
),
),
),
Positioned(
top: 34,
right: 5,
child: iconButton(
context: context,
icon: Icons.edit,
onPressed: () => onCropImage(index),
size: 24,
iconSize: 14,
bgColor: color,
),
),
Positioned(
top: 5,
right: 5,
child: iconButton(
context: context,
icon: Icons.clear,
onPressed: onClear,
size: 24,
iconSize: 14,
bgColor: color,
),
),
],
);
}
Future<void> onCropImage(int index) async {
final theme = Theme.of(context);
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: pathList[index],
uiSettings: [
AndroidUiSettings(
toolbarTitle: '裁剪',
toolbarColor: theme.colorScheme.secondaryContainer,
toolbarWidgetColor: theme.colorScheme.onSecondaryContainer,
),
IOSUiSettings(
title: '裁剪',
),
],
);
if (croppedFile != null) {
pathList[index] = croppedFile.path;
}
}
void onPickImage([VoidCallback? callback]) {
EasyThrottle.throttle('imagePicker', const Duration(milliseconds: 500),
() async {
try {
List<XFile> pickedFiles = await imagePicker.pickMultiImage(
limit: limit,
imageQuality: 100,
);
if (pickedFiles.isNotEmpty) {
for (int i = 0; i < pickedFiles.length; i++) {
if (pathList.length == limit) {
SmartDialog.showToast('最多选择$limit张图片');
break;
} else {
pathList.add(pickedFiles[i].path);
}
}
callback?.call();
}
} catch (e) {
SmartDialog.showToast(e.toString());
}
});
}
void onChooseEmote(dynamic emote, double? width, double? height) {
if (emote is e.Emote) {
final isTextEmote = width == null;
onInsertText(
isTextEmote ? emote.text! : '\uFFFC',
RichTextType.emoji,
rawText: emote.text!,
emote: isTextEmote
? null
: Emote(
url: emote.url!,
width: width,
height: height,
),
);
} else if (emote is Emoticon) {
onInsertText(
'\uFFFC',
RichTextType.emoji,
rawText: emote.emoji!,
emote: Emote(
url: emote.url!,
width: width!,
height: height,
),
);
}
}
List<Map<String, dynamic>>? getRichContent() {
if (editController.items.isEmpty) return null;
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": "",
});
case RichTextType.at:
list.add({
"raw_text": '@${e.rawText}',
"type": 2,
"biz_id": e.id,
});
case RichTextType.emoji:
list.add({
"raw_text": e.rawText,
"type": 9,
"biz_id": "",
});
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;
Future<void> onMention([bool fromClick = false]) async {
controller.keepChatPanel();
await DynMentionPanel.onDynMention(
context,
offset: _mentionOffset,
callback: (offset) => _mentionOffset = offset,
).then((MentionItem? res) {
if (res != null) {
onInsertText(
'@${res.name} ',
RichTextType.at,
rawText: res.name,
id: res.uid,
fromClick: fromClick,
);
}
});
controller.restoreChatPanel();
}
void onInsertText(
String text,
RichTextType type, {
String? rawText,
Emote? emote,
String? id,
bool? fromClick,
}) {
if (text.isEmpty) {
return;
}
enablePublish.value = true;
var oldValue = editController.value;
final selection = oldValue.selection;
if (selection.isValid) {
TextEditingDelta delta;
if (selection.isCollapsed) {
if (type == RichTextType.at && fromClick == false) {
delta = RichTextEditingDeltaReplacement(
oldText: oldValue.text,
replacementText: text,
replacedRange:
TextRange(start: selection.start - 1, end: selection.end),
selection: TextSelection.collapsed(
offset: selection.start - 1 + text.length,
),
composing: TextRange.empty,
rawText: rawText,
type: type,
emote: emote,
id: id,
);
} else {
delta = RichTextEditingDeltaInsertion(
oldText: oldValue.text,
textInserted: text,
insertionOffset: selection.start,
selection: TextSelection.collapsed(
offset: selection.start + text.length,
),
composing: TextRange.empty,
rawText: rawText,
type: type,
emote: emote,
id: id,
);
}
} else {
delta = RichTextEditingDeltaReplacement(
oldText: oldValue.text,
replacementText: text,
replacedRange: selection,
selection: TextSelection.collapsed(
offset: selection.start + text.length,
),
composing: TextRange.empty,
rawText: rawText,
type: type,
emote: emote,
id: id,
);
}
final newValue = delta.apply(oldValue);
if (oldValue == newValue) {
return;
}
editController
..syncRichText(delta)
..value = newValue.copyWith(selection: editController.newSelection);
} else {
editController.value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
);
editController.items
..clear()
..add(
RichTextItem(
type: type,
text: text,
rawText: rawText,
range: TextRange(
start: 0,
end: text.length,
),
emote: emote,
id: id,
),
);
}
}
@override
void onSave() {
widget.onSave?.call(editController.items);
}
}