Merge branch 'design'

This commit is contained in:
guozhigq
2024-10-19 15:46:22 +08:00
46 changed files with 782 additions and 783 deletions

View File

@ -15,6 +15,4 @@ class Constants {
// 59b43e04ad6965f34319062b478f83dd TV端
static const String appSec = '59b43e04ad6965f34319062b478f83dd';
static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
static const String thirdApi =
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
}

View File

@ -7,6 +7,7 @@ class HttpError extends StatelessWidget {
required this.fn,
this.btnText,
this.isShowBtn = true,
this.isInSliver = true,
super.key,
});
@ -14,46 +15,42 @@ class HttpError extends StatelessWidget {
final Function()? fn;
final String? btnText;
final bool isShowBtn;
final bool isInSliver;
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: SizedBox(
height: 400,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
"assets/images/error.svg",
height: 200,
),
const SizedBox(height: 30),
Text(
errMsg ?? '请求异常',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 20),
if (isShowBtn)
FilledButton.tonal(
onPressed: () {
fn!();
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((states) {
return Theme.of(context).colorScheme.primary.withAlpha(20);
}),
),
child: Text(
btnText ?? '点击重试',
style:
TextStyle(color: Theme.of(context).colorScheme.primary),
),
Color primary = Theme.of(context).colorScheme.primary;
final errorContent = SizedBox(
height: 400,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset("assets/images/error.svg", height: 200),
const SizedBox(height: 30),
Text(
errMsg ?? '请求异常',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 20),
if (isShowBtn)
FilledButton.tonal(
onPressed: () => fn?.call(),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((states) {
return primary.withAlpha(20);
}),
),
],
),
child: Text(btnText ?? '点击重试', style: TextStyle(color: primary)),
),
],
),
);
if (isInSliver) {
return SliverToBoxAdapter(child: errorContent);
} else {
return Center(child: errorContent);
}
}
}

View File

@ -20,6 +20,7 @@ class NetworkImgLayer extends StatelessWidget {
// 图片质量 默认1%
this.quality,
this.origAspectRatio,
this.radius,
});
final String? src;
@ -30,6 +31,18 @@ class NetworkImgLayer extends StatelessWidget {
final Duration? fadeInDuration;
final int? quality;
final double? origAspectRatio;
final double? radius;
BorderRadius getBorderRadius(String? type, double? radius) {
return BorderRadius.circular(
radius ??
(type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x),
);
}
@override
Widget build(BuildContext context) {
@ -72,13 +85,7 @@ class NetworkImgLayer extends StatelessWidget {
return src != '' && src != null
? ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(
type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x,
),
borderRadius: getBorderRadius(type, radius),
child: CachedNetworkImage(
imageUrl: imageUrl,
width: width,
@ -107,11 +114,7 @@ class NetworkImgLayer extends StatelessWidget {
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
borderRadius: BorderRadius.circular(type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x),
borderRadius: getBorderRadius(type, radius),
),
child: type == 'bg'
? const SizedBox()

View File

@ -104,7 +104,7 @@ class Api {
// 评论列表
// https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11
static const String replyList = '/x/v2/reply';
static const String replyList = '/x/v2/reply/main';
// 楼中楼
static const String replyReplyList = '/x/v2/reply/reply';

View File

@ -21,7 +21,6 @@ class HtmlHttp {
}
try {
Document rootTree = parse(response.data);
// log(response.data.body.toString());
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.fixed-author-header')!;
@ -52,7 +51,6 @@ class HtmlHttp {
.className
.split(' ')[1]
.split('-')[2];
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
return {
'status': true,
'avatar': avatar,
@ -76,20 +74,10 @@ class HtmlHttp {
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.up-left')!;
// 头像
// String avatar =
// authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!;
// print(avatar);
// avatar = 'https:${avatar.split('@')[0]}';
String uname = authorHeader.querySelector('.up-name')!.text.trim();
// 动态详情
Element opusDetail = appDom.querySelector('.article-content')!;
// 发布时间
// String updateTime =
// opusDetail.querySelector('.opus-module-author__pub__text')!.text;
// print(updateTime);
//
String opusContent =
opusDetail.querySelector('#read-article-holder')!.innerHtml;
RegExp digitRegExp = RegExp(r'\d+');

View File

@ -8,7 +8,6 @@ import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
// import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/id_utils.dart';
import '../utils/storage.dart';
@ -171,15 +170,6 @@ class Request {
dio = Dio(options);
/// fix 第三方登录 302重定向 跟iOS代理问题冲突
// ..httpClientAdapter = Http2Adapter(
// ConnectionManager(
// idleTimeout: const Duration(milliseconds: 10000),
// onClientCreate: (_, ClientSetting config) =>
// config.onBadCertificate = (_) => true,
// ),
// );
/// 设置代理
if (enableSystemProxy) {
dio.httpClientAdapter = IOHttpClientAdapter(
@ -247,11 +237,26 @@ class Request {
}
}
/*
* get请求
*/
getWithoutCookie(url, {data}) {
return get(
url,
data: data,
options: Options(
headers: {
'cookie': 'buvid3= ; b_nut= ; sid= ',
'user-agent': headerUa(type: 'pc'),
},
),
);
}
/*
* post请求
*/
post(url, {data, queryParameters, options, cancelToken, extra}) async {
// print('post-data: $data');
Response response;
try {
response = await dio.post(
@ -262,7 +267,6 @@ class Request {
options ?? Options(contentType: Headers.formUrlEncodedContentType),
cancelToken: cancelToken,
);
// print('post success: ${response.data}');
return response;
} on DioException catch (e) {
Response errResponse = Response(
@ -318,7 +322,7 @@ class Request {
}
} else {
headerUa =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15';
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36';
}
return headerUa;
}

View File

@ -3,8 +3,6 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import '../utils/storage.dart';
class ApiInterceptor extends Interceptor {
@override
@ -19,21 +17,7 @@ class ApiInterceptor extends Interceptor {
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
try {
if (response.statusCode == 302) {
final List<String> locations = response.headers['location']!;
if (locations.isNotEmpty) {
if (locations.first.startsWith('https://www.mcbbs.net')) {
final Uri uri = Uri.parse(locations.first);
final String? accessKey = uri.queryParameters['access_key'];
final String? mid = uri.queryParameters['mid'];
try {
Box localCache = GStrorage.localCache;
localCache.put(LocalCacheKey.accessKey,
<String, String?>{'mid': mid, 'value': accessKey});
} catch (_) {}
}
}
}
// 在响应之后处理数据
} catch (err) {
print('ApiInterceptor: $err');
}

View File

@ -278,10 +278,10 @@ class MsgHttp {
'data': MessageLikeModel.fromJson(res.data['data']),
};
} catch (err) {
return {'status': false, 'date': [], 'msg': err.toString()};
return {'status': false, 'data': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import '../models/video/reply/data.dart';
import '../models/video/reply/emote.dart';
import 'api.dart';
@ -6,17 +8,16 @@ import 'init.dart';
class ReplyHttp {
static Future replyList({
required int oid,
required int pageNum,
required String nextOffset,
required int type,
int? ps,
int sort = 1,
}) async {
var res = await Request().get(Api.replyList, data: {
'oid': oid,
'pn': pageNum,
'type': type,
'sort': sort,
'ps': ps ?? 20
'pagination_str': jsonEncode({'offset': nextOffset}),
'mode': sort + 2,
});
if (res.data['code'] == 0) {
return {
@ -52,19 +53,13 @@ class ReplyHttp {
if (res.data['code'] == 0) {
return {
'status': true,
'data': ReplyData.fromJson(res.data['data']),
'data': ReplyReplyData.fromJson(res.data['data']),
};
} else {
Map errMap = {
-400: '请求错误',
-404: '无此项',
12002: '评论区已关闭',
12009: '评论主体的type不合法',
};
return {
'status': false,
'date': [],
'msg': errMap[res.data['code']] ?? '请求异常',
'msg': res.data['message'],
};
}
}

View File

@ -1,10 +1,6 @@
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';
import '../models/user/fav_folder.dart';
@ -218,25 +214,6 @@ class UserHttp {
}
}
// 获取用户凭证 失效
static Future thirdLogin() async {
var res = await Request().get(
'https://passport.bilibili.com/login/app/third',
data: {
'appkey': Constants.appKey,
'api': Constants.thirdApi,
'sign': Constants.thirdSign,
},
);
try {
if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
Request().get(res.data['data']['confirm_uri']);
}
} catch (err) {
SmartDialog.showNotify(msg: '获取用户凭证: $err', notifyType: NotifyType.error);
}
}
// 清空稍后再看
static Future toViewClear() async {
var res = await Request().post(
@ -283,30 +260,6 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
// // 相互关系查询
// static Future relationSearch(int mid) async {
// Map params = await WbiSign().makSign({
// 'mid': mid,
// 'token': '',
// 'platform': 'web',
// 'web_location': 1550101,
// });
// var res = await Request().get(
// Api.relationSearch,
// data: {
// 'mid': mid,
// 'w_rid': params['w_rid'],
// 'wts': params['wts'],
// },
// );
// if (res.data['code'] == 0) {
// // relation 主动状态
// // 被动状态
// return {'status': true, 'data': res.data['data']};
// } else {
// return {'status': false, 'msg': res.data['message']};
// }
// }
// 搜索历史记录
static Future searchHistory(
@ -436,31 +389,6 @@ class UserHttp {
}
}
// 稍后再看播放全部
// 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);

View File

@ -1,6 +1,7 @@
class ReplyContent {
ReplyContent({
this.message,
this.message2,
this.atNameToMid, // @的用户的mid null
this.members, // 被@的用户List 如果有的话 []
this.emote, // 表情包 如果有的话 null
@ -13,6 +14,7 @@ class ReplyContent {
});
String? message;
String? message2;
Map? atNameToMid;
List<MemberItemModel>? members;
Map? emote;
@ -24,10 +26,17 @@ class ReplyContent {
Map? topicsMeta;
ReplyContent.fromJson(Map<String, dynamic> json) {
message = json['message']
message = message2 = json['message']
.replaceAll('&gt;', '>')
.replaceAll('&#34;', '"')
.replaceAll('&#39;', "'");
.replaceAll('&#39;', "'")
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', ' ');
atNameToMid = json['at_name_to_mid'] ?? {};
members = json['members'] != null
? json['members']
@ -39,8 +48,8 @@ class ReplyContent {
pictures = json['pictures'] ?? [];
vote = json['vote'] ?? {};
richText = json['rich_text'] ?? {};
// 不包含@ 笔记 图片的时候,文字可折叠
isText = atNameToMid!.isEmpty && vote!.isEmpty && pictures!.isEmpty;
// 不包含@ 笔记的时候,文字可折叠
isText = atNameToMid!.isEmpty && vote!.isEmpty;
topicsMeta = json['topics_meta'] ?? {};
}
}

View File

@ -6,6 +6,98 @@ import 'upper.dart';
class ReplyData {
ReplyData({
this.cursor,
this.config,
this.replies,
this.topReplies,
this.upper,
});
ReplyCursor? cursor;
ReplyConfig? config;
late List<ReplyItemModel>? replies;
late List<ReplyItemModel>? topReplies;
ReplyUpper? upper;
ReplyData.fromJson(Map<String, dynamic> json) {
cursor = ReplyCursor.fromJson(json['cursor']);
config = ReplyConfig.fromJson(json['config']);
replies = json['replies'] != null
? json['replies']
.map<ReplyItemModel>(
(item) => ReplyItemModel.fromJson(item, json['upper']['mid']))
.toList()
: [];
topReplies = json['top_replies'] != null
? json['top_replies']
.map<ReplyItemModel>((item) => ReplyItemModel.fromJson(
item, json['upper']['mid'],
isTopStatus: true))
.toList()
: [];
upper = ReplyUpper.fromJson(json['upper']);
}
}
class ReplyCursor {
ReplyCursor({
this.isBegin,
this.prev,
this.next,
this.isEnd,
this.mode,
this.modeText,
this.allCount,
this.supportMode,
this.name,
this.paginationReply,
this.sessionId,
});
bool? isBegin;
int? prev;
int? next;
bool? isEnd;
int? mode;
String? modeText;
int? allCount;
List<int>? supportMode;
String? name;
PaginationReply? paginationReply;
String? sessionId;
ReplyCursor.fromJson(Map<String, dynamic> json) {
isBegin = json['is_begin'];
prev = json['prev'];
next = json['next'];
isEnd = json['is_end'];
mode = json['mode'];
modeText = json['mode_text'];
allCount = json['all_count'];
supportMode = json['support_mode'].cast<int>();
name = json['name'];
paginationReply = json['pagination_reply'] != null
? PaginationReply.fromJson(json['pagination_reply'])
: null;
sessionId = json['session_id'];
}
}
class PaginationReply {
PaginationReply({
this.nextOffset,
this.prevOffset,
});
String? nextOffset;
String? prevOffset;
PaginationReply.fromJson(Map<String, dynamic> json) {
nextOffset = json['next_offset'];
prevOffset = json['prev_offset'];
}
}
class ReplyReplyData {
ReplyReplyData({
this.page,
this.config,
this.replies,
@ -19,7 +111,7 @@ class ReplyData {
late List<ReplyItemModel>? topReplies;
ReplyUpper? upper;
ReplyData.fromJson(Map<String, dynamic> json) {
ReplyReplyData.fromJson(Map<String, dynamic> json) {
page = ReplyPage.fromJson(json['page']);
config = ReplyConfig.fromJson(json['config']);
replies = json['replies'] != null

View File

@ -183,8 +183,10 @@ class _BangumiPageState extends State<BangumiPage>
return HttpError(
errMsg: data['msg'],
fn: () {
_futureBuilderFuture =
_bangumidController.queryBangumiListFeed();
setState(() {
_futureBuilderFuture =
_bangumidController.queryBangumiListFeed();
});
},
);
}

View File

@ -77,10 +77,10 @@ class _BlackListPageState extends State<BlackListPage> {
List<BlackListItem> list = _blackListController.blackList;
return Obx(
() => list.isEmpty
? CustomScrollView(
slivers: [
HttpError(errMsg: '你没有拉黑任何人哦_', fn: () => {})
],
? HttpError(
errMsg: '你没有拉黑任何人哦_',
fn: () => {},
isInSliver: false,
)
: ListView.builder(
controller: scrollController,
@ -119,13 +119,10 @@ class _BlackListPageState extends State<BlackListPage> {
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => _blackListController.queryBlacklist(),
)
],
return HttpError(
errMsg: data['msg'],
fn: () => _blackListController.queryBlacklist(),
isInSliver: false,
);
}
} else {

View File

@ -14,7 +14,7 @@ class DynamicDetailController extends GetxController {
int? type;
dynamic item;
int? floor;
int currentPage = 0;
String nextOffset = "";
bool isLoadingMore = false;
RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
@ -49,25 +49,25 @@ class DynamicDetailController extends GetxController {
Future queryReplyList({reqType = 'init'}) async {
if (reqType == 'init') {
currentPage = 0;
nextOffset = "";
}
var res = await ReplyHttp.replyList(
oid: oid!,
pageNum: currentPage + 1,
nextOffset: nextOffset,
type: type!,
sort: _sortType.index,
);
if (res['status']) {
List<ReplyItemModel> replies = res['data'].replies;
acount.value = res['data'].page.acount;
acount.value = res['data'].cursor.allCount;
nextOffset = res['data'].cursor.paginationReply.nextOffset ?? "";
if (replies.isNotEmpty) {
currentPage++;
noMore.value = '加载中...';
if (replies.length < 20) {
if (res['data'].cursor.isEnd == true) {
noMore.value = '没有更多了';
}
} else {
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
noMore.value = nextOffset == "" ? '还没有评论' : '没有更多了';
}
if (reqType == 'init') {
// 添加置顶回复

View File

@ -103,17 +103,14 @@ class _FansPageState extends State<FansPage> {
),
);
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data['msg'],
fn: () {
_futureBuilderFuture =
_fansController.queryFans('init');
},
)
],
return HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_futureBuilderFuture = _fansController.queryFans('init');
});
},
isInSliver: false,
);
}
} else {

View File

@ -112,23 +112,19 @@ class _FavPageState extends State<FavPage> {
),
);
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture = _favController.queryFavFolder();
});
}
},
),
],
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture = _favController.queryFavFolder();
});
}
},
isInSliver: false,
);
}
} else {

View File

@ -94,13 +94,14 @@ class _FollowListState extends State<FollowList> {
: const CustomScrollView(slivers: [NoData()]),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => widget.ctr.queryFollowings('init'),
)
],
return HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_futureBuilderFuture = widget.ctr.queryFollowings('init');
});
},
isInSliver: false,
);
}
} else {

View File

@ -112,13 +112,10 @@ class _OwnerFollowListState extends State<OwnerFollowList>
: const CustomScrollView(slivers: [NoData()]),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => widget.ctr.queryFollowings('init'),
)
],
return HttpError(
errMsg: data['msg'],
fn: () => widget.ctr.queryFollowings('init'),
isInSliver: false,
);
}
} else {

View File

@ -82,10 +82,10 @@ class _FollowSearchPageState extends State<FollowSearchPage> {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data == null) {
return CustomScrollView(
slivers: [
HttpError(errMsg: snapshot.data['msg'], fn: reRequest)
],
return HttpError(
errMsg: snapshot.data['msg'],
fn: reRequest,
isInSliver: false,
);
}
if (data['status']) {
@ -101,15 +101,17 @@ class _FollowSearchPageState extends State<FollowSearchPage> {
);
}),
)
: CustomScrollView(
slivers: [HttpError(errMsg: '未搜索到结果', fn: reRequest)],
: HttpError(
errMsg: '未搜索到结果',
fn: reRequest,
isInSliver: false,
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(errMsg: snapshot.data['msg'], fn: reRequest)
],
return HttpError(
errMsg: snapshot.data['msg'],
fn: reRequest,
isInSliver: false,
);
}
} else {

View File

@ -15,7 +15,7 @@ class HtmlRenderController extends GetxController {
RxInt oid = (-1).obs;
late Map response;
int? floor;
int currentPage = 0;
String nextOffset = "";
bool isLoadingMore = false;
RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
@ -52,21 +52,21 @@ class HtmlRenderController extends GetxController {
Future queryReplyList({reqType = 'init'}) async {
var res = await ReplyHttp.replyList(
oid: oid.value,
pageNum: currentPage + 1,
nextOffset: nextOffset,
type: type,
sort: _sortType.index,
);
if (res['status']) {
List<ReplyItemModel> replies = res['data'].replies;
acount.value = res['data'].page.acount;
acount.value = res['data'].cursor.allCount;
nextOffset = res['data'].cursor.paginationReply.nextOffset ?? "";
if (replies.isNotEmpty) {
currentPage++;
noMore.value = '加载中...';
if (replies.length < 20) {
if (res['data'].cursor.isEnd == true) {
noMore.value = '没有更多了';
}
} else {
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
noMore.value = nextOffset == "" ? '还没有评论' : '没有更多了';
}
if (reqType == 'init') {
// 添加置顶回复
@ -102,7 +102,7 @@ class HtmlRenderController extends GetxController {
}
sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels;
currentPage = 0;
nextOffset = "";
replyList.clear();
queryReplyList(reqType: 'init');
}

View File

@ -380,13 +380,10 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
isInSliver: false,
);
}
} else {

View File

@ -22,11 +22,11 @@ class LaterController extends GetxController {
userInfo = userInfoCache.get('userInfoCache');
}
Future queryLaterList() async {
Future queryLaterList({type = 'init'}) async {
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
isLoading.value = true;
isLoading.value = type == 'init';
var res = await UserHttp.seeYouLater();
if (res['status']) {
count = res['data']['count'];

View File

@ -66,67 +66,74 @@ class _LaterPageState extends State<LaterPage> {
const SizedBox(width: 8),
],
),
body: CustomScrollView(
controller: _laterController.scrollController,
slivers: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
return Obx(
() => _laterController.laterList.isNotEmpty &&
!_laterController.isLoading.value
? SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
var videoItem = _laterController.laterList[index];
return VideoCardH(
videoItem: videoItem,
source: 'later',
onPressedFn: () => _laterController.toViewDel(
aid: videoItem.aid));
}, childCount: _laterController.laterList.length),
)
: _laterController.isLoading.value
? const SliverToBoxAdapter(
child: Center(child: Text('加载中')),
)
: const NoData(),
);
body: RefreshIndicator(
onRefresh: () async {
await _laterController.queryLaterList(type: 'onRefresh');
},
child: CustomScrollView(
controller: _laterController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
return Obx(
() => _laterController.laterList.isNotEmpty &&
!_laterController.isLoading.value
? SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
var videoItem =
_laterController.laterList[index];
return VideoCardH(
videoItem: videoItem,
source: 'later',
onPressedFn: () => _laterController
.toViewDel(aid: videoItem.aid));
}, childCount: _laterController.laterList.length),
)
: _laterController.isLoading.value
? const SliverToBoxAdapter(
child: Center(child: Text('加载中')),
)
: const NoData(),
);
} else {
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_laterController.queryLaterList();
});
}
},
);
}
} else {
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_laterController.queryLaterList();
});
}
},
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
);
}
},
),
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).padding.bottom + 10,
},
),
)
],
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).padding.bottom + 10,
),
)
],
),
),
floatingActionButton: Obx(
() => _laterController.laterList.isNotEmpty

View File

@ -11,6 +11,7 @@ import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/rank/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/storage.dart';
import './controller.dart';
@ -126,6 +127,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
double sheetHeight = MediaQuery.sizeOf(context).height -
MediaQuery.of(context).padding.top -
MediaQuery.sizeOf(context).width * 9 / 16;
GlobalDataCache().sheetHeight = sheetHeight;
localCache.put('sheetHeight', sheetHeight);
localCache.put('statusBarHeight', statusBarHeight);

View File

@ -138,16 +138,10 @@ class _MemberArticlePageState extends State<MemberArticlePage> {
}
Widget _buildError(String errMsg) {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: HttpError(
errMsg: errMsg,
fn: () {},
),
),
],
return HttpError(
errMsg: errMsg,
fn: () {},
isInSliver: false,
);
}

View File

@ -164,13 +164,10 @@ class _MemberSearchPageState extends State<MemberSearchPage>
),
);
} else {
return CustomScrollView(
slivers: <Widget>[
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
isInSliver: false,
);
}
} else {

View File

@ -85,18 +85,14 @@ class _MessageLikePageState extends State<MessageLikePage> {
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageLikeCtr.queryMessageLike();
});
},
)
],
return HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture = _messageLikeCtr.queryMessageLike();
});
},
isInSliver: false,
);
}
} else {

View File

@ -82,18 +82,15 @@ class _MessageReplyPageState extends State<MessageReplyPage> {
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageReplyCtr.queryMessageReply();
});
},
)
],
return HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageReplyCtr.queryMessageReply();
});
},
isInSliver: false,
);
}
} else {

View File

@ -63,18 +63,15 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageSystemCtr.queryMessageSystem();
});
},
)
],
return HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageSystemCtr.queryMessageSystem();
});
},
isInSliver: false,
);
}
} else {

View File

@ -1,10 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/storage.dart';
class SSearchController extends GetxController {
@ -12,7 +15,7 @@ class SSearchController extends GetxController {
RxString searchKeyWord = ''.obs;
Rx<TextEditingController> controller = TextEditingController().obs;
RxList<HotSearchItem> hotSearchList = <HotSearchItem>[].obs;
Box histiryWord = GStrorage.historyword;
Box localCache = GStrorage.localCache;
List historyCacheList = [];
RxList historyList = [].obs;
RxList<SearchSuggestItem> searchSuggestList = <SearchSuggestItem>[].obs;
@ -22,48 +25,55 @@ class SSearchController extends GetxController {
RxString defaultSearch = ''.obs;
Box setting = GStrorage.setting;
bool enableHotKey = true;
bool enableSearchSuggest = true;
late StreamController<bool> clearStream = StreamController<bool>.broadcast();
@override
void onInit() {
super.onInit();
// 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) {
if (Get.parameters['keyword'] != null) {
onClickKeyword(Get.parameters['keyword']!);
final parameters = Get.parameters;
if (parameters.keys.isNotEmpty) {
final keyword = parameters['keyword'];
if (keyword != null) {
onClickKeyword(keyword);
}
if (Get.parameters['hintText'] != null) {
hintText = Get.parameters['hintText']!;
final hint = parameters['hintText'];
if (hint != null) {
hintText = hint;
searchKeyWord.value = hintText;
}
}
historyCacheList = histiryWord.get('cacheList') ?? [];
historyCacheList = GlobalDataCache().historyCacheList;
historyList.value = historyCacheList;
enableHotKey = setting.get(SettingBoxKey.enableHotKey, defaultValue: true);
enableSearchSuggest = GlobalDataCache().enableSearchSuggest;
}
void onChange(value) {
searchKeyWord.value = value;
if (value == '') {
searchSuggestList.value = [];
clearStream.add(false);
return;
}
_debouncer.call(() => querySearchSuggest(value));
clearStream.add(true);
if (enableSearchSuggest) {
_debouncer.call(() => querySearchSuggest(value));
}
}
void onClear() {
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
controller.value.clear();
searchKeyWord.value = '';
searchSuggestList.value = [];
} else {
Get.back();
}
controller.value.clear();
searchKeyWord.value = '';
searchSuggestList.value = [];
clearStream.add(false);
}
// 搜索
void submit() {
// ignore: unrelated_type_equality_checks
if (searchKeyWord == '') {
if (searchKeyWord.value == '') {
return;
}
List arr = historyCacheList.where((e) => e != searchKeyWord.value).toList();
@ -73,7 +83,7 @@ class SSearchController extends GetxController {
historyList.value = historyCacheList;
// 手动刷新
historyList.refresh();
histiryWord.put('cacheList', historyCacheList);
localCache.put('cacheList', historyCacheList);
searchFocusNode.unfocus();
Get.toNamed('/searchResult', parameters: {'keyword': searchKeyWord.value});
}
@ -117,13 +127,14 @@ class SSearchController extends GetxController {
int index = historyList.indexOf(word);
historyList.removeAt(index);
historyList.refresh();
histiryWord.put('cacheList', historyList);
localCache.put('cacheList', historyList);
}
onClearHis() {
historyList.value = [];
historyCacheList = [];
historyList.refresh();
histiryWord.put('cacheList', []);
localCache.put('cacheList', []);
SmartDialog.showToast('搜索历史已清空');
}
}

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
@ -54,7 +53,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
actions: [
IconButton(
onPressed: () => _searchController.submit(),
icon: const Icon(CupertinoIcons.search, size: 22),
icon: const Icon(Icons.search),
),
const SizedBox(width: 10)
],
@ -68,13 +67,19 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
decoration: InputDecoration(
hintText: _searchController.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _searchController.onClear(),
suffixIcon: StreamBuilder(
initialData: false,
stream: _searchController.clearStream.stream,
builder: (_, snapshot) {
if (snapshot.data == true) {
return IconButton(
icon: const Icon(Icons.clear, size: 22),
onPressed: () => _searchController.onClear(),
);
} else {
return const SizedBox();
}
},
),
),
onSubmitted: (String value) => _searchController.submit(),
@ -84,7 +89,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 12),
const SizedBox(height: 6),
// 搜索建议
_searchSuggest(),
// 热搜
@ -135,7 +140,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
padding: const EdgeInsets.fromLTRB(6, 0, 6, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -153,7 +158,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
padding: MaterialStateProperty.all(const EdgeInsets.only(
left: 10, top: 6, bottom: 6, right: 10)),
),
onPressed: () => ctr.queryHotSearchList(),
onPressed: ctr.queryHotSearchList,
icon: const Icon(Icons.refresh_outlined, size: 18),
label: const Text('刷新'),
),
@ -187,13 +192,10 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
isInSliver: false,
);
}
} else {
@ -202,6 +204,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
return HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList,
onClick: () {},
);
} else {
return const SizedBox();
@ -220,13 +223,13 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
return Obx(
() => Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(10, 25, 6, 0),
padding: const EdgeInsets.fromLTRB(10, 20, 4, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_searchController.historyList.isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 0, 2),
padding: const EdgeInsets.fromLTRB(6, 0, 6, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -237,10 +240,19 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
TextButton(
onPressed: () => _searchController.onClearHis(),
child: const Text('清空'),
)
SizedBox(
height: 34,
child: TextButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all(
const EdgeInsets.only(
left: 10, top: 6, bottom: 6, right: 10)),
),
onPressed: _searchController.onClearHis,
icon: const Icon(Icons.clear_all_outlined, size: 18),
label: const Text('清空'),
),
),
],
),
),

View File

@ -1,15 +1,15 @@
// ignore: file_names
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
class HotKeyword extends StatelessWidget {
final double? width;
final List? hotSearchList;
final Function? onClick;
final double width;
final List hotSearchList;
final Function onClick;
const HotKeyword({
this.width,
this.hotSearchList,
this.onClick,
required this.width,
required this.hotSearchList,
required this.onClick,
super.key,
});
@ -18,45 +18,67 @@ class HotKeyword extends StatelessWidget {
return Wrap(
runSpacing: 0.4,
spacing: 5.0,
children: [
for (var i in hotSearchList!)
SizedBox(
width: width! / 2 - 4,
child: Material(
borderRadius: BorderRadius.circular(3),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onClick!(i.keyword),
child: Padding(
padding: EdgeInsets.only(
left: 2,
right: hotSearchList!.indexOf(i) % 2 == 1 ? 10 : 0),
child: Row(
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.fromLTRB(6, 5, 4, 5),
child: Text(
i.keyword!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(fontSize: 14),
),
),
),
if (i.icon != null && i.icon != '')
SizedBox(
height: 15,
child: CachedNetworkImage(
imageUrl: i.icon!, height: 15.0),
),
],
),
),
),
),
),
],
children: hotSearchList.map((item) {
return HotKeywordItem(
width: width,
item: item,
onClick: onClick,
isRightPadding: hotSearchList.indexOf(item) % 2 == 1,
);
}).toList(),
);
}
}
class HotKeywordItem extends StatelessWidget {
final double width;
final dynamic item;
final Function onClick;
final bool isRightPadding;
const HotKeywordItem({
required this.width,
required this.item,
required this.onClick,
required this.isRightPadding,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width / 2 - 4,
child: Material(
borderRadius: BorderRadius.circular(4),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onClick.call(item.keyword),
child: Padding(
padding: EdgeInsets.only(left: 2, right: isRightPadding ? 10 : 0),
child: Row(
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.fromLTRB(6, 5, 4, 5),
child: Text(
item.keyword,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(fontSize: 14),
),
),
),
if (item.icon != null && item.icon != '')
SizedBox(
height: 15,
child:
CachedNetworkImage(imageUrl: item.icon!, height: 15.0),
),
],
),
),
),
),
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:pilipala/utils/feed_back.dart';
class SearchText extends StatelessWidget {
final String? searchText;
@ -17,30 +18,31 @@ class SearchText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Material(
color: isSelect
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
? colorScheme.primaryContainer
: colorScheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
child: Padding(
padding: EdgeInsets.zero,
child: InkWell(
onTap: () {
onSelect!(searchText);
onSelect?.call(searchText);
},
onLongPress: () {
onLongSelect!(searchText);
feedBack();
onLongSelect?.call(searchText);
},
borderRadius: BorderRadius.circular(6),
child: Padding(
padding:
const EdgeInsets.only(top: 5, bottom: 5, left: 11, right: 11),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 11),
child: Text(
searchText!,
style: TextStyle(
color: isSelect
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
),
),

View File

@ -109,33 +109,25 @@ class _SearchPanelState extends State<SearchPanel>
}
});
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
),
],
return HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
isInSliver: false,
);
}
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: '没有相关数据',
fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
),
],
return HttpError(
errMsg: '没有相关数据',
fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
isInSliver: false,
);
}
} else {

View File

@ -174,14 +174,11 @@ Widget searchArticlePanel(BuildContext context, ctr, list) {
);
},
)
: CustomScrollView(
slivers: [
HttpError(
errMsg: '没有数据',
isShowBtn: false,
fn: () => {},
)
],
: HttpError(
errMsg: '没有数据',
isShowBtn: false,
fn: () => {},
isInSliver: false,
),
);
}

View File

@ -46,14 +46,11 @@ class SearchVideoPanel extends StatelessWidget {
);
},
)
: CustomScrollView(
slivers: [
HttpError(
errMsg: '没有数据',
isShowBtn: false,
fn: () => {},
)
],
: HttpError(
errMsg: '没有数据',
isShowBtn: false,
fn: () => {},
isInSliver: false,
),
),
// 分类筛选

View File

@ -5,6 +5,7 @@ import 'package:hive/hive.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/reply_sort_type.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/storage.dart';
import '../home/index.dart';
@ -146,6 +147,15 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableHotKey,
defaultVal: true,
),
SetSwitchItem(
title: '展示搜索建议',
subTitle: '输入搜索内容时展示建议词',
setKey: SettingBoxKey.enableSearchSuggest,
defaultVal: true,
callFn: (val) {
GlobalDataCache().enableSearchSuggest = val;
},
),
SetSwitchItem(
title: '搜索默认词',
subTitle: '是否展示搜索框默认词',

View File

@ -68,30 +68,27 @@ class _SubPageState extends State<SubPage> {
),
);
} else {
return const CustomScrollView(
physics: NeverScrollableScrollPhysics(),
slivers: [HttpError(errMsg: '', btnText: '没有数据', fn: null)],
return const HttpError(
errMsg: '',
btnText: '没有数据',
fn: null,
isInSliver: false,
);
}
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_subController.querySubFolder();
});
}
},
),
],
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture = _subController.querySubFolder();
});
}
},
isInSliver: false,
);
}
} else {

View File

@ -242,6 +242,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
showBottomSheet(
context: context,
enableDrag: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(25),
topRight: Radius.circular(25),
),
),
builder: (BuildContext context) {
return AiDetail(modelResult: videoIntroController.modelResult);
},

View File

@ -21,11 +21,9 @@ class VideoReplyController extends GetxController {
// rpid 请求楼中楼回复
String? rpid;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
// 当前页
int currentPage = 0;
String nextOffset = "";
bool isLoadingMore = false;
RxString noMore = ''.obs;
int ps = 20;
RxInt count = 0.obs;
// 当前回复的回复
ReplyItemModel? currentReplyItem;
@ -57,7 +55,7 @@ class VideoReplyController extends GetxController {
}
isLoadingMore = true;
if (type == 'init') {
currentPage = 0;
nextOffset = '';
noMore.value = '';
}
if (noMore.value == '没有更多了') {
@ -66,28 +64,20 @@ class VideoReplyController extends GetxController {
}
final res = await ReplyHttp.replyList(
oid: aid!,
pageNum: currentPage + 1,
ps: ps,
nextOffset: nextOffset,
type: ReplyType.video.index,
sort: _sortType.index,
);
if (res['status']) {
final List<ReplyItemModel> replies = res['data'].replies;
nextOffset = res['data'].cursor.paginationReply.nextOffset ?? "";
if (replies.isNotEmpty) {
noMore.value = '加载中...';
/// 第一页回复数小于20
if (currentPage == 0 && replies.length < 18) {
noMore.value = '没有更多了';
}
currentPage++;
if (replyList.length == res['data'].page.acount) {
if (res['data'].cursor.isEnd == true) {
noMore.value = '没有更多了';
}
} else {
// 未登录状态replies可能返回null
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
noMore.value = nextOffset == "" ? '还没有评论' : '没有更多了';
}
if (type == 'init') {
// 添加置顶回复
@ -99,7 +89,7 @@ class VideoReplyController extends GetxController {
}
}
replies.insertAll(0, res['data'].topReplies);
count.value = res['data'].page.count;
count.value = res['data'].cursor.allCount;
replyList.value = replies;
} else {
replyList.addAll(replies);
@ -130,7 +120,7 @@ class VideoReplyController extends GetxController {
}
sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels;
currentPage = 0;
nextOffset = "";
noMore.value = '';
replyList.clear();
queryReplyList(type: 'init');

View File

@ -238,28 +238,53 @@ class ReplyItem extends StatelessWidget {
// title
Container(
margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4),
child: Text.rich(
style: const TextStyle(height: 1.75),
maxLines:
replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999,
overflow: TextOverflow.ellipsis,
TextSpan(
children: [
if (replyItem!.isTop!)
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'TOP',
size: 'small',
stack: 'normal',
type: 'line',
fs: 9,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
String text = replyItem?.content?.message ?? '';
bool didExceedMaxLines = false;
final double maxWidth = boxConstraints.maxWidth;
TextPainter? textPainter;
final int maxLines =
replyItem!.content!.isText! && replyLevel == '1' ? 6 : 999;
try {
textPainter = TextPainter(
text: TextSpan(text: text),
maxLines: maxLines,
textDirection: Directionality.of(context),
);
textPainter.layout(maxWidth: maxWidth);
didExceedMaxLines = textPainter.didExceedMaxLines;
} catch (e) {
debugPrint('Error while measuring text: $e');
didExceedMaxLines = false;
}
return Text.rich(
style: const TextStyle(height: 1.75),
TextSpan(
children: [
if (replyItem!.isTop!)
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'TOP',
size: 'small',
stack: 'normal',
type: 'line',
fs: 9,
),
),
buildContent(
context,
replyItem!,
replyReply,
null,
didExceedMaxLines,
textPainter,
),
buildContent(context, replyItem!, replyReply, null),
],
),
),
],
),
);
}),
),
// 操作区域
bottonAction(context, replyItem!.replyControl, replySave),
@ -465,8 +490,8 @@ class ReplyItemRow extends StatelessWidget {
fs: 9,
),
),
buildContent(
context, replies![i], replyReply, replyItem),
buildContent(context, replies![i], replyReply,
replyItem, false, null),
],
),
),
@ -508,7 +533,13 @@ class ReplyItemRow extends StatelessWidget {
}
InlineSpan buildContent(
BuildContext context, replyItem, replyReply, fReplyItem) {
BuildContext context,
replyItem,
replyReply,
fReplyItem,
bool didExceedMaxLines,
TextPainter? textPainter,
) {
final String routePath = Get.currentRoute;
bool isVideoPage = routePath.startsWith('/video');
ColorScheme colorScheme = Theme.of(context).colorScheme;
@ -519,6 +550,25 @@ InlineSpan buildContent(
final content = replyItem.content;
final List<InlineSpan> spanChilds = <InlineSpan>[];
if (didExceedMaxLines && content.message != '') {
final textSize = textPainter!.size;
var position = textPainter.getPositionForOffset(
Offset(
textSize.width,
textSize.height,
),
);
final endOffset = textPainter.getOffsetBefore(position.offset);
if (endOffset != null && endOffset > 0) {
content.message = content.message.substring(0, endOffset);
} else {
content.message = content.message.substring(0, position.offset);
}
} else {
content.message = content.message2;
}
// 投票
if (content.vote.isNotEmpty) {
content.message.splitMapJoin(RegExp(r"\{vote:.*?\}"),
@ -547,13 +597,6 @@ InlineSpan buildContent(
});
}
content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
content.message = content.message
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', ' ');
// 构建正则表达式
final List<String> specialTokens = [
...content.emote.keys,
@ -874,6 +917,18 @@ InlineSpan buildContent(
}
}
}
if (didExceedMaxLines) {
spanChilds.add(
TextSpan(
text: '\n查看更多',
style: TextStyle(
color: colorScheme.primary,
),
),
);
}
// 图片渲染
if (content.pictures.isNotEmpty) {
final List<String> picList = <String>[];

View File

@ -1,16 +1,11 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/ai.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/utils.dart';
Box localCache = GStrorage.localCache;
late double sheetHeight;
class AiDetail extends StatelessWidget {
final ModelResult? modelResult;
@ -21,124 +16,21 @@ class AiDetail extends StatelessWidget {
@override
Widget build(BuildContext context) {
sheetHeight = localCache.get('sheetHeight');
return Container(
color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.only(left: 14, right: 14),
height: sheetHeight,
padding: const EdgeInsets.only(left: 16, right: 16),
height: GlobalDataCache().sheetHeight,
child: Column(
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: const BorderRadius.all(Radius.circular(3)),
),
),
),
),
),
_buildHeader(context),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
if (modelResult!.resultType != 0 &&
modelResult!.summary != '') ...[
SelectableText(
modelResult!.summary!,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
if (modelResult!.summary != '') ...[
_buildSummaryText(modelResult!.summary!),
const SizedBox(height: 20),
],
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: modelResult!.outline!.length,
itemBuilder: (context, index) {
final outline = modelResult!.outline![index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
outline.title!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
const SizedBox(height: 6),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: outline.partOutline!.length,
itemBuilder: (context, i) {
final part = outline.partOutline![i];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
try {
final controller =
Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'],
);
controller.plPlayerController.seekTo(
Duration(
seconds: Utils.duration(
Utils.tampToSeektime(
part.timestamp!),
).toInt(),
),
);
} catch (_) {}
},
child: SelectableText.rich(
TextSpan(
style: TextStyle(
fontSize: 13,
color: Theme.of(context)
.colorScheme
.onSurface,
height: 1.5,
),
children: [
TextSpan(
text: Utils.tampToSeektime(
part.timestamp!),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
const TextSpan(text: ' '),
TextSpan(text: part.content!),
],
),
),
),
const SizedBox(height: 20),
],
);
},
),
],
);
},
)
_buildOutlineList(context),
],
),
),
@ -148,77 +40,113 @@ class AiDetail extends StatelessWidget {
);
}
InlineSpan buildContent(BuildContext context, content) {
List descV2 = content.descV2;
// type
// 1 普通文本
// 2 @用户
List<TextSpan> spanChilds = List.generate(descV2.length, (index) {
final currentDesc = descV2[index];
switch (currentDesc.type) {
case 1:
List<InlineSpan> spanChildren = [];
RegExp urlRegExp = RegExp(r'https?://\S+\b');
Iterable<Match> matches = urlRegExp.allMatches(currentDesc.rawText);
Widget _buildHeader(BuildContext context) {
return Center(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).hintColor,
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
height: 4,
width: 40,
margin: const EdgeInsets.symmetric(vertical: 16),
),
);
}
int previousEndIndex = 0;
for (Match match in matches) {
if (match.start > previousEndIndex) {
spanChildren.add(TextSpan(
text: currentDesc.rawText
.substring(previousEndIndex, match.start)));
}
spanChildren.add(
TextSpan(
text: match.group(0),
style: TextStyle(
color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色
recognizer: TapGestureRecognizer()
..onTap = () {
// 处理点击事件
try {
Get.toNamed(
'/webview',
parameters: {
'url': match.group(0)!,
'type': 'url',
'pageTitle': match.group(0)!,
},
);
} catch (err) {
SmartDialog.showToast(err.toString());
}
},
),
);
previousEndIndex = match.end;
}
Widget _buildSummaryText(String summary) {
return SelectableText(
summary,
textAlign: TextAlign.justify,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.6,
),
);
}
if (previousEndIndex < currentDesc.rawText.length) {
spanChildren.add(TextSpan(
text: currentDesc.rawText.substring(previousEndIndex)));
}
Widget _buildOutlineList(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: modelResult!.outline!.length,
itemBuilder: (context, index) {
final outline = modelResult!.outline![index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildOutlineTitle(outline.title!),
const SizedBox(height: 20),
_buildPartOutlineList(context, outline.partOutline!),
],
);
},
);
}
TextSpan result = TextSpan(children: spanChildren);
return result;
case 2:
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
return TextSpan(
text: '@${currentDesc.rawText}',
style: TextStyle(color: colorSchemePrimary),
Widget _buildOutlineTitle(String title) {
return SelectableText(
title,
textAlign: TextAlign.justify,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
);
}
Widget _buildPartOutlineList(
BuildContext context, List<PartOutline> partOutline) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: partOutline.length,
itemBuilder: (context, i) {
final part = partOutline[i];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPartText(context, part),
const SizedBox(height: 20),
],
);
},
);
}
void _onPartTap(BuildContext context, int timestamp) {
try {
final controller = Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'],
);
controller.plPlayerController.seekTo(
Duration(seconds: timestamp),
);
} catch (_) {}
}
Widget _buildPartText(BuildContext context, PartOutline part) {
return SelectableText.rich(
TextSpan(
style: TextStyle(
fontSize: 15,
color: Theme.of(context).colorScheme.onSurface,
),
children: [
TextSpan(
text: Utils.tampToSeektime(part.timestamp!),
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/member?mid=${currentDesc.bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
);
default:
return const TextSpan();
}
});
return TextSpan(children: spanChilds);
..onTap = () => _onPartTap(context, part.timestamp!),
),
const TextSpan(text: ' '),
TextSpan(text: part.content!),
],
),
);
}
}

View File

@ -15,6 +15,7 @@ class GlobalDataCache {
late FullScreenGestureMode fullScreenGestureMode;
late bool enablePlayerControlAnimation;
late List<String> actionTypeSort;
late double sheetHeight;
String? wWebid;
/// 播放器相关
@ -44,6 +45,10 @@ class GlobalDataCache {
late List<double> speedsList;
// 用户信息
UserInfoData? userInfo;
// 搜索历史
late List historyCacheList;
//
late bool enableSearchSuggest = true;
// 私有构造函数
GlobalDataCache._();
@ -103,5 +108,9 @@ class GlobalDataCache {
speedsList.addAll(playSpeedSystem);
userInfo = userInfoCache.get('userInfoCache');
sheetHeight = localCache.get('sheetHeight', defaultValue: 0.0);
historyCacheList = localCache.get('cacheList', defaultValue: []);
enableSearchSuggest =
setting.get(SettingBoxKey.enableSearchSuggest, defaultValue: true);
}
}

View File

@ -5,7 +5,6 @@ import 'package:pilipala/models/user/info.dart';
class GStrorage {
static late final Box<dynamic> userInfo;
static late final Box<dynamic> historyword;
static late final Box<dynamic> localCache;
static late final Box<dynamic> setting;
static late final Box<dynamic> video;
@ -26,18 +25,11 @@ class GStrorage {
localCache = await Hive.openBox(
'localCache',
compactionStrategy: (int entries, int deletedEntries) {
return deletedEntries > 4;
return deletedEntries > 10;
},
);
// 设置
setting = await Hive.openBox('setting');
// 搜索历史
historyword = await Hive.openBox(
'historyWord',
compactionStrategy: (int entries, int deletedEntries) {
return deletedEntries > 10;
},
);
// 视频设置
video = await Hive.openBox('video');
}
@ -52,8 +44,6 @@ class GStrorage {
// user.close();
userInfo.compact();
userInfo.close();
historyword.compact();
historyword.close();
localCache.compact();
localCache.close();
setting.compact();
@ -117,6 +107,7 @@ class SettingBoxKey {
replySortType = 'replySortType',
defaultDynamicType = 'defaultDynamicType',
enableHotKey = 'enableHotKey',
enableSearchSuggest = 'enableSearchSuggest',
enableQuickFav = 'enableQuickFav',
enableWordRe = 'enableWordRe',
enableSearchWord = 'enableSearchWord',

View File

@ -351,6 +351,9 @@ class Utils {
// 时间戳转时间
static tampToSeektime(number) {
if (number is String && int.tryParse(number) == null) {
return number;
}
int hours = number ~/ 60;
int minutes = number % 60;