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
|
||||
// features=itemOpusStyle
|
||||
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_rec_video_item.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_detail_res.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/wbi_sign.dart';
|
||||
|
||||
/// res.data['code'] == 0 请求正常返回结果
|
||||
/// res.data['data'] 为结果
|
||||
@ -420,4 +422,23 @@ class VideoHttp {
|
||||
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/video.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/pages/video/detail/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||
@ -62,6 +63,7 @@ class VideoIntroController extends GetxController {
|
||||
Timer? timer;
|
||||
bool isPaused = false;
|
||||
String heroTag = Get.arguments['heroTag'];
|
||||
late ModelResult modelResult;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -561,4 +563,25 @@ class VideoIntroController extends GetxController {
|
||||
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/models/video_detail_res.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/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
@ -226,6 +227,17 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
arguments: {'face': face, 'heroTag': memberHeroTag});
|
||||
}
|
||||
|
||||
// ai总结
|
||||
showAiBottomSheet() {
|
||||
showBottomSheet(
|
||||
context: context,
|
||||
enableDrag: true,
|
||||
builder: (BuildContext context) {
|
||||
return AiDetail(modelResult: videoIntroController.modelResult);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ThemeData t = Theme.of(context);
|
||||
@ -238,70 +250,91 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => showIntroDetail(),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
!loadingStatus
|
||||
? widget.videoDetail!.title
|
||||
: videoItem['title'],
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
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
|
||||
// SingleChildScrollView(
|
||||
// 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