Merge branch 'main' into feature-minePage

This commit is contained in:
guozhigq
2024-09-28 21:24:06 +08:00
57 changed files with 3794 additions and 332 deletions

View File

@ -1,5 +1,6 @@
import UIKit
import Flutter
import AVFoundation
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
@ -8,6 +9,14 @@ import Flutter
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
//
do {
try AVAudioSession.sharedInstance().setCategory(.playback, options: [.duckOthers])
} catch {
print("Failed to set audio session category: \(error)")
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -1,6 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:get/get.dart';
import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';
import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';
import 'package:pilipala/utils/highlight.dart';
import 'network_img_layer.dart';
// ignore: must_be_immutable
@ -22,6 +26,20 @@ class HtmlRender extends StatelessWidget {
data: htmlContent,
onLinkTap: (String? url, Map<String, String> buildContext, attributes) {},
extensions: [
TagExtension(
tagsToExtend: <String>{'pre'},
builder: (ExtensionContext extensionContext) {
final Map<String, dynamic> attributes = extensionContext.attributes;
final String lang = attributes['data-lang'] as String;
final String code = attributes['codecontent'] as String;
List<String> selectedLanguages = [lang.split('@').first];
TextSpan? result = highlightExistingText(code, selectedLanguages);
if (result == null) {
return const Center(child: Text('代码块渲染失败'));
}
return SelectableText.rich(result);
},
),
TagExtension(
tagsToExtend: <String>{'img'},
builder: (ExtensionContext extensionContext) {
@ -44,20 +62,52 @@ class HtmlRender extends StatelessWidget {
if (isMall) {
return const SizedBox();
}
// bool inTable =
// extensionContext.element!.previousElementSibling == null ||
// extensionContext.element!.nextElementSibling == null;
// imgUrl = Utils().imageUrl(imgUrl!);
// return Image.network(
// imgUrl,
// width: isEmote ? 22 : null,
// height: isEmote ? 22 : null,
// );
return NetworkImgLayer(
width: isEmote ? 22 : Get.size.width - 24,
height: isEmote ? 22 : 200,
src: imgUrl,
return InkWell(
onTap: () {
Navigator.of(context).push(
HeroDialogRoute<void>(
builder: (BuildContext context) =>
InteractiveviewerGallery(
sources: imgList ?? [imgUrl],
initIndex: imgList?.indexOf(imgUrl) ?? 0,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: imgUrl,
child: CachedNetworkImage(
fadeInDuration:
const Duration(milliseconds: 0),
imageUrl: imgUrl,
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
),
),
);
},
child: CachedNetworkImage(imageUrl: imgUrl),
);
// return NetworkImgLayer(
// width: isEmote ? 22 : Get.size.width - 24,
// height: isEmote ? 22 : 200,
// src: imgUrl,
// );
} catch (err) {
return const SizedBox();
}
@ -66,7 +116,7 @@ class HtmlRender extends StatelessWidget {
],
style: {
'html': Style(
fontSize: FontSize.medium,
fontSize: FontSize.large,
lineHeight: LineHeight.percent(140),
),
'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
@ -78,7 +128,7 @@ class HtmlRender extends StatelessWidget {
margin: Margins.only(bottom: 10),
),
'span': Style(
fontSize: FontSize.medium,
fontSize: FontSize.large,
height: Height(1.65),
),
'div': Style(height: Height.auto()),

View File

@ -60,17 +60,13 @@ class VideoCardV extends StatelessWidget {
// 动态
case 'picture':
try {
String dynamicType = 'picture';
String uri = videoItem.uri;
String id = '';
if (videoItem.uri.startsWith('bilibili://article/')) {
// https://www.bilibili.com/read/cv27063554
dynamicType = 'read';
RegExp regex = RegExp(r'\d+');
Match match = regex.firstMatch(videoItem.uri)!;
String matchedNumber = match.group(0)!;
videoItem.param = int.parse(matchedNumber);
id = 'cv${videoItem.param}';
}
if (uri.startsWith('http')) {
String path = Uri.parse(uri).path;
@ -88,11 +84,10 @@ class VideoCardV extends StatelessWidget {
return;
}
}
Get.toNamed('/htmlRender', parameters: {
'url': uri,
Get.toNamed('/read', parameters: {
'title': videoItem.title,
'id': id,
'dynamicType': dynamicType
'id': videoItem.param,
'articleType': 'read'
});
} catch (err) {
SmartDialog.showToast(err.toString());

View File

@ -571,4 +571,17 @@ class Api {
/// 直播间发送弹幕
static const String sendLiveMsg = '${HttpString.liveBaseUrl}/msg/send';
/// 我的关注 - 正在直播
static const String getFollowingLive =
'${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following';
/// 稍后再看&收藏夹视频列表
static const String mediaList = '/x/v2/medialist/resource/list';
/// 用户专栏
static const String opusList = '/x/polymer/web-dynamic/v1/opus/feed/space';
///
static const String getViewInfo = '/x/article/viewinfo';
}

View File

@ -46,7 +46,8 @@ class ApiInterceptor extends Interceptor {
// 处理网络请求错误
// handler.next(err);
String url = err.requestOptions.uri.toString();
if (!url.contains('heartbeat')) {
final excludedPatterns = RegExp(r'heartbeat|seg\.so|online/total');
if (!excludedPatterns.hasMatch(url)) {
SmartDialog.showToast(
await dioError(err),
displayType: SmartToastType.onlyRefresh,

View File

@ -1,3 +1,5 @@
import 'package:pilipala/models/live/follow.dart';
import '../models/live/item.dart';
import '../models/live/room_info.dart';
import '../models/live/room_info_h5.dart';
@ -117,4 +119,27 @@ class LiveHttp {
};
}
}
// 我的关注 正在直播
static Future liveFollowing({int? pn, int? ps}) async {
var res = await Request().get(Api.getFollowingLive, data: {
'page': pn,
'page_size': ps,
'platform': 'web',
'ignoreRecord': 1,
'hit_ab': true,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': LiveFollowingModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -1,5 +1,8 @@
import 'dart:convert';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:html/parser.dart';
import 'package:pilipala/models/member/article.dart';
import 'package:pilipala/models/member/like.dart';
import '../common/constants.dart';
import '../models/dynamics/result.dart';
@ -556,4 +559,60 @@ class MemberHttp {
};
}
}
static Future getWWebid({required int mid}) async {
var res = await Request().get('https://space.bilibili.com/$mid/article');
String? headContent = parse(res.data).head?.outerHtml;
final regex = RegExp(
r'<script id="__RENDER_DATA__" type="application/json">(.*?)</script>');
if (headContent != null) {
final match = regex.firstMatch(headContent);
if (match != null && match.groupCount >= 1) {
final content = match.group(1);
String decodedString = Uri.decodeComponent(content!);
Map<String, dynamic> map = jsonDecode(decodedString);
return {'status': true, 'data': map['access_id']};
} else {
return {'status': false, 'data': '请检查登录状态'};
}
}
return {'status': false, 'data': '请检查登录状态'};
}
// 获取用户专栏
static Future getMemberArticle({
required int mid,
required int pn,
required String wWebid,
String? offset,
}) async {
Map params = await WbiSign().makSign({
'host_mid': mid,
'page': pn,
'offset': offset,
'web_location': 333.999,
'w_webid': wWebid,
});
var res = await Request().get(Api.opusList, data: {
'host_mid': mid,
'page': pn,
'offset': offset,
'web_location': 333.999,
'w_webid': wWebid,
'w_rid': params['w_rid'],
'wts': params['wts'],
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': MemberArticleDataModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'] ?? '请求异常',
};
}
}
}

116
lib/http/read.dart Normal file
View File

@ -0,0 +1,116 @@
import 'dart:convert';
import 'package:html/parser.dart';
import 'package:pilipala/models/read/opus.dart';
import 'package:pilipala/models/read/read.dart';
import 'package:pilipala/utils/wbi_sign.dart';
import 'index.dart';
class ReadHttp {
static List<String> extractScriptContents(String htmlContent) {
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
List<String> scriptContents = [];
for (Match match in matches) {
String scriptContent = match.group(1)!;
scriptContents.add(scriptContent);
}
return scriptContents;
}
// 解析专栏 opus格式
static Future parseArticleOpus({required String id}) async {
var res = await Request().get('https://www.bilibili.com/opus/$id', extra: {
'ua': 'pc',
});
String? headContent = parse(res.data).head?.outerHtml;
var document = parse(headContent);
var linkTags = document.getElementsByTagName('link');
bool isCv = false;
String cvId = '';
for (var linkTag in linkTags) {
var attributes = linkTag.attributes;
if (attributes.containsKey('rel') &&
attributes['rel'] == 'canonical' &&
attributes.containsKey('data-vue-meta') &&
attributes['data-vue-meta'] == 'true') {
final String cvHref = linkTag.attributes['href']!;
RegExp regex = RegExp(r'cv(\d+)');
RegExpMatch? match = regex.firstMatch(cvHref);
if (match != null) {
cvId = match.group(1)!;
} else {
print('No match found.');
}
isCv = true;
break;
}
}
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': OpusDataModel.fromJson(jsonData),
'isCv': isCv,
'cvId': cvId,
};
}
// 解析专栏 cv格式
static Future parseArticleCv({required String id}) async {
var res = await Request().get(
'https://www.bilibili.com/read/cv$id',
extra: {'ua': 'pc'},
);
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': ReadDataModel.fromJson(jsonData),
};
}
//
static Future getViewInfo({required String id}) async {
Map params = await WbiSign().makSign({
'id': id,
'mobi_app': 'pc',
'from': 'web',
'gaia_source': 'main_web',
'web_location': 333.976,
});
var res = await Request().get(
Api.getViewInfo,
data: {
'id': id,
'mobi_app': 'pc',
'from': 'web',
'gaia_source': 'main_web',
'web_location': 333.976,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -143,7 +143,11 @@ class SearchHttp {
}
final dynamic res =
await Request().get(Api.ab2c, data: <String, dynamic>{...data});
if (res.data['code'] == 0) {
return res.data['data'].first['cid'];
} else {
return -1;
}
}
static Future<Map<String, dynamic>> bangumiInfo(

View File

@ -1,4 +1,9 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:html/parser.dart';
import 'package:pilipala/models/video/later.dart';
import '../common/constants.dart';
import '../models/model_hot_video_item.dart';
import '../models/user/fav_detail.dart';
@ -430,4 +435,106 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
// 稍后再看播放全部
// static Future toViewPlayAll({required int oid, required String bvid}) async {
// var res = await Request().get(
// Api.watchLaterHtml,
// data: {
// 'oid': oid,
// 'bvid': bvid,
// },
// );
// String scriptContent =
// extractScriptContents(parse(res.data).body!.outerHtml)[0];
// int startIndex = scriptContent.indexOf('{');
// int endIndex = scriptContent.lastIndexOf('};');
// String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// // 解析JSON字符串为Map
// Map<String, dynamic> jsonData = json.decode(jsonContent);
// // 输出解析后的数据
// return {
// 'status': true,
// 'data': jsonData['resourceList']
// .map((e) => MediaVideoItemModel.fromJson(e))
// .toList()
// };
// }
static List<String> extractScriptContents(String htmlContent) {
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
List<String> scriptContents = [];
for (Match match in matches) {
String scriptContent = match.group(1)!;
scriptContents.add(scriptContent);
}
return scriptContents;
}
// 稍后再看列表
static Future getMediaList({
required int type,
required int bizId,
required int ps,
int? oid,
}) async {
var res = await Request().get(
Api.mediaList,
data: {
'mobi_app': 'web',
'type': type,
'biz_id': bizId,
'oid': oid ?? '',
'otype': 2,
'ps': ps,
'direction': false,
'desc': true,
'sort_field': 1,
'tid': 0,
'with_current': false,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']['media_list'] != null
? res.data['data']['media_list']
.map<MediaVideoItemModel>(
(e) => MediaVideoItemModel.fromJson(e))
.toList()
: []
};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 解析收藏夹视频
static Future parseFavVideo({
required int mediaId,
required int oid,
required String bvid,
}) async {
var res = await Request().get(
'https://www.bilibili.com/list/ml$mediaId',
data: {
'oid': mediaId,
'bvid': bvid,
},
);
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': jsonData['resourceList']
.map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
.toList()
};
}
}

126
lib/models/live/follow.dart Normal file
View File

@ -0,0 +1,126 @@
class LiveFollowingModel {
int? count;
List<LiveFollowingItemModel>? list;
int? liveCount;
int? neverLivedCount;
List? neverLivedFaces;
int? pageSize;
String? title;
int? totalPage;
LiveFollowingModel({
this.count,
this.list,
this.liveCount,
this.neverLivedCount,
this.neverLivedFaces,
this.pageSize,
this.title,
this.totalPage,
});
LiveFollowingModel.fromJson(Map<String, dynamic> json) {
count = json['count'];
if (json['list'] != null) {
list = <LiveFollowingItemModel>[];
json['list'].forEach((v) {
list!.add(LiveFollowingItemModel.fromJson(v));
});
}
liveCount = json['live_count'];
neverLivedCount = json['never_lived_count'];
if (json['never_lived_faces'] != null) {
neverLivedFaces = <dynamic>[];
json['never_lived_faces'].forEach((v) {
neverLivedFaces!.add(v);
});
}
pageSize = json['pageSize'];
title = json['title'];
totalPage = json['totalPage'];
}
}
class LiveFollowingItemModel {
int? roomId;
int? uid;
String? uname;
String? title;
String? face;
int? liveStatus;
int? recordNum;
String? recentRecordId;
int? isAttention;
int? clipNum;
int? fansNum;
String? areaName;
String? areaValue;
String? tags;
String? recentRecordIdV2;
int? recordNumV2;
int? recordLiveTime;
String? areaNameV2;
String? roomNews;
String? watchIcon;
String? textSmall;
String? roomCover;
String? pic;
int? parentAreaId;
int? areaId;
LiveFollowingItemModel({
this.roomId,
this.uid,
this.uname,
this.title,
this.face,
this.liveStatus,
this.recordNum,
this.recentRecordId,
this.isAttention,
this.clipNum,
this.fansNum,
this.areaName,
this.areaValue,
this.tags,
this.recentRecordIdV2,
this.recordNumV2,
this.recordLiveTime,
this.areaNameV2,
this.roomNews,
this.watchIcon,
this.textSmall,
this.roomCover,
this.pic,
this.parentAreaId,
this.areaId,
});
LiveFollowingItemModel.fromJson(Map<String, dynamic> json) {
roomId = json['roomid'];
uid = json['uid'];
uname = json['uname'];
title = json['title'];
face = json['face'];
liveStatus = json['live_status'];
recordNum = json['record_num'];
recentRecordId = json['recent_record_id'];
isAttention = json['is_attention'];
clipNum = json['clipnum'];
fansNum = json['fans_num'];
areaName = json['area_name'];
areaValue = json['area_value'];
tags = json['tags'];
recentRecordIdV2 = json['recent_record_id_v2'];
recordNumV2 = json['record_num_v2'];
recordLiveTime = json['record_live_time'];
areaNameV2 = json['area_name_v2'];
roomNews = json['room_news'];
watchIcon = json['watch_icon'];
textSmall = json['text_small'];
roomCover = json['room_cover'];
pic = json['room_cover'];
parentAreaId = json['parent_area_id'];
areaId = json['area_id'];
}
}

View File

@ -1,11 +1,13 @@
class RoomInfoModel {
RoomInfoModel({
this.roomId,
this.isPortrait,
this.liveStatus,
this.liveTime,
this.playurlInfo,
});
int? roomId;
bool? isPortrait;
int? liveStatus;
int? liveTime;
PlayurlInfo? playurlInfo;
@ -13,6 +15,7 @@ class RoomInfoModel {
RoomInfoModel.fromJson(Map<String, dynamic> json) {
roomId = json['room_id'];
liveStatus = json['live_status'];
isPortrait = json['is_portrait'];
liveTime = json['live_time'];
playurlInfo = PlayurlInfo.fromJson(json['playurl_info']);
}

View File

@ -0,0 +1,46 @@
class MemberArticleDataModel {
MemberArticleDataModel({
this.hasMore,
this.items,
this.offset,
this.updateNum,
});
bool? hasMore;
List<MemberArticleItemModel>? items;
String? offset;
int? updateNum;
MemberArticleDataModel.fromJson(Map<String, dynamic> json) {
hasMore = json['has_more'];
items = json['items']
.map<MemberArticleItemModel>((e) => MemberArticleItemModel.fromJson(e))
.toList();
offset = json['offset'];
updateNum = json['update_num'];
}
}
class MemberArticleItemModel {
MemberArticleItemModel({
this.content,
this.cover,
this.jumpUrl,
this.opusId,
this.stat,
});
String? content;
Map? cover;
String? jumpUrl;
String? opusId;
Map? stat;
MemberArticleItemModel.fromJson(Map<String, dynamic> json) {
content = json['content'];
cover = json['cover'];
jumpUrl = json['jump_url'];
opusId = json['opus_id'];
stat = json['stat'];
}
}

485
lib/models/read/opus.dart Normal file
View File

@ -0,0 +1,485 @@
class OpusDataModel {
OpusDataModel({
this.id,
this.detail,
this.type,
this.theme,
this.themeMode,
});
String? id;
OpusDetailDataModel? detail;
int? type;
String? theme;
String? themeMode;
OpusDataModel.fromJson(Map<String, dynamic> json) {
id = json['id'];
detail = json['detail'] != null
? OpusDetailDataModel.fromJson(json['detail'])
: null;
type = json['type'];
theme = json['theme'];
themeMode = json['themeMode'];
}
}
class OpusDetailDataModel {
OpusDetailDataModel({
this.basic,
this.idStr,
this.modules,
this.type,
});
Basic? basic;
String? idStr;
List<OpusModuleDataModel>? modules;
int? type;
OpusDetailDataModel.fromJson(Map<String, dynamic> json) {
basic = json['basic'] != null ? Basic.fromJson(json['basic']) : null;
idStr = json['id_str'];
if (json['modules'] != null) {
modules = <OpusModuleDataModel>[];
json['modules'].forEach((v) {
modules!.add(OpusModuleDataModel.fromJson(v));
});
}
type = json['type'];
}
}
class Basic {
Basic({
this.commentIdStr,
this.commentType,
this.ridStr,
this.title,
this.uid,
});
String? commentIdStr;
int? commentType;
String? ridStr;
String? title;
int? uid;
Basic.fromJson(Map<String, dynamic> json) {
commentIdStr = json['comment_id_str'];
commentType = json['comment_type'];
ridStr = json['rid_str'];
title = json['title'];
uid = json['uid'];
}
}
class OpusModuleDataModel {
OpusModuleDataModel({
this.moduleTitle,
this.moduleAuthor,
this.moduleContent,
this.moduleExtend,
this.moduleBottom,
this.moduleStat,
});
ModuleTop? moduleTop;
ModuleTitle? moduleTitle;
ModuleAuthor? moduleAuthor;
ModuleContent? moduleContent;
ModuleExtend? moduleExtend;
ModuleBottom? moduleBottom;
ModuleStat? moduleStat;
OpusModuleDataModel.fromJson(Map<String, dynamic> json) {
moduleTop = json['module_top'] != null
? ModuleTop.fromJson(json['module_top'])
: null;
moduleTitle = json['module_title'] != null
? ModuleTitle.fromJson(json['module_title'])
: null;
moduleAuthor = json['module_author'] != null
? ModuleAuthor.fromJson(json['module_author'])
: null;
moduleContent = json['module_content'] != null
? ModuleContent.fromJson(json['module_content'])
: null;
moduleExtend = json['module_extend'] != null
? ModuleExtend.fromJson(json['module_extend'])
: null;
moduleBottom = json['module_bottom'] != null
? ModuleBottom.fromJson(json['module_bottom'])
: null;
moduleStat = json['module_stat'] != null
? ModuleStat.fromJson(json['module_stat'])
: null;
}
}
class ModuleTop {
ModuleTop({
this.type,
this.video,
});
int? type;
Map? video;
ModuleTop.fromJson(Map<String, dynamic> json) {
type = json['type'];
video = json['video'];
}
}
class ModuleTitle {
ModuleTitle({
this.text,
});
String? text;
ModuleTitle.fromJson(Map<String, dynamic> json) {
text = json['text'];
}
}
class ModuleAuthor {
ModuleAuthor({
this.face,
this.mid,
this.name,
this.pubTime,
});
String? face;
int? mid;
String? name;
String? pubTime;
ModuleAuthor.fromJson(Map<String, dynamic> json) {
face = json['face'];
mid = json['mid'];
name = json['name'];
pubTime = json['pub_time'];
}
}
class ModuleContent {
ModuleContent({
this.paragraphs,
this.moduleType,
});
List<ModuleParagraph>? paragraphs;
String? moduleType;
ModuleContent.fromJson(Map<String, dynamic> json) {
if (json['paragraphs'] != null) {
paragraphs = <ModuleParagraph>[];
json['paragraphs'].forEach((v) {
paragraphs!.add(ModuleParagraph.fromJson(v));
});
}
moduleType = json['module_type'];
}
}
class ModuleParagraph {
ModuleParagraph({
this.align,
this.paraType,
this.pic,
this.text,
});
// 0 左对齐 1 居中 2 右对齐
int? align;
int? paraType;
Pics? pic;
ModuleParagraphText? text;
LinkCard? linkCard;
ModuleParagraph.fromJson(Map<String, dynamic> json) {
align = json['align'];
paraType = json['para_type'] == null && json['link_card'] != null
? 3
: json['para_type'];
pic = json['pic'] != null ? Pics.fromJson(json['pic']) : null;
text = json['text'] != null
? ModuleParagraphText.fromJson(json['text'])
: null;
linkCard =
json['link_card'] != null ? LinkCard.fromJson(json['link_card']) : null;
}
}
class Pics {
Pics({
this.pics,
this.style,
});
List<Pic>? pics;
int? style;
Pics.fromJson(Map<String, dynamic> json) {
if (json['pics'] != null) {
pics = <Pic>[];
json['pics'].forEach((v) {
pics!.add(Pic.fromJson(v));
});
}
style = json['style'];
}
}
class Pic {
Pic({
this.height,
this.size,
this.url,
this.width,
this.aspectRatio,
this.scale,
});
int? height;
double? size;
String? url;
int? width;
double? aspectRatio;
double? scale;
Pic.fromJson(Map<String, dynamic> json) {
height = json['height'];
size = json['size'];
url = json['url'];
width = json['width'];
aspectRatio = json['width'] / json['height'];
scale = customDivision(json['width'], 600);
}
}
class LinkCard {
LinkCard({
this.cover,
this.descSecond,
this.duration,
this.jumpUrl,
this.title,
});
String? cover;
String? descSecond;
String? duration;
String? jumpUrl;
String? title;
LinkCard.fromJson(Map<String, dynamic> json) {
cover = json['card']['cover'];
descSecond = json['card']['desc_second'];
duration = json['card']['duration'];
jumpUrl = json['card']['jump_url'];
title = json['card']['title'];
}
}
class ModuleParagraphText {
ModuleParagraphText({
this.nodes,
});
List<ModuleParagraphTextNode>? nodes;
ModuleParagraphText.fromJson(Map<String, dynamic> json) {
if (json['nodes'] != null) {
nodes = <ModuleParagraphTextNode>[];
json['nodes'].forEach((v) {
nodes!.add(ModuleParagraphTextNode.fromJson(v));
});
}
}
}
class ModuleParagraphTextNode {
ModuleParagraphTextNode({
this.type,
this.nodeType,
this.word,
});
String? type;
int? nodeType;
ModuleParagraphTextNodeWord? word;
ModuleParagraphTextNode.fromJson(Map<String, dynamic> json) {
type = json['type'];
nodeType = json['node_type'];
word = json['word'] != null
? ModuleParagraphTextNodeWord.fromJson(json['word'])
: null;
}
}
class ModuleParagraphTextNodeWord {
ModuleParagraphTextNodeWord({
this.color,
this.fontSize,
this.style,
this.words,
});
String? color;
int? fontSize;
ModuleParagraphTextNodeWordStyle? style;
String? words;
ModuleParagraphTextNodeWord.fromJson(Map<String, dynamic> json) {
color = json['color'];
fontSize = json['font_size'];
style = json['style'] != null
? ModuleParagraphTextNodeWordStyle.fromJson(json['style'])
: null;
words = json['words'];
}
}
class ModuleParagraphTextNodeWordStyle {
ModuleParagraphTextNodeWordStyle({
this.bold,
});
bool? bold;
ModuleParagraphTextNodeWordStyle.fromJson(Map<String, dynamic> json) {
bold = json['bold'];
}
}
class ModuleExtend {
ModuleExtend({
this.items,
});
List<ModuleExtendItem>? items;
ModuleExtend.fromJson(Map<String, dynamic> json) {
if (json['items'] != null) {
items = <ModuleExtendItem>[];
json['items'].forEach((v) {
items!.add(ModuleExtendItem.fromJson(v));
});
}
}
}
class ModuleExtendItem {
ModuleExtendItem({
this.bizId,
this.bizType,
this.icon,
this.jumpUrl,
this.text,
});
dynamic bizId;
int? bizType;
dynamic icon;
String? jumpUrl;
String? text;
ModuleExtendItem.fromJson(Map<String, dynamic> json) {
bizId = json['biz_id'];
bizType = json['biz_type'];
icon = json['icon'];
jumpUrl = json['jump_url'];
text = json['text'];
}
}
class ModuleBottom {
ModuleBottom({
this.shareInfo,
});
ShareInfo? shareInfo;
ModuleBottom.fromJson(Map<String, dynamic> json) {
shareInfo = json['share_info'] != null
? ShareInfo.fromJson(json['share_info'])
: null;
}
}
class ShareInfo {
ShareInfo({
this.pic,
this.summary,
this.title,
});
String? pic;
String? summary;
String? title;
ShareInfo.fromJson(Map<String, dynamic> json) {
pic = json['pic'];
summary = json['summary'];
title = json['title'];
}
}
class ModuleStat {
ModuleStat({
this.coin,
this.comment,
this.favorite,
this.forward,
this.like,
});
StatItem? coin;
StatItem? comment;
StatItem? favorite;
StatItem? forward;
StatItem? like;
ModuleStat.fromJson(Map<String, dynamic> json) {
coin = json['coin'] != null ? StatItem.fromJson(json['coin']) : null;
comment =
json['comment'] != null ? StatItem.fromJson(json['comment']) : null;
favorite =
json['favorite'] != null ? StatItem.fromJson(json['favorite']) : null;
forward =
json['forward'] != null ? StatItem.fromJson(json['forward']) : null;
like = json['like'] != null ? StatItem.fromJson(json['like']) : null;
}
}
class StatItem {
StatItem({
this.count,
this.forbidden,
this.status,
});
int? count;
bool? forbidden;
bool? status;
StatItem.fromJson(Map<String, dynamic> json) {
count = json['count'];
forbidden = json['forbidden'];
status = json['status'];
}
}
double customDivision(int a, int b) {
double result = a / b;
if (result < 1) {
return result;
} else {
return 1.0;
}
}

286
lib/models/read/read.dart Normal file
View File

@ -0,0 +1,286 @@
import 'package:pilipala/models/member/info.dart';
import 'opus.dart';
class ReadDataModel {
ReadDataModel({
this.cvid,
this.readInfo,
this.readViewInfo,
this.upInfo,
this.catalogList,
this.recommendInfoList,
this.hiddenInteraction,
this.isModern,
});
int? cvid;
ReadInfo? readInfo;
Map? readViewInfo;
Map? upInfo;
List<dynamic>? catalogList;
List<dynamic>? recommendInfoList;
bool? hiddenInteraction;
bool? isModern;
ReadDataModel.fromJson(Map<String, dynamic> json) {
cvid = json['cvid'];
readInfo =
json['readInfo'] != null ? ReadInfo.fromJson(json['readInfo']) : null;
readViewInfo = json['readViewInfo'];
upInfo = json['upInfo'];
if (json['catalogList'] != null) {
catalogList = <dynamic>[];
json['catalogList'].forEach((v) {
catalogList!.add(v);
});
}
if (json['recommendInfoList'] != null) {
recommendInfoList = <dynamic>[];
json['recommendInfoList'].forEach((v) {
recommendInfoList!.add(v);
});
}
hiddenInteraction = json['hiddenInteraction'];
isModern = json['isModern'];
}
}
class ReadInfo {
ReadInfo({
this.id,
this.category,
this.title,
this.summary,
this.bannerUrl,
this.author,
this.publishTime,
this.ctime,
this.mtime,
this.stats,
this.attributes,
this.words,
this.originImageUrls,
this.content,
this.opus,
this.dynIdStr,
this.totalArtNum,
});
int? id;
Map? category;
String? title;
String? summary;
String? bannerUrl;
Author? author;
int? publishTime;
int? ctime;
int? mtime;
Map? stats;
int? attributes;
int? words;
List<String>? originImageUrls;
String? content;
Opus? opus;
String? dynIdStr;
int? totalArtNum;
ReadInfo.fromJson(Map<String, dynamic> json) {
id = json['id'];
category = json['category'];
title = json['title'];
summary = json['summary'];
bannerUrl = json['banner_url'];
author = Author.fromJson(json['author']);
publishTime = json['publish_time'];
ctime = json['ctime'];
mtime = json['mtime'];
stats = json['stats'];
attributes = json['attributes'];
words = json['words'];
if (json['origin_image_urls'] != null) {
originImageUrls = <String>[];
json['origin_image_urls'].forEach((v) {
originImageUrls!.add(v);
});
}
content = json['content'];
opus = json['opus'] != null ? Opus.fromJson(json['opus']) : null;
dynIdStr = json['dyn_id_str'];
totalArtNum = json['total_art_num'];
}
}
class Author {
Author({
this.mid,
this.name,
this.face,
this.vip,
this.fans,
this.level,
});
int? mid;
String? name;
String? face;
Vip? vip;
int? fans;
int? level;
Author.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
name = json['name'];
face = json['face'];
vip = json['vip'] != null ? Vip.fromJson(json['vip']) : null;
fans = json['fans'];
level = json['level'];
}
}
class Opus {
// "opus_id": 976625853207150600,
// "opus_source": 2,
// "title": "真的很想骂人 但又没什么好骂的",
// "content": {
// "paragraphs": [{
// "para_type": 1,
// "text": {
// "nodes": [{
// "node_type": 1,
// "word": {
// "words": "21年玩到今年4月的号没了 ow1的时候45的号 玩了三年 后面第9赛季一个英杰5的号虽然是偷的 但我任何违规行为都没有还是给我封了) 最近玩的号叫velleity 只和队友打天梯以及训练赛 又没了 连带着我一个一把没玩过只玩过一场训练赛的小号也没了 实在是无话可说了...",
// "font_size": 17,
// "style": {},
// "font_level": "regular"
// }
// }]
// }
// }, {
// "para_type": 2,
// "pic": {
// "pics": [{
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Fba4e57459451fe74dcb70fd20bde9823316082117.jpg",
// "width": 1600,
// "height": 1000,
// "size": 588.482421875
// }],
// "style": 1
// }
// }, {
// "para_type": 1,
// "text": {
// "nodes": [{
// "node_type": 1,
// "word": {
// "words": "\n",
// "font_size": 17,
// "style": {},
// "font_level": "regular"
// }
// }]
// }
// }, {
// "para_type": 2,
// "pic": {
// "pics": [{
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002F0945be6b621091ddb8189482a87a36fb316082117.jpg",
// "width": 1600,
// "height": 1002,
// "size": 665.7861328125
// }],
// "style": 1
// }
// }, {
// "para_type": 1,
// "text": {
// "nodes": [{
// "node_type": 1,
// "word": {
// "words": "\n",
// "font_size": 17,
// "style": {},
// "font_level": "regular"
// }
// }]
// }
// }, {
// "para_type": 2,
// "pic": {
// "pics": [{
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Ffa60649f8786578a764a1e68a2c5d23f316082117.jpg",
// "width": 1600,
// "height": 999,
// "size": 332.970703125
// }],
// "style": 1
// }
// }, {
// "para_type": 1,
// "text": {
// "nodes": [{
// "node_type": 1,
// "word": {
// "words": "\n",
// "font_size": 17,
// "style": {},
// "font_level": "regular"
// }
// }]
// }
// }]
// },
// "pub_info": {
// "uid": 316082117,
// "pub_time": 1726226826
// },
// "article": {
// "category_id": 15,
// "cover": [{
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Fbanner\u002Feb10074186a62f98c18e1b5b9deb38be316082117.png",
// "width": 1071,
// "height": 315,
// "size": 225.625
// }]
// },
// "version": {
// "cvid": 38660379,
// "version_id": 101683514411343360
// }
Opus({
this.opusId,
this.opusSource,
this.title,
this.content,
});
int? opusId;
int? opusSource;
String? title;
Content? content;
Opus.fromJson(Map<String, dynamic> json) {
opusId = json['opus_id'];
opusSource = json['opus_source'];
title = json['title'];
content =
json['content'] != null ? Content.fromJson(json['content']) : null;
}
}
class Content {
Content({
this.paragraphs,
});
List<ModuleParagraph>? paragraphs;
Content.fromJson(Map<String, dynamic> json) {
if (json['paragraphs'] != null) {
paragraphs = <ModuleParagraph>[];
json['paragraphs'].forEach((v) {
paragraphs!.add(ModuleParagraph.fromJson(v));
});
}
}
}

270
lib/models/video/later.dart Normal file
View File

@ -0,0 +1,270 @@
class MediaVideoItemModel {
MediaVideoItemModel({
this.id,
this.offset,
this.index,
this.intro,
this.attr,
this.tid,
this.copyRight,
this.cntInfo,
this.cover,
this.duration,
this.pubtime,
this.likeState,
this.favState,
this.page,
this.pages,
this.title,
this.type,
this.upper,
this.link,
this.bvId,
this.shortLink,
this.rights,
this.elecInfo,
this.coin,
this.progressPercent,
this.badge,
this.forbidFav,
this.moreType,
this.businessOid,
});
int? id;
int? offset;
int? index;
String? intro;
int? attr;
int? tid;
int? copyRight;
Map? cntInfo;
String? cover;
int? duration;
int? pubtime;
int? likeState;
int? favState;
int? page;
List<Page>? pages;
String? title;
int? type;
Upper? upper;
String? link;
String? bvId;
String? shortLink;
Rights? rights;
dynamic elecInfo;
Coin? coin;
double? progressPercent;
dynamic badge;
bool? forbidFav;
int? moreType;
int? businessOid;
factory MediaVideoItemModel.fromJson(Map<String, dynamic> json) =>
MediaVideoItemModel(
id: json["id"],
offset: json["offset"],
index: json["index"],
intro: json["intro"],
attr: json["attr"],
tid: json["tid"],
copyRight: json["copy_right"],
cntInfo: json["cnt_info"],
cover: json["cover"],
duration: json["duration"],
pubtime: json["pubtime"],
likeState: json["like_state"],
favState: json["fav_state"],
page: json["page"],
// json["pages"] 可能为null
pages: json["pages"] == null
? []
: List<Page>.from(json["pages"].map((x) => Page.fromJson(x))),
title: json["title"],
type: json["type"],
upper: Upper.fromJson(json["upper"]),
link: json["link"],
bvId: json["bv_id"],
shortLink: json["short_link"],
rights: Rights.fromJson(json["rights"]),
elecInfo: json["elec_info"],
coin: Coin.fromJson(json["coin"]),
progressPercent: json["progress_percent"].toDouble(),
badge: json["badge"],
forbidFav: json["forbid_fav"],
moreType: json["more_type"],
businessOid: json["business_oid"],
);
}
class Coin {
Coin({
this.maxNum,
this.coinNumber,
});
int? maxNum;
int? coinNumber;
factory Coin.fromJson(Map<String, dynamic> json) => Coin(
maxNum: json["max_num"],
coinNumber: json["coin_number"],
);
}
class Page {
Page({
this.id,
this.title,
this.intro,
this.duration,
this.link,
this.page,
this.metas,
this.from,
this.dimension,
});
int? id;
String? title;
String? intro;
int? duration;
String? link;
int? page;
List<Meta>? metas;
String? from;
Dimension? dimension;
factory Page.fromJson(Map<String, dynamic> json) => Page(
id: json["id"],
title: json["title"],
intro: json["intro"],
duration: json["duration"],
link: json["link"],
page: json["page"],
metas: List<Meta>.from(json["metas"].map((x) => Meta.fromJson(x))),
from: json["from"],
dimension: Dimension.fromJson(json["dimension"]),
);
}
class Dimension {
Dimension({
this.width,
this.height,
this.rotate,
});
int? width;
int? height;
int? rotate;
factory Dimension.fromJson(Map<String, dynamic> json) => Dimension(
width: json["width"],
height: json["height"],
rotate: json["rotate"],
);
}
class Meta {
Meta({
this.quality,
this.size,
});
int? quality;
int? size;
factory Meta.fromJson(Map<String, dynamic> json) => Meta(
quality: json["quality"],
size: json["size"],
);
}
class Rights {
Rights({
this.bp,
this.elec,
this.download,
this.movie,
this.pay,
this.ugcPay,
this.hd5,
this.noReprint,
this.autoplay,
this.noBackground,
});
int? bp;
int? elec;
int? download;
int? movie;
int? pay;
int? ugcPay;
int? hd5;
int? noReprint;
int? autoplay;
int? noBackground;
factory Rights.fromJson(Map<String, dynamic> json) => Rights(
bp: json["bp"],
elec: json["elec"],
download: json["download"],
movie: json["movie"],
pay: json["pay"],
ugcPay: json["ugc_pay"],
hd5: json["hd5"],
noReprint: json["no_reprint"],
autoplay: json["autoplay"],
noBackground: json["no_background"],
);
}
class Upper {
Upper({
this.mid,
this.name,
this.face,
this.followed,
this.fans,
this.vipType,
this.vipStatue,
this.vipDueDate,
this.vipPayType,
this.officialRole,
this.officialTitle,
this.officialDesc,
this.displayName,
});
int? mid;
String? name;
String? face;
int? followed;
int? fans;
int? vipType;
int? vipStatue;
int? vipDueDate;
int? vipPayType;
int? officialRole;
String? officialTitle;
String? officialDesc;
String? displayName;
factory Upper.fromJson(Map<String, dynamic> json) => Upper(
mid: json["mid"],
name: json["name"],
face: json["face"],
followed: json["followed"],
fans: json["fans"],
vipType: json["vip_type"],
vipStatue: json["vip_statue"],
vipDueDate: json["vip_due_date"],
vipPayType: json["vip_pay_type"],
officialRole: json["official_role"],
officialTitle: json["official_title"],
officialDesc: json["official_desc"],
displayName: json["display_name"],
);
}

View File

@ -146,20 +146,26 @@ class DynamicsController extends GetxController {
/// 专栏文章查看
case 'DYNAMIC_TYPE_ARTICLE':
String title = item.modules.moduleDynamic.major.opus.title;
String url = item.modules.moduleDynamic.major.opus.jumpUrl;
if (url.contains('opus') || url.contains('read')) {
String jumpUrl = item.modules.moduleDynamic.major.opus.jumpUrl;
String url =
jumpUrl.startsWith('//') ? jumpUrl.split('//').last : jumpUrl;
if (jumpUrl.contains('opus') || jumpUrl.contains('read')) {
RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(url);
Iterable<Match> matches = digitRegExp.allMatches(jumpUrl);
String number = matches.first.group(0)!;
if (url.contains('read')) {
number = 'cv$number';
}
Get.toNamed('/htmlRender', parameters: {
'url': url.startsWith('//') ? url.split('//').last : url,
if (jumpUrl.contains('read')) {
Get.toNamed('/read', parameters: {
'title': title,
'id': number,
'dynamicType': url.split('//').last.split('/')[1]
'articleType': url.split('/')[1]
});
} else {
Get.toNamed('/opus', parameters: {
'title': title,
'id': number,
'articleType': 'opus'
});
}
} else {
Get.toNamed(
'/webview',

View File

@ -32,7 +32,8 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
late DynamicDetailController _dynamicDetailController;
late AnimationController fabAnimationCtr;
Future? _futureBuilderFuture;
late StreamController<bool> titleStreamC; // appBar title
late StreamController<bool> titleStreamC =
StreamController<bool>.broadcast(); // appBar title
late ScrollController scrollController;
bool _visibleTitle = false;
String? action;
@ -48,7 +49,6 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
super.initState();
// floor 1原创 2转发
init();
titleStreamC = StreamController<bool>();
if (action == 'comment') {
_visibleTitle = true;
titleStreamC.add(true);

View File

@ -6,17 +6,17 @@ import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/user/fav_detail.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/fav/index.dart';
import 'package:pilipala/utils/utils.dart';
class FavDetailController extends GetxController {
FavFolderItemData? item;
Rx<FavDetailData> favDetailData = FavDetailData().obs;
int? mediaId;
late String heroTag;
int currentPage = 1;
bool isLoadingMore = false;
RxMap favInfo = {}.obs;
RxList favList = [].obs;
RxList<FavDetailItemData> favList = <FavDetailItemData>[].obs;
RxString loadingText = '加载中...'.obs;
RxInt mediaCount = 0.obs;
late String isOwner;
@ -128,4 +128,22 @@ class FavDetailController extends GetxController {
},
);
}
Future toViewPlayAll() async {
final FavDetailItemData firstItem = favList.first;
final String heroTag = Utils.makeHeroTag(firstItem.bvid);
Get.toNamed(
'/video?bvid=${firstItem.bvid}&cid=${firstItem.cid}',
arguments: {
'videoItem': firstItem,
'heroTag': heroTag,
'sourceType': 'fav',
'mediaId': favInfo['id'],
'oid': firstItem.id,
'favTitle': favInfo['title'],
'favInfo': favInfo,
'count': favInfo['media_count'],
},
);
}
}

View File

@ -22,7 +22,8 @@ class _FavDetailPageState extends State<FavDetailPage> {
late final ScrollController _controller = ScrollController();
final FavDetailController _favDetailController =
Get.put(FavDetailController());
late StreamController<bool> titleStreamC; // a
late StreamController<bool> titleStreamC =
StreamController<bool>.broadcast(); // a
Future? _futureBuilderFuture;
late String mediaId;
@ -31,7 +32,6 @@ class _FavDetailPageState extends State<FavDetailPage> {
super.initState();
mediaId = Get.parameters['mediaId']!;
_futureBuilderFuture = _favDetailController.queryUserFavFolderDetail();
titleStreamC = StreamController<bool>();
_controller.addListener(
() {
if (_controller.offset > 160) {
@ -260,6 +260,15 @@ class _FavDetailPageState extends State<FavDetailPage> {
)
],
),
floatingActionButton: Obx(
() => _favDetailController.mediaCount > 0
? FloatingActionButton.extended(
onPressed: _favDetailController.toViewPlayAll,
label: const Text('播放全部'),
icon: const Icon(Icons.playlist_play),
)
: const SizedBox(),
),
);
}
}

View File

@ -48,7 +48,7 @@ class FollowSearchController extends GetxController {
return {'status': true, 'data': <FollowItemModel>[].obs};
}
if (type == 'init') {
ps = 1;
pn = 1;
}
var res = await MemberHttp.getfollowSearch(
mid: mid,

View File

@ -43,14 +43,17 @@ class HistoryItem extends StatelessWidget {
}
if (videoItem.history.business.contains('article')) {
int cid = videoItem.history.cid ??
// videoItem.history.oid ??
videoItem.history.oid ??
await SearchHttp.ab2c(aid: aid, bvid: bvid);
if (cid == -1) {
return SmartDialog.showToast('无法获取文章内容');
}
Get.toNamed(
'/webview',
'/read',
parameters: {
'url': 'https://www.bilibili.com/read/cv$cid',
'type': 'note',
'pageTitle': videoItem.title
'title': videoItem.title,
'id': cid.toString(),
'articleType': 'read',
},
);
} else if (videoItem.history.business == 'live') {

View File

@ -6,6 +6,7 @@ import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/model_hot_video_item.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class LaterController extends GetxController {
final ScrollController scrollController = ScrollController();
@ -48,7 +49,7 @@ class LaterController extends GetxController {
aid != null ? '即将移除该视频,确定是否移除' : '即将删除所有已观看视频,此操作不可恢复。确定是否删除?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
onPressed: SmartDialog.dismiss,
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
@ -87,7 +88,7 @@ class LaterController extends GetxController {
content: const Text('确定要清空你的稍后再看列表吗?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
onPressed: SmartDialog.dismiss,
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
@ -109,4 +110,19 @@ class LaterController extends GetxController {
},
);
}
// 稍后再看播放全部
Future toViewPlayAll() async {
final HotVideoItemModel firstItem = laterList.first;
final String heroTag = Utils.makeHeroTag(firstItem.bvid);
Get.toNamed(
'/video?bvid=${firstItem.bvid}&cid=${firstItem.cid}',
arguments: {
'videoItem': firstItem,
'heroTag': heroTag,
'sourceType': 'watchLater',
'count': laterList.length,
},
);
}
}

View File

@ -128,6 +128,15 @@ class _LaterPageState extends State<LaterPage> {
)
],
),
floatingActionButton: Obx(
() => _laterController.laterList.isNotEmpty
? FloatingActionButton.extended(
onPressed: _laterController.toViewPlayAll,
label: const Text('播放全部'),
icon: const Icon(Icons.playlist_play),
)
: const SizedBox(),
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/follow.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/storage.dart';
@ -11,6 +12,8 @@ class LiveController extends GetxController {
int _currentPage = 1;
RxInt crossAxisCount = 2.obs;
RxList<LiveItemModel> liveList = <LiveItemModel>[].obs;
RxList<LiveFollowingItemModel> liveFollowingList =
<LiveFollowingItemModel>[].obs;
bool flag = false;
OverlayEntry? popupDialog;
Box setting = GStrorage.setting;
@ -44,6 +47,7 @@ class LiveController extends GetxController {
// 下拉刷新
Future onRefresh() async {
queryLiveList('init');
fetchLiveFollowing();
}
// 上拉加载
@ -61,4 +65,17 @@ class LiveController extends GetxController {
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
//
Future fetchLiveFollowing() async {
var res = await LiveHttp.liveFollowing(pn: 1, ps: 20);
if (res['status']) {
liveFollowingList.value =
(res['data'].list as List<LiveFollowingItemModel>)
.where((LiveFollowingItemModel item) =>
item.liveStatus == 1 && item.recordLiveTime == 0) // 根据条件过滤
.toList();
}
return res;
}
}

View File

@ -6,6 +6,8 @@ import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/live/follow.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'controller.dart';
@ -22,6 +24,7 @@ class _LivePageState extends State<LivePage>
with AutomaticKeepAliveClientMixin {
final LiveController _liveController = Get.put(LiveController());
late Future _futureBuilderFuture;
late Future _futureBuilderFuture2;
late ScrollController scrollController;
@override
@ -31,6 +34,7 @@ class _LivePageState extends State<LivePage>
void initState() {
super.initState();
_futureBuilderFuture = _liveController.queryLiveList('init');
_futureBuilderFuture2 = _liveController.fetchLiveFollowing();
scrollController = _liveController.scrollController;
scrollController.addListener(
() {
@ -69,6 +73,7 @@ class _LivePageState extends State<LivePage>
child: CustomScrollView(
controller: _liveController.scrollController,
slivers: [
buildFollowingList(),
SliverPadding(
// 单列布局 EdgeInsets.zero
padding:
@ -147,4 +152,148 @@ class _LivePageState extends State<LivePage>
),
);
}
// 关注的up直播
Widget buildFollowingList() {
return SliverPadding(
padding: const EdgeInsets.only(top: 16),
sliver: SliverToBoxAdapter(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(
() => Text.rich(
TextSpan(
children: [
const TextSpan(
text: ' 我的关注 ',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
TextSpan(
text: ' ${_liveController.liveFollowingList.length}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.primary,
),
),
TextSpan(
text: '人正在直播',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
],
),
),
),
FutureBuilder(
future: _futureBuilderFuture2,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map? data = snapshot.data;
if (data?['status']) {
RxList list = _liveController.liveFollowingList;
// ignore: invalid_use_of_protected_member
return Obx(() => LiveFollowingListView(list: list.value));
} else {
return SizedBox(
height: 80,
child: Center(
child: Text(
data?['msg'] ?? '',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: 12,
),
),
),
);
}
} else {
return const SizedBox();
}
},
),
],
),
),
);
}
}
class LiveFollowingListView extends StatelessWidget {
final List list;
const LiveFollowingListView({super.key, required this.list});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final LiveFollowingItemModel item = list[index];
return Padding(
padding: const EdgeInsets.fromLTRB(3, 12, 3, 0),
child: Column(
children: [
InkWell(
onTap: () {
Get.toNamed(
'/liveRoom?roomid=${item.roomId}',
arguments: {
'liveItem': item,
'heroTag': item.roomId.toString()
},
);
},
child: Container(
width: 54,
height: 54,
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(27),
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 1.5,
),
),
child: NetworkImgLayer(
width: 50,
height: 50,
type: 'avatar',
src: list[index].face,
),
),
),
const SizedBox(height: 6),
SizedBox(
width: 62,
child: Text(
list[index].uname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12,
),
),
),
],
),
);
},
itemCount: list.length,
),
);
}
}

View File

@ -48,6 +48,7 @@ class LiveRoomController extends GetxController {
// 直播间弹幕开关 默认打开
RxBool danmakuSwitch = true.obs;
late String buvid;
RxBool isPortrait = false.obs;
@override
void onInit() {
@ -58,11 +59,12 @@ class LiveRoomController extends GetxController {
if (Get.arguments != null) {
liveItem = Get.arguments['liveItem'];
heroTag = Get.arguments['heroTag'] ?? '';
if (liveItem != null && liveItem.pic != null && liveItem.pic != '') {
cover = liveItem.pic;
}
if (liveItem != null && liveItem.cover != null && liveItem.cover != '') {
cover = liveItem.cover;
if (liveItem != null) {
cover = (liveItem.pic != null && liveItem.pic != '')
? liveItem.pic
: (liveItem.cover != null && liveItem.cover != '')
? liveItem.cover
: null;
}
Request.getBuvid().then((value) => buvid = value);
}
@ -100,6 +102,7 @@ class LiveRoomController extends GetxController {
Future queryLiveInfo() async {
var res = await LiveHttp.liveRoomInfo(roomId: roomId, qn: currentQn);
if (res['status']) {
isPortrait.value = res['data'].isPortrait;
List<CodecItem> codec =
res['data'].playurlInfo.playurl.stream.first.format.first.codec;
CodecItem item = codec.first;

View File

@ -115,6 +115,9 @@ class _LiveRoomPageState extends State<LiveRoomPage>
plPlayerController = _liveRoomController.plPlayerController;
return PLVideoPlayer(
controller: plPlayerController,
alignment: _liveRoomController.isPortrait.value
? Alignment.topCenter
: Alignment.center,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
@ -178,10 +181,83 @@ class _LiveRoomPageState extends State<LiveRoomPage>
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppBar(
Obx(
() => SizedBox(
height: MediaQuery.of(context).padding.top +
(_liveRoomController.isPortrait.value ||
MediaQuery.of(context).orientation ==
Orientation.landscape
? 0
: kToolbarHeight),
),
),
PopScope(
canPop: plPlayerController.isFullScreen.value != true,
onPopInvoked: (bool didPop) {
if (plPlayerController.isFullScreen.value == true) {
plPlayerController.triggerFullScreen(status: false);
}
if (MediaQuery.of(context).orientation ==
Orientation.landscape) {
verticalScreen();
}
},
child: Obx(
() => Container(
width: Get.size.width,
height: MediaQuery.of(context).orientation ==
Orientation.landscape
? Get.size.height
: !_liveRoomController.isPortrait.value
? Get.size.width * 9 / 16
: Get.size.height -
MediaQuery.of(context).padding.top,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(6)),
),
child: videoPlayerPanel,
),
),
),
],
),
// 定位 快速滑动到底部
Positioned(
right: 20,
bottom: MediaQuery.of(context).padding.bottom + 80,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 4),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: fabAnimationCtr,
curve: Curves.easeInOut,
)),
child: ElevatedButton.icon(
onPressed: () {
_scrollToBottom();
},
icon: const Icon(Icons.keyboard_arrow_down), // 图标
label: const Text('新消息'), // 文字
style: ElevatedButton.styleFrom(
// primary: Colors.blue, // 按钮背景颜色
// onPrimary: Colors.white, // 按钮文字颜色
padding: const EdgeInsets.fromLTRB(14, 12, 20, 12), // 按钮内边距
),
),
),
),
// 顶栏
Positioned(
top: 0,
left: 0,
right: 0,
child: AppBar(
centerTitle: false,
titleSpacing: 0,
backgroundColor: Colors.transparent,
@ -213,8 +289,8 @@ class _LiveRoomPageState extends State<LiveRoomPage>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.roomInfoH5.value
.anchorInfo!.baseInfo!.uname!,
_liveRoomController.roomInfoH5.value.anchorInfo!
.baseInfo!.uname!,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
@ -238,77 +314,33 @@ class _LiveRoomPageState extends State<LiveRoomPage>
},
),
),
PopScope(
canPop: plPlayerController.isFullScreen.value != true,
onPopInvoked: (bool didPop) {
if (plPlayerController.isFullScreen.value == true) {
plPlayerController.triggerFullScreen(status: false);
}
if (MediaQuery.of(context).orientation ==
Orientation.landscape) {
verticalScreen();
}
},
child: SizedBox(
width: Get.size.width,
height: MediaQuery.of(context).orientation ==
Orientation.landscape
? Get.size.height
: Get.size.width * 9 / 16,
child: videoPlayerPanel,
),
),
// 显示消息的列表
buildMessageListUI(
// 消息列表
Obx(
() => Positioned(
top: MediaQuery.of(context).padding.top +
kToolbarHeight +
(_liveRoomController.isPortrait.value
? Get.size.width
: Get.size.width * 9 / 16),
bottom: 90 + MediaQuery.of(context).padding.bottom,
left: 0,
right: 0,
child: buildMessageListUI(
context,
_liveRoomController,
_scrollController,
),
// Container(
// padding: const EdgeInsets.only(
// left: 14, right: 14, top: 4, bottom: 4),
// margin: const EdgeInsets.only(
// bottom: 6,
// left: 14,
// ),
// decoration: BoxDecoration(
// color: Colors.grey.withOpacity(0.1),
// borderRadius: const BorderRadius.all(Radius.circular(20)),
// ),
// child: Obx(
// () => AnimatedSwitcher(
// duration: const Duration(milliseconds: 300),
// transitionBuilder:
// (Widget child, Animation<double> animation) {
// return FadeTransition(opacity: animation, child: child);
// },
// child: Text.rich(
// key:
// ValueKey(_liveRoomController.joinRoomTip['userName']),
// TextSpan(
// style: const TextStyle(color: Colors.white),
// children: [
// TextSpan(
// text:
// '${_liveRoomController.joinRoomTip['userName']} ',
// style: TextStyle(
// color: Colors.white.withOpacity(0.6),
// ),
// ),
// TextSpan(
// text:
// '${_liveRoomController.joinRoomTip['message']}',
// style: const TextStyle(color: Colors.white),
// ),
// ],
// ),
// ),
// ),
// ),
// ),
const SizedBox(height: 10),
// 弹幕输入框
Container(
),
),
// 消息输入框
Visibility(
visible: MediaQuery.of(context).orientation == Orientation.portrait,
child: Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: EdgeInsets.only(
left: 14,
right: 14,
@ -384,32 +416,6 @@ class _LiveRoomPageState extends State<LiveRoomPage>
],
),
),
],
),
// 定位 快速滑动到底部
Positioned(
right: 20,
bottom: MediaQuery.of(context).padding.bottom + 80,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 4),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: fabAnimationCtr,
curve: Curves.easeInOut,
)),
child: ElevatedButton.icon(
onPressed: () {
_scrollToBottom();
},
icon: const Icon(Icons.keyboard_arrow_down), // 图标
label: const Text('新消息'), // 文字
style: ElevatedButton.styleFrom(
// primary: Colors.blue, // 按钮背景颜色
// onPrimary: Colors.white, // 按钮文字颜色
padding: const EdgeInsets.fromLTRB(14, 12, 20, 12), // 按钮内边距
),
),
),
),
],
@ -467,7 +473,9 @@ Widget buildMessageListUI(
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.1),
color: liveRoomController.isPortrait.value
? Colors.black.withOpacity(0.3)
: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
margin: EdgeInsets.only(

View File

@ -240,4 +240,6 @@ class MemberController extends GetxController {
}
void pushfavPage() => Get.toNamed('/fav?mid=$mid');
// 跳转图文专栏
void pushArticlePage() => Get.toNamed('/memberArticle?mid=$mid');
}

View File

@ -29,7 +29,8 @@ class _MemberPageState extends State<MemberPage>
late Future _memberCoinsFuture;
late Future _memberLikeFuture;
final ScrollController _extendNestCtr = ScrollController();
final StreamController<bool> appbarStream = StreamController<bool>();
final StreamController<bool> appbarStream =
StreamController<bool>.broadcast();
late int mid;
@override
@ -170,32 +171,44 @@ class _MemberPageState extends State<MemberPage>
),
/// 视频
Obx(() => ListTile(
Obx(
() => ListTile(
onTap: _memberController.pushArchivesPage,
title: Text(
'${_memberController.isOwner.value ? '' : 'Ta'}的投稿'),
trailing: const Icon(Icons.arrow_forward_outlined,
size: 19),
)),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 他的收藏夹
Obx(() => ListTile(
Obx(
() => ListTile(
onTap: _memberController.pushfavPage,
title: Text(
'${_memberController.isOwner.value ? '' : 'Ta'}的收藏'),
trailing: const Icon(Icons.arrow_forward_outlined,
size: 19),
)),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 专栏
Obx(() => ListTile(
Obx(
() => ListTile(
onTap: _memberController.pushArticlePage,
title: Text(
'${_memberController.isOwner.value ? '' : 'Ta'}的专栏'))),
'${_memberController.isOwner.value ? '' : 'Ta'}的专栏'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 合集
Obx(() => ListTile(
Obx(
() => ListTile(
title: Text(
'${_memberController.isOwner.value ? '' : 'Ta'}的合集'))),
'${_memberController.isOwner.value ? '' : 'Ta'}的合集')),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
@ -406,7 +419,7 @@ class _MemberPageState extends State<MemberPage>
? '个人认证:'
: '企业认证:',
style: TextStyle(
color: Theme.of(context).primaryColor,
color: Theme.of(context).colorScheme.primary,
),
children: [
TextSpan(

View File

@ -27,10 +27,13 @@ class MemberArchiveController extends GetxController {
// 获取用户投稿
Future getMemberArchive(type) async {
if (isLoading.value) {
return;
}
isLoading.value = true;
if (type == 'init') {
pn = 1;
archivesList.clear();
isLoading.value = true;
}
var res = await MemberHttp.memberArchive(
mid: mid,

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/member/article.dart';
class MemberArticleController extends GetxController {
final ScrollController scrollController = ScrollController();
late int mid;
int pn = 1;
String? offset;
bool hasMore = true;
String? wWebid;
RxBool isLoading = false.obs;
RxList<MemberArticleItemModel> articleList = <MemberArticleItemModel>[].obs;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
}
// 获取wWebid
Future getWWebid() async {
var res = await MemberHttp.getWWebid(mid: mid);
if (res['status']) {
wWebid = res['data'];
} else {
wWebid = '-1';
SmartDialog.showToast(res['msg']);
}
}
Future getMemberArticle(type) async {
if (isLoading.value) {
return;
}
isLoading.value = true;
if (wWebid == null) {
await getWWebid();
}
if (type == 'init') {
pn = 1;
articleList.clear();
}
var res = await MemberHttp.getMemberArticle(
mid: mid,
pn: pn,
offset: offset,
wWebid: wWebid!,
);
if (res['status']) {
offset = res['data'].offset;
hasMore = res['data'].hasMore!;
if (type == 'init') {
articleList.value = res['data'].items;
}
if (type == 'onLoad') {
articleList.addAll(res['data'].items);
}
pn += 1;
} else {
SmartDialog.showToast(res['msg']);
}
isLoading.value = false;
return res;
}
}

View File

@ -0,0 +1,4 @@
library member_article;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,176 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/skeleton.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class MemberArticlePage extends StatefulWidget {
const MemberArticlePage({super.key});
@override
State<MemberArticlePage> createState() => _MemberArticlePageState();
}
class _MemberArticlePageState extends State<MemberArticlePage> {
late MemberArticleController _memberArticleController;
late Future _futureBuilderFuture;
late ScrollController scrollController;
late int mid;
@override
void initState() {
super.initState();
mid = int.parse(Get.parameters['mid']!);
final String heroTag = Utils.makeHeroTag(mid);
_memberArticleController = Get.put(MemberArticleController(), tag: heroTag);
_futureBuilderFuture = _memberArticleController.getMemberArticle('init');
scrollController = _memberArticleController.scrollController;
scrollController.addListener(_scrollListener);
}
void _scrollListener() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'member_archives', const Duration(milliseconds: 500), () {
_memberArticleController.getMemberArticle('onLoad');
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
centerTitle: false,
title: const Text('Ta的图文', style: TextStyle(fontSize: 16)),
),
body: FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data != null) {
return _buildContent(snapshot.data as Map);
} else {
return _buildError(snapshot.data['msg']);
}
} else {
return ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return _buildSkeleton();
},
);
}
},
),
);
}
Widget _buildContent(Map data) {
RxList list = _memberArticleController.articleList;
if (data['status']) {
return Obx(
() => list.isNotEmpty
? ListView.separated(
controller: scrollController,
itemCount: list.length,
separatorBuilder: (BuildContext context, int index) {
return Divider(
height: 10,
color: Theme.of(context).dividerColor.withOpacity(0.15),
);
},
itemBuilder: (BuildContext context, int index) {
return _buildListItem(list[index]);
},
)
: const CustomScrollView(
physics: NeverScrollableScrollPhysics(),
slivers: [
NoData(),
],
),
);
} else {
return _buildError(data['msg']);
}
}
Widget _buildListItem(dynamic item) {
return ListTile(
onTap: () {
Get.toNamed('/opus', parameters: {
'title': item.content,
'id': item.opusId,
'articleType': 'opus',
});
},
leading: NetworkImgLayer(
width: 50,
height: 50,
type: 'emote',
src: item.cover['url'],
),
title: Text(
item.content,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'${item.stat["like"]}人点赞',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
),
);
}
Widget _buildError(String errMsg) {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: HttpError(
errMsg: errMsg,
fn: () {},
),
),
],
);
}
Widget _buildSkeleton() {
return Skeleton(
child: ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(4),
),
),
title: Container(
height: 16,
color: Theme.of(context).colorScheme.onInverseSurface,
),
subtitle: Container(
height: 11,
color: Theme.of(context).colorScheme.onInverseSurface,
),
),
);
}
}

View File

@ -21,7 +21,7 @@ class MemberSeasonsController extends GetxController {
mid = int.parse(Get.parameters['mid']!);
category = Get.parameters['category']!;
if (category == '0') {
seasonId = int.parse(Get.parameters['seriesId']!);
seasonId = int.parse(Get.parameters['seasonId']!);
}
if (category == '1') {
seriesId = int.parse(Get.parameters['seriesId']!);

View File

@ -203,9 +203,9 @@ class LikeItem extends StatelessWidget {
Text.rich(TextSpan(children: [
TextSpan(text: nickNameList.join('')),
const TextSpan(text: ' '),
if (item.users!.length > 1)
if (item.counts! > 1)
TextSpan(
text: '等总计${item.users!.length}',
text: '等总计${item.counts}',
style: TextStyle(color: outline),
),
TextSpan(

View File

@ -0,0 +1,100 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/read.dart';
import 'package:pilipala/models/read/opus.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';
import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';
class OpusController extends GetxController {
late String url;
RxString title = ''.obs;
late String id;
late String articleType;
Rx<OpusDataModel> opusData = OpusDataModel().obs;
final ScrollController scrollController = ScrollController();
late StreamController<bool> appbarStream = StreamController<bool>.broadcast();
@override
void onInit() {
super.onInit();
title.value = Get.parameters['title'] ?? '';
id = Get.parameters['id']!;
articleType = Get.parameters['articleType']!;
if (articleType == 'opus') {
url = 'https://www.bilibili.com/opus/$id';
}
scrollController.addListener(_scrollListener);
}
Future fetchOpusData() async {
var res = await ReadHttp.parseArticleOpus(id: id);
if (res['status']) {
List<String> keys = res.keys.toList();
if (keys.contains('isCv') && res['isCv']) {
Get.offNamed('/read', parameters: {
'id': res['cvId'],
'title': title.value,
'articleType': 'cv',
});
} else {
opusData.value = res['data'];
}
}
return res;
}
void _scrollListener() {
final double offset = scrollController.position.pixels;
if (offset > 100) {
appbarStream.add(true);
} else {
appbarStream.add(false);
}
}
void onPreviewImg(picList, initIndex, context) {
Navigator.of(context).push(
HeroDialogRoute<void>(
builder: (BuildContext context) => InteractiveviewerGallery(
sources: picList,
initIndex: initIndex,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: picList[index],
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
),
),
);
}
@override
void onClose() {
scrollController.removeListener(_scrollListener);
appbarStream.close();
super.onClose();
}
}

View File

@ -0,0 +1,4 @@
library opus;
export 'controller.dart';
export 'view.dart';

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:pilipala/models/read/opus.dart';
class TextHelper {
static Alignment getAlignment(int? align) {
switch (align) {
case 1:
return Alignment.center;
case 0:
return Alignment.centerLeft;
case 2:
return Alignment.centerRight;
default:
return Alignment.centerLeft;
}
}
static TextSpan buildTextSpan(
ModuleParagraphTextNode node, int? align, BuildContext context) {
// 获取node的所有key
if (node.nodeType != null) {
return TextSpan(
text: node.word?.words ?? '',
style: TextStyle(
fontSize:
node.word?.fontSize != null ? node.word!.fontSize! * 0.95 : 14,
fontWeight: node.word?.style?.bold != null
? FontWeight.bold
: FontWeight.normal,
height: align == 1 ? 2 : 1.5,
color: node.word?.color != null
? Color(int.parse(node.word!.color!.substring(1, 7), radix: 16) +
0xFF000000)
: Theme.of(context).colorScheme.onBackground,
),
);
} else {
switch (node.type) {
case 'TEXT_NODE_TYPE_WORD':
return TextSpan(
text: node.word?.words ?? '',
style: TextStyle(
fontSize: node.word?.fontSize != null
? node.word!.fontSize! * 0.95
: 14,
fontWeight: node.word?.style?.bold != null
? FontWeight.bold
: FontWeight.normal,
height: align == 1 ? 2 : 1.5,
color: node.word?.color != null
? Color(
int.parse(node.word!.color!.substring(1, 7), radix: 16) +
0xFF000000)
: Theme.of(context).colorScheme.onBackground,
),
);
default:
return const TextSpan(text: '');
}
}
}
}

286
lib/pages/opus/view.dart Normal file
View File

@ -0,0 +1,286 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/read/opus.dart';
import 'controller.dart';
import 'text_helper.dart';
class OpusPage extends StatefulWidget {
const OpusPage({super.key});
@override
State<OpusPage> createState() => _OpusPageState();
}
class _OpusPageState extends State<OpusPage> {
final OpusController controller = Get.put(OpusController());
late Future _futureBuilderFuture;
@override
void initState() {
super.initState();
_futureBuilderFuture = controller.fetchOpusData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(),
body: SingleChildScrollView(
controller: controller.scrollController,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(),
_buildFutureContent(),
],
),
),
);
}
AppBar _buildAppBar() {
return AppBar(
title: StreamBuilder(
stream: controller.appbarStream.stream.distinct(),
initialData: false,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
child: Obx(
() => Text(
controller.title.value,
style: const TextStyle(fontSize: 16),
),
),
);
},
),
actions: [
IconButton(
icon: const Icon(Icons.more_vert_rounded),
onPressed: () {},
),
const SizedBox(width: 16),
],
);
}
Widget _buildTitle() {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: Obx(
() => Text(
controller.title.value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 1,
height: 1.5,
),
),
),
);
}
Widget _buildFutureContent() {
return FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
return _buildContent(controller.opusData.value);
} else {
return _buildError(snapshot.data['message']);
}
} else {
return _buildLoading();
}
},
);
}
Widget _buildContent(OpusDataModel opusData) {
if (opusData.detail == null) {
return const SizedBox();
}
final modules = opusData.detail!.modules!;
late ModuleContent moduleContent;
// 获取所有的图片链接
final List<String> picList = [];
final int moduleIndex =
modules.indexWhere((module) => module.moduleContent != null);
if (moduleIndex != -1) {
moduleContent = modules[moduleIndex].moduleContent!;
for (var paragraph in moduleContent.paragraphs!) {
if (paragraph.paraType == 2) {
for (var pic in paragraph.pic!.pics!) {
picList.add(pic.url!);
}
}
}
} else {
print('No moduleContent found');
}
return Padding(
padding: EdgeInsets.fromLTRB(
16, 0, 16, MediaQuery.of(context).padding.bottom + 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: _buildStatsWidget(opusData),
),
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: _buildAuthorWidget(opusData),
),
...moduleContent.paragraphs!.map(
(ModuleParagraph paragraph) {
return Column(
children: [
if (paragraph.paraType == 1) ...[
Container(
alignment: TextHelper.getAlignment(paragraph.align),
margin: const EdgeInsets.only(bottom: 10),
child: Text.rich(
TextSpan(
children: paragraph.text?.nodes?.map((node) {
return TextHelper.buildTextSpan(
node, paragraph.align, context);
}).toList() ??
[],
),
),
)
] else if (paragraph.paraType == 2) ...[
...paragraph.pic?.pics?.map(
(Pic pic) => Center(
child: Padding(
padding:
const EdgeInsets.only(top: 10, bottom: 10),
child: InkWell(
onTap: () {
controller.onPreviewImg(
picList,
picList.indexOf(pic.url!),
context,
);
},
child: NetworkImgLayer(
src: pic.url,
width: (Get.size.width - 32) * pic.scale!,
height: (Get.size.width - 32) *
pic.scale! /
pic.aspectRatio!,
type: 'emote',
),
),
),
),
) ??
[],
] else
const SizedBox(),
],
);
},
),
],
),
);
}
Widget _buildAuthorWidget(OpusDataModel opusData) {
final modules = opusData.detail!.modules!;
late ModuleAuthor moduleAuthor;
final int moduleIndex =
modules.indexWhere((module) => module.moduleAuthor != null);
if (moduleIndex != -1) {
moduleAuthor = modules[moduleIndex].moduleAuthor!;
} else {
return const SizedBox();
}
return Row(
children: [
NetworkImgLayer(
width: 48,
height: 48,
type: 'avatar',
src: moduleAuthor.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
moduleAuthor.name!,
style: const TextStyle(
fontWeight: FontWeight.w500,
),
),
StyledText(moduleAuthor.pubTime!),
],
),
],
);
}
Widget _buildStatsWidget(OpusDataModel opusData) {
final modules = opusData.detail!.modules!;
final ModuleStat moduleStat = modules.last.moduleStat!;
return Row(
children: [
StyledText('${moduleStat.comment!.count}评论'),
const SizedBox(width: 10),
StyledText('${moduleStat.like!.count}'),
const SizedBox(width: 10),
StyledText('${moduleStat.favorite!.count}转发'),
],
);
}
Widget _buildError(String message) {
return SizedBox(
height: 100,
child: Center(
child: Text(message),
),
);
}
Widget _buildLoading() {
return const SizedBox(
height: 100,
child: Center(
child: CircularProgressIndicator(),
),
);
}
}
class StyledText extends StatelessWidget {
final String text;
const StyledText(this.text, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
text,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.outline,
),
);
}
}

View File

@ -0,0 +1,94 @@
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/read.dart';
import 'package:pilipala/models/read/read.dart';
import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';
import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';
class ReadPageController extends GetxController {
late String url;
RxString title = ''.obs;
late String id;
late String articleType;
Rx<ReadDataModel> cvData = ReadDataModel().obs;
final ScrollController scrollController = ScrollController();
late StreamController<bool> appbarStream = StreamController<bool>.broadcast();
@override
void onInit() {
super.onInit();
title.value = Get.parameters['title'] ?? '';
id = Get.parameters['id']!;
articleType = Get.parameters['articleType']!;
scrollController.addListener(_scrollListener);
fetchViewInfo();
}
Future fetchCvData() async {
var res = await ReadHttp.parseArticleCv(id: id);
if (res['status']) {
cvData.value = res['data'];
title.value = cvData.value.readInfo!.title!;
}
return res;
}
void _scrollListener() {
final double offset = scrollController.position.pixels;
if (offset > 100) {
appbarStream.add(true);
} else {
appbarStream.add(false);
}
}
void onPreviewImg(picList, initIndex, context) {
Navigator.of(context).push(
HeroDialogRoute<void>(
builder: (BuildContext context) => InteractiveviewerGallery(
sources: picList,
initIndex: initIndex,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: picList[index],
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
),
),
);
}
void fetchViewInfo() {
ReadHttp.getViewInfo(id: id);
}
@override
void onClose() {
scrollController.removeListener(_scrollListener);
appbarStream.close();
super.onClose();
}
}

View File

@ -0,0 +1,4 @@
library read;
export 'controller.dart';
export 'view.dart';

342
lib/pages/read/view.dart Normal file
View File

@ -0,0 +1,342 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/html_render.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/read/opus.dart';
import 'package:pilipala/models/read/read.dart';
import 'package:pilipala/pages/opus/text_helper.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class ReadPage extends StatefulWidget {
const ReadPage({super.key});
@override
State<ReadPage> createState() => _ReadPageState();
}
class _ReadPageState extends State<ReadPage> {
final ReadPageController controller = Get.put(ReadPageController());
late Future _futureBuilderFuture;
@override
void initState() {
super.initState();
_futureBuilderFuture = controller.fetchCvData();
}
List<String> extractDataSrc(String input) {
final regex = RegExp(r'data-src="([^"]*)"');
final matches = regex.allMatches(input);
return matches.map((match) {
final dataSrc = match.group(1)!;
return dataSrc.startsWith('//') ? 'https:$dataSrc' : dataSrc;
}).toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(),
body: SingleChildScrollView(
controller: controller.scrollController,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(),
_buildFutureContent(),
],
),
),
);
}
AppBar _buildAppBar() {
return AppBar(
title: StreamBuilder(
stream: controller.appbarStream.stream.distinct(),
initialData: false,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
child: Obx(
() => Text(
controller.title.value,
style: const TextStyle(fontSize: 16),
),
),
);
},
),
actions: [
IconButton(
icon: const Icon(Icons.more_vert_rounded),
onPressed: () {},
),
const SizedBox(width: 16),
],
);
}
Widget _buildTitle() {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: Obx(
() => Text(
controller.title.value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 1,
height: 1.5,
),
),
),
);
}
Widget _buildFutureContent() {
return FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
return _buildContent(snapshot.data['data']);
} else {
return _buildError(snapshot.data['message']);
}
} else {
return _buildLoading();
}
},
);
}
Widget _buildContent(ReadDataModel cvData) {
final List<String> picList = _extractPicList(cvData);
final List<String> imgList = extractDataSrc(cvData.readInfo!.content!);
return Padding(
padding: EdgeInsets.fromLTRB(
16, 0, 16, MediaQuery.of(context).padding.bottom + 40),
child: cvData.readInfo!.opus == null
? _buildNonOpusContent(cvData, imgList)
: _buildOpusContent(cvData, picList),
);
}
List<String> _extractPicList(ReadDataModel cvData) {
final List<String> picList = [];
if (cvData.readInfo!.opus != null) {
final List<ModuleParagraph> paragraphs =
cvData.readInfo!.opus!.content!.paragraphs!;
for (var paragraph in paragraphs) {
if (paragraph.paraType == 2) {
for (var pic in paragraph.pic!.pics!) {
picList.add(pic.url!);
}
}
}
}
return picList;
}
Widget _buildNonOpusContent(ReadDataModel cvData, List<String> imgList) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: _buildStatsWidget(cvData),
),
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: _buildAuthorWidget(cvData),
),
HtmlRender(
htmlContent: cvData.readInfo!.content!,
imgList: imgList,
),
],
);
}
Widget _buildOpusContent(ReadDataModel cvData, List<String> picList) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: _buildStatsWidget(cvData),
),
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: _buildAuthorWidget(cvData),
),
...cvData.readInfo!.opus!.content!.paragraphs!.map(
(ModuleParagraph paragraph) {
return Column(
children: [
if (paragraph.paraType == 1)
_buildTextParagraph(paragraph)
else if (paragraph.paraType == 2)
..._buildPics(paragraph, picList)
else
const SizedBox(),
],
);
},
),
],
);
}
Widget _buildTextParagraph(ModuleParagraph paragraph) {
return Container(
alignment: TextHelper.getAlignment(paragraph.align),
margin: const EdgeInsets.only(bottom: 10),
child: Text.rich(
TextSpan(
children: paragraph.text?.nodes?.map((node) {
return TextHelper.buildTextSpan(node, paragraph.align, context);
}).toList() ??
[],
),
),
);
}
Widget _buildError(String message) {
return SizedBox(
height: 100,
child: Center(
child: Text(message),
),
);
}
Widget _buildLoading() {
return const SizedBox(
height: 100,
child: Center(
child: CircularProgressIndicator(),
),
);
}
Widget _buildStatsWidget(ReadDataModel cvData) {
return Row(
children: [
StyledText(Utils.CustomStamp_str(
timestamp: cvData.readInfo!.publishTime!,
date: 'YY-MM-DD hh:mm',
toInt: false,
)),
const SizedBox(width: 10),
StyledText('${Utils.numFormat(cvData.readInfo!.stats!['view'])}浏览'),
const StyledText(' · '),
StyledText('${cvData.readInfo!.stats!['like']}点赞'),
// const StyledText(' · '),
// StyledText('${cvData.readInfo!.stats!['reply']}评论'),
],
);
}
Widget _buildAuthorWidget(ReadDataModel cvData) {
final Author author = cvData.readInfo!.author!;
return Row(
children: [
NetworkImgLayer(
width: 48,
height: 48,
type: 'avatar',
src: author.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
author.name!,
style: TextStyle(
color: author.vip!.nicknameColor != null
? Color(author.vip!.nicknameColor!)
: null,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 6),
Image.asset(
'assets/images/lv/lv${author.level}.png',
height: 11,
),
],
),
Row(
children: [
StyledText('粉丝: ${Utils.numFormat(author.fans)}'),
const SizedBox(width: 10),
StyledText(
'文章: ${Utils.numFormat(cvData.readInfo!.totalArtNum)}'),
],
),
],
),
],
);
}
List<Widget> _buildPics(ModuleParagraph paragraph, List<String> picList) {
return paragraph.pic?.pics
?.map(
(Pic pic) => Center(
child: Padding(
padding: const EdgeInsets.only(top: 10, bottom: 10),
child: InkWell(
onTap: () {
controller.onPreviewImg(
picList,
picList.indexOf(pic.url!),
context,
);
},
child: NetworkImgLayer(
src: pic.url,
width: (Get.size.width - 32) * pic.scale!,
height:
(Get.size.width - 32) * pic.scale! / pic.aspectRatio!,
type: 'emote',
),
),
),
),
)
.toList() ??
[];
}
}
class StyledText extends StatelessWidget {
final String text;
const StyledText(this.text, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
text,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.outline,
),
);
}
}

View File

@ -14,11 +14,10 @@ Widget searchArticlePanel(BuildContext context, ctr, list) {
itemBuilder: (context, index) {
return InkWell(
onTap: () {
Get.toNamed('/htmlRender', parameters: {
'url': 'www.bilibili.com/read/cv${list[index].id}',
Get.toNamed('/read', parameters: {
'title': list[index].subTitle,
'id': 'cv${list[index].id}',
'dynamicType': 'read'
'id': list[index].id.toString(),
'articleType': 'read'
});
},
child: Padding(

View File

@ -261,13 +261,16 @@ class VideoPanelController extends GetxController {
onShowFilterSheet(searchPanelCtr) {
showModalBottomSheet(
context: Get.context!,
isScrollControlled: true,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setState) {
return Container(
color: Theme.of(Get.context!).colorScheme.surface,
padding: const EdgeInsets.only(top: 12),
child: Column(
return Padding(
padding: EdgeInsets.only(
top: 12, bottom: MediaQuery.of(context).padding.bottom + 20),
child: Wrap(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
@ -295,9 +298,10 @@ class VideoPanelController extends GetxController {
onSelect: (value) async {
currentTimeFilterval.value = i['value'];
setState(() {});
SmartDialog.showToast("${i['label']}」的筛选结果");
SearchPanelController ctr =
Get.find<SearchPanelController>(
SmartDialog.showToast(
"${i['label']}」的筛选结果");
SearchPanelController ctr = Get.find<
SearchPanelController>(
tag: 'video${searchPanelCtr.keyword!}');
ctr.duration.value = i['value'];
Get.back();
@ -326,13 +330,14 @@ class VideoPanelController extends GetxController {
SearchText(
searchText: i['label'],
searchTextIdx: i['value'],
isSelect: currentPartFilterval.value == i['value'],
isSelect:
currentPartFilterval.value == i['value'],
onSelect: (value) async {
currentPartFilterval.value = i['value'];
setState(() {});
SmartDialog.showToast("${i['label']}」的筛选结果");
SearchPanelController ctr =
Get.find<SearchPanelController>(
SearchPanelController ctr = Get.find<
SearchPanelController>(
tag: 'video${searchPanelCtr.keyword!}');
ctr.tids.value = i['value'];
Get.back();
@ -347,6 +352,8 @@ class VideoPanelController extends GetxController {
)
],
),
],
),
);
},
);

View File

@ -24,14 +24,14 @@ class _SubDetailPageState extends State<SubDetailPage> {
late final ScrollController _controller = ScrollController();
final SubDetailController _subDetailController =
Get.put(SubDetailController());
late StreamController<bool> titleStreamC; // a
late StreamController<bool> titleStreamC =
StreamController<bool>.broadcast(); // a
late Future _futureBuilderFuture;
@override
void initState() {
super.initState();
_futureBuilderFuture = _subDetailController.queryUserSeasonList();
titleStreamC = StreamController<bool>();
_controller.addListener(
() {
if (_controller.offset > 160) {

View File

@ -7,9 +7,11 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/video/later.dart';
import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video/reply/item.dart';
@ -24,7 +26,10 @@ import '../../../models/video/subTitile/content.dart';
import '../../../http/danmaku.dart';
import '../../../plugin/pl_player/models/bottom_control_type.dart';
import '../../../utils/id_utils.dart';
import 'introduction/controller.dart';
import 'reply/controller.dart';
import 'widgets/header_control.dart';
import 'widgets/watch_later_list.dart';
class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin {
@ -37,9 +42,10 @@ class VideoDetailController extends GetxController
Map videoItem = {};
// 视频类型 默认投稿视频
SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;
// 页面来源 稍后再看 收藏夹
RxString sourceType = 'normal'.obs;
/// tabs相关配置
int tabInitialIndex = 0;
late TabController tabCtr;
RxList<String> tabs = <String>['简介', '评论'].obs;
@ -110,6 +116,9 @@ class VideoDetailController extends GetxController
RxDouble sheetHeight = 0.0.obs;
RxString archiveSourceType = 'dash'.obs;
ScrollController? replyScrillController;
List<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[];
RxBool isWatchLaterVisible = false.obs;
RxString watchLaterTitle = ''.obs;
@override
void onInit() {
@ -119,9 +128,7 @@ class VideoDetailController extends GetxController
if (argMap.containsKey('videoItem')) {
var args = argMap['videoItem'];
updateCover(args.pic);
}
if (argMap.containsKey('pic')) {
} else if (argMap.containsKey('pic')) {
updateCover(argMap['pic']);
}
@ -160,6 +167,21 @@ class VideoDetailController extends GetxController
bvid: bvid,
videoType: videoType,
);
sourceType.value = argMap['sourceType'] ?? 'normal';
isWatchLaterVisible.value =
sourceType.value == 'watchLater' || sourceType.value == 'fav';
if (sourceType.value == 'watchLater') {
watchLaterTitle.value = '稍后再看';
fetchMediaList();
}
if (sourceType.value == 'fav') {
watchLaterTitle.value = argMap['favTitle'];
queryFavVideoList();
}
tabCtr.addListener(() {
onTabChanged();
});
}
showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) {
@ -561,4 +583,101 @@ class VideoDetailController extends GetxController
duration: const Duration(milliseconds: 300), curve: Curves.ease);
}
}
void toggeleWatchLaterVisible(bool val) {
if (sourceType.value == 'watchLater' || sourceType.value == 'fav') {
isWatchLaterVisible.value = !isWatchLaterVisible.value;
}
}
// 获取稍后再看列表
Future fetchMediaList() async {
final Map argMap = Get.arguments;
var count = argMap['count'];
var res = await UserHttp.getMediaList(
type: 2,
bizId: userInfo.mid,
ps: count,
);
if (res['status']) {
mediaList = res['data'].reversed.toList();
} else {
SmartDialog.showToast(res['msg']);
}
}
// 稍后再看面板展开
showMediaListPanel() {
replyReplyBottomSheetCtr =
scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
return MediaListPanel(
sheetHeight: sheetHeight.value,
mediaList: mediaList,
changeMediaList: changeMediaList,
panelTitle: watchLaterTitle.value,
bvid: bvid,
mediaId: Get.arguments['mediaId'],
hasMore: mediaList.length != Get.arguments['count'],
);
});
replyReplyBottomSheetCtr?.closed.then((value) {
isWatchLaterVisible.value = true;
});
}
// 切换稍后再看
Future changeMediaList(bvidVal, cidVal, aidVal, coverVal) async {
final VideoIntroController videoIntroCtr =
Get.find<VideoIntroController>(tag: heroTag);
bvid = bvidVal;
oid.value = aidVal ?? IdUtils.bv2av(bvid);
cid.value = cidVal;
danmakuCid.value = cidVal;
cover.value = coverVal;
queryVideoUrl();
clearSubtitleContent();
await getSubtitle();
setSubtitleContent();
// 重新请求评论
try {
/// 未渲染回复组件时可能异常
final VideoReplyController videoReplyCtr =
Get.find<VideoReplyController>(tag: heroTag);
videoReplyCtr.aid = aidVal;
videoReplyCtr.queryReplyList(type: 'init');
} catch (_) {}
videoIntroCtr.lastPlayCid.value = cidVal;
videoIntroCtr.bvid = bvidVal;
replyReplyBottomSheetCtr!.close();
await videoIntroCtr.queryVideoIntro();
}
// 获取收藏夹视频列表
Future queryFavVideoList() async {
final Map argMap = Get.arguments;
var mediaId = argMap['mediaId'];
var oid = argMap['oid'];
var res = await UserHttp.parseFavVideo(
mediaId: mediaId,
oid: oid,
bvid: bvid,
);
if (res['status']) {
mediaList = res['data'];
}
}
// 监听tabBarView切换
void onTabChanged() {
isWatchLaterVisible.value = tabCtr.index == 0;
}
@override
void onClose() {
super.onClose();
plPlayerController.dispose();
tabCtr.removeListener(() {
onTabChanged();
});
}
}

View File

@ -33,7 +33,7 @@ class VideoIntroController extends GetxController {
// 视频详情 请求返回
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
// up主粉丝数
Map userStat = {'follower': '-'};
RxInt follower = 0.obs;
// 是否点赞
RxBool hasLike = false.obs;
// 是否投币
@ -115,7 +115,7 @@ class VideoIntroController extends GetxController {
Future queryUserStat() async {
var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!);
if (result['status']) {
userStat = result['data'];
follower.value = result['data']['follower'];
}
}

View File

@ -144,7 +144,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
final Box<dynamic> setting = GStrorage.setting;
late double sheetHeight;
late final dynamic owner;
late final dynamic follower;
late int mid;
late String memberHeroTag;
late bool enableAi;
@ -177,7 +176,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
sheetHeight = localCache.get('sheetHeight');
owner = widget.videoDetail!.owner;
follower = Utils.numFormat(videoIntroController.userStat['follower']);
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
_expandableCtr = ExpandableController(initialExpanded: false);
@ -470,15 +468,18 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 10),
Text(owner.name, style: const TextStyle(fontSize: 13)),
Text(widget.videoDetail!.owner!.name!,
style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
follower,
Obx(
() => Text(
Utils.numFormat(videoIntroController.follower.value),
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
),
),
),
const Spacer(),
Obx(
() {

View File

@ -68,6 +68,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late final AppLifecycleListener _lifecycleListener;
late double statusHeight;
// 稍后再看控制器
// late AnimationController _laterCtr;
// late Animation<Offset> _laterOffsetAni;
@override
void initState() {
super.initState();
@ -104,6 +108,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
WidgetsBinding.instance.addObserver(this);
lifecycleListener();
// watchLaterControllerInit();
}
// 获取视频资源,初始化播放器
@ -211,6 +216,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
vdCtr.bottomList.removeAt(3);
}
}
vdCtr.toggeleWatchLaterVisible(!isFullScreen);
});
}
@ -236,6 +242,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
appbarStream.close();
WidgetsBinding.instance.removeObserver(this);
_lifecycleListener.dispose();
// _laterCtr.dispose();
// _laterOffsetAni.removeListener(() {});
super.dispose();
}
@ -482,6 +490,21 @@ class _VideoDetailPageState extends State<VideoDetailPage>
);
}
/// 稍后再看控制器初始化
// void watchLaterControllerInit() {
// _laterCtr = AnimationController(
// duration: const Duration(milliseconds: 300),
// vsync: this,
// );
// _laterOffsetAni = Tween<Offset>(
// begin: const Offset(0.0, 1.0),
// end: Offset.zero,
// ).animate(CurvedAnimation(
// parent: _laterCtr,
// curve: Curves.easeInOut,
// ));
// }
@override
Widget build(BuildContext context) {
final sizeContext = MediaQuery.sizeOf(context);
@ -595,6 +618,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
child: AppBar(
backgroundColor: Colors.black,
elevation: 0,
scrolledUnderElevation: 0,
),
),
body: ExtendedNestedScrollView(
@ -757,6 +781,62 @@ class _VideoDetailPageState extends State<VideoDetailPage>
null,
);
}),
),
/// 稍后再看列表
Obx(
() => Visibility(
visible: vdCtr.sourceType.value == 'watchLater' ||
vdCtr.sourceType.value == 'fav',
child: AnimatedPositioned(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
left: 12,
bottom: vdCtr.isWatchLaterVisible.value
? MediaQuery.of(context).padding.bottom + 12
: -100,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
vdCtr.toggeleWatchLaterVisible(
!vdCtr.isWatchLaterVisible.value);
vdCtr.showMediaListPanel();
},
borderRadius: const BorderRadius.all(Radius.circular(14)),
child: Container(
width: Get.width - 24,
height: 54,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.95),
borderRadius:
const BorderRadius.all(Radius.circular(14)),
),
child: Row(children: [
const Icon(Icons.playlist_play, size: 24),
const SizedBox(width: 10),
Text(
vdCtr.watchLaterTitle.value,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
fontWeight: FontWeight.bold,
letterSpacing: 0.2,
),
),
const Spacer(),
const Icon(Icons.keyboard_arrow_up_rounded, size: 26),
]),
),
),
),
),
),
)
],
),

View File

@ -0,0 +1,229 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/video/later.dart';
import 'package:pilipala/utils/utils.dart';
class MediaListPanel extends StatefulWidget {
const MediaListPanel({
this.sheetHeight,
required this.mediaList,
this.changeMediaList,
this.panelTitle,
this.bvid,
this.mediaId,
this.hasMore = false,
super.key,
});
final double? sheetHeight;
final List<MediaVideoItemModel> mediaList;
final Function? changeMediaList;
final String? panelTitle;
final String? bvid;
final int? mediaId;
final bool hasMore;
@override
State<MediaListPanel> createState() => _MediaListPanelState();
}
class _MediaListPanelState extends State<MediaListPanel> {
RxList<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[].obs;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
mediaList.value = widget.mediaList;
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
if (widget.hasMore) {
EasyThrottle.throttle(
'queryFollowDynamic', const Duration(seconds: 1), () {
loadMore();
});
}
}
});
}
void loadMore() async {
var res = await UserHttp.getMediaList(
type: 3,
bizId: widget.mediaId!,
ps: 20,
oid: mediaList.last.id,
);
if (res['status']) {
if (res['data'].isNotEmpty) {
mediaList.addAll(res['data']);
}
} else {
SmartDialog.showToast(res['msg']);
}
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
centerTitle: false,
title: Text(
widget.panelTitle ?? '稍后再看',
style: Theme.of(context).textTheme.titleSmall,
),
actions: [
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () {
Navigator.pop(context);
},
),
const SizedBox(width: 14),
],
),
Expanded(
child: Material(
color: Theme.of(context).colorScheme.surface,
child: Obx(
() => ListView.builder(
controller: _scrollController,
itemCount: mediaList.length,
itemBuilder: ((context, index) {
var item = mediaList[index];
return InkWell(
onTap: () async {
String bvid = item.bvId!;
int? aid = item.id;
String cover = item.cover ?? '';
final int cid =
await SearchHttp.ab2c(aid: aid, bvid: bvid);
widget.changeMediaList?.call(bvid, cid, aid, cover);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 8),
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
const double width = 120;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
final double maxWidth =
boxConstraints.maxWidth;
final double maxHeight =
boxConstraints.maxHeight;
return Stack(
children: [
NetworkImgLayer(
src: item.cover ?? '',
width: maxWidth,
height: maxHeight,
),
PBadge(
text: Utils.timeFormat(
item.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
],
);
},
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(
10, 0, 6, 0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
item.title as String,
textAlign: TextAlign.start,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
color: item.bvId == widget.bvid
? Theme.of(context)
.colorScheme
.primary
: null,
),
),
const Spacer(),
Text(
item.upper?.name as String,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Theme.of(context)
.colorScheme
.outline,
),
),
const SizedBox(height: 2),
Row(
children: [
StatView(
view: item.cntInfo!['play']
as int),
const SizedBox(width: 8),
StatDanMu(
danmu:
item.cntInfo!['danmaku']
as int),
],
),
],
),
),
)
],
),
);
},
),
),
);
}),
),
),
),
),
],
),
);
}
}

View File

@ -41,6 +41,7 @@ class PLVideoPlayer extends StatefulWidget {
this.customWidgets,
this.showEposideCb,
this.fullScreenCb,
this.alignment = Alignment.center,
super.key,
});
@ -55,6 +56,7 @@ class PLVideoPlayer extends StatefulWidget {
final List<Widget>? customWidgets;
final Function? showEposideCb;
final Function? fullScreenCb;
final Alignment? alignment;
@override
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
@ -393,6 +395,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
key: ValueKey(_.videoFit.value),
controller: videoController,
controls: NoVideoControls,
alignment: widget.alignment!,
pauseUponEnteringBackgroundMode: !enableBackgroundPlay,
resumeUponEnteringForegroundMode: true,
subtitleViewConfiguration: const SubtitleViewConfiguration(

View File

@ -5,11 +5,14 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/fav_edit/index.dart';
import 'package:pilipala/pages/follow_search/view.dart';
import 'package:pilipala/pages/member_article/index.dart';
import 'package:pilipala/pages/message/at/index.dart';
import 'package:pilipala/pages/message/like/index.dart';
import 'package:pilipala/pages/message/reply/index.dart';
import 'package:pilipala/pages/message/system/index.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/opus/index.dart';
import 'package:pilipala/pages/read/index.dart';
import 'package:pilipala/pages/setting/pages/logs.dart';
import '../pages/about/index.dart';
@ -186,6 +189,13 @@ class Routes {
CustomGetPage(name: '/mine', page: () => const MinePage()),
// 收藏夹编辑
CustomGetPage(name: '/favEdit', page: () => const FavEditPage()),
// 专栏
CustomGetPage(name: '/opus', page: () => const OpusPage()),
CustomGetPage(name: '/read', page: () => const ReadPage()),
// 用户专栏
CustomGetPage(
name: '/memberArticle', page: () => const MemberArticlePage()),
];
}

View File

@ -82,14 +82,11 @@ class PiliSchame {
case 'opus':
if (path.startsWith('/detail')) {
var opusId = path.split('/').last;
Get.toNamed(
'/webview',
parameters: {
'url': 'https://www.bilibili.com/opus/$opusId',
'type': 'url',
'pageTitle': '',
},
);
Get.toNamed('/opus', arguments: {
'title': '',
'id': opusId,
'articleType': 'opus',
});
}
break;
case 'search':
@ -97,12 +94,14 @@ class PiliSchame {
break;
case 'article':
final String id = path.split('/').last.split('?').first;
Get.toNamed('/htmlRender', parameters: {
'url': 'https://www.bilibili.com/read/cv$id',
Get.toNamed(
'/read',
parameters: {
'title': 'cv$id',
'id': 'cv$id',
'dynamicType': 'read'
});
'id': id,
'dynamicType': 'read',
},
);
break;
case 'pgc':
if (path.contains('ep')) {
@ -243,12 +242,12 @@ class PiliSchame {
break;
case 'read':
print('专栏');
String id = 'cv${Utils.matchNum(query!['id']!).first}';
Get.toNamed('/htmlRender', parameters: {
String id = Utils.matchNum(query!['id']!).first.toString();
Get.toNamed('/read', parameters: {
'url': value.dataString!,
'title': '',
'id': id,
'dynamicType': 'read'
'articleType': 'read'
});
break;
case 'space':

14
lib/utils/highlight.dart Normal file
View File

@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:re_highlight/languages/all.dart';
import 'package:re_highlight/re_highlight.dart';
import 'package:re_highlight/styles/all.dart';
TextSpan? highlightExistingText(String text, List<String> languages) {
final Highlight highlight = Highlight();
highlight.registerLanguages(builtinAllLanguages);
final HighlightResult result = highlight.highlightAuto(text, languages);
final TextSpanRenderer renderer =
TextSpanRenderer(const TextStyle(), builtinAllThemes['github']!);
result.render(renderer);
return renderer.span;
}

View File

@ -1234,6 +1234,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.0"
re_highlight:
dependency: "direct main"
description:
name: re_highlight
sha256: "6c4ac3f76f939fb7ca9df013df98526634e17d8f7460e028bd23a035870024f2"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.3"
rxdart:
dependency: transitive
description:

View File

@ -149,6 +149,8 @@ dependencies:
bottom_sheet: ^4.0.4
web_socket_channel: ^2.4.5
brotli: ^0.6.0
# 文本语法高亮
re_highlight: ^0.0.3
dev_dependencies:
flutter_test: