feat: ai总结
This commit is contained in:
BIN
assets/images/ai.png
Normal file
BIN
assets/images/ai.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.7 KiB |
@ -327,4 +327,13 @@ class Api {
|
|||||||
// id=849312409672744983
|
// id=849312409672744983
|
||||||
// features=itemOpusStyle
|
// features=itemOpusStyle
|
||||||
static const String dynamicDetail = '/x/polymer/web-dynamic/v1/detail';
|
static const String dynamicDetail = '/x/polymer/web-dynamic/v1/detail';
|
||||||
|
|
||||||
|
// AI总结
|
||||||
|
/// https://api.bilibili.com/x/web-interface/view/conclusion/get?
|
||||||
|
/// bvid=BV1ju4y1s7kn&
|
||||||
|
/// cid=1296086601&
|
||||||
|
/// up_mid=4641697&
|
||||||
|
/// w_rid=1607c6c5a4a35a1297e31992220900ae&
|
||||||
|
/// wts=1697033079
|
||||||
|
static const String aiConclusion = '/x/web-interface/view/conclusion/get';
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,11 @@ import 'package:pilipala/models/home/rcmd/result.dart';
|
|||||||
import 'package:pilipala/models/model_hot_video_item.dart';
|
import 'package:pilipala/models/model_hot_video_item.dart';
|
||||||
import 'package:pilipala/models/model_rec_video_item.dart';
|
import 'package:pilipala/models/model_rec_video_item.dart';
|
||||||
import 'package:pilipala/models/user/fav_folder.dart';
|
import 'package:pilipala/models/user/fav_folder.dart';
|
||||||
|
import 'package:pilipala/models/video/ai.dart';
|
||||||
import 'package:pilipala/models/video/play/url.dart';
|
import 'package:pilipala/models/video/play/url.dart';
|
||||||
import 'package:pilipala/models/video_detail_res.dart';
|
import 'package:pilipala/models/video_detail_res.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
import 'package:pilipala/utils/wbi_sign.dart';
|
||||||
|
|
||||||
/// res.data['code'] == 0 请求正常返回结果
|
/// res.data['code'] == 0 请求正常返回结果
|
||||||
/// res.data['data'] 为结果
|
/// res.data['data'] 为结果
|
||||||
@ -420,4 +422,23 @@ class VideoHttp {
|
|||||||
return {'status': true, 'data': res.data['data']};
|
return {'status': true, 'data': res.data['data']};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future aiConclusion({
|
||||||
|
String? bvid,
|
||||||
|
int? cid,
|
||||||
|
int? upMid,
|
||||||
|
}) async {
|
||||||
|
Map params = await WbiSign().makSign({
|
||||||
|
'bvid': bvid,
|
||||||
|
'cid': cid,
|
||||||
|
'up_mid': upMid,
|
||||||
|
});
|
||||||
|
var res = await Request().get(Api.aiConclusion, data: params);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': AiConclusionModel.fromJson(res.data['data']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
80
lib/models/video/ai.dart
Normal file
80
lib/models/video/ai.dart
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
class AiConclusionModel {
|
||||||
|
AiConclusionModel({
|
||||||
|
this.code,
|
||||||
|
this.modelResult,
|
||||||
|
this.stid,
|
||||||
|
this.status,
|
||||||
|
this.likeNum,
|
||||||
|
this.dislikeNum,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? code;
|
||||||
|
ModelResult? modelResult;
|
||||||
|
String? stid;
|
||||||
|
int? status;
|
||||||
|
int? likeNum;
|
||||||
|
int? dislikeNum;
|
||||||
|
|
||||||
|
AiConclusionModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
code = json['code'];
|
||||||
|
modelResult = ModelResult.fromJson(json['model_result']);
|
||||||
|
stid = json['stid'];
|
||||||
|
status = json['status'];
|
||||||
|
likeNum = json['like_num'];
|
||||||
|
dislikeNum = json['dislike_num'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModelResult {
|
||||||
|
ModelResult({
|
||||||
|
this.resultType,
|
||||||
|
this.summary,
|
||||||
|
this.outline,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? resultType;
|
||||||
|
String? summary;
|
||||||
|
List<OutlineItem>? outline;
|
||||||
|
|
||||||
|
ModelResult.fromJson(Map<String, dynamic> json) {
|
||||||
|
resultType = json['result_type'];
|
||||||
|
summary = json['summary'];
|
||||||
|
outline = json['result_type'] == 2
|
||||||
|
? json['outline']
|
||||||
|
.map<OutlineItem>((e) => OutlineItem.fromJson(e))
|
||||||
|
.toList()
|
||||||
|
: <OutlineItem>[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OutlineItem {
|
||||||
|
OutlineItem({
|
||||||
|
this.title,
|
||||||
|
this.partOutline,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? title;
|
||||||
|
List<PartOutline>? partOutline;
|
||||||
|
|
||||||
|
OutlineItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
title = json['title'];
|
||||||
|
partOutline = json['part_outline']
|
||||||
|
.map<PartOutline>((e) => PartOutline.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PartOutline {
|
||||||
|
PartOutline({
|
||||||
|
this.timestamp,
|
||||||
|
this.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? timestamp;
|
||||||
|
String? content;
|
||||||
|
|
||||||
|
PartOutline.fromJson(Map<String, dynamic> json) {
|
||||||
|
timestamp = json['timestamp'];
|
||||||
|
content = json['content'];
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import 'package:pilipala/http/constants.dart';
|
|||||||
import 'package:pilipala/http/user.dart';
|
import 'package:pilipala/http/user.dart';
|
||||||
import 'package:pilipala/http/video.dart';
|
import 'package:pilipala/http/video.dart';
|
||||||
import 'package:pilipala/models/user/fav_folder.dart';
|
import 'package:pilipala/models/user/fav_folder.dart';
|
||||||
|
import 'package:pilipala/models/video/ai.dart';
|
||||||
import 'package:pilipala/models/video_detail_res.dart';
|
import 'package:pilipala/models/video_detail_res.dart';
|
||||||
import 'package:pilipala/pages/video/detail/controller.dart';
|
import 'package:pilipala/pages/video/detail/controller.dart';
|
||||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||||
@ -62,6 +63,7 @@ class VideoIntroController extends GetxController {
|
|||||||
Timer? timer;
|
Timer? timer;
|
||||||
bool isPaused = false;
|
bool isPaused = false;
|
||||||
String heroTag = Get.arguments['heroTag'];
|
String heroTag = Get.arguments['heroTag'];
|
||||||
|
late ModelResult modelResult;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -561,4 +563,25 @@ class VideoIntroController extends GetxController {
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ai总结
|
||||||
|
Future aiConclusion() async {
|
||||||
|
SmartDialog.showLoading(msg: '正在生产ai总结');
|
||||||
|
var res = await VideoHttp.aiConclusion(
|
||||||
|
bvid: bvid,
|
||||||
|
cid: lastPlayCid.value,
|
||||||
|
upMid: videoDetail.value.owner!.mid!,
|
||||||
|
);
|
||||||
|
if (res['status']) {
|
||||||
|
if (res['data'].modelResult.resultType == 0) {
|
||||||
|
SmartDialog.showToast('该视频不支持ai总结');
|
||||||
|
}
|
||||||
|
if (res['data'].modelResult.resultType == 2 ||
|
||||||
|
res['data'].modelResult.resultType == 1) {
|
||||||
|
modelResult = res['data'].modelResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import 'package:pilipala/common/widgets/stat/danmu.dart';
|
|||||||
import 'package:pilipala/common/widgets/stat/view.dart';
|
import 'package:pilipala/common/widgets/stat/view.dart';
|
||||||
import 'package:pilipala/models/video_detail_res.dart';
|
import 'package:pilipala/models/video_detail_res.dart';
|
||||||
import 'package:pilipala/pages/video/detail/introduction/controller.dart';
|
import 'package:pilipala/pages/video/detail/introduction/controller.dart';
|
||||||
|
import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
|
||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import 'package:pilipala/utils/utils.dart';
|
import 'package:pilipala/utils/utils.dart';
|
||||||
@ -226,6 +227,17 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
arguments: {'face': face, 'heroTag': memberHeroTag});
|
arguments: {'face': face, 'heroTag': memberHeroTag});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ai总结
|
||||||
|
showAiBottomSheet() {
|
||||||
|
showBottomSheet(
|
||||||
|
context: context,
|
||||||
|
enableDrag: true,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AiDetail(modelResult: videoIntroController.modelResult);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
ThemeData t = Theme.of(context);
|
ThemeData t = Theme.of(context);
|
||||||
@ -238,70 +250,91 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
? Column(
|
? Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
onTap: () => showIntroDetail(),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 6),
|
|
||||||
child: Text(
|
|
||||||
!loadingStatus
|
|
||||||
? widget.videoDetail!.title
|
|
||||||
: videoItem['title'],
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 17,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: () => showIntroDetail(),
|
onTap: () => showIntroDetail(),
|
||||||
child: Row(
|
child: Text(
|
||||||
children: [
|
!loadingStatus
|
||||||
StatView(
|
? widget.videoDetail!.title
|
||||||
theme: 'gray',
|
: videoItem['title'],
|
||||||
view: !widget.loadingStatus
|
style: const TextStyle(
|
||||||
? widget.videoDetail!.stat!.view
|
fontSize: 18,
|
||||||
: videoItem['stat'].view,
|
fontWeight: FontWeight.bold,
|
||||||
size: 'medium',
|
),
|
||||||
),
|
maxLines: 2,
|
||||||
const SizedBox(width: 10),
|
overflow: TextOverflow.ellipsis,
|
||||||
StatDanMu(
|
|
||||||
theme: 'gray',
|
|
||||||
danmu: !widget.loadingStatus
|
|
||||||
? widget.videoDetail!.stat!.danmaku
|
|
||||||
: videoItem['stat'].danmaku,
|
|
||||||
size: 'medium',
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text(
|
|
||||||
Utils.dateFormat(
|
|
||||||
!widget.loadingStatus
|
|
||||||
? widget.videoDetail!.pubdate
|
|
||||||
: videoItem['pubdate'],
|
|
||||||
formatType: 'detail'),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: t.colorScheme.outline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
if (videoIntroController.isShowOnlineTotal)
|
|
||||||
Obx(
|
|
||||||
() => Text(
|
|
||||||
'${videoIntroController.total.value}人在看',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: t.colorScheme.outline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 7),
|
Stack(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: () => showIntroDetail(),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 7, bottom: 6),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
StatView(
|
||||||
|
theme: 'gray',
|
||||||
|
view: !widget.loadingStatus
|
||||||
|
? widget.videoDetail!.stat!.view
|
||||||
|
: videoItem['stat'].view,
|
||||||
|
size: 'medium',
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
StatDanMu(
|
||||||
|
theme: 'gray',
|
||||||
|
danmu: !widget.loadingStatus
|
||||||
|
? widget.videoDetail!.stat!.danmaku
|
||||||
|
: videoItem['stat'].danmaku,
|
||||||
|
size: 'medium',
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
Utils.dateFormat(
|
||||||
|
!widget.loadingStatus
|
||||||
|
? widget.videoDetail!.pubdate
|
||||||
|
: videoItem['pubdate'],
|
||||||
|
formatType: 'detail'),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: t.colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
if (videoIntroController.isShowOnlineTotal)
|
||||||
|
Obx(
|
||||||
|
() => Text(
|
||||||
|
'${videoIntroController.total.value}人在看',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: t.colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 10,
|
||||||
|
top: 6,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
var res = await videoIntroController.aiConclusion();
|
||||||
|
if (res['status']) {
|
||||||
|
if (res['data'].modelResult.resultType == 2 ||
|
||||||
|
res['data'].modelResult.resultType == 1) {
|
||||||
|
showAiBottomSheet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child:
|
||||||
|
Image.asset('assets/images/ai.png', height: 22),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
// 点赞收藏转发 布局样式1
|
// 点赞收藏转发 布局样式1
|
||||||
// SingleChildScrollView(
|
// SingleChildScrollView(
|
||||||
// padding: const EdgeInsets.only(top: 7, bottom: 7),
|
// padding: const EdgeInsets.only(top: 7, bottom: 7),
|
||||||
|
236
lib/pages/video/detail/widgets/ai_detail.dart
Normal file
236
lib/pages/video/detail/widgets/ai_detail.dart
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:pilipala/models/video/ai.dart';
|
||||||
|
import 'package:pilipala/pages/video/detail/index.dart';
|
||||||
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
|
Box localCache = GStrorage.localCache;
|
||||||
|
late double sheetHeight;
|
||||||
|
|
||||||
|
class AiDetail extends StatelessWidget {
|
||||||
|
final ModelResult? modelResult;
|
||||||
|
|
||||||
|
const AiDetail({
|
||||||
|
Key? key,
|
||||||
|
this.modelResult,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
sheetHeight = localCache.get('sheetHeight');
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).colorScheme.background,
|
||||||
|
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||||
|
height: sheetHeight,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () => Get.back(),
|
||||||
|
child: Container(
|
||||||
|
height: 35,
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 32,
|
||||||
|
height: 3,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
modelResult!.summary!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: modelResult!.outline!.length,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
modelResult!.outline![index].title!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: modelResult!
|
||||||
|
.outline![index].partOutline!.length,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
children: [
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onBackground,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: Utils.tampToSeektime(
|
||||||
|
modelResult!
|
||||||
|
.outline![index]
|
||||||
|
.partOutline![i]
|
||||||
|
.timestamp!),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
// 跳转到指定位置
|
||||||
|
try {
|
||||||
|
Get.find<VideoDetailController>(
|
||||||
|
tag: Get.arguments[
|
||||||
|
'heroTag'])
|
||||||
|
.plPlayerController
|
||||||
|
.seekTo(
|
||||||
|
Duration(
|
||||||
|
seconds:
|
||||||
|
Utils.duration(
|
||||||
|
Utils.tampToSeektime(modelResult!
|
||||||
|
.outline![
|
||||||
|
index]
|
||||||
|
.partOutline![
|
||||||
|
i]
|
||||||
|
.timestamp!)
|
||||||
|
.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const TextSpan(text: ' '),
|
||||||
|
TextSpan(
|
||||||
|
text: modelResult!
|
||||||
|
.outline![index]
|
||||||
|
.partOutline![i]
|
||||||
|
.content!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InlineSpan buildContent(BuildContext context, content) {
|
||||||
|
List descV2 = content.descV2;
|
||||||
|
// type
|
||||||
|
// 1 普通文本
|
||||||
|
// 2 @用户
|
||||||
|
List<TextSpan> spanChilds = List.generate(descV2.length, (index) {
|
||||||
|
final currentDesc = descV2[index];
|
||||||
|
switch (currentDesc.type) {
|
||||||
|
case 1:
|
||||||
|
List<InlineSpan> spanChildren = [];
|
||||||
|
RegExp urlRegExp = RegExp(r'https?://\S+\b');
|
||||||
|
Iterable<Match> matches = urlRegExp.allMatches(currentDesc.rawText);
|
||||||
|
|
||||||
|
int previousEndIndex = 0;
|
||||||
|
for (Match match in matches) {
|
||||||
|
if (match.start > previousEndIndex) {
|
||||||
|
spanChildren.add(TextSpan(
|
||||||
|
text: currentDesc.rawText
|
||||||
|
.substring(previousEndIndex, match.start)));
|
||||||
|
}
|
||||||
|
spanChildren.add(
|
||||||
|
TextSpan(
|
||||||
|
text: match.group(0),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
// 处理点击事件
|
||||||
|
try {
|
||||||
|
Get.toNamed(
|
||||||
|
'/webview',
|
||||||
|
parameters: {
|
||||||
|
'url': match.group(0)!,
|
||||||
|
'type': 'url',
|
||||||
|
'pageTitle': match.group(0)!,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
SmartDialog.showToast(err.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
previousEndIndex = match.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousEndIndex < currentDesc.rawText.length) {
|
||||||
|
spanChildren.add(TextSpan(
|
||||||
|
text: currentDesc.rawText.substring(previousEndIndex)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSpan result = TextSpan(children: spanChildren);
|
||||||
|
return result;
|
||||||
|
case 2:
|
||||||
|
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
|
||||||
|
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
|
||||||
|
return TextSpan(
|
||||||
|
text: '@${currentDesc.rawText}',
|
||||||
|
style: TextStyle(color: colorSchemePrimary),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
Get.toNamed(
|
||||||
|
'/member?mid=${currentDesc.bizId}',
|
||||||
|
arguments: {'face': '', 'heroTag': heroTag},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return const TextSpan();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return TextSpan(children: spanChilds);
|
||||||
|
}
|
||||||
|
}
|
@ -286,4 +286,15 @@ class Utils {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 时间戳转时间
|
||||||
|
static tampToSeektime(number) {
|
||||||
|
int hours = number ~/ 60;
|
||||||
|
int minutes = number % 60;
|
||||||
|
|
||||||
|
String formattedHours = hours.toString().padLeft(2, '0');
|
||||||
|
String formattedMinutes = minutes.toString().padLeft(2, '0');
|
||||||
|
|
||||||
|
return '$formattedHours:$formattedMinutes';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user