diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index be2b2144..c1865fd5 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -40,9 +40,13 @@
android:label="PiliPala"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
- android:enableOnBackInvokedCallback="true">
+ xmlns:tools="http://schemas.android.com/tools"
+ android:enableOnBackInvokedCallback="true"
+ android:allowBackup="false"
+ android:fullBackupContent="false"
+ tools:replace="android:allowBackup">
+
+
+
+
+
+
+
+
+
+
+
+
+
+=======
+>>>>>>> main
UIBackgroundModes
audio
diff --git a/lib/common/widgets/html_render.dart b/lib/common/widgets/html_render.dart
new file mode 100644
index 00000000..2e97ceed
--- /dev/null
+++ b/lib/common/widgets/html_render.dart
@@ -0,0 +1,96 @@
+import 'package:get/get.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_html/flutter_html.dart';
+import 'package:pilipala/common/widgets/network_img_layer.dart';
+
+// ignore: must_be_immutable
+class HtmlRender extends StatelessWidget {
+ String? htmlContent;
+ final int? imgCount;
+ final List? imgList;
+
+ HtmlRender({
+ this.htmlContent,
+ this.imgCount,
+ this.imgList,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Html(
+ data: htmlContent,
+ onLinkTap: (url, buildContext, attributes) => {},
+ extensions: [
+ TagExtension(
+ tagsToExtend: {"img"},
+ builder: (extensionContext) {
+ try {
+ Map attributes = extensionContext.attributes;
+ List key = attributes.keys.toList();
+ String? imgUrl = key.contains('src')
+ ? attributes['src']
+ : attributes['data-src'];
+ if (imgUrl!.startsWith('//')) {
+ imgUrl = 'https:$imgUrl';
+ }
+ if (imgUrl.startsWith('http://')) {
+ imgUrl = imgUrl.replaceAll('http://', 'https://');
+ }
+ imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;
+ bool isEmote = imgUrl.contains('/emote/');
+ bool isMall = imgUrl.contains('/mall/');
+ 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,
+ );
+ } catch (err) {
+ print(err);
+ return const SizedBox();
+ }
+ },
+ ),
+ ],
+ style: {
+ "html": Style(
+ fontSize: FontSize.medium,
+ lineHeight: LineHeight.percent(140),
+ ),
+ "body": Style(margin: Margins.zero, padding: HtmlPaddings.zero),
+ "a": Style(
+ color: Theme.of(context).colorScheme.primary,
+ textDecoration: TextDecoration.none,
+ ),
+ "p": Style(
+ margin: Margins.only(bottom: 10),
+ ),
+ "span": Style(
+ fontSize: FontSize.medium,
+ height: Height(1.65),
+ ),
+ "div": Style(height: Height.auto()),
+ "li > p": Style(
+ display: Display.inline,
+ ),
+ "li": Style(
+ padding: HtmlPaddings.only(bottom: 4),
+ textAlign: TextAlign.justify,
+ ),
+ "img": Style(margin: Margins.only(top: 4, bottom: 4)),
+ },
+ );
+ }
+}
diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart
index 02c7c217..39358fda 100644
--- a/lib/common/widgets/video_card_v.dart
+++ b/lib/common/widgets/video_card_v.dart
@@ -5,6 +5,7 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
+import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart';
@@ -27,6 +28,11 @@ class VideoCardV extends StatelessWidget {
this.longPressEnd,
}) : super(key: key);
+ bool isStringNumeric(String str) {
+ RegExp numericRegex = RegExp(r'^\d+$');
+ return numericRegex.hasMatch(str);
+ }
+
void onPushDetail(heroTag) async {
String goto = videoItem.goto;
switch (goto) {
@@ -62,6 +68,47 @@ class VideoCardV extends StatelessWidget {
'heroTag': heroTag,
});
break;
+ // 动态
+ 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;
+ if (isStringNumeric(path.split('/')[1])) {
+ // 请求接口
+ var res =
+ await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);
+ if (res['status']) {
+ Get.toNamed('/dynamicDetail', arguments: {
+ 'item': res['data'],
+ 'floor': 1,
+ 'action': 'detail'
+ });
+ }
+ return;
+ }
+ }
+ Get.toNamed('/htmlRender', parameters: {
+ 'url': uri,
+ 'title': videoItem.title,
+ 'id': id,
+ 'dynamicType': dynamicType
+ });
+ } catch (err) {
+ SmartDialog.showToast(err.toString());
+ }
+ break;
default:
SmartDialog.showToast(videoItem.goto);
Get.toNamed(
@@ -112,12 +159,22 @@ class VideoCardV extends StatelessWidget {
height: maxHeight,
),
),
- if (crossAxisCount == 1 && videoItem.duration != null)
- PBadge(
- bottom: 10,
- right: 10,
- text: videoItem.duration,
- )
+ if (videoItem.duration != null)
+ if (crossAxisCount == 1) ...[
+ PBadge(
+ bottom: 10,
+ right: 10,
+ text: videoItem.duration,
+ )
+ ] else ...[
+ PBadge(
+ bottom: 6,
+ right: 7,
+ size: 'small',
+ type: 'gray',
+ text: videoItem.duration,
+ )
+ ],
],
);
}),
@@ -174,7 +231,7 @@ class VideoContent extends StatelessWidget {
],
),
if (crossAxisCount > 1) ...[
- const SizedBox(height: 3),
+ const SizedBox(height: 2),
VideoStat(
videoItem: videoItem,
),
@@ -247,7 +304,7 @@ class VideoContent extends StatelessWidget {
},
),
] else ...[
- const SizedBox(height: 26)
+ const SizedBox(height: 24)
]
],
),
@@ -268,23 +325,18 @@ class VideoStat extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Row(
- children: [
- Text(
- '${videoItem.stat.view}观看',
- style: TextStyle(
- fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
- color: Theme.of(context).colorScheme.outline,
- ),
+ return RichText(
+ maxLines: 1,
+ text: TextSpan(
+ style: TextStyle(
+ fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
+ color: Theme.of(context).colorScheme.outline,
),
- Text(
- ' • ${videoItem.stat.danmu}弹幕',
- style: TextStyle(
- fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
- color: Theme.of(context).colorScheme.outline,
- ),
- ),
- ],
+ children: [
+ TextSpan(text: '${videoItem.stat.view}观看'),
+ TextSpan(text: ' • ${videoItem.stat.danmu}弹幕'),
+ ],
+ ),
);
}
}
diff --git a/lib/http/api.dart b/lib/http/api.dart
index e1e011eb..042d8e11 100644
--- a/lib/http/api.dart
+++ b/lib/http/api.dart
@@ -97,6 +97,9 @@ class Api {
// 操作用户关系
static const String relationMod = '/x/relation/modify';
+ // 相互关系查询
+ static const String relationSearch = '/x/space/wbi/acc/relation';
+
// 评论列表
// 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';
@@ -126,12 +129,14 @@ class Api {
static const String userFavFolder = '/x/v3/fav/folder/created/list';
/// 收藏夹 详情
- /// media_id int 收藏夹id
+ /// media_id 当前收藏夹id 搜索全部时为默认收藏夹id
/// pn int 当前页
/// ps int pageSize
/// keyword String 搜索词
/// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿
/// tid int 分区id
+ /// platform web
+ /// type 0 当前收藏夹 1 全部收藏夹
// https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0
static const String userFavFolderDetail = '/x/v3/fav/resource/list';
@@ -164,6 +169,12 @@ class Api {
// 清空历史记录
static const String clearHistory = '/x/v2/history/clear';
+ // 删除某条历史记录
+ static const String delHistory = '/x/v2/history/delete';
+
+ // 搜索历史记录
+ static const String searchHistory = '/x/web-goblin/history/search';
+
// 热搜
static const String hotSearchList =
'https://s.search.bilibili.com/main/hotword';
@@ -239,6 +250,9 @@ class Api {
// wts=1689767832
static const String memberArchive = '/x/space/wbi/arc/search';
+ // 用户动态搜索
+ static const String memberDynamicSearch = '/x/space/dynamic/search';
+
// 用户动态
static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';
@@ -285,6 +299,9 @@ class Api {
// 黑名单
static const String blackLst = '/x/relation/blacks';
+ // 移除黑名单
+ static const String removeBlack = '/x/relation/modify';
+
// github 获取最新版
static const String latestApp =
'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
@@ -294,4 +311,65 @@ class Api {
static const String onlineTotal = '/x/player/online/total';
static const String webDanmaku = '/x/v2/dm/web/seg.so';
+
+ // up主分组
+ static const String followUpTag = '/x/relation/tags';
+
+ // 设置Up主分组
+ // 0 添加至默认分组 否则使用,分割tagid
+ static const String addUsers = '/x/relation/tags/addUsers';
+
+ // 获取指定分组下的up
+ static const String followUpGroup = '/x/relation/tag';
+
+ // 获取某个动态详情
+ // timezone_offset=-480
+ // id=849312409672744983
+ // features=itemOpusStyle
+ static const String dynamicDetail = '/x/polymer/web-dynamic/v1/detail';
+
+ // AI总结
+ /// https://api.bilibili.com/x/web-interface/view/conclusion/get?
+ /// bvid=BV1ju4y1s7kn&
+ /// cid=1296086601&
+ /// up_mid=4641697&
+ /// w_rid=1607c6c5a4a35a1297e31992220900ae&
+ /// wts=1697033079
+ static const String aiConclusion = '/x/web-interface/view/conclusion/get';
+
+ // captcha验证码
+ static const String getCaptcha =
+ 'https://passport.bilibili.com/x/passport-login/captcha?source=main_web';
+
+ // web端短信验证码
+ static const String smsCode =
+ 'https://passport.bilibili.com/x/passport-login/web/sms/send';
+
+ // web端验证码登录
+
+ // web端密码登录
+
+ // app端短信验证码
+ static const String appSmsCode =
+ 'https://passport.bilibili.com/x/passport-login/sms/send';
+
+ // app端验证码登录
+
+ // 获取短信验证码
+ // static const String appSafeSmsCode =
+ // 'https://passport.bilibili.com/x/safecenter/common/sms/send';
+
+ /// app端密码登录
+ /// username
+ /// password
+ /// key
+ /// rhash
+ static const String loginInByPwdApi =
+ 'https://passport.bilibili.com/x/passport-login/oauth2/login';
+
+ /// 密码加密密钥
+ /// disable_rcmd
+ /// local_id
+ static const getWebKey =
+ 'https://passport.bilibili.com/x/passport-login/web/key';
}
diff --git a/lib/http/black.dart b/lib/http/black.dart
index 599b088b..81a7c0c9 100644
--- a/lib/http/black.dart
+++ b/lib/http/black.dart
@@ -23,4 +23,31 @@ class BlackHttp {
};
}
}
+
+ // 移除黑名单
+ static Future removeBlack({required int fid}) async {
+ var res = await Request().post(
+ Api.removeBlack,
+ queryParameters: {
+ 'act': 6,
+ 'csrf': await Request.getCsrf(),
+ 'fid': fid,
+ 'jsonp': 'jsonp',
+ 're_src': 116,
+ },
+ );
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': [],
+ 'msg': '操作成功',
+ };
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
}
diff --git a/lib/http/constants.dart b/lib/http/constants.dart
index 1f5319fb..cf10a606 100644
--- a/lib/http/constants.dart
+++ b/lib/http/constants.dart
@@ -2,4 +2,37 @@ class HttpString {
static const String baseUrl = 'https://www.bilibili.com';
static const String baseApiUrl = 'https://api.bilibili.com';
static const String tUrl = 'https://api.vc.bilibili.com';
+ static const List validateStatusCodes = [
+ 302,
+ 304,
+ 307,
+ 400,
+ 401,
+ 403,
+ 404,
+ 405,
+ 409,
+ 412,
+ 500,
+ 503,
+ 504,
+ 509,
+ 616,
+ 617,
+ 625,
+ 626,
+ 628,
+ 629,
+ 632,
+ 643,
+ 650,
+ 652,
+ 658,
+ 662,
+ 688,
+ 689,
+ 701,
+ 799,
+ 8888
+ ];
}
diff --git a/lib/http/danmaku.dart b/lib/http/danmaku.dart
index 020c89ea..87f08d8b 100644
--- a/lib/http/danmaku.dart
+++ b/lib/http/danmaku.dart
@@ -17,17 +17,11 @@ class DanmakaHttp {
'oid': cid,
'segment_index': segmentIndex,
};
-
- // 计算函数
- Future computeTask(Map params) async {
- var response = await Request().get(
- Api.webDanmaku,
- data: params,
- extra: {'resType': ResponseType.bytes},
- );
- return DmSegMobileReply.fromBuffer(response.data);
- }
-
- return await compute(computeTask, params);
+ var response = await Request().get(
+ Api.webDanmaku,
+ data: params,
+ extra: {'resType': ResponseType.bytes},
+ );
+ return DmSegMobileReply.fromBuffer(response.data);
}
}
diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart
index 3d0d2506..7a22ab13 100644
--- a/lib/http/dynamics.dart
+++ b/lib/http/dynamics.dart
@@ -28,6 +28,7 @@ class DynamicsHttp {
'data': DynamicsDataModel.fromJson(res.data['data']),
};
} catch (err) {
+ print(err);
return {
'status': false,
'data': [],
@@ -85,4 +86,35 @@ class DynamicsHttp {
};
}
}
+
+ //
+ static Future dynamicDetail({
+ String? id,
+ }) async {
+ var res = await Request().get(Api.dynamicDetail, data: {
+ 'timezone_offset': -480,
+ 'id': id,
+ 'features': 'itemOpusStyle',
+ });
+ if (res.data['code'] == 0) {
+ try {
+ return {
+ 'status': true,
+ 'data': DynamicItemModel.fromJson(res.data['data']['item']),
+ };
+ } catch (err) {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': err.toString(),
+ };
+ }
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
}
diff --git a/lib/http/html.dart b/lib/http/html.dart
new file mode 100644
index 00000000..41570d0a
--- /dev/null
+++ b/lib/http/html.dart
@@ -0,0 +1,103 @@
+import 'package:html/dom.dart';
+import 'package:html/parser.dart';
+import 'package:pilipala/http/index.dart';
+
+class HtmlHttp {
+ // article
+ static Future reqHtml(id, dynamicType) async {
+ var response = await Request().get(
+ "https://www.bilibili.com/opus/$id",
+ extra: {'ua': 'pc'},
+ );
+
+ if (response.data.contains('Redirecting to')) {
+ RegExp regex = RegExp(r'//([\w\.]+)/(\w+)/(\w+)');
+ Match match = regex.firstMatch(response.data)!;
+ String matchedString = match.group(0)!;
+ response = await Request().get(
+ 'https:$matchedString' + '/',
+ extra: {'ua': 'pc'},
+ );
+ }
+ 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')!;
+ // 头像
+ String avatar = authorHeader.querySelector('img')!.attributes['src']!;
+ avatar = 'https:${avatar.split('@')[0]}';
+ String uname = authorHeader
+ .querySelector('.fixed-author-header__author__name')!
+ .text;
+
+ // 动态详情
+ Element opusDetail = appDom.querySelector('.opus-detail')!;
+ // 发布时间
+ String updateTime =
+ opusDetail.querySelector('.opus-module-author__pub__text')!.text;
+ //
+ String opusContent =
+ opusDetail.querySelector('.opus-module-content')!.innerHtml;
+ String test = opusDetail
+ .querySelector('.horizontal-scroll-album__pic__img')!
+ .innerHtml;
+ String commentId = opusDetail
+ .querySelector('.bili-comment-container')!
+ .className
+ .split(' ')[1]
+ .split('-')[2];
+ // List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
+ return {
+ 'status': true,
+ 'avatar': avatar,
+ 'uname': uname,
+ 'updateTime': updateTime,
+ 'content': test + opusContent,
+ 'commentId': int.parse(commentId)
+ };
+ } catch (err) {
+ print('err: $err');
+ }
+ }
+
+ // read
+ static Future reqReadHtml(id, dynamicType) async {
+ var response = await Request().get(
+ "https://www.bilibili.com/$dynamicType/$id/",
+ extra: {'ua': 'pc'},
+ );
+ Document rootTree = parse(response.data);
+ 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+');
+ Iterable matches = digitRegExp.allMatches(id);
+ String number = matches.first.group(0)!;
+ return {
+ 'status': true,
+ 'avatar': '',
+ 'uname': uname,
+ 'updateTime': '',
+ 'content': opusContent,
+ 'commentId': int.parse(number)
+ };
+ }
+}
diff --git a/lib/http/init.dart b/lib/http/init.dart
index 1e821062..6a60dca0 100644
--- a/lib/http/init.dart
+++ b/lib/http/init.dart
@@ -4,12 +4,13 @@ import 'dart:io';
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:cookie_jar/cookie_jar.dart';
+import 'package:dio/io.dart';
+import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/interceptor.dart';
-import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
class Request {
@@ -17,6 +18,11 @@ class Request {
static late CookieManager cookieManager;
static late final Dio dio;
factory Request() => _instance;
+ Box setting = GStrorage.setting;
+ static Box localCache = GStrorage.localCache;
+ late dynamic enableSystemProxy;
+ late String systemProxyHost;
+ late String systemProxyPort;
/// 设置cookie
static setCookie() async {
@@ -41,8 +47,8 @@ class Request {
log("setCookie, ${e.toString()}");
}
}
- setOptionsHeaders(userInfo);
}
+ setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null);
if (cookie.isEmpty) {
try {
@@ -60,9 +66,6 @@ class Request {
static Future getCsrf() async {
var cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseApiUrl));
- // for (var i in cookies) {
- // print(i);
- // }
String token = '';
if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) {
token = cookies.firstWhere((e) => e.name == 'bili_jct').value;
@@ -70,8 +73,10 @@ class Request {
return token;
}
- static setOptionsHeaders(userInfo) {
- dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
+ static setOptionsHeaders(userInfo, status) {
+ if (status) {
+ dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
+ }
dio.options.headers['env'] = 'prod';
dio.options.headers['app-key'] = 'android64';
dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
@@ -92,18 +97,47 @@ class Request {
//响应流上前后两次接受到数据的间隔,单位为毫秒。
receiveTimeout: const Duration(milliseconds: 12000),
//Http请求头.
- headers: {
- // 'cookie': '',
- },
+ headers: {},
);
+ enableSystemProxy =
+ setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false);
+ systemProxyHost =
+ localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');
+ systemProxyPort =
+ localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');
+
dio = Dio(options)
+
+ /// fix 第三方登录 302重定向 跟iOS代理问题冲突
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
- // Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
+ )
+
+ /// 设置代理
+ ..httpClientAdapter = IOHttpClientAdapter(
+ createHttpClient: () {
+ final client = HttpClient();
+ // Config the client.
+ client.findProxy = (uri) {
+ if (enableSystemProxy) {
+ print('🌹:$systemProxyHost');
+ print('🌹:$systemProxyPort');
+
+ // return 'PROXY host:port';
+ return 'PROXY $systemProxyHost:$systemProxyPort';
+ } else {
+ // 不设置代理
+ return 'DIRECT';
+ }
+ };
+ client.badCertificateCallback =
+ (X509Certificate cert, String host, int port) => true;
+ return client;
+ },
);
//添加拦截器
@@ -118,30 +152,26 @@ class Request {
dio.transformer = BackgroundTransformer();
dio.options.validateStatus = (status) {
- return status! >= 200 && status < 300 || status == 304 || status == 302;
+ return status! >= 200 && status < 300 ||
+ HttpString.validateStatusCodes.contains(status);
};
}
/*
* get请求
*/
- get(url, {data, cacheOptions, options, cancelToken, extra}) async {
+ get(url, {data, options, cancelToken, extra}) async {
Response response;
- Options options;
- String ua = 'pc';
+ Options options = Options();
ResponseType resType = ResponseType.json;
if (extra != null) {
- ua = extra!['ua'] ?? 'pc';
resType = extra!['resType'] ?? ResponseType.json;
+ if (extra['ua'] != null) {
+ options.headers = {'user-agent': headerUa(type: extra['ua'])};
+ }
}
- if (cacheOptions != null) {
- cacheOptions.headers = {'user-agent': headerUa(ua)};
- options = cacheOptions;
- } else {
- options = Options();
- options.headers = {'user-agent': headerUa(ua)};
- options.responseType = resType;
- }
+ options.responseType = resType;
+
try {
response = await dio.get(
url,
@@ -208,15 +238,19 @@ class Request {
token.cancel("cancelled");
}
- String headerUa(ua) {
+ String headerUa({type = 'mob'}) {
String headerUa = '';
- if (ua == 'mob') {
- headerUa = Platform.isIOS
- ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
- : 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36';
+ if (type == 'mob') {
+ if (Platform.isIOS) {
+ headerUa =
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1';
+ } else {
+ headerUa =
+ 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36';
+ }
} else {
headerUa =
- 'Mozilla/5.0 (MaciMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36';
+ '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';
}
return headerUa;
}
diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart
index 4b9e8770..7b398caa 100644
--- a/lib/http/interceptor.dart
+++ b/lib/http/interceptor.dart
@@ -46,7 +46,10 @@ class ApiInterceptor extends Interceptor {
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 处理网络请求错误
// handler.next(err);
- SmartDialog.showToast(await dioError(err));
+ SmartDialog.showToast(
+ await dioError(err),
+ displayType: SmartToastType.onlyRefresh,
+ );
super.onError(err, handler);
}
diff --git a/lib/http/login.dart b/lib/http/login.dart
new file mode 100644
index 00000000..8d2a254e
--- /dev/null
+++ b/lib/http/login.dart
@@ -0,0 +1,177 @@
+import 'dart:convert';
+import 'dart:math';
+import 'package:crypto/crypto.dart';
+
+import 'package:dio/dio.dart';
+import 'package:encrypt/encrypt.dart';
+import 'package:pilipala/http/index.dart';
+import 'package:pilipala/models/login/index.dart';
+import 'package:pilipala/utils/login.dart';
+import 'package:uuid/uuid.dart';
+
+class LoginHttp {
+ static Future queryCaptcha() async {
+ var res = await Request().get(Api.getCaptcha);
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': CaptchaDataModel.fromJson(res.data['data']),
+ };
+ } else {
+ return {'status': false, 'data': res.message};
+ }
+ }
+
+ static Future sendSmsCode({
+ int? cid,
+ required int tel,
+ required String token,
+ required String challenge,
+ required String validate,
+ required String seccode,
+ }) async {
+ var res = await Request().post(
+ Api.appSmsCode,
+ data: {
+ 'cid': cid,
+ 'tel': tel,
+ "source": "main_web",
+ 'token': token,
+ 'challenge': challenge,
+ 'validate': validate,
+ 'seccode': seccode,
+ },
+ options: Options(
+ contentType: Headers.formUrlEncodedContentType,
+ // headers: {'user-agent': ApiConstants.userAgent}
+ ),
+ );
+ print(res);
+ }
+
+ // web端验证码
+ static Future sendWebSmsCode({
+ int? cid,
+ required int tel,
+ required String token,
+ required String challenge,
+ required String validate,
+ required String seccode,
+ }) async {
+ Map data = {
+ 'cid': cid,
+ 'tel': tel,
+ 'token': token,
+ 'challenge': challenge,
+ 'validate': validate,
+ 'seccode': seccode,
+ };
+ FormData formData = FormData.fromMap({...data});
+ var res = await Request().post(
+ Api.smsCode,
+ data: formData,
+ options: Options(
+ contentType: Headers.formUrlEncodedContentType,
+ ),
+ );
+ print(res);
+ }
+
+ // web端验证码登录
+ static Future loginInByWebSmsCode() async {}
+
+ // web端密码登录
+ static Future liginInByWebPwd() async {}
+
+ // app端验证码
+ static Future sendAppSmsCode({
+ int? cid,
+ required int tel,
+ required String token,
+ required String challenge,
+ required String validate,
+ required String seccode,
+ }) async {
+ Map data = {
+ 'cid': cid,
+ 'tel': tel,
+ 'login_session_id': const Uuid().v4().replaceAll('-', ''),
+ 'recaptcha_token': token,
+ 'gee_challenge': challenge,
+ 'gee_validate': validate,
+ 'gee_seccode': seccode,
+ 'channel': 'bili',
+ 'buvid': buvid(),
+ 'local_id': buvid(),
+ // 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
+ 'statistics': {
+ "appId": 1,
+ "platform": 3,
+ "version": "7.52.0",
+ "abtest": ""
+ },
+ };
+ // FormData formData = FormData.fromMap({...data});
+ var res = await Request().post(
+ Api.appSmsCode,
+ data: data,
+ options: Options(
+ contentType: Headers.formUrlEncodedContentType,
+ ),
+ );
+ print(res);
+ }
+
+ static String buvid() {
+ var mac = [];
+ var random = Random();
+
+ for (var i = 0; i < 6; i++) {
+ var min = 0;
+ var max = 0xff;
+ var num = (random.nextInt(max - min + 1) + min).toRadixString(16);
+ mac.add(num);
+ }
+
+ var md5Str = md5.convert(utf8.encode(mac.join(':'))).toString();
+ var md5Arr = md5Str.split('');
+ return 'XY${md5Arr[2]}${md5Arr[12]}${md5Arr[22]}$md5Str';
+ }
+
+ // 获取盐hash跟PubKey
+ static Future getWebKey() async {
+ var res = await Request().get(Api.getWebKey,
+ data: {'disable_rcmd': 0, 'local_id': LoginUtils.generateBuvid()});
+ if (res.data['code'] == 0) {
+ return {'status': true, 'data': res.data['data']};
+ } else {
+ return {'status': false, 'data': {}, 'msg': res.data['message']};
+ }
+ }
+
+ // app端密码登录
+ static Future loginInByMobPwd({
+ required String tel,
+ required String password,
+ required String key,
+ required String rhash,
+ }) async {
+ dynamic publicKey = RSAKeyParser().parse(key);
+ String passwordEncryptyed =
+ Encrypter(RSA(publicKey: publicKey)).encrypt(rhash + password).base64;
+ Map data = {
+ 'username': tel,
+ 'password': passwordEncryptyed,
+ 'local_id': LoginUtils.generateBuvid(),
+ 'disable_rcmd': "0",
+ };
+ var res = await Request().post(
+ Api.loginInByPwdApi,
+ data: data,
+ options: Options(
+ contentType: Headers.formUrlEncodedContentType,
+ ),
+ );
+ print(res);
+ }
+}
diff --git a/lib/http/member.dart b/lib/http/member.dart
index 995ccc23..a48dbffd 100644
--- a/lib/http/member.dart
+++ b/lib/http/member.dart
@@ -1,7 +1,9 @@
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/dynamics/result.dart';
+import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/info.dart';
+import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/wbi_sign.dart';
class MemberHttp {
@@ -18,6 +20,7 @@ class MemberHttp {
var res = await Request().get(
Api.memberInfo,
data: params,
+ extra: {'ua': 'pc'},
);
if (res.data['code'] == 0) {
return {
@@ -65,7 +68,7 @@ class MemberHttp {
int ps = 30,
int tid = 0,
int? pn,
- String keyword = '',
+ String? keyword,
String order = 'pubdate',
bool orderAvoided = true,
}) async {
@@ -74,7 +77,7 @@ class MemberHttp {
'ps': ps,
'tid': tid,
'pn': pn,
- 'keyword': keyword,
+ 'keyword': keyword ?? '',
'order': order,
'platform': 'web',
'web_location': 1550101,
@@ -83,6 +86,7 @@ class MemberHttp {
var res = await Request().get(
Api.memberArchive,
data: params,
+ extra: {'ua': 'pc'},
);
if (res.data['code'] == 0) {
return {
@@ -119,4 +123,96 @@ class MemberHttp {
};
}
}
+
+ // 搜索用户动态
+ static Future memberDynamicSearch({int? pn, int? ps, int? mid}) async {
+ var res = await Request().get(Api.memberDynamic, data: {
+ 'keyword': '海拔',
+ 'mid': mid,
+ 'pn': pn,
+ 'ps': ps,
+ 'platform': 'web'
+ });
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': DynamicsDataModel.fromJson(res.data['data']),
+ };
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
+
+ // 查询分组
+ static Future followUpTags() async {
+ var res = await Request().get(Api.followUpTag);
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': res.data['data']
+ .map((e) => MemberTagItemModel.fromJson(e))
+ .toList()
+ };
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
+
+ // 设置分组
+ static Future addUsers(int? fids, String? tagids) async {
+ var res = await Request().post(Api.addUsers, queryParameters: {
+ 'fids': fids,
+ 'tagids': tagids ?? '0',
+ 'csrf': await Request.getCsrf(),
+ }, data: {
+ 'cross_domain': true
+ });
+ if (res.data['code'] == 0) {
+ return {'status': true, 'data': [], 'msg': '操作成功'};
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
+
+ // 获取某分组下的up
+ static Future followUpGroup(
+ int? mid,
+ int? tagid,
+ int? pn,
+ int? ps,
+ ) async {
+ var res = await Request().get(Api.followUpGroup, data: {
+ 'mid': mid,
+ 'tagid': tagid,
+ 'pn': pn,
+ 'ps': ps,
+ });
+ if (res.data['code'] == 0) {
+ // FollowItemModel
+ return {
+ 'status': true,
+ 'data': res.data['data']
+ .map((e) => FollowItemModel.fromJson(e))
+ .toList()
+ };
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
}
diff --git a/lib/http/reply.dart b/lib/http/reply.dart
index 5dcbce6e..790a017f 100644
--- a/lib/http/reply.dart
+++ b/lib/http/reply.dart
@@ -26,7 +26,7 @@ class ReplyHttp {
Map errMap = {
-400: '请求错误',
-404: '无此项',
- 12002: '当前页面评论功能已关闭"',
+ 12002: '当前页面评论功能已关闭',
12009: '评论主体的type不合法',
12061: 'UP主已关闭评论区',
};
diff --git a/lib/http/search.dart b/lib/http/search.dart
index 5d99e3e0..b94ace2c 100644
--- a/lib/http/search.dart
+++ b/lib/http/search.dart
@@ -1,37 +1,63 @@
+import 'dart:convert';
+
+import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/result.dart';
import 'package:pilipala/models/search/suggest.dart';
+import 'package:pilipala/utils/storage.dart';
class SearchHttp {
+ static Box setting = GStrorage.setting;
static Future hotSearchList() async {
var res = await Request().get(Api.hotSearchList);
- if (res.data['code'] == 0) {
+ if (res.data is String) {
+ Map resultMap = json.decode(res.data);
+ if (resultMap['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': HotSearchModel.fromJson(resultMap),
+ };
+ }
+ } else if (res.data is Map && res.data['code'] == 0) {
return {
'status': true,
'data': HotSearchModel.fromJson(res.data),
};
- } else {
- return {
- 'status': false,
- 'data': [],
- 'msg': '请求错误 🙅',
- };
}
+
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': '请求错误 🙅',
+ };
}
// 获取搜索建议
static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
- if (res.data['code'] == 0) {
- res.data['result']['term'] = term;
- return {
- 'status': true,
- 'data': SearchSuggestModel.fromJson(res.data['result']),
- };
+ if (res.data is String) {
+ Map resultMap = json.decode(res.data);
+ if (resultMap['code'] == 0) {
+ if (resultMap['result'] is Map) {
+ resultMap['result']['term'] = term;
+ }
+ return {
+ 'status': true,
+ 'data': resultMap['result'] is Map
+ ? SearchSuggestModel.fromJson(resultMap['result'])
+ : [],
+ };
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': '请求错误 🙅',
+ };
+ }
} else {
return {
'status': false,
@@ -61,29 +87,44 @@ class SearchHttp {
var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
Object data;
- switch (searchType) {
- case SearchType.video:
- data = SearchVideoModel.fromJson(res.data['data']);
- break;
- case SearchType.live_room:
- data = SearchLiveModel.fromJson(res.data['data']);
- break;
- case SearchType.bili_user:
- data = SearchUserModel.fromJson(res.data['data']);
- break;
- case SearchType.media_bangumi:
- data = SearchMBangumiModel.fromJson(res.data['data']);
- break;
+ try {
+ switch (searchType) {
+ case SearchType.video:
+ List blackMidsList =
+ setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
+ for (var i in res.data['data']['result']) {
+ // 屏蔽推广和拉黑用户
+ i['available'] = !blackMidsList.contains(i['mid']);
+ }
+ data = SearchVideoModel.fromJson(res.data['data']);
+ break;
+ case SearchType.live_room:
+ data = SearchLiveModel.fromJson(res.data['data']);
+ break;
+ case SearchType.bili_user:
+ data = SearchUserModel.fromJson(res.data['data']);
+ break;
+ case SearchType.media_bangumi:
+ data = SearchMBangumiModel.fromJson(res.data['data']);
+ break;
+ case SearchType.article:
+ data = SearchArticleModel.fromJson(res.data['data']);
+ break;
+ }
+ return {
+ 'status': true,
+ 'data': data,
+ };
+ } catch (err) {
+ print(err);
}
- return {
- 'status': true,
- 'data': data,
- };
} else {
return {
'status': false,
'data': [],
- 'msg': res.data['data']['numPages'] == 0 ? '没有相关数据' : '请求错误 🙅',
+ 'msg': res.data['data'] != null && res.data['data']['numPages'] == 0
+ ? '没有相关数据'
+ : res.data['message'],
};
}
}
diff --git a/lib/http/user.dart b/lib/http/user.dart
index 404502b3..1ab465e0 100644
--- a/lib/http/user.dart
+++ b/lib/http/user.dart
@@ -8,6 +8,7 @@ import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/user/history.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart';
+import 'package:pilipala/utils/wbi_sign.dart';
class UserHttp {
static Future userStat({required int mid}) async {
@@ -70,14 +71,15 @@ class UserHttp {
required int pn,
required int ps,
String keyword = '',
- String order = 'mtime'}) async {
+ String order = 'mtime',
+ int type = 0}) async {
var res = await Request().get(Api.userFavFolderDetail, data: {
'media_id': mediaId,
'pn': pn,
'ps': ps,
'keyword': keyword,
'order': order,
- 'type': 0,
+ 'type': type,
'tid': 0,
'platform': 'web'
});
@@ -231,4 +233,64 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
+
+ // 删除历史记录
+ static Future delHistory(kid) async {
+ var res = await Request().post(
+ Api.delHistory,
+ queryParameters: {
+ 'kid': kid,
+ 'jsonp': 'jsonp',
+ 'csrf': await Request.getCsrf(),
+ },
+ );
+ if (res.data['code'] == 0) {
+ return {'status': true, 'msg': '已删除'};
+ } else {
+ 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(
+ {required int pn, required String keyword}) async {
+ var res = await Request().get(
+ Api.searchHistory,
+ data: {
+ 'pn': pn,
+ 'keyword': keyword,
+ 'business': 'all',
+ },
+ );
+ if (res.data['code'] == 0) {
+ return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
+ } else {
+ return {'status': false, 'msg': res.data['message']};
+ }
+ }
}
diff --git a/lib/http/video.dart b/lib/http/video.dart
index a6084a6c..9429a04b 100644
--- a/lib/http/video.dart
+++ b/lib/http/video.dart
@@ -9,9 +9,11 @@ import 'package:pilipala/models/home/rcmd/result.dart';
import 'package:pilipala/models/model_hot_video_item.dart';
import 'package:pilipala/models/model_rec_video_item.dart';
import 'package:pilipala/models/user/fav_folder.dart';
+import 'package:pilipala/models/video/ai.dart';
import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/utils/storage.dart';
+import 'package:pilipala/utils/wbi_sign.dart';
/// res.data['code'] == 0 请求正常返回结果
/// res.data['data'] 为结果
@@ -20,6 +22,9 @@ import 'package:pilipala/utils/storage.dart';
class VideoHttp {
static Box localCache = GStrorage.localCache;
static Box setting = GStrorage.setting;
+ static bool enableRcmdDynamic =
+ setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
+ static Box userInfoCache = GStrorage.userInfo;
// 首页推荐视频
static Future rcmdVideoList({required int ps, required int freshIdx}) async {
@@ -73,6 +78,7 @@ class VideoHttp {
for (var i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' &&
+ (!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) {
list.add(RecVideoItemAppModel.fromJson(i));
@@ -130,6 +136,11 @@ class VideoHttp {
// 'platform': '',
// 'high_quality': ''
};
+ // 免登录查看1080p
+ if (userInfoCache.get('userInfoCache') == null &&
+ setting.get(SettingBoxKey.p1080, defaultValue: true)) {
+ data['try_look'] = 1;
+ }
try {
var res = await Request().get(Api.videoUrl, data: data);
if (res.data['code'] == 0) {
@@ -411,4 +422,23 @@ class VideoHttp {
return {'status': true, 'data': res.data['data']};
}
}
+
+ static Future aiConclusion({
+ String? bvid,
+ int? cid,
+ int? upMid,
+ }) async {
+ Map params = await WbiSign().makSign({
+ 'bvid': bvid,
+ 'cid': cid,
+ 'up_mid': upMid,
+ });
+ var res = await Request().get(Api.aiConclusion, data: params);
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': AiConclusionModel.fromJson(res.data['data']),
+ };
+ }
+ }
}
diff --git a/lib/main.dart b/lib/main.dart
index ca9318ca..98413178 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,5 +1,11 @@
+<<<<<<< HEAD
import 'package:audio_service/audio_service.dart';
+=======
+import 'dart:io';
+
+>>>>>>> main
import 'package:flutter/services.dart';
+import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@@ -14,6 +20,7 @@ import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart';
import 'package:pilipala/pages/main/view.dart';
+import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/storage.dart';
@@ -26,6 +33,7 @@ void main() async {
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
.then((_) async {
await GStrorage.init();
+<<<<<<< HEAD
await AudioService.init(
builder: () => MyAudioHandler(),
@@ -38,6 +46,9 @@ void main() async {
),
);
+=======
+ await setupServiceLocator();
+>>>>>>> main
runApp(const MyApp());
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
@@ -74,6 +85,23 @@ class MyApp extends StatelessWidget {
double textScale =
setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);
+ // 强制设置高帧率
+ if (Platform.isAndroid) {
+ try {
+ late List modes;
+ FlutterDisplayMode.supported.then((value) {
+ modes = value;
+ var storageDisplay = setting.get(SettingBoxKey.displayMode);
+ DisplayMode f = DisplayMode.auto;
+ if (storageDisplay != null) {
+ f = modes.firstWhere((e) => e.toString() == storageDisplay);
+ }
+ DisplayMode preferred = modes.toList().firstWhere((el) => el == f);
+ FlutterDisplayMode.setPreferredMode(preferred);
+ });
+ } catch (_) {}
+ }
+
return DynamicColorBuilder(
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme? lightColorScheme;
diff --git a/lib/models/common/search_type.dart b/lib/models/common/search_type.dart
index 491ee7b4..d7d13aec 100644
--- a/lib/models/common/search_type.dart
+++ b/lib/models/common/search_type.dart
@@ -12,20 +12,20 @@ enum SearchType {
live_room,
// 主播:live_user
// live_user,
- // 专栏:article
- // article,
// 话题:topic
// topic,
// 用户:bili_user
bili_user,
+ // 专栏:article
+ article,
// 相簿:photo
// photo
}
extension SearchTypeExtension on SearchType {
String get type =>
- ['video', 'media_bangumi', 'live_room', 'bili_user'][index];
- String get label => ['视频', '番剧', '直播间', '用户'][index];
+ ['video', 'media_bangumi', 'live_room', 'bili_user', 'article'][index];
+ String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index];
}
// 搜索类型为视频、专栏及相簿时
diff --git a/lib/models/dynamics/result.dart b/lib/models/dynamics/result.dart
index 05c5245e..d8aff7b5 100644
--- a/lib/models/dynamics/result.dart
+++ b/lib/models/dynamics/result.dart
@@ -244,7 +244,9 @@ class Vote {
choiceCnt = json['choice_cnt'];
share = json['share'];
defaultShare = json['default_share'];
- endTime = json['end_time'];
+ endTime = json['end_time'] is int
+ ? json['end_time']
+ : int.parse(json['end_time']);
joinNum = json['join_num'];
status = json['status'];
type = json['type'];
diff --git a/lib/models/follow/result.dart b/lib/models/follow/result.dart
index c6656165..2f1cedf5 100644
--- a/lib/models/follow/result.dart
+++ b/lib/models/follow/result.dart
@@ -8,7 +8,7 @@ class FollowDataModel {
List? list;
FollowDataModel.fromJson(Map json) {
- total = json['total'];
+ total = json['total'] ?? 0;
list = json['list']
.map((e) => FollowItemModel.fromJson(e))
.toList();
@@ -19,7 +19,7 @@ class FollowItemModel {
FollowItemModel({
this.mid,
this.attribute,
- this.mtime,
+ // this.mtime,
this.tag,
this.special,
this.uname,
@@ -30,7 +30,7 @@ class FollowItemModel {
int? mid;
int? attribute;
- int? mtime;
+ // int? mtime;
List? tag;
int? special;
String? uname;
@@ -41,7 +41,7 @@ class FollowItemModel {
FollowItemModel.fromJson(Map json) {
mid = json['mid'];
attribute = json['attribute'];
- mtime = json['mtime'];
+ // mtime = json['mtime'];
tag = json['tag'];
special = json['special'];
uname = json['uname'];
diff --git a/lib/models/login/index.dart b/lib/models/login/index.dart
new file mode 100644
index 00000000..a4f2e3c0
--- /dev/null
+++ b/lib/models/login/index.dart
@@ -0,0 +1,49 @@
+class CaptchaDataModel {
+ CaptchaDataModel({
+ this.type,
+ this.token,
+ this.geetest,
+ this.tencent,
+ this.validate,
+ this.seccode,
+ });
+
+ String? type;
+ String? token;
+ GeetestData? geetest;
+ Tencent? tencent;
+ String? validate;
+ String? seccode;
+
+ CaptchaDataModel.fromJson(Map json) {
+ type = json["type"];
+ token = json["token"];
+ geetest =
+ json["geetest"] != null ? GeetestData.fromJson(json["geetest"]) : null;
+ tencent =
+ json["tencent"] != null ? Tencent.fromJson(json["tencent"]) : null;
+ }
+}
+
+class GeetestData {
+ GeetestData({
+ this.challenge,
+ this.gt,
+ });
+
+ String? challenge;
+ String? gt;
+
+ GeetestData.fromJson(Map json) {
+ challenge = json["challenge"];
+ gt = json["gt"];
+ }
+}
+
+class Tencent {
+ Tencent({this.appid});
+ String? appid;
+ Tencent.fromJson(Map json) {
+ appid = json["appid"];
+ }
+}
diff --git a/lib/models/member/tags.dart b/lib/models/member/tags.dart
new file mode 100644
index 00000000..33f7c1f8
--- /dev/null
+++ b/lib/models/member/tags.dart
@@ -0,0 +1,23 @@
+class MemberTagItemModel {
+ MemberTagItemModel({
+ this.count,
+ this.name,
+ this.tagid,
+ this.tip,
+ this.checked,
+ });
+
+ int? count;
+ String? name;
+ int? tagid;
+ String? tip;
+ bool? checked;
+
+ MemberTagItemModel.fromJson(Map json) {
+ count = json['count'];
+ name = json['name'];
+ tagid = json['tagid'];
+ tip = json['tip'];
+ checked = false;
+ }
+}
diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart
index 91070215..3d381ed9 100644
--- a/lib/models/search/result.dart
+++ b/lib/models/search/result.dart
@@ -6,6 +6,7 @@ class SearchVideoModel {
List? list;
SearchVideoModel.fromJson(Map json) {
list = json['result']
+ .where((e) => e['available'] == true)
.map((e) => SearchVideoItemModel.fromJson(e))
.toList();
}
@@ -17,7 +18,7 @@ class SearchVideoItemModel {
this.id,
this.cid,
// this.author,
- // this.mid,
+ this.mid,
// this.typeid,
// this.typename,
this.arcurl,
@@ -47,7 +48,7 @@ class SearchVideoItemModel {
int? id;
int? cid;
// String? author;
- // String? mid;
+ int? mid;
// String? typeid;
// String? typename;
String? arcurl;
@@ -80,6 +81,7 @@ class SearchVideoItemModel {
arcurl = json['arcurl'];
aid = json['aid'];
bvid = json['bvid'];
+ mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']);
description = json['description'];
@@ -376,3 +378,75 @@ class SearchMBangumiItemModel {
indexShow = json['index_show'];
}
}
+
+class SearchArticleModel {
+ SearchArticleModel({this.list});
+
+ List? list;
+
+ SearchArticleModel.fromJson(Map json) {
+ list = json['result'] != null
+ ? json['result']
+ .map(
+ (e) => SearchArticleItemModel.fromJson(e))
+ .toList()
+ : [];
+ }
+}
+
+class SearchArticleItemModel {
+ SearchArticleItemModel({
+ this.pubTime,
+ this.like,
+ this.title,
+ this.subTitle,
+ this.rankOffset,
+ this.mid,
+ this.imageUrls,
+ this.id,
+ this.categoryId,
+ this.view,
+ this.reply,
+ this.desc,
+ this.rankScore,
+ this.type,
+ this.templateId,
+ this.categoryName,
+ });
+
+ int? pubTime;
+ int? like;
+ List? title;
+ String? subTitle;
+ int? rankOffset;
+ int? mid;
+ List? imageUrls;
+ int? id;
+ int? categoryId;
+ int? view;
+ int? reply;
+ String? desc;
+ int? rankScore;
+ String? type;
+ int? templateId;
+ String? categoryName;
+
+ SearchArticleItemModel.fromJson(Map json) {
+ pubTime = json['pub_time'];
+ like = json['like'];
+ title = Em.regTitle(json['title']);
+ subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
+ rankOffset = json['rank_offset'];
+ mid = json['mid'];
+ imageUrls = json['image_urls'];
+ id = json['id'];
+ categoryId = json['category_id'];
+ view = json['view'];
+ reply = json['reply'];
+ desc = json['desc'];
+ rankScore = json['rank_score'];
+ type = json['type'];
+ templateId = json['templateId'];
+ categoryName = json['category_name'];
+ }
+}
diff --git a/lib/models/user/history.dart b/lib/models/user/history.dart
index 669874b4..5c7c9278 100644
--- a/lib/models/user/history.dart
+++ b/lib/models/user/history.dart
@@ -3,17 +3,23 @@ class HistoryData {
this.cursor,
this.tab,
this.list,
+ this.page,
});
Cursor? cursor;
List? tab;
List? list;
+ Map? page;
HistoryData.fromJson(Map json) {
- cursor = Cursor.fromJson(json['cursor']);
- tab = json['tab'].map((e) => HisTabItem.fromJson(e)).toList();
- list =
- json['list'].map((e) => HisListItem.fromJson(e)).toList();
+ cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
+ tab = json['tab'] != null
+ ? json['tab'].map((e) => HisTabItem.fromJson(e)).toList()
+ : [];
+ list = json['list'] != null
+ ? json['list'].map((e) => HisListItem.fromJson(e)).toList()
+ : [];
+ page = json['page'];
}
}
@@ -79,6 +85,7 @@ class HisListItem {
this.kid,
this.tagName,
this.liveStatus,
+ this.checked,
});
String? title;
@@ -105,6 +112,7 @@ class HisListItem {
int? kid;
String? tagName;
int? liveStatus;
+ bool? checked;
HisListItem.fromJson(Map json) {
title = json['title'];
@@ -131,6 +139,7 @@ class HisListItem {
kid = json['kid'];
tagName = json['tag_name'];
liveStatus = json['live_status'];
+ checked = false;
}
}
diff --git a/lib/models/video/ai.dart b/lib/models/video/ai.dart
new file mode 100644
index 00000000..a06fa79d
--- /dev/null
+++ b/lib/models/video/ai.dart
@@ -0,0 +1,80 @@
+class AiConclusionModel {
+ AiConclusionModel({
+ this.code,
+ this.modelResult,
+ this.stid,
+ this.status,
+ this.likeNum,
+ this.dislikeNum,
+ });
+
+ int? code;
+ ModelResult? modelResult;
+ String? stid;
+ int? status;
+ int? likeNum;
+ int? dislikeNum;
+
+ AiConclusionModel.fromJson(Map json) {
+ code = json['code'];
+ modelResult = ModelResult.fromJson(json['model_result']);
+ stid = json['stid'];
+ status = json['status'];
+ likeNum = json['like_num'];
+ dislikeNum = json['dislike_num'];
+ }
+}
+
+class ModelResult {
+ ModelResult({
+ this.resultType,
+ this.summary,
+ this.outline,
+ });
+
+ int? resultType;
+ String? summary;
+ List? outline;
+
+ ModelResult.fromJson(Map json) {
+ resultType = json['result_type'];
+ summary = json['summary'];
+ outline = json['result_type'] == 2
+ ? json['outline']
+ .map((e) => OutlineItem.fromJson(e))
+ .toList()
+ : [];
+ }
+}
+
+class OutlineItem {
+ OutlineItem({
+ this.title,
+ this.partOutline,
+ });
+
+ String? title;
+ List? partOutline;
+
+ OutlineItem.fromJson(Map json) {
+ title = json['title'];
+ partOutline = json['part_outline']
+ .map((e) => PartOutline.fromJson(e))
+ .toList();
+ }
+}
+
+class PartOutline {
+ PartOutline({
+ this.timestamp,
+ this.content,
+ });
+
+ int? timestamp;
+ String? content;
+
+ PartOutline.fromJson(Map json) {
+ timestamp = json['timestamp'];
+ content = json['content'];
+ }
+}
diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart
index 76173fae..31808e1c 100644
--- a/lib/pages/about/index.dart
+++ b/lib/pages/about/index.dart
@@ -184,7 +184,7 @@ class AboutController extends GetxController {
// 获取远程版本
Future getRemoteApp() async {
- var result = await Request().get(Api.latestApp);
+ var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'});
data = LatestDataModel.fromJson(result.data);
remoteAppInfo = data;
remoteVersion.value = data.tagName!;
diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart
index c027f8af..f37a3310 100644
--- a/lib/pages/bangumi/introduction/controller.dart
+++ b/lib/pages/bangumi/introduction/controller.dart
@@ -9,6 +9,7 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart';
+import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart';
@@ -21,7 +22,7 @@ class BangumiIntroController extends GetxController {
? int.parse(Get.parameters['seasonId']!)
: null;
var epId = Get.parameters['epId'] != null
- ? int.parse(Get.parameters['epId']!)
+ ? int.tryParse(Get.parameters['epId']!)
: null;
// 是否预渲染 骨架屏
@@ -257,7 +258,7 @@ class BangumiIntroController extends GetxController {
VideoDetailController videoDetailCtr =
Get.find(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid;
- videoDetailCtr.cid = cid;
+ videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl();
// 重新请求评论
@@ -292,4 +293,31 @@ class BangumiIntroController extends GetxController {
}
return result;
}
+
+ /// 列表循环或者顺序播放时,自动播放下一个
+ void nextPlay() {
+ late List episodes;
+ if (bangumiDetail.value.episodes != null) {
+ episodes = bangumiDetail.value.episodes!;
+ }
+ VideoDetailController videoDetailCtr =
+ Get.find(tag: Get.arguments['heroTag']);
+ int currentIndex =
+ episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value);
+ int nextIndex = currentIndex + 1;
+ PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
+ // 列表循环
+ if (platRepeat == PlayRepeat.listCycle) {
+ if (nextIndex == episodes.length - 1) {
+ nextIndex = 0;
+ }
+ }
+ if (nextIndex <= episodes.length - 1 &&
+ platRepeat == PlayRepeat.listOrder) {}
+
+ int cid = episodes[nextIndex].cid!;
+ String bvid = episodes[nextIndex].bvid!;
+ int aid = episodes[nextIndex].aid!;
+ changeSeasonOrbangu(bvid, cid, aid);
+ }
}
diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart
index 7d31c108..af47d7da 100644
--- a/lib/pages/bangumi/introduction/view.dart
+++ b/lib/pages/bangumi/introduction/view.dart
@@ -34,10 +34,12 @@ class BangumiIntroPanel extends StatefulWidget {
class _BangumiIntroPanelState extends State
with AutomaticKeepAliveClientMixin {
- final BangumiIntroController bangumiIntroController =
- Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
+ late BangumiIntroController bangumiIntroController;
+ late VideoDetailController videoDetailCtr;
BangumiInfoModel? bangumiDetail;
late Future _futureBuilderFuture;
+ late int cid;
+ late String heroTag;
// 添加页面缓存
@override
@@ -46,10 +48,19 @@ class _BangumiIntroPanelState extends State
@override
void initState() {
super.initState();
+ heroTag = Get.arguments['heroTag'];
+ cid = widget.cid!;
+ bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
+ videoDetailCtr = Get.find(tag: heroTag);
bangumiIntroController.bangumiDetail.listen((value) {
bangumiDetail = value;
});
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
+ videoDetailCtr.cid.listen((p0) {
+ print('🐶🐶$p0');
+ cid = p0;
+ setState(() {});
+ });
}
@override
@@ -61,22 +72,25 @@ class _BangumiIntroPanelState extends State
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
// 请求成功
+
return BangumiInfo(
loadingStatus: false,
bangumiDetail: bangumiDetail,
+ cid: cid,
);
} else {
// 请求错误
- return HttpError(
- errMsg: snapshot.data['msg'],
- fn: () => Get.back(),
- );
+ // return HttpError(
+ // errMsg: snapshot.data['msg'],
+ // fn: () => Get.back(),
+ // );
+ return SizedBox();
}
} else {
return BangumiInfo(
loadingStatus: true,
bangumiDetail: bangumiDetail,
- cid: widget.cid,
+ cid: cid,
);
}
},
@@ -117,6 +131,12 @@ class _BangumiInfoState extends State {
bangumiItem = bangumiIntroController.bangumiItem;
sheetHeight = localCache.get('sheetHeight');
cid = widget.cid!;
+ print('cid: $cid');
+ videoDetailCtr.cid.listen((p0) {
+ cid = p0;
+ print('cid: $cid');
+ setState(() {});
+ });
}
// 收藏
@@ -260,9 +280,15 @@ class _BangumiInfoState extends State {
children: [
Text(
!widget.loadingStatus
- ? widget.bangumiDetail!.areas!
- .first['name']
- : bangumiItem!.areas!.first['name'],
+ ? (widget.bangumiDetail!.areas!
+ .isNotEmpty
+ ? widget.bangumiDetail!.areas!
+ .first['name']
+ : '')
+ : (bangumiItem!.areas!.isNotEmpty
+ ? bangumiItem!
+ .areas!.first['name']
+ : ''),
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart
index f9c3e37d..e48715eb 100644
--- a/lib/pages/bangumi/view.dart
+++ b/lib/pages/bangumi/view.dart
@@ -113,6 +113,9 @@ class _BangumiPageState extends State
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
+ if (snapshot.data == null) {
+ return const SizedBox();
+ }
Map data = snapshot.data as Map;
List list = _bangumidController.bangumiFollowList;
if (data['status']) {
@@ -198,7 +201,7 @@ class _BangumiPageState extends State
},
),
),
- const LoadingMore()
+ LoadingMore()
],
),
);
diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart
index 9c55448d..bb27a38a 100644
--- a/lib/pages/bangumi/widgets/bangumi_panel.dart
+++ b/lib/pages/bangumi/widgets/bangumi_panel.dart
@@ -1,7 +1,9 @@
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/bangumi/info.dart';
+import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/storage.dart';
class BangumiPanel extends StatefulWidget {
@@ -30,16 +32,28 @@ class _BangumiPanelState extends State {
dynamic userInfo;
// 默认未开通
int vipStatus = 0;
+ late int cid;
+ String heroTag = Get.arguments['heroTag'];
+ late final VideoDetailController videoDetailCtr;
@override
void initState() {
super.initState();
- currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
+ cid = widget.cid!;
+ currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
scrollToIndex();
userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) {
vipStatus = userInfo.vipStatus;
}
+ videoDetailCtr = Get.find(tag: heroTag);
+
+ videoDetailCtr.cid.listen((p0) {
+ cid = p0;
+ setState(() {});
+ currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
+ scrollToIndex();
+ });
}
@override
diff --git a/lib/pages/blacklist/index.dart b/lib/pages/blacklist/index.dart
index 63792532..09cbaee8 100644
--- a/lib/pages/blacklist/index.dart
+++ b/lib/pages/blacklist/index.dart
@@ -1,4 +1,5 @@
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/common/widgets/http_error.dart';
@@ -60,7 +61,7 @@ class _BlackListPageState extends State {
centerTitle: false,
title: Obx(
() => Text(
- '黑名单管理 (${_blackListController.blackList.length} / 5000)',
+ '黑名单管理 - ${_blackListController.total.value}',
style: Theme.of(context).textTheme.titleMedium,
),
),
@@ -104,10 +105,11 @@ class _BlackListPageState extends State {
overflow: TextOverflow.ellipsis,
),
dense: true,
- // trailing: TextButton(
- // onPressed: () {},
- // child: const Text('移除'),
- // ),
+ trailing: TextButton(
+ onPressed: () => _blackListController
+ .removeBlack(list[index].mid),
+ child: const Text('移除'),
+ ),
);
},
),
@@ -136,6 +138,7 @@ class _BlackListPageState extends State {
class BlackListController extends GetxController {
int currentPage = 1;
int pageSize = 50;
+ RxInt total = 0.obs;
RxList blackList = [BlackListItem()].obs;
Future queryBlacklist({type = 'init'}) async {
@@ -146,6 +149,7 @@ class BlackListController extends GetxController {
if (result['status']) {
if (type == 'init') {
blackList.value = result['data'].list;
+ total.value = result['data'].total;
} else {
blackList.addAll(result['data'].list);
}
@@ -154,4 +158,13 @@ class BlackListController extends GetxController {
}
return result;
}
+
+ Future removeBlack(mid) async {
+ var result = await BlackHttp.removeBlack(fid: mid);
+ if (result['status']) {
+ blackList.removeWhere((e) => e.mid == mid);
+ total.value = total.value - 1;
+ SmartDialog.showToast(result['msg']);
+ }
+ }
}
diff --git a/lib/pages/danmaku/controller.dart b/lib/pages/danmaku/controller.dart
index ebe7712d..38d09e04 100644
--- a/lib/pages/danmaku/controller.dart
+++ b/lib/pages/danmaku/controller.dart
@@ -10,22 +10,34 @@ class PlDanmakuController {
// 按 6min 分段
int segCount = 0;
List dmSegList = [];
- int currentSegIndex = 0;
+ // 已请求的段落标记
+ List hasrequestSeg = [];
+ int currentSegIndex = 1;
int currentDmIndex = 0;
void calcSegment() {
+ dmSegList.clear();
+ // 视频分段数
segCount = (videoDuration.inSeconds / (60 * 6)).ceil();
+ dmSegList = List.generate(
+ segCount < 1 ? 1 : segCount, (index) => DmSegMobileReply());
+ // 当前分段
+ try {
+ currentSegIndex =
+ (playerController.position.value.inSeconds / (60 * 6)).ceil();
+ currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
+ } catch (_) {}
}
Future> queryDanmaku() async {
- dmSegList.clear();
- for (int segIndex = 1; segIndex <= segCount; segIndex++) {
- DmSegMobileReply result =
- await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: segIndex);
- if (result.elems.isNotEmpty) {
- result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
- dmSegList.add(result);
- }
+ // dmSegList.clear();
+ DmSegMobileReply result =
+ await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: currentSegIndex);
+ if (result.elems.isNotEmpty) {
+ result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
+ // dmSegList.add(result);
+ currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
+ dmSegList[currentSegIndex - 1] = result;
}
if (dmSegList.isNotEmpty) {
findClosestPositionIndex(playerController.position.value.inMilliseconds);
diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart
index 972c96c3..317d47e9 100644
--- a/lib/pages/danmaku/view.dart
+++ b/lib/pages/danmaku/view.dart
@@ -1,3 +1,4 @@
+import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
@@ -29,6 +30,11 @@ class _PlDanmakuState extends State {
bool danmuPlayStatus = true;
Box setting = GStrorage.setting;
late bool enableShowDanmaku;
+ late List blockTypes;
+ late double showArea;
+ late double opacityVal;
+ late double fontSizeVal;
+ late double danmakuSpeedVal;
@override
void initState() {
@@ -58,6 +64,11 @@ class _PlDanmakuState extends State {
}
}
});
+ blockTypes = playerController.blockTypes;
+ showArea = playerController.showArea;
+ opacityVal = playerController.opacityVal;
+ fontSizeVal = playerController.fontSizeVal;
+ danmakuSpeedVal = playerController.danmakuSpeedVal;
}
// 播放器状态监听
@@ -75,12 +86,23 @@ class _PlDanmakuState extends State {
_controller!.onResume();
danmuPlayStatus = true;
}
- PlDanmakuController ctr = _plDanmakuController;
- int currentPosition = position.inMilliseconds;
-
if (!playerController.isOpenDanmu.value) {
return;
}
+ PlDanmakuController ctr = _plDanmakuController;
+ int currentPosition = position.inMilliseconds;
+ blockTypes = playerController.blockTypes;
+ // 根据position判断是否有已缓存弹幕。没有则请求对应段
+ int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
+ segIndex = segIndex < 1 ? 1 : segIndex;
+ if (ctr.dmSegList[segIndex - 1].elems.isEmpty &&
+ !ctr.hasrequestSeg.contains(segIndex - 1)) {
+ ctr.hasrequestSeg.add(segIndex - 1);
+ ctr.currentSegIndex = segIndex;
+ EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
+ ctr.queryDanmaku();
+ });
+ }
// 超出分段数返回
if (ctr.currentSegIndex >= ctr.dmSegList.length) {
return;
@@ -99,14 +121,17 @@ class _PlDanmakuState extends State {
var delta = currentPosition - element.progress;
if (delta >= 0 && delta < 200) {
- _controller!.addItems([
- DanmakuItem(
- element.content,
- color: DmUtils.decimalToColor(element.color),
- time: element.progress,
- type: DmUtils.getPosition(element.mode),
- )
- ]);
+ // 屏蔽彩色弹幕
+ if (blockTypes.contains(6) ? element.color == 16777215 : true) {
+ _controller!.addItems([
+ DanmakuItem(
+ element.content,
+ color: DmUtils.decimalToColor(element.color),
+ time: element.progress,
+ type: DmUtils.getPosition(element.mode),
+ )
+ ]);
+ }
ctr.currentDmIndex++;
} else {
if (!playerController.isOpenDanmu.value) {
@@ -126,22 +151,30 @@ class _PlDanmakuState extends State {
@override
Widget build(BuildContext context) {
- return Obx(
- () => AnimatedOpacity(
- opacity: playerController.isOpenDanmu.value ? 1 : 0,
- duration: const Duration(milliseconds: 100),
- child: DanmakuView(
- createdController: (DanmakuController e) async {
- widget.playerController.danmakuController = _controller = e;
- },
- option: DanmakuOption(
- fontSize: 15,
- area: 0.5,
- duration: 5,
+ return LayoutBuilder(builder: (context, box) {
+ double initDuration = box.maxWidth / 12;
+ return Obx(
+ () => AnimatedOpacity(
+ opacity: playerController.isOpenDanmu.value ? 1 : 0,
+ duration: const Duration(milliseconds: 100),
+ child: DanmakuView(
+ createdController: (DanmakuController e) async {
+ widget.playerController.danmakuController = _controller = e;
+ },
+ option: DanmakuOption(
+ fontSize: 15 * fontSizeVal,
+ area: showArea,
+ opacity: opacityVal,
+ hideTop: blockTypes.contains(5),
+ hideScroll: blockTypes.contains(2),
+ hideBottom: blockTypes.contains(4),
+ duration: initDuration /
+ (danmakuSpeedVal * widget.playerController.playbackSpeed),
+ ),
+ statusChanged: (isPlaying) {},
),
- statusChanged: (isPlaying) {},
),
- ),
- );
+ );
+ });
}
}
diff --git a/lib/pages/dynamics/controller.dart b/lib/pages/dynamics/controller.dart
index 5b524510..26ba2b22 100644
--- a/lib/pages/dynamics/controller.dart
+++ b/lib/pages/dynamics/controller.dart
@@ -149,10 +149,30 @@ class DynamicsController extends GetxController {
case 'DYNAMIC_TYPE_ARTICLE':
String title = item.modules.moduleDynamic.major.opus.title;
String url = item.modules.moduleDynamic.major.opus.jumpUrl;
- Get.toNamed(
- '/webview',
- parameters: {'url': 'https:$url', 'type': 'note', 'pageTitle': title},
- );
+ if (url.contains('opus') || url.contains('read')) {
+ RegExp digitRegExp = RegExp(r'\d+');
+ Iterable matches = digitRegExp.allMatches(url);
+ String number = matches.first.group(0)!;
+ if (url.contains('read')) {
+ number = 'cv$number';
+ }
+ Get.toNamed('/htmlRender', parameters: {
+ 'url': url.startsWith('//') ? url.split('//').last : url,
+ 'title': title,
+ 'id': number,
+ 'dynamicType': url.split('//').last.split('/')[1]
+ });
+ } else {
+ Get.toNamed(
+ '/webview',
+ parameters: {
+ 'url': 'https:$url',
+ 'type': 'note',
+ 'pageTitle': title
+ },
+ );
+ }
+
break;
case 'DYNAMIC_TYPE_PGC':
print('番剧');
diff --git a/lib/pages/dynamics/deatil/controller.dart b/lib/pages/dynamics/deatil/controller.dart
index 96ab65a6..62f0245d 100644
--- a/lib/pages/dynamics/deatil/controller.dart
+++ b/lib/pages/dynamics/deatil/controller.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/reply.dart';
@@ -17,6 +18,7 @@ class DynamicDetailController extends GetxController {
RxString noMore = ''.obs;
RxList replyList = [ReplyItemModel()].obs;
RxInt acount = 0.obs;
+ final ScrollController scrollController = ScrollController();
ReplySortType _sortType = ReplySortType.time;
RxString sortTypeTitle = ReplySortType.time.titles.obs;
diff --git a/lib/pages/dynamics/deatil/view.dart b/lib/pages/dynamics/deatil/view.dart
index 6a779ba3..116e0d27 100644
--- a/lib/pages/dynamics/deatil/view.dart
+++ b/lib/pages/dynamics/deatil/view.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_reply.dart';
import 'package:pilipala/common/widgets/http_error.dart';
@@ -9,7 +10,10 @@ import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/pages/dynamics/deatil/index.dart';
import 'package:pilipala/pages/dynamics/widgets/author_panel.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
+import 'package:pilipala/pages/video/detail/replyNew/index.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
+import 'package:pilipala/utils/feed_back.dart';
+import 'package:pilipala/utils/id_utils.dart';
import '../widgets/dynamic_panel.dart';
@@ -21,15 +25,18 @@ class DynamicDetailPage extends StatefulWidget {
State createState() => _DynamicDetailPageState();
}
-class _DynamicDetailPageState extends State {
- late DynamicDetailController? _dynamicDetailController;
+class _DynamicDetailPageState extends State
+ with TickerProviderStateMixin {
+ late DynamicDetailController _dynamicDetailController;
+ late AnimationController fabAnimationCtr;
Future? _futureBuilderFuture;
late StreamController titleStreamC; // appBar title
- final ScrollController scrollController = ScrollController();
+ late ScrollController scrollController;
bool _visibleTitle = false;
String? action;
// 回复类型
late int type;
+ bool _isFabVisible = true;
@override
void initState() {
@@ -38,39 +45,42 @@ class _DynamicDetailPageState extends State {
// floor 1原创 2转发
if (Get.arguments['floor'] == 1) {
oid = int.parse(Get.arguments['item'].basic!['comment_id_str']);
+ print(oid);
} else {
- oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
+ try {
+ String type = Get.arguments['item'].modules.moduleDynamic.major.type;
+
+ /// TODO
+ if (type == 'MAJOR_TYPE_OPUS') {
+ } else {
+ oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
+ }
+ } catch (_) {}
}
- int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11;
+ int commentType = 11;
+ try {
+ commentType = Get.arguments['item'].basic!['comment_type'];
+ } catch (_) {}
type = (commentType == 0) ? 11 : commentType;
action =
Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
- _dynamicDetailController = Get.put(DynamicDetailController(oid, type));
- _futureBuilderFuture = _dynamicDetailController!.queryReplyList();
+ _dynamicDetailController =
+ Get.put(DynamicDetailController(oid, type), tag: oid.toString());
+ _futureBuilderFuture = _dynamicDetailController.queryReplyList();
titleStreamC = StreamController();
- scrollController.addListener(_listen);
if (action == 'comment') {
_visibleTitle = true;
titleStreamC.add(true);
}
- }
- void _listen() async {
- if (scrollController.position.pixels >=
- scrollController.position.maxScrollExtent - 300) {
- EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
- _dynamicDetailController!.queryReplyList(reqType: 'onLoad');
- });
- }
-
- if (scrollController.offset > 55 && !_visibleTitle) {
- _visibleTitle = true;
- titleStreamC.add(true);
- } else if (scrollController.offset <= 55 && _visibleTitle) {
- _visibleTitle = false;
- titleStreamC.add(false);
- }
+ fabAnimationCtr = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 300),
+ );
+ fabAnimationCtr.forward();
+ // 滚动事件监听
+ scrollListener();
}
void replyReply(replyItem) {
@@ -97,9 +107,58 @@ class _DynamicDetailPageState extends State {
);
}
+ void scrollListener() {
+ scrollController = _dynamicDetailController.scrollController;
+ scrollController.addListener(
+ () {
+ // 分页加载
+ if (scrollController.position.pixels >=
+ scrollController.position.maxScrollExtent - 300) {
+ EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
+ _dynamicDetailController.queryReplyList(reqType: 'onLoad');
+ });
+ }
+
+ // 标题
+ if (scrollController.offset > 55 && !_visibleTitle) {
+ _visibleTitle = true;
+ titleStreamC.add(true);
+ } else if (scrollController.offset <= 55 && _visibleTitle) {
+ _visibleTitle = false;
+ titleStreamC.add(false);
+ }
+
+ // fab按钮
+ final ScrollDirection direction =
+ scrollController.position.userScrollDirection;
+ if (direction == ScrollDirection.forward) {
+ _showFab();
+ } else if (direction == ScrollDirection.reverse) {
+ _hideFab();
+ }
+ },
+ );
+ }
+
+ void _showFab() {
+ if (!_isFabVisible) {
+ _isFabVisible = true;
+ fabAnimationCtr.forward();
+ }
+ }
+
+ void _hideFab() {
+ if (_isFabVisible) {
+ _isFabVisible = false;
+ fabAnimationCtr.reverse();
+ }
+ }
+
@override
void dispose() {
scrollController.removeListener(() {});
+ fabAnimationCtr.dispose();
+ scrollController.dispose();
super.dispose();
}
@@ -118,7 +177,7 @@ class _DynamicDetailPageState extends State {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
duration: const Duration(milliseconds: 300),
- child: author(_dynamicDetailController!.item, context),
+ child: AuthorPanel(item: _dynamicDetailController.item),
);
},
),
@@ -126,155 +185,206 @@ class _DynamicDetailPageState extends State {
),
body: RefreshIndicator(
onRefresh: () async {
- await _dynamicDetailController!.queryReplyList();
+ await _dynamicDetailController.queryReplyList();
},
- child: CustomScrollView(
- controller: scrollController,
- slivers: [
- if (action != 'comment')
- SliverToBoxAdapter(
- child: DynamicPanel(
- item: _dynamicDetailController!.item,
- source: 'detail',
- ),
- ),
- SliverPersistentHeader(
- delegate: _MySliverPersistentHeaderDelegate(
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surface,
- border: Border(
- top: BorderSide(
- width: 0.6,
- color: Theme.of(context).dividerColor.withOpacity(0.05),
- ),
+ child: Stack(
+ children: [
+ CustomScrollView(
+ controller: scrollController,
+ slivers: [
+ if (action != 'comment')
+ SliverToBoxAdapter(
+ child: DynamicPanel(
+ item: _dynamicDetailController.item,
+ source: 'detail',
),
),
- height: 45,
- padding: const EdgeInsets.only(left: 12, right: 6),
- child: Row(
- children: [
- Obx(
- () => AnimatedSwitcher(
- duration: const Duration(milliseconds: 400),
- transitionBuilder:
- (Widget child, Animation animation) {
- return ScaleTransition(
- scale: animation, child: child);
- },
- child: Text(
- '${_dynamicDetailController!.acount.value}',
- key: ValueKey(
- _dynamicDetailController!.acount.value),
+ SliverPersistentHeader(
+ delegate: _MySliverPersistentHeaderDelegate(
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surface,
+ border: Border(
+ top: BorderSide(
+ width: 0.6,
+ color: Theme.of(context)
+ .dividerColor
+ .withOpacity(0.05),
),
),
),
- const Text('条回复'),
- const Spacer(),
- SizedBox(
- height: 35,
- child: TextButton.icon(
- onPressed: () =>
- _dynamicDetailController!.queryBySort(),
- icon: const Icon(Icons.sort, size: 16),
- label: Obx(() => Text(
- _dynamicDetailController!.sortTypeLabel.value,
- style: const TextStyle(fontSize: 13),
- )),
- ),
- )
- ],
- ),
- ),
- ),
- pinned: true,
- ),
- FutureBuilder(
- future: _futureBuilderFuture,
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done) {
- Map data = snapshot.data as Map;
- if (snapshot.data['status']) {
- // 请求成功
- return Obx(
- () => _dynamicDetailController!.replyList.isEmpty &&
- _dynamicDetailController!.isLoadingMore
- ? SliverList(
- delegate:
- SliverChildBuilderDelegate((context, index) {
- return const VideoReplySkeleton();
- }, childCount: 8),
- )
- : SliverList(
- delegate: SliverChildBuilderDelegate(
- (context, index) {
- if (index ==
- _dynamicDetailController!
- .replyList.length) {
- return Container(
- padding: EdgeInsets.only(
- bottom: MediaQuery.of(context)
- .padding
- .bottom),
- height: MediaQuery.of(context)
- .padding
- .bottom +
- 100,
- child: Center(
- child: Obx(
- () => Text(
- _dynamicDetailController!
- .noMore.value,
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(context)
- .colorScheme
- .outline,
- ),
- ),
- ),
- ),
- );
- } else {
- return ReplyItem(
- replyItem: _dynamicDetailController!
- .replyList[index],
- showReplyRow: true,
- replyLevel: '1',
- replyReply: (replyItem) =>
- replyReply(replyItem),
- replyType: ReplyType.values[type],
- addReply: (replyItem) {
- _dynamicDetailController!
- .replyList[index].replies!
- .add(replyItem);
- },
- );
- }
- },
- childCount:
- _dynamicDetailController!.replyList.length +
- 1,
+ height: 45,
+ padding: const EdgeInsets.only(left: 12, right: 6),
+ child: Row(
+ children: [
+ Obx(
+ () => AnimatedSwitcher(
+ duration: const Duration(milliseconds: 400),
+ transitionBuilder:
+ (Widget child, Animation animation) {
+ return ScaleTransition(
+ scale: animation, child: child);
+ },
+ child: Text(
+ '${_dynamicDetailController.acount.value}',
+ key: ValueKey(
+ _dynamicDetailController.acount.value),
),
),
+ ),
+ const Text('条回复'),
+ const Spacer(),
+ SizedBox(
+ height: 35,
+ child: TextButton.icon(
+ onPressed: () =>
+ _dynamicDetailController.queryBySort(),
+ icon: const Icon(Icons.sort, size: 16),
+ label: Obx(() => Text(
+ _dynamicDetailController
+ .sortTypeLabel.value,
+ style: const TextStyle(fontSize: 13),
+ )),
+ ),
+ )
+ ],
+ ),
+ ),
+ ),
+ pinned: true,
+ ),
+ FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ Map data = snapshot.data as Map;
+ if (snapshot.data['status']) {
+ // 请求成功
+ return Obx(
+ () => _dynamicDetailController.replyList.isEmpty &&
+ _dynamicDetailController.isLoadingMore
+ ? SliverList(
+ delegate: SliverChildBuilderDelegate(
+ (context, index) {
+ return const VideoReplySkeleton();
+ }, childCount: 8),
+ )
+ : SliverList(
+ delegate: SliverChildBuilderDelegate(
+ (context, index) {
+ if (index ==
+ _dynamicDetailController
+ .replyList.length) {
+ return Container(
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(context)
+ .padding
+ .bottom),
+ height: MediaQuery.of(context)
+ .padding
+ .bottom +
+ 100,
+ child: Center(
+ child: Obx(
+ () => Text(
+ _dynamicDetailController
+ .noMore.value,
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context)
+ .colorScheme
+ .outline,
+ ),
+ ),
+ ),
+ ),
+ );
+ } else {
+ return ReplyItem(
+ replyItem: _dynamicDetailController
+ .replyList[index],
+ showReplyRow: true,
+ replyLevel: '1',
+ replyReply: (replyItem) =>
+ replyReply(replyItem),
+ replyType: ReplyType.values[type],
+ addReply: (replyItem) {
+ _dynamicDetailController
+ .replyList[index].replies!
+ .add(replyItem);
+ },
+ );
+ }
+ },
+ childCount: _dynamicDetailController
+ .replyList.length +
+ 1,
+ ),
+ ),
+ );
+ } else {
+ // 请求错误
+ return HttpError(
+ errMsg: data['msg'],
+ fn: () => setState(() {}),
+ );
+ }
+ } else {
+ // 骨架屏
+ return SliverList(
+ delegate: SliverChildBuilderDelegate((context, index) {
+ return const VideoReplySkeleton();
+ }, childCount: 8),
+ );
+ }
+ },
+ )
+ ],
+ ),
+ Positioned(
+ bottom: MediaQuery.of(context).padding.bottom + 14,
+ right: 14,
+ child: SlideTransition(
+ position: Tween(
+ begin: const Offset(0, 2),
+ end: const Offset(0, 0),
+ ).animate(CurvedAnimation(
+ parent: fabAnimationCtr,
+ curve: Curves.easeInOut,
+ )),
+ child: FloatingActionButton(
+ heroTag: null,
+ onPressed: () {
+ feedBack();
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ builder: (BuildContext context) {
+ return VideoReplyNewDialog(
+ oid: _dynamicDetailController.oid ??
+ IdUtils.bv2av(Get.parameters['bvid']!),
+ root: 0,
+ parent: 0,
+ replyType: ReplyType.values[type],
+ );
+ },
+ ).then(
+ (value) => {
+ // 完成评论,数据添加
+ if (value != null && value['data'] != null)
+ {
+ _dynamicDetailController.replyList
+ .add(value['data']),
+ _dynamicDetailController.acount.value++
+ }
+ },
);
- } else {
- // 请求错误
- return HttpError(
- errMsg: data['msg'],
- fn: () => setState(() {}),
- );
- }
- } else {
- // 骨架屏
- return SliverList(
- delegate: SliverChildBuilderDelegate((context, index) {
- return const VideoReplySkeleton();
- }, childCount: 8),
- );
- }
- },
- )
+ },
+ tooltip: '评论动态',
+ child: const Icon(Icons.reply),
+ ),
+ ),
+ ),
],
),
),
diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart
index d7d40021..cad4bbd7 100644
--- a/lib/pages/dynamics/view.dart
+++ b/lib/pages/dynamics/view.dart
@@ -212,6 +212,9 @@ class _DynamicsPageState extends State
future: _futureBuilderFutureUp,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
+ if (snapshot.data == null) {
+ return const SliverToBoxAdapter(child: SizedBox());
+ }
Map data = snapshot.data;
if (data['status']) {
return Obx(() => UpPanel(_dynamicsController.upData.value));
@@ -232,6 +235,9 @@ class _DynamicsPageState extends State
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
+ if (snapshot.data == null) {
+ return const SliverToBoxAdapter(child: SizedBox());
+ }
Map data = snapshot.data;
if (data['status']) {
List list =
diff --git a/lib/pages/dynamics/widgets/additional_panel.dart b/lib/pages/dynamics/widgets/additional_panel.dart
index e283fcf9..fa11f217 100644
--- a/lib/pages/dynamics/widgets/additional_panel.dart
+++ b/lib/pages/dynamics/widgets/additional_panel.dart
@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/http/search.dart';
/// TODO 点击跳转
Widget addWidget(item, context, type, {floor = 1}) {
@@ -19,8 +22,27 @@ Widget addWidget(item, context, type, {floor = 1}) {
: Theme.of(context).colorScheme.background;
switch (type) {
case 'ADDITIONAL_TYPE_UGC':
+ // 转发的投稿
return InkWell(
- onTap: () {},
+ onTap: () async {
+ String text = dynamicProperty[type].jumpUrl;
+ RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false);
+ Iterable matches = bvRegex.allMatches(text);
+ if (matches.isNotEmpty) {
+ Match match = matches.first;
+ String bvid = match.group(0)!;
+ String cover = dynamicProperty[type].cover;
+ try {
+ int cid = await SearchHttp.ab2c(bvid: bvid);
+ Get.toNamed('/video?bvid=$bvid&cid=$cid',
+ arguments: {'pic': cover, 'heroTag': bvid});
+ } catch (err) {
+ SmartDialog.showToast(err.toString());
+ }
+ } else {
+ print("No match found.");
+ }
+ },
child: Container(
padding:
const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
@@ -61,101 +83,111 @@ Widget addWidget(item, context, type, {floor = 1}) {
);
case 'ADDITIONAL_TYPE_RESERVE':
return dynamicProperty[type].state != -1
- ? Padding(
- padding: const EdgeInsets.only(top: 8),
- child: InkWell(
- onTap: () {},
- child: Container(
- width: double.infinity,
- padding: const EdgeInsets.only(
- left: 12, top: 10, right: 12, bottom: 10),
- color: bgColor,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- dynamicProperty[type].title,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
+ ? dynamicProperty[type].title != null
+ ? Padding(
+ padding: const EdgeInsets.only(top: 8),
+ child: InkWell(
+ onTap: () {},
+ child: Container(
+ width: double.infinity,
+ padding: const EdgeInsets.only(
+ left: 12, top: 10, right: 12, bottom: 10),
+ color: bgColor,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ dynamicProperty[type].title,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 1),
+ Text.rich(
+ TextSpan(
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.outline,
+ fontSize: Theme.of(context)
+ .textTheme
+ .labelMedium!
+ .fontSize),
+ children: [
+ if (dynamicProperty[type].desc1 != null)
+ TextSpan(
+ text:
+ dynamicProperty[type].desc1['text']),
+ const TextSpan(text: ' '),
+ if (dynamicProperty[type].desc2 != null)
+ TextSpan(
+ text:
+ dynamicProperty[type].desc2['text']),
+ ],
+ ),
+ )
+ ],
),
- const SizedBox(height: 1),
- Text.rich(
- TextSpan(
- style: TextStyle(
- color: Theme.of(context).colorScheme.outline,
- fontSize: Theme.of(context)
- .textTheme
- .labelMedium!
- .fontSize),
- children: [
- TextSpan(text: dynamicProperty[type].desc1['text']),
- const TextSpan(text: ' '),
- TextSpan(text: dynamicProperty[type].desc2['text']),
- ],
- ),
- )
- ],
- ),
- // TextButton(onPressed: () {}, child: Text('123'))
- ),
- ),
- )
- : const SizedBox();
- case 'ADDITIONAL_TYPE_GOODS':
- return Padding(
- padding: const EdgeInsets.only(top: 6),
- child: InkWell(
- onTap: () {},
- child: Container(
- padding:
- const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
- decoration: BoxDecoration(
- color: bgColor,
- borderRadius: const BorderRadius.all(Radius.circular(6)),
- ),
- child: Row(
- children: [
- NetworkImgLayer(
- width: 75,
- height: 75,
- src: dynamicProperty[type].items.first.cover,
- ),
- const SizedBox(width: 10),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: MainAxisAlignment.start,
- children: [
- Text(
- dynamicProperty[type].items.first.name,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- Text(
- dynamicProperty[type].items.first.brief,
- maxLines: 1,
- style: TextStyle(
- color: Theme.of(context).colorScheme.outline,
- fontSize: Theme.of(context)
- .textTheme
- .labelMedium!
- .fontSize,
- ),
- ),
- const SizedBox(height: 2),
- Text(
- dynamicProperty[type].items.first.price,
- style: TextStyle(
- color: Theme.of(context).colorScheme.primary,
- ),
- ),
- ],
+ // TextButton(onPressed: () {}, child: Text('123'))
),
),
- ],
- ),
- ),
- ));
+ )
+ : const SizedBox()
+ : const SizedBox();
+ case 'ADDITIONAL_TYPE_GOODS':
+ // 商品
+ return const SizedBox();
+ // return Padding(
+ // padding: const EdgeInsets.only(top: 6),
+ // child: InkWell(
+ // onTap: () {},
+ // child: Container(
+ // padding:
+ // const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
+ // decoration: BoxDecoration(
+ // color: bgColor,
+ // borderRadius: const BorderRadius.all(Radius.circular(6)),
+ // ),
+ // child: Row(
+ // children: [
+ // NetworkImgLayer(
+ // width: 75,
+ // height: 75,
+ // src: dynamicProperty[type].items.first.cover,
+ // ),
+ // const SizedBox(width: 10),
+ // Expanded(
+ // child: Column(
+ // crossAxisAlignment: CrossAxisAlignment.start,
+ // mainAxisAlignment: MainAxisAlignment.start,
+ // children: [
+ // Text(
+ // dynamicProperty[type].items.first.name,
+ // maxLines: 1,
+ // overflow: TextOverflow.ellipsis,
+ // ),
+ // Text(
+ // dynamicProperty[type].items.first.brief,
+ // maxLines: 1,
+ // style: TextStyle(
+ // color: Theme.of(context).colorScheme.outline,
+ // fontSize: Theme.of(context)
+ // .textTheme
+ // .labelMedium!
+ // .fontSize,
+ // ),
+ // ),
+ // const SizedBox(height: 2),
+ // Text(
+ // dynamicProperty[type].items.first.price,
+ // style: TextStyle(
+ // color: Theme.of(context).colorScheme.primary,
+ // ),
+ // ),
+ // ],
+ // ),
+ // ),
+ // ],
+ // ),
+ // ),
+ // ),);
case 'ADDITIONAL_TYPE_MATCH':
return const SizedBox();
case 'ADDITIONAL_TYPE_COMMON':
diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart
index 67a21371..b6ea5eb9 100644
--- a/lib/pages/dynamics/widgets/author_panel.dart
+++ b/lib/pages/dynamics/widgets/author_panel.dart
@@ -1,65 +1,163 @@
import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/http/user.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart';
-Widget author(item, context) {
- String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid);
- return Row(
- children: [
- GestureDetector(
- onTap: () {
- feedBack();
- Get.toNamed(
- '/member?mid=${item.modules.moduleAuthor.mid}',
- arguments: {
- 'face': item.modules.moduleAuthor.face,
- 'heroTag': heroTag
- },
- );
- },
- child: Hero(
- tag: heroTag,
- child: NetworkImgLayer(
- width: 40,
- height: 40,
- type: 'avatar',
- src: item.modules.moduleAuthor.face,
+class AuthorPanel extends StatelessWidget {
+ final dynamic item;
+ const AuthorPanel({super.key, required this.item});
+
+ @override
+ Widget build(BuildContext context) {
+ String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid);
+ return Row(
+ children: [
+ GestureDetector(
+ onTap: () {
+ // 番剧
+ if (item.modules.moduleAuthor.type == 'AUTHOR_TYPE_PGC') {
+ return;
+ }
+ feedBack();
+ Get.toNamed(
+ '/member?mid=${item.modules.moduleAuthor.mid}',
+ arguments: {
+ 'face': item.modules.moduleAuthor.face,
+ 'heroTag': heroTag
+ },
+ );
+ },
+ child: Hero(
+ tag: heroTag,
+ child: NetworkImgLayer(
+ width: 40,
+ height: 40,
+ type: 'avatar',
+ src: item.modules.moduleAuthor.face,
+ ),
),
),
- ),
- const SizedBox(width: 10),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- item.modules.moduleAuthor.name,
- style: TextStyle(
- color: item.modules.moduleAuthor!.vip != null &&
- item.modules.moduleAuthor!.vip['status'] > 0
- ? const Color.fromARGB(255, 251, 100, 163)
- : Theme.of(context).colorScheme.onBackground,
- fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
+ const SizedBox(width: 10),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ item.modules.moduleAuthor.name,
+ style: TextStyle(
+ color: item.modules.moduleAuthor!.vip != null &&
+ item.modules.moduleAuthor!.vip['status'] > 0
+ ? const Color.fromARGB(255, 251, 100, 163)
+ : Theme.of(context).colorScheme.onBackground,
+ fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
+ ),
+ ),
+ DefaultTextStyle.merge(
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.outline,
+ fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
+ ),
+ child: Row(
+ children: [
+ Text(item.modules.moduleAuthor.pubTime),
+ if (item.modules.moduleAuthor.pubTime != '' &&
+ item.modules.moduleAuthor.pubAction != '')
+ const Text(' '),
+ Text(item.modules.moduleAuthor.pubAction),
+ ],
+ ),
+ )
+ ],
+ ),
+ const Spacer(),
+ if (item.type == 'DYNAMIC_TYPE_AV')
+ SizedBox(
+ width: 32,
+ height: 32,
+ child: IconButton(
+ style: ButtonStyle(
+ padding: MaterialStateProperty.all(EdgeInsets.zero),
+ ),
+ onPressed: () {
+ showModalBottomSheet(
+ context: context,
+ useRootNavigator: true,
+ isScrollControlled: true,
+ builder: (context) {
+ return MorePanel(item: item);
+ },
+ );
+ },
+ icon: const Icon(Icons.more_vert_outlined, size: 18),
),
),
- DefaultTextStyle.merge(
- style: TextStyle(
- color: Theme.of(context).colorScheme.outline,
- fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
+ ],
+ );
+ }
+}
+
+class MorePanel extends StatelessWidget {
+ final dynamic item;
+ const MorePanel({super.key, required this.item});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
+ // clipBehavior: Clip.hardEdge,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ 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.outline,
+ borderRadius: const BorderRadius.all(Radius.circular(3))),
+ ),
+ ),
),
- child: Row(
- children: [
- Text(item.modules.moduleAuthor.pubTime),
- if (item.modules.moduleAuthor.pubTime != '' &&
- item.modules.moduleAuthor.pubAction != '')
- const Text(' '),
- Text(item.modules.moduleAuthor.pubAction),
- ],
+ ),
+ ListTile(
+ onTap: () async {
+ try {
+ String bvid = item.modules.moduleDynamic.major.archive.bvid;
+ var res = await UserHttp.toViewLater(bvid: bvid);
+ SmartDialog.showToast(res['msg']);
+ Get.back();
+ } catch (err) {
+ SmartDialog.showToast('出错了:${err.toString()}');
+ }
+ },
+ minLeadingWidth: 0,
+ // dense: true,
+ leading: const Icon(Icons.watch_later_outlined, size: 19),
+ title: Text(
+ '稍后再看',
+ style: Theme.of(context).textTheme.titleSmall,
),
- )
+ ),
+ const Divider(thickness: 0.1, height: 1),
+ ListTile(
+ onTap: () => Get.back(),
+ minLeadingWidth: 0,
+ dense: true,
+ title: Text(
+ '取消',
+ style: TextStyle(color: Theme.of(context).colorScheme.outline),
+ textAlign: TextAlign.center,
+ ),
+ ),
],
),
- ],
- );
+ );
+ }
}
diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart
index 34324d2e..680d21a2 100644
--- a/lib/pages/dynamics/widgets/content_panel.dart
+++ b/lib/pages/dynamics/widgets/content_panel.dart
@@ -1,40 +1,183 @@
// 内容
import 'package:flutter/material.dart';
+import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/models/dynamics/result.dart';
+import 'package:pilipala/pages/preview/index.dart';
import 'rich_node_panel.dart';
-Widget content(item, context, source) {
- TextStyle authorStyle =
- TextStyle(color: Theme.of(context).colorScheme.primary);
- return Container(
- width: double.infinity,
- padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (item.modules.moduleDynamic.topic != null) ...[
- GestureDetector(
- child: Text(
- '#${item.modules.moduleDynamic.topic.name}',
- style: authorStyle,
- ),
- ),
- ],
- IgnorePointer(
- // 禁用SelectableRegion的触摸交互功能
- ignoring: source == 'detail' ? false : true,
- child: SelectableRegion(
- magnifierConfiguration: const TextMagnifierConfiguration(),
- focusNode: FocusNode(),
- selectionControls: MaterialTextSelectionControls(),
- child: Text.rich(
- richNode(item, context),
- maxLines: source == 'detail' ? 999 : 3,
- overflow: TextOverflow.ellipsis,
- ),
+// ignore: must_be_immutable
+class Content extends StatefulWidget {
+ dynamic item;
+ String? source;
+ Content({
+ super.key,
+ this.item,
+ this.source,
+ });
+
+ @override
+ State createState() => _ContentState();
+}
+
+class _ContentState extends State {
+ late bool hasPics;
+ List pics = [];
+
+ @override
+ void initState() {
+ super.initState();
+ hasPics = widget.item.modules.moduleDynamic.major != null &&
+ widget.item.modules.moduleDynamic.major.opus != null &&
+ widget.item.modules.moduleDynamic.major.opus.pics.isNotEmpty;
+ if (hasPics) {
+ pics = widget.item.modules.moduleDynamic.major.opus.pics;
+ }
+ }
+
+ InlineSpan picsNodes() {
+ List spanChilds = [];
+ int len = pics.length;
+ List picList = [];
+
+ if (len == 1) {
+ OpusPicsModel pictureItem = pics.first;
+ picList.add(pictureItem.url!);
+ spanChilds.add(const TextSpan(text: '\n'));
+ spanChilds.add(
+ WidgetSpan(
+ child: LayoutBuilder(
+ builder: (context, BoxConstraints box) {
+ return GestureDetector(
+ onTap: () {
+ showDialog(
+ useSafeArea: false,
+ context: context,
+ builder: (context) {
+ return ImagePreview(initialPage: 0, imgList: picList);
+ },
+ );
+ },
+ child: Padding(
+ padding: const EdgeInsets.only(top: 4),
+ child: NetworkImgLayer(
+ src: pictureItem.url,
+ width: box.maxWidth / 2,
+ height: box.maxWidth *
+ 0.5 *
+ (pictureItem.height != null && pictureItem.width != null
+ ? pictureItem.height! / pictureItem.width!
+ : 1),
+ ),
+ ),
+ );
+ },
),
),
- ],
- ),
- );
+ );
+ }
+ if (len > 1) {
+ List list = [];
+ for (var i = 0; i < len; i++) {
+ picList.add(pics[i].url!);
+ list.add(
+ LayoutBuilder(
+ builder: (context, BoxConstraints box) {
+ return GestureDetector(
+ onTap: () {
+ showDialog(
+ useSafeArea: false,
+ context: context,
+ builder: (context) {
+ return ImagePreview(initialPage: i, imgList: picList);
+ },
+ );
+ },
+ child: NetworkImgLayer(
+ src: pics[i].url,
+ width: box.maxWidth,
+ height: box.maxWidth,
+ ),
+ );
+ },
+ ),
+ );
+ }
+ spanChilds.add(
+ WidgetSpan(
+ child: LayoutBuilder(
+ builder: (context, BoxConstraints box) {
+ double maxWidth = box.maxWidth;
+ double crossCount = len < 3 ? 2 : 3;
+ double height = maxWidth /
+ crossCount *
+ (len % crossCount == 0
+ ? len ~/ crossCount
+ : len ~/ crossCount + 1) +
+ 6;
+ return Container(
+ padding: const EdgeInsets.only(top: 6),
+ height: height,
+ child: GridView.count(
+ padding: EdgeInsets.zero,
+ physics: const NeverScrollableScrollPhysics(),
+ crossAxisCount: crossCount.toInt(),
+ mainAxisSpacing: 4.0,
+ crossAxisSpacing: 4.0,
+ childAspectRatio: 1,
+ children: list,
+ ),
+ );
+ },
+ ),
+ ),
+ );
+ }
+ return TextSpan(
+ children: spanChilds,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ TextStyle authorStyle =
+ TextStyle(color: Theme.of(context).colorScheme.primary);
+
+ return Container(
+ width: double.infinity,
+ padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (widget.item.modules.moduleDynamic.topic != null) ...[
+ GestureDetector(
+ child: Text(
+ '#${widget.item.modules.moduleDynamic.topic.name}',
+ style: authorStyle,
+ ),
+ ),
+ ],
+ IgnorePointer(
+ // 禁用SelectableRegion的触摸交互功能
+ ignoring: widget.source == 'detail' ? false : true,
+ child: SelectableRegion(
+ magnifierConfiguration: const TextMagnifierConfiguration(),
+ focusNode: FocusNode(),
+ selectionControls: MaterialTextSelectionControls(),
+ child: Text.rich(
+ /// fix 默认20px高度
+ style: const TextStyle(height: 0),
+ richNode(widget.item, context),
+ maxLines: widget.source == 'detail' ? 999 : 3,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ),
+ if (hasPics) ...[
+ Text.rich(picsNodes()),
+ ]
+ ],
+ ),
+ );
+ }
}
diff --git a/lib/pages/dynamics/widgets/dynamic_panel.dart b/lib/pages/dynamics/widgets/dynamic_panel.dart
index ef0bc8cc..c85cad45 100644
--- a/lib/pages/dynamics/widgets/dynamic_panel.dart
+++ b/lib/pages/dynamics/widgets/dynamic_panel.dart
@@ -39,10 +39,11 @@ class DynamicPanel extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
- child: author(item, context),
+ child: AuthorPanel(item: item),
),
- if (item!.modules!.moduleDynamic!.desc != null)
- content(item, context, source),
+ if (item!.modules!.moduleDynamic!.desc != null ||
+ item!.modules!.moduleDynamic!.major != null)
+ Content(item: item, source: source),
forWard(item, context, _dynamicsController, source),
const SizedBox(height: 2),
if (source == null) ActionPanel(item: item),
diff --git a/lib/pages/dynamics/widgets/forward_panel.dart b/lib/pages/dynamics/widgets/forward_panel.dart
index 55972e37..5a8d9f17 100644
--- a/lib/pages/dynamics/widgets/forward_panel.dart
+++ b/lib/pages/dynamics/widgets/forward_panel.dart
@@ -44,19 +44,21 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
],
),
const SizedBox(height: 2),
- if (item.modules.moduleDynamic.topic != null) ...[
- Padding(
- padding: floor == 2
- ? EdgeInsets.zero
- : const EdgeInsets.only(left: 12, right: 12),
- child: GestureDetector(
- child: Text(
- '#${item.modules.moduleDynamic.topic.name}',
- style: authorStyle,
- ),
- ),
- ),
- ],
+
+ /// fix #话题跟content重复
+ // if (item.modules.moduleDynamic.topic != null) ...[
+ // Padding(
+ // padding: floor == 2
+ // ? EdgeInsets.zero
+ // : const EdgeInsets.only(left: 12, right: 12),
+ // child: GestureDetector(
+ // child: Text(
+ // '#${item.modules.moduleDynamic.topic.name}',
+ // style: authorStyle,
+ // ),
+ // ),
+ // ),
+ // ],
Text.rich(
richNode(item, context),
// 被转发状态(floor=2) 隐藏
@@ -71,6 +73,8 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
: const EdgeInsets.only(left: 12, right: 12),
child: picWidget(item, context),
),
+
+ /// 附加内容 商品信息、直播预约等等
if (item.modules.moduleDynamic.additional != null)
addWidget(
item,
@@ -133,7 +137,12 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
],
),
const SizedBox(height: 8),
- Text(item.modules.moduleDynamic.desc.text)
+ Text.rich(
+ richNode(item, context),
+ // 被转发状态(floor=2) 隐藏
+ maxLines: source == 'detail' && floor != 2 ? 999 : 4,
+ overflow: TextOverflow.ellipsis,
+ ),
],
)
: item.modules.moduleDynamic.additional != null
diff --git a/lib/pages/dynamics/widgets/pic_panel.dart b/lib/pages/dynamics/widgets/pic_panel.dart
index 9ee8be53..25b22c21 100644
--- a/lib/pages/dynamics/widgets/pic_panel.dart
+++ b/lib/pages/dynamics/widgets/pic_panel.dart
@@ -1,20 +1,22 @@
import 'package:flutter/material.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/pages/preview/index.dart';
Widget picWidget(item, context) {
String type = item.modules.moduleDynamic.major.type;
List pictures = [];
if (type == 'MAJOR_TYPE_OPUS') {
- pictures = item.modules.moduleDynamic.major.opus.pics;
+ /// fix 图片跟rich_node_panel重复
+ // pictures = item.modules.moduleDynamic.major.opus.pics;
+ return const SizedBox();
}
if (type == 'MAJOR_TYPE_DRAW') {
pictures = item.modules.moduleDynamic.major.draw.items;
}
int len = pictures.length;
- List picList = [];
+ List picList = [];
List list = [];
for (var i = 0; i < len; i++) {
picList.add(pictures[i].src ?? pictures[i].url);
@@ -23,11 +25,14 @@ Widget picWidget(item, context) {
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
- Get.toNamed('/preview',
- arguments: {'initialPage': i, 'imgList': picList});
+ showDialog(
+ useSafeArea: false,
+ context: context,
+ builder: (context) {
+ return ImagePreview(initialPage: i, imgList: picList);
+ },
+ );
},
- // child: Hero(
- // tag: pictures[i].src ?? pictures[i].url,
child: NetworkImgLayer(
src: pictures[i].src ?? pictures[i].url,
width: box.maxWidth,
diff --git a/lib/pages/dynamics/widgets/rich_node_panel.dart b/lib/pages/dynamics/widgets/rich_node_panel.dart
index 78d5aaba..a66772a4 100644
--- a/lib/pages/dynamics/widgets/rich_node_panel.dart
+++ b/lib/pages/dynamics/widgets/rich_node_panel.dart
@@ -1,175 +1,324 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/models/dynamics/result.dart';
+import 'package:pilipala/pages/preview/index.dart';
// 富文本
InlineSpan richNode(item, context) {
- TextStyle authorStyle =
- TextStyle(color: Theme.of(context).colorScheme.primary);
- List spanChilds = [];
- for (var i in item.modules.moduleDynamic.desc.richTextNodes) {
- if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
- spanChilds.add(
- TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));
+ final spacer = _VerticalSpaceSpan(0.0);
+ try {
+ TextStyle authorStyle =
+ TextStyle(color: Theme.of(context).colorScheme.primary);
+ List spanChilds = [];
+ String contentType = 'desc';
+
+ dynamic richTextNodes;
+ if (item.modules.moduleDynamic.desc != null) {
+ richTextNodes = item.modules.moduleDynamic.desc.richTextNodes;
+ } else if (item.modules.moduleDynamic.major != null) {
+ contentType = 'major';
+ // 动态页面 richTextNodes 层级可能与主页动态层级不同
+ richTextNodes =
+ item.modules.moduleDynamic.major.opus.summary.richTextNodes;
}
- // @用户
- if (i.type == 'RICH_TEXT_NODE_TYPE_AT') {
- spanChilds.add(
- WidgetSpan(
- alignment: PlaceholderAlignment.middle,
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- GestureDetector(
- onTap: () => Get.toNamed('/member?mid=${i.rid}',
- arguments: {'face': null}),
+ if (richTextNodes == null || richTextNodes.isEmpty) {
+ return spacer;
+ } else {
+ for (var i in richTextNodes) {
+ /// fix 渲染专栏时内容会重复
+ // if (item.modules.moduleDynamic.major.opus.title == null &&
+ // i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
+ if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
+ spanChilds.add(
+ TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));
+ }
+ // @用户
+ if (i.type == 'RICH_TEXT_NODE_TYPE_AT') {
+ spanChilds.add(
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ GestureDetector(
+ onTap: () => Get.toNamed('/member?mid=${i.rid}',
+ arguments: {'face': null}),
+ child: Text(
+ ' ${i.text}',
+ style: authorStyle,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+ // 话题
+ if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {
+ spanChilds.add(
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: GestureDetector(
+ onTap: () {},
child: Text(
- ' ${i.text}',
+ '${i.origText}',
style: authorStyle,
),
),
- ],
- ),
- ),
- );
- }
- // 话题
- if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {
- spanChilds.add(
- WidgetSpan(
- alignment: PlaceholderAlignment.middle,
- child: GestureDetector(
- onTap: () {},
- child: Text(
- '${i.origText}',
- style: authorStyle,
),
- ),
- ),
- );
- }
- // 网页链接
- if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') {
- spanChilds.add(
- WidgetSpan(
- alignment: PlaceholderAlignment.middle,
- child: Icon(
- Icons.link,
- size: 20,
- color: Theme.of(context).colorScheme.primary,
- ),
- ),
- );
- spanChilds.add(
- WidgetSpan(
- alignment: PlaceholderAlignment.middle,
- child: GestureDetector(
- onTap: () {
- Get.toNamed(
- '/webview',
- parameters: {'url': i.origText, 'type': 'url', 'pageTitle': ''},
- );
- },
- child: Text(
- i.text,
- style: authorStyle,
+ );
+ }
+ // 网页链接
+ if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') {
+ spanChilds.add(
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: Icon(
+ Icons.link,
+ size: 20,
+ color: Theme.of(context).colorScheme.primary,
+ ),
),
- ),
- ),
- );
- }
- // 投票
- if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') {
- spanChilds.add(
- WidgetSpan(
- alignment: PlaceholderAlignment.middle,
- child: GestureDetector(
- onTap: () {
- String dynamicId = item.basic['comment_id_str'];
- Get.toNamed(
- '/webview',
- parameters: {
- 'url':
- 'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',
- 'type': 'vote',
- 'pageTitle': '投票'
+ );
+ spanChilds.add(
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: GestureDetector(
+ onTap: () {
+ Get.toNamed(
+ '/webview',
+ parameters: {
+ 'url': i.origText,
+ 'type': 'url',
+ 'pageTitle': ''
+ },
+ );
},
- );
- },
- child: Text(
- '投票:${i.text}',
- style: authorStyle,
+ child: Text(
+ i.text,
+ style: authorStyle,
+ ),
+ ),
),
- ),
- ),
- );
- }
- // 表情
- if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI') {
- spanChilds.add(
- WidgetSpan(
- child: NetworkImgLayer(
- src: i.emoji.iconUrl,
- type: 'emote',
- width: i.emoji.size * 20,
- height: i.emoji.size * 20,
- ),
- ),
- );
- }
- // 抽奖
- if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') {
- spanChilds.add(
- WidgetSpan(
- alignment: PlaceholderAlignment.middle,
- child: Icon(
- Icons.redeem_rounded,
- size: 16,
- color: Theme.of(context).colorScheme.primary,
- ),
- ),
- );
- spanChilds.add(
- WidgetSpan(
- alignment: PlaceholderAlignment.middle,
- child: GestureDetector(
- onTap: () {},
- child: Text(
- '${i.origText} ',
- style: authorStyle,
+ );
+ }
+ // 投票
+ if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') {
+ spanChilds.add(
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: GestureDetector(
+ onTap: () {
+ try {
+ String dynamicId = item.basic['comment_id_str'];
+ Get.toNamed(
+ '/webview',
+ parameters: {
+ 'url':
+ 'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',
+ 'type': 'vote',
+ 'pageTitle': '投票'
+ },
+ );
+ } catch (_) {}
+ },
+ child: Text(
+ '投票:${i.text}',
+ style: authorStyle,
+ ),
+ ),
),
- ),
- ),
- );
- }
+ );
+ }
+ // 表情
+ if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI') {
+ spanChilds.add(
+ WidgetSpan(
+ child: NetworkImgLayer(
+ src: i.emoji.iconUrl,
+ type: 'emote',
+ width: i.emoji.size * 20,
+ height: i.emoji.size * 20,
+ ),
+ ),
+ );
+ }
+ // 抽奖
+ if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') {
+ spanChilds.add(
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: Icon(
+ Icons.redeem_rounded,
+ size: 16,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ );
+ spanChilds.add(
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: GestureDetector(
+ onTap: () {},
+ child: Text(
+ '${i.origText} ',
+ style: authorStyle,
+ ),
+ ),
+ ),
+ );
+ }
- /// TODO 商品
- if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') {
- spanChilds.add(
- WidgetSpan(
- alignment: PlaceholderAlignment.middle,
- child: Icon(
- Icons.shopping_bag_outlined,
- size: 16,
- color: Theme.of(context).colorScheme.primary,
- ),
- ),
- );
- spanChilds.add(
- WidgetSpan(
- alignment: PlaceholderAlignment.middle,
- child: GestureDetector(
- onTap: () {},
- child: Text(
- '${i.text} ',
- style: authorStyle,
+ /// TODO 商品
+ if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') {
+ spanChilds.add(
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: Icon(
+ Icons.shopping_bag_outlined,
+ size: 16,
+ color: Theme.of(context).colorScheme.primary,
+ ),
),
- ),
- ),
+ );
+ spanChilds.add(
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: GestureDetector(
+ onTap: () {},
+ child: Text(
+ '${i.text} ',
+ style: authorStyle,
+ ),
+ ),
+ ),
+ );
+ }
+ }
+ // if (contentType == 'major' &&
+ // item.modules.moduleDynamic.major.opus.pics.isNotEmpty) {
+ // // 图片可能跟其他widget重复渲染
+ // List pics = item.modules.moduleDynamic.major.opus.pics;
+ // int len = pics.length;
+ // List picList = [];
+
+ // if (len == 1) {
+ // OpusPicsModel pictureItem = pics.first;
+ // picList.add(pictureItem.url!);
+ // spanChilds.add(const TextSpan(text: '\n'));
+ // spanChilds.add(
+ // WidgetSpan(
+ // child: LayoutBuilder(
+ // builder: (context, BoxConstraints box) {
+ // return GestureDetector(
+ // onTap: () {
+ // showDialog(
+ // useSafeArea: false,
+ // context: context,
+ // builder: (context) {
+ // return ImagePreview(initialPage: 0, imgList: picList);
+ // },
+ // );
+ // },
+ // child: Padding(
+ // padding: const EdgeInsets.only(top: 4),
+ // child: NetworkImgLayer(
+ // src: pictureItem.url,
+ // width: box.maxWidth / 2,
+ // height: box.maxWidth *
+ // 0.5 *
+ // (pictureItem.height != null &&
+ // pictureItem.width != null
+ // ? pictureItem.height! / pictureItem.width!
+ // : 1),
+ // ),
+ // ),
+ // );
+ // },
+ // ),
+ // ),
+ // );
+ // }
+ // if (len > 1) {
+ // List list = [];
+ // for (var i = 0; i < len; i++) {
+ // picList.add(pics[i].url!);
+ // list.add(
+ // LayoutBuilder(
+ // builder: (context, BoxConstraints box) {
+ // return GestureDetector(
+ // onTap: () {
+ // showDialog(
+ // useSafeArea: false,
+ // context: context,
+ // builder: (context) {
+ // return ImagePreview(initialPage: i, imgList: picList);
+ // },
+ // );
+ // },
+ // child: NetworkImgLayer(
+ // src: pics[i].url,
+ // width: box.maxWidth,
+ // height: box.maxWidth,
+ // ),
+ // );
+ // },
+ // ),
+ // );
+ // }
+ // spanChilds.add(
+ // WidgetSpan(
+ // child: LayoutBuilder(
+ // builder: (context, BoxConstraints box) {
+ // double maxWidth = box.maxWidth;
+ // double crossCount = len < 3 ? 2 : 3;
+ // double height = maxWidth /
+ // crossCount *
+ // (len % crossCount == 0
+ // ? len ~/ crossCount
+ // : len ~/ crossCount + 1) +
+ // 6;
+ // return Container(
+ // padding: const EdgeInsets.only(top: 6),
+ // height: height,
+ // child: GridView.count(
+ // padding: EdgeInsets.zero,
+ // physics: const NeverScrollableScrollPhysics(),
+ // crossAxisCount: crossCount.toInt(),
+ // mainAxisSpacing: 4.0,
+ // crossAxisSpacing: 4.0,
+ // childAspectRatio: 1,
+ // children: list,
+ // ),
+ // );
+ // },
+ // ),
+ // ),
+ // );
+ // }
+ // spanChilds.add(
+ // WidgetSpan(
+ // child: NetworkImgLayer(
+ // src: pics.first.url,
+ // type: 'emote',
+ // width: 100,
+ // height: 200,
+ // ),
+ // ),
+ // );
+ // }
+ return TextSpan(
+ children: spanChilds,
);
}
+ } catch (err) {
+ print('❌rich_node_panel err: $err');
+ return spacer;
}
- return TextSpan(
- children: spanChilds,
- );
+}
+
+class _VerticalSpaceSpan extends WidgetSpan {
+ _VerticalSpaceSpan(double height)
+ : super(child: SizedBox(height: height, width: double.infinity));
}
diff --git a/lib/pages/dynamics/widgets/up_panel.dart b/lib/pages/dynamics/widgets/up_panel.dart
index a12956a5..3db6c357 100644
--- a/lib/pages/dynamics/widgets/up_panel.dart
+++ b/lib/pages/dynamics/widgets/up_panel.dart
@@ -91,7 +91,10 @@ class _UpPanelState extends State {
),
Material(
child: InkWell(
- onTap: () => {feedBack(), Get.toNamed('/follow')},
+ onTap: () => {
+ feedBack(),
+ Get.toNamed('/follow?mid=${userInfo.mid}')
+ },
child: Container(
height: 100,
padding: const EdgeInsets.only(left: 10, right: 10),
diff --git a/lib/pages/dynamics/widgets/video_panel.dart b/lib/pages/dynamics/widgets/video_panel.dart
index 04c1ae19..32a6e21c 100644
--- a/lib/pages/dynamics/widgets/video_panel.dart
+++ b/lib/pages/dynamics/widgets/video_panel.dart
@@ -57,20 +57,21 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
const SizedBox(height: 6),
],
// const SizedBox(height: 4),
- if (item.modules.moduleDynamic.topic != null) ...[
- Padding(
- padding: floor == 2
- ? EdgeInsets.zero
- : const EdgeInsets.only(left: 12, right: 12),
- child: GestureDetector(
- child: Text(
- '#${item.modules.moduleDynamic.topic.name}',
- style: authorStyle,
- ),
- ),
- ),
- const SizedBox(height: 6),
- ],
+ /// fix #话题跟content重复
+ // if (item.modules.moduleDynamic.topic != null) ...[
+ // Padding(
+ // padding: floor == 2
+ // ? EdgeInsets.zero
+ // : const EdgeInsets.only(left: 12, right: 12),
+ // child: GestureDetector(
+ // child: Text(
+ // '#${item.modules.moduleDynamic.topic.name}',
+ // style: authorStyle,
+ // ),
+ // ),
+ // ),
+ // const SizedBox(height: 6),
+ // ],
if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[
Text.rich(richNode(item, context)),
const SizedBox(height: 6),
diff --git a/lib/pages/fan/view.dart b/lib/pages/fan/view.dart
index 6a0af3c6..3eea5093 100644
--- a/lib/pages/fan/view.dart
+++ b/lib/pages/fan/view.dart
@@ -16,13 +16,16 @@ class FansPage extends StatefulWidget {
}
class _FansPageState extends State {
- final FansController _fansController = Get.put(FansController());
+ late String mid;
+ late FansController _fansController;
final ScrollController scrollController = ScrollController();
Future? _futureBuilderFuture;
@override
void initState() {
super.initState();
+ mid = Get.parameters['mid']!;
+ _fansController = Get.put(FansController(), tag: mid);
_futureBuilderFuture = _fansController.queryFans('init');
scrollController.addListener(
() async {
diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart
index 1196efc9..b980914a 100644
--- a/lib/pages/fav/view.dart
+++ b/lib/pages/fav/view.dart
@@ -44,6 +44,14 @@ class _FavPageState extends State {
'我的收藏',
style: Theme.of(context).textTheme.titleMedium,
),
+ actions: [
+ IconButton(
+ onPressed: () => Get.toNamed(
+ '/favSearch?searchType=1&mediaId=${_favController.favFolderData.value.list!.first.id}'),
+ icon: const Icon(Icons.search_outlined),
+ ),
+ const SizedBox(width: 6),
+ ],
),
body: FutureBuilder(
future: _futureBuilderFuture,
diff --git a/lib/pages/favDetail/controller.dart b/lib/pages/favDetail/controller.dart
index 8b772716..c2c63dd5 100644
--- a/lib/pages/favDetail/controller.dart
+++ b/lib/pages/favDetail/controller.dart
@@ -14,7 +14,7 @@ class FavDetailController extends GetxController {
int currentPage = 1;
bool isLoadingMore = false;
RxMap favInfo = {}.obs;
- RxList favList = [FavDetailItemData()].obs;
+ RxList favList = [].obs;
RxString loadingText = '加载中...'.obs;
int mediaCount = 0;
@@ -61,15 +61,13 @@ class FavDetailController extends GetxController {
aid: id, addIds: '', delIds: mediaId.toString());
if (result['status']) {
if (result['data']['prompt']) {
- List dataList = favDetailData.value.medias!;
+ List dataList = favList;
for (var i in dataList) {
if (i.id == id) {
dataList.remove(i);
break;
}
}
- favDetailData.value.medias = dataList;
- favDetailData.refresh();
SmartDialog.showToast('取消收藏');
}
}
diff --git a/lib/pages/favDetail/view.dart b/lib/pages/favDetail/view.dart
index 426bfa8f..fedc85fd 100644
--- a/lib/pages/favDetail/view.dart
+++ b/lib/pages/favDetail/view.dart
@@ -92,13 +92,18 @@ class _FavDetailPageState extends State {
);
},
),
- // actions: [
- // IconButton(
- // onPressed: () {},
- // icon: const Icon(Icons.more_vert),
- // ),
- // const SizedBox(width: 4)
- // ],
+ actions: [
+ IconButton(
+ onPressed: () => Get.toNamed(
+ '/favSearch?searchType=0&mediaId=${Get.parameters['mediaId']!}'),
+ icon: const Icon(Icons.search_outlined),
+ ),
+ // IconButton(
+ // onPressed: () {},
+ // icon: const Icon(Icons.more_vert),
+ // ),
+ const SizedBox(width: 6),
+ ],
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
@@ -168,7 +173,7 @@ class _FavDetailPageState extends State {
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx(
() => Text(
- '共${_favDetailController.favInfo['media_count'] ?? '-'}条视频',
+ '共${_favDetailController.favList.length}条视频',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
@@ -187,14 +192,20 @@ class _FavDetailPageState extends State {
if (_favDetailController.item!.mediaCount == 0) {
return const NoData();
} else {
+ List favList = _favDetailController.favList;
return Obx(
- () => SliverList(
- delegate: SliverChildBuilderDelegate((context, index) {
- return FavVideoCardH(
- videoItem: _favDetailController.favList[index],
- );
- }, childCount: _favDetailController.favList.length),
- ),
+ () => favList.isEmpty
+ ? const SliverToBoxAdapter(child: SizedBox())
+ : SliverList(
+ delegate:
+ SliverChildBuilderDelegate((context, index) {
+ return FavVideoCardH(
+ videoItem: favList[index],
+ callFn: () => _favDetailController
+ .onCancelFav(favList[index].id),
+ );
+ }, childCount: favList.length),
+ ),
);
}
} else {
diff --git a/lib/pages/favDetail/widget/fav_video_card.dart b/lib/pages/favDetail/widget/fav_video_card.dart
index 61ac06f1..471f19bc 100644
--- a/lib/pages/favDetail/widget/fav_video_card.dart
+++ b/lib/pages/favDetail/widget/fav_video_card.dart
@@ -10,134 +10,109 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
-import '../controller.dart';
-
// 收藏视频卡片 - 水平布局
class FavVideoCardH extends StatelessWidget {
final dynamic videoItem;
- final FavDetailController _favDetailController =
- Get.put(FavDetailController());
+ final Function? callFn;
- FavVideoCardH({Key? key, required this.videoItem}) : super(key: key);
+ const FavVideoCardH({Key? key, required this.videoItem, this.callFn})
+ : super(key: key);
@override
Widget build(BuildContext context) {
int id = videoItem.id;
String bvid = videoItem.bvid ?? IdUtils.av2bv(id);
String heroTag = Utils.makeHeroTag(id);
- return Dismissible(
- movementDuration: const Duration(milliseconds: 300),
- background: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.errorContainer,
- ),
- child: const Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.clear_all_rounded),
- SizedBox(width: 6),
- Text('取消收藏')
- ],
- )),
- direction: DismissDirection.endToStart,
- key: ValueKey(videoItem.id),
- onDismissed: (DismissDirection direction) {
- _favDetailController.onCancelFav(videoItem.id);
- // widget.onDeleteNotice();
- },
- child: InkWell(
- onTap: () async {
- // int? seasonId;
- String? epId;
- if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') {
- videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
- // seasonId = videoItem.ogv['season_id'];
- epId = videoItem.epId;
- } else if (videoItem.page == 0 || videoItem.page > 1) {
- var result = await VideoHttp.videoIntro(bvid: bvid);
- if (result['status']) {
- epId = result['data'].epId;
- }
+ return InkWell(
+ onTap: () async {
+ // int? seasonId;
+ String? epId;
+ if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') {
+ videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
+ // seasonId = videoItem.ogv['season_id'];
+ epId = videoItem.epId;
+ } else if (videoItem.page == 0 || videoItem.page > 1) {
+ var result = await VideoHttp.videoIntro(bvid: bvid);
+ if (result['status']) {
+ epId = result['data'].epId;
}
+ }
- Map parameters = {
- 'bvid': bvid,
- 'cid': videoItem.cid.toString(),
- 'epId': epId ?? '',
- };
- // if (seasonId != null) {
- // parameters['seasonId'] = seasonId.toString();
- // }
- Get.toNamed('/video', parameters: parameters, arguments: {
- 'videoItem': videoItem,
- 'heroTag': heroTag,
- 'videoType':
- epId != null ? SearchType.media_bangumi : SearchType.video,
- });
- },
- child: Column(
- children: [
- Padding(
- padding: const EdgeInsets.fromLTRB(
- StyleString.safeSpace, 5, StyleString.safeSpace, 5),
- child: LayoutBuilder(
- builder: (context, boxConstraints) {
- double width =
- (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
- return SizedBox(
- height: width / StyleString.aspectRatio,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.start,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- AspectRatio(
- aspectRatio: StyleString.aspectRatio,
- child: LayoutBuilder(
- builder: (context, boxConstraints) {
- double maxWidth = boxConstraints.maxWidth;
- double maxHeight = boxConstraints.maxHeight;
- return Stack(
- children: [
- Hero(
- tag: heroTag,
- child: NetworkImgLayer(
- src: videoItem.pic,
- width: maxWidth,
- height: maxHeight,
+ Map parameters = {
+ 'bvid': bvid,
+ 'cid': videoItem.cid.toString(),
+ 'epId': epId ?? '',
+ };
+ // if (seasonId != null) {
+ // parameters['seasonId'] = seasonId.toString();
+ // }
+ Get.toNamed('/video', parameters: parameters, arguments: {
+ 'videoItem': videoItem,
+ 'heroTag': heroTag,
+ 'videoType':
+ epId != null ? SearchType.media_bangumi : SearchType.video,
+ });
+ },
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(
+ StyleString.safeSpace, 5, StyleString.safeSpace, 5),
+ child: LayoutBuilder(
+ builder: (context, boxConstraints) {
+ double width =
+ (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
+ return SizedBox(
+ height: width / StyleString.aspectRatio,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ AspectRatio(
+ aspectRatio: StyleString.aspectRatio,
+ child: LayoutBuilder(
+ builder: (context, boxConstraints) {
+ double maxWidth = boxConstraints.maxWidth;
+ double maxHeight = boxConstraints.maxHeight;
+ return Stack(
+ children: [
+ Hero(
+ tag: heroTag,
+ child: NetworkImgLayer(
+ src: videoItem.pic,
+ width: maxWidth,
+ height: maxHeight,
+ ),
+ ),
+ Positioned(
+ right: 4,
+ bottom: 4,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ vertical: 1, horizontal: 6),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(4),
+ color: Colors.black54.withOpacity(0.4)),
+ child: Text(
+ Utils.timeFormat(videoItem.duration!),
+ style: const TextStyle(
+ fontSize: 11, color: Colors.white),
),
),
- Positioned(
- right: 4,
- bottom: 4,
- child: Container(
- padding: const EdgeInsets.symmetric(
- vertical: 1, horizontal: 6),
- decoration: BoxDecoration(
- borderRadius:
- BorderRadius.circular(4),
- color:
- Colors.black54.withOpacity(0.4)),
- child: Text(
- Utils.timeFormat(videoItem.duration!),
- style: const TextStyle(
- fontSize: 11, color: Colors.white),
- ),
- ),
- )
- ],
- );
- },
- ),
+ )
+ ],
+ );
+ },
),
- VideoContent(videoItem: videoItem)
- ],
- ),
- );
- },
- ),
+ ),
+ VideoContent(videoItem: videoItem, callFn: callFn)
+ ],
+ ),
+ );
+ },
),
- ],
- ),
+ ),
+ ],
),
);
}
@@ -145,7 +120,8 @@ class FavVideoCardH extends StatelessWidget {
class VideoContent extends StatelessWidget {
final dynamic videoItem;
- const VideoContent({super.key, required this.videoItem});
+ final Function? callFn;
+ const VideoContent({super.key, required this.videoItem, this.callFn});
@override
Widget build(BuildContext context) {
@@ -173,7 +149,6 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline,
),
),
- const SizedBox(height: 2),
Row(
children: [
StatView(
@@ -181,7 +156,51 @@ class VideoContent extends StatelessWidget {
view: videoItem.cntInfo['play'],
),
const SizedBox(width: 8),
- StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku'])
+ StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
+ const Spacer(),
+ SizedBox(
+ width: 26,
+ height: 26,
+ child: IconButton(
+ style: ButtonStyle(
+ padding: MaterialStateProperty.all(EdgeInsets.zero),
+ ),
+ onPressed: () {
+ showDialog(
+ context: Get.context!,
+ builder: (context) {
+ return AlertDialog(
+ title: const Text('提示'),
+ content: const Text('要取消收藏吗?'),
+ actions: [
+ TextButton(
+ onPressed: () => Get.back(),
+ child: Text(
+ '取消',
+ style: TextStyle(
+ color: Theme.of(context)
+ .colorScheme
+ .outline),
+ )),
+ TextButton(
+ onPressed: () async {
+ await callFn!();
+ Get.back();
+ },
+ child: const Text('确定取消'),
+ )
+ ],
+ );
+ },
+ );
+ },
+ icon: Icon(
+ Icons.clear_outlined,
+ color: Theme.of(context).colorScheme.outline,
+ size: 18,
+ ),
+ ),
+ ),
],
),
],
diff --git a/lib/pages/fav_search/controller.dart b/lib/pages/fav_search/controller.dart
new file mode 100644
index 00000000..642fea6b
--- /dev/null
+++ b/lib/pages/fav_search/controller.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/http/user.dart';
+import 'package:pilipala/models/user/fav_detail.dart';
+
+class FavSearchController extends GetxController {
+ final ScrollController scrollController = ScrollController();
+ Rx controller = TextEditingController().obs;
+ final FocusNode searchFocusNode = FocusNode();
+ RxString searchKeyWord = ''.obs; // 搜索词
+ String hintText = '请输入已收藏视频名称'; // 默认
+ RxBool loadingStatus = false.obs; // 加载状态
+ RxString loadingText = '加载中...'.obs; // 加载提示
+ bool hasMore = false;
+ late int searchType;
+ late int mediaId;
+
+ int currentPage = 1; // 当前页
+ int count = 0; // 总数
+ RxList favList = [].obs;
+
+ @override
+ void onInit() {
+ super.onInit();
+ searchType = int.parse(Get.parameters['searchType']!);
+ mediaId = int.parse(Get.parameters['mediaId']!);
+ }
+
+ // 清空搜索
+ void onClear() {
+ if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
+ controller.value.clear();
+ searchKeyWord.value = '';
+ } else {
+ Get.back();
+ }
+ }
+
+ void onChange(value) {
+ searchKeyWord.value = value;
+ }
+
+ // 提交搜索内容
+ void submit() {
+ loadingStatus.value = true;
+ currentPage = 1;
+ searchFav();
+ }
+
+ // 搜索收藏夹视频
+ Future searchFav({type = 'init'}) async {
+ var res = await await UserHttp.userFavFolderDetail(
+ pn: currentPage,
+ ps: 20,
+ mediaId: mediaId,
+ keyword: searchKeyWord.value,
+ type: searchType,
+ );
+ if (res['status']) {
+ if (currentPage == 1 && type == 'init') {
+ favList.value = res['data'].medias;
+ } else if (type == 'onLoad') {
+ favList.addAll(res['data'].medias);
+ }
+ hasMore = res['data'].hasMore;
+ }
+ currentPage += 1;
+ loadingStatus.value = false;
+ }
+
+ onLoad() {
+ if (!hasMore) return;
+ searchFav(type: 'onLoad');
+ }
+}
diff --git a/lib/pages/fav_search/index.dart b/lib/pages/fav_search/index.dart
new file mode 100644
index 00000000..b811585f
--- /dev/null
+++ b/lib/pages/fav_search/index.dart
@@ -0,0 +1,4 @@
+library fav_search;
+
+export './controller.dart';
+export './view.dart';
diff --git a/lib/pages/fav_search/view.dart b/lib/pages/fav_search/view.dart
new file mode 100644
index 00000000..83c2440b
--- /dev/null
+++ b/lib/pages/fav_search/view.dart
@@ -0,0 +1,116 @@
+import 'package:easy_debounce/easy_throttle.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/common/skeleton/video_card_h.dart';
+import 'package:pilipala/common/widgets/no_data.dart';
+import 'package:pilipala/pages/favDetail/widget/fav_video_card.dart';
+
+import 'controller.dart';
+
+class FavSearchPage extends StatefulWidget {
+ final int? sourceType;
+ final int? mediaId;
+ const FavSearchPage({super.key, this.sourceType, this.mediaId});
+
+ @override
+ State createState() => _FavSearchPageState();
+}
+
+class _FavSearchPageState extends State {
+ final FavSearchController _favSearchCtr = Get.put(FavSearchController());
+ late ScrollController scrollController;
+
+ @override
+ void initState() {
+ super.initState();
+
+ scrollController = _favSearchCtr.scrollController;
+ scrollController.addListener(
+ () {
+ if (scrollController.position.pixels >=
+ scrollController.position.maxScrollExtent - 300) {
+ EasyThrottle.throttle('fav', const Duration(seconds: 1), () {
+ _favSearchCtr.onLoad();
+ });
+ }
+ },
+ );
+ }
+
+ @override
+ void dispose() {
+ scrollController.removeListener(() {});
+ scrollController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ titleSpacing: 0,
+ actions: [
+ IconButton(
+ onPressed: () => _favSearchCtr.submit(),
+ icon: const Icon(Icons.search_outlined, size: 22)),
+ const SizedBox(width: 10)
+ ],
+ title: Obx(
+ () => TextField(
+ autofocus: true,
+ focusNode: _favSearchCtr.searchFocusNode,
+ controller: _favSearchCtr.controller.value,
+ textInputAction: TextInputAction.search,
+ onChanged: (value) => _favSearchCtr.onChange(value),
+ decoration: InputDecoration(
+ hintText: _favSearchCtr.hintText,
+ border: InputBorder.none,
+ suffixIcon: IconButton(
+ icon: Icon(
+ Icons.clear,
+ size: 22,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ onPressed: () => _favSearchCtr.onClear(),
+ ),
+ ),
+ onSubmitted: (String value) => _favSearchCtr.submit(),
+ ),
+ ),
+ ),
+ body: Obx(
+ () => _favSearchCtr.loadingStatus.value && _favSearchCtr.favList.isEmpty
+ ? ListView.builder(
+ itemCount: 10,
+ itemBuilder: (context, index) {
+ return const VideoCardHSkeleton();
+ },
+ )
+ : _favSearchCtr.favList.isNotEmpty
+ ? ListView.builder(
+ controller: scrollController,
+ itemCount: _favSearchCtr.favList.length + 1,
+ itemBuilder: (context, index) {
+ if (index == _favSearchCtr.favList.length) {
+ return Container(
+ height: MediaQuery.of(context).padding.bottom + 60,
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(context).padding.bottom),
+ );
+ } else {
+ return FavVideoCardH(
+ videoItem: _favSearchCtr.favList[index],
+ callFn: () => null,
+ );
+ }
+ },
+ )
+ : const CustomScrollView(
+ slivers: [
+ NoData(),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/follow/controller.dart b/lib/pages/follow/controller.dart
index fe1bfabc..fe4b6100 100644
--- a/lib/pages/follow/controller.dart
+++ b/lib/pages/follow/controller.dart
@@ -1,20 +1,28 @@
+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/http/follow.dart';
+import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/follow/result.dart';
+import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/storage.dart';
-class FollowController extends GetxController {
+/// 查看自己的关注时,可以查看分类
+/// 查看其他人的关注时,只可以看全部
+class FollowController extends GetxController with GetTickerProviderStateMixin {
Box userInfoCache = GStrorage.userInfo;
int pn = 1;
int ps = 20;
int total = 0;
- RxList followList = [FollowItemModel()].obs;
+ RxList followList = [].obs;
late int mid;
late String name;
var userInfo;
RxString loadingText = '加载中...'.obs;
+ RxBool isOwner = false.obs;
+ late List followTags;
+ late TabController tabController;
@override
void onInit() {
@@ -23,6 +31,7 @@ class FollowController extends GetxController {
mid = Get.parameters['mid'] != null
? int.parse(Get.parameters['mid']!)
: userInfo.mid;
+ isOwner.value = mid == userInfo.mid;
name = Get.parameters['name'] ?? userInfo.uname;
}
@@ -56,4 +65,20 @@ class FollowController extends GetxController {
}
return res;
}
+
+ // 当查看当前用户的关注时,请求关注分组
+ Future followUpTags() async {
+ if (userInfo != null && mid == userInfo.mid) {
+ var res = await MemberHttp.followUpTags();
+ if (res['status']) {
+ followTags = res['data'];
+ tabController = TabController(
+ initialIndex: 0,
+ length: res['data'].length,
+ vsync: this,
+ );
+ }
+ return res;
+ }
+ }
}
diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart
index 9e290f0f..a4f1011b 100644
--- a/lib/pages/follow/view.dart
+++ b/lib/pages/follow/view.dart
@@ -1,12 +1,8 @@
-import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
-import 'package:pilipala/common/widgets/http_error.dart';
-import 'package:pilipala/common/widgets/no_data.dart';
-import 'package:pilipala/models/follow/result.dart';
-
import 'controller.dart';
-import 'widgets/follow_item.dart';
+import 'widgets/follow_list.dart';
+import 'widgets/owner_follow_list.dart';
class FollowPage extends StatefulWidget {
const FollowPage({super.key});
@@ -16,30 +12,15 @@ class FollowPage extends StatefulWidget {
}
class _FollowPageState extends State {
- final FollowController _followController = Get.put(FollowController());
+ late String mid;
+ late FollowController _followController;
final ScrollController scrollController = ScrollController();
- Future? _futureBuilderFuture;
@override
void initState() {
super.initState();
- _futureBuilderFuture = _followController.queryFollowings('init');
- scrollController.addListener(
- () async {
- if (scrollController.position.pixels >=
- scrollController.position.maxScrollExtent - 200) {
- EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
- _followController.queryFollowings('onLoad');
- });
- }
- },
- );
- }
-
- @override
- void dispose() {
- scrollController.removeListener(() {});
- super.dispose();
+ mid = Get.parameters['mid']!;
+ _followController = Get.put(FollowController(), tag: mid);
}
@override
@@ -51,73 +32,57 @@ class _FollowPageState extends State {
titleSpacing: 0,
centerTitle: false,
title: Text(
- '${_followController.name}的关注',
+ _followController.isOwner.value
+ ? '我的关注'
+ : '${_followController.name}的关注',
style: Theme.of(context).textTheme.titleMedium,
),
),
- body: RefreshIndicator(
- onRefresh: () async =>
- await _followController.queryFollowings('init'),
- child: FutureBuilder(
- future: _futureBuilderFuture,
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done) {
- var data = snapshot.data;
- if (data['status']) {
- List list = _followController.followList;
- return Obx(
- () => list.isNotEmpty
- ? ListView.builder(
- controller: scrollController,
- itemCount: list.length + 1,
- itemBuilder: (BuildContext context, int index) {
- if (index == list.length) {
- return Container(
- height:
- MediaQuery.of(context).padding.bottom +
- 60,
- padding: EdgeInsets.only(
- bottom: MediaQuery.of(context)
- .padding
- .bottom),
- child: Center(
- child: Obx(
- () => Text(
- _followController.loadingText.value,
- style: TextStyle(
- color: Theme.of(context)
- .colorScheme
- .outline,
- fontSize: 13),
- ),
- ),
- ),
- );
- } else {
- return followItem(item: list[index]);
- }
- },
- )
- : const CustomScrollView(
- slivers: [NoData()],
+ body: Obx(
+ () => !_followController.isOwner.value
+ ? FollowList(ctr: _followController)
+ : FutureBuilder(
+ future: _followController.followUpTags(),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ var data = snapshot.data;
+ if (data['status']) {
+ return Column(
+ children: [
+ TabBar(
+ controller: _followController.tabController,
+ isScrollable: true,
+ tabs: [
+ for (var i in data['data']) ...[
+ Tab(text: i.name),
+ ]
+ ]),
+ Expanded(
+ child: TabBarView(
+ controller: _followController.tabController,
+ children: [
+ for (var i = 0;
+ i < _followController.tabController.length;
+ i++) ...[
+ OwnerFollowList(
+ ctr: _followController,
+ tagItem: _followController.followTags[i],
+ )
+ ]
+ ],
+ ),
),
- );
- } else {
- return CustomScrollView(
- slivers: [
- HttpError(
- errMsg: data['msg'],
- fn: () => _followController.queryFollowings('init'),
- )
- ],
- );
- }
- } else {
- // 骨架屏
- return const SizedBox();
- }
- },
- )),
+ ],
+ );
+ } else {
+ return const SizedBox();
+ }
+ } else {
+ return const SizedBox();
+ }
+ },
+ ),
+ ),
);
}
}
diff --git a/lib/pages/follow/widgets/follow_item.dart b/lib/pages/follow/widgets/follow_item.dart
index d9b2617b..3f9e4f3c 100644
--- a/lib/pages/follow/widgets/follow_item.dart
+++ b/lib/pages/follow/widgets/follow_item.dart
@@ -1,38 +1,71 @@
import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/models/follow/result.dart';
+import 'package:pilipala/pages/follow/index.dart';
+import 'package:pilipala/pages/video/detail/introduction/widgets/group_panel.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart';
-Widget followItem({item}) {
- String heroTag = Utils.makeHeroTag(item!.mid);
- return ListTile(
- onTap: () {
- feedBack();
- Get.toNamed('/member?mid=${item.mid}',
- arguments: {'face': item.face, 'heroTag': heroTag});
- },
- leading: Hero(
- tag: heroTag,
- child: NetworkImgLayer(
- width: 45,
- height: 45,
- type: 'avatar',
- src: item.face,
+class FollowItem extends StatelessWidget {
+ final FollowItemModel item;
+ final FollowController? ctr;
+ const FollowItem({super.key, required this.item, this.ctr});
+
+ @override
+ Widget build(BuildContext context) {
+ String heroTag = Utils.makeHeroTag(item.mid);
+ return ListTile(
+ onTap: () {
+ feedBack();
+ Get.toNamed('/member?mid=${item.mid}',
+ arguments: {'face': item.face, 'heroTag': heroTag});
+ },
+ leading: Hero(
+ tag: heroTag,
+ child: NetworkImgLayer(
+ width: 45,
+ height: 45,
+ type: 'avatar',
+ src: item.face,
+ ),
),
- ),
- title: Text(
- item.uname,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(fontSize: 14),
- ),
- subtitle: Text(
- item.sign,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- dense: true,
- trailing: const SizedBox(width: 6),
- );
+ title: Text(
+ item.uname!,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: const TextStyle(fontSize: 14),
+ ),
+ subtitle: Text(
+ item.sign!,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ dense: true,
+ trailing: ctr!.isOwner.value
+ ? SizedBox(
+ height: 34,
+ child: TextButton(
+ onPressed: () async {
+ await Get.bottomSheet(
+ GroupPanel(mid: item.mid!),
+ isScrollControlled: true,
+ );
+ },
+ style: TextButton.styleFrom(
+ padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
+ foregroundColor: Theme.of(context).colorScheme.outline,
+ backgroundColor:
+ Theme.of(context).colorScheme.onInverseSurface, // 设置按钮背景色
+ ),
+ child: const Text(
+ '已关注',
+ style: TextStyle(fontSize: 12),
+ ),
+ ),
+ )
+ : const SizedBox(),
+ );
+ }
}
diff --git a/lib/pages/follow/widgets/follow_list.dart b/lib/pages/follow/widgets/follow_list.dart
new file mode 100644
index 00000000..d198bec2
--- /dev/null
+++ b/lib/pages/follow/widgets/follow_list.dart
@@ -0,0 +1,114 @@
+import 'package:easy_debounce/easy_throttle.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/common/widgets/http_error.dart';
+import 'package:pilipala/common/widgets/no_data.dart';
+import 'package:pilipala/models/follow/result.dart';
+import 'package:pilipala/pages/follow/index.dart';
+
+import 'follow_item.dart';
+
+class FollowList extends StatefulWidget {
+ final FollowController ctr;
+ const FollowList({
+ super.key,
+ required this.ctr,
+ });
+
+ @override
+ State createState() => _FollowListState();
+}
+
+class _FollowListState extends State {
+ late Future _futureBuilderFuture;
+ final ScrollController scrollController = ScrollController();
+
+ @override
+ void initState() {
+ super.initState();
+ _futureBuilderFuture = widget.ctr.queryFollowings('init');
+ scrollController.addListener(
+ () async {
+ if (scrollController.position.pixels >=
+ scrollController.position.maxScrollExtent - 200) {
+ EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
+ widget.ctr.queryFollowings('onLoad');
+ });
+ }
+ },
+ );
+ }
+
+ @override
+ void dispose() {
+ scrollController.removeListener(() {});
+ scrollController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return RefreshIndicator(
+ onRefresh: () async => await widget.ctr.queryFollowings('init'),
+ child: FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ var data = snapshot.data;
+ if (data['status']) {
+ List list = widget.ctr.followList;
+ return Obx(
+ () => list.isNotEmpty
+ ? ListView.builder(
+ controller: scrollController,
+ itemCount: list.length + 1,
+ itemBuilder: (BuildContext context, int index) {
+ if (index == list.length) {
+ return Container(
+ height:
+ MediaQuery.of(context).padding.bottom + 60,
+ padding: EdgeInsets.only(
+ bottom:
+ MediaQuery.of(context).padding.bottom),
+ child: Center(
+ child: Obx(
+ () => Text(
+ widget.ctr.loadingText.value,
+ style: TextStyle(
+ color: Theme.of(context)
+ .colorScheme
+ .outline,
+ fontSize: 13),
+ ),
+ ),
+ ),
+ );
+ } else {
+ return FollowItem(
+ item: list[index],
+ ctr: widget.ctr,
+ );
+ }
+ },
+ )
+ : const CustomScrollView(slivers: [NoData()]),
+ );
+ } else {
+ return CustomScrollView(
+ slivers: [
+ HttpError(
+ errMsg: data['msg'],
+ fn: () => widget.ctr.queryFollowings('init'),
+ )
+ ],
+ );
+ }
+ } else {
+ // 骨架屏
+ return const SizedBox();
+ }
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/pages/follow/widgets/owner_follow_list.dart b/lib/pages/follow/widgets/owner_follow_list.dart
new file mode 100644
index 00000000..0dcd785d
--- /dev/null
+++ b/lib/pages/follow/widgets/owner_follow_list.dart
@@ -0,0 +1,134 @@
+import 'dart:math';
+
+import 'package:easy_debounce/easy_throttle.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/common/widgets/http_error.dart';
+import 'package:pilipala/common/widgets/no_data.dart';
+import 'package:pilipala/http/member.dart';
+import 'package:pilipala/models/follow/result.dart';
+import 'package:pilipala/models/member/tags.dart';
+import 'package:pilipala/pages/follow/index.dart';
+import 'follow_item.dart';
+
+class OwnerFollowList extends StatefulWidget {
+ final FollowController ctr;
+ final MemberTagItemModel? tagItem;
+ const OwnerFollowList({super.key, required this.ctr, this.tagItem});
+
+ @override
+ State createState() => _OwnerFollowListState();
+}
+
+class _OwnerFollowListState extends State
+ with AutomaticKeepAliveClientMixin {
+ late int mid;
+ late Future _futureBuilderFuture;
+ final ScrollController scrollController = ScrollController();
+ int pn = 1;
+ int ps = 20;
+ late MemberTagItemModel tagItem;
+ RxList followList = [].obs;
+
+ @override
+ bool get wantKeepAlive => true;
+
+ @override
+ void initState() {
+ super.initState();
+ mid = widget.ctr.mid;
+ tagItem = widget.tagItem!;
+ _futureBuilderFuture = followUpGroup('init');
+ scrollController.addListener(
+ () async {
+ if (scrollController.position.pixels >=
+ scrollController.position.maxScrollExtent - 200) {
+ EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
+ followUpGroup('onLoad');
+ });
+ }
+ },
+ );
+ }
+
+ // 获取分组下up
+ Future followUpGroup(type) async {
+ if (type == 'init') {
+ pn = 1;
+ }
+ var res = await MemberHttp.followUpGroup(mid, tagItem.tagid, pn, ps);
+ if (res['status']) {
+ if (res['data'].isNotEmpty) {
+ if (type == 'init') {
+ followList.value = res['data'];
+ } else {
+ followList.addAll(res['data']);
+ }
+ pn += 1;
+ }
+ }
+ return res;
+ }
+
+ @override
+ void dispose() {
+ scrollController.removeListener(() {});
+ scrollController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return RefreshIndicator(
+ onRefresh: () async => await followUpGroup('init'),
+ child: FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ var data = snapshot.data;
+ if (data['status']) {
+ return Obx(
+ () => followList.isNotEmpty
+ ? ListView.builder(
+ physics: const AlwaysScrollableScrollPhysics(),
+ controller: scrollController,
+ itemCount: followList.length + 1,
+ itemBuilder: (BuildContext context, int index) {
+ if (index == followList.length) {
+ return Container(
+ height:
+ MediaQuery.of(context).padding.bottom + 60,
+ padding: EdgeInsets.only(
+ bottom:
+ MediaQuery.of(context).padding.bottom),
+ );
+ } else {
+ return FollowItem(
+ item: followList[index],
+ ctr: widget.ctr,
+ );
+ }
+ },
+ )
+ : const CustomScrollView(slivers: [NoData()]),
+ );
+ } else {
+ return CustomScrollView(
+ slivers: [
+ HttpError(
+ errMsg: data['msg'],
+ fn: () => widget.ctr.queryFollowings('init'),
+ )
+ ],
+ );
+ }
+ } else {
+ // 骨架屏
+ return const SizedBox();
+ }
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/pages/history/controller.dart b/lib/pages/history/controller.dart
index ae897499..e7822cd9 100644
--- a/lib/pages/history/controller.dart
+++ b/lib/pages/history/controller.dart
@@ -8,11 +8,13 @@ import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController {
final ScrollController scrollController = ScrollController();
- RxList historyList = [HisListItem()].obs;
+ RxList historyList = [].obs;
RxBool isLoadingMore = false.obs;
RxBool pauseStatus = false.obs;
Box localCache = GStrorage.localCache;
RxBool isLoading = false.obs;
+ RxBool enableMultiple = false.obs;
+ RxInt checkedCount = 0.obs;
@override
void onInit() {
@@ -121,4 +123,80 @@ class HistoryController extends GetxController {
},
);
}
+
+ // 删除某条历史记录
+ Future delHistory(kid, business) async {
+ String resKid = 'archive_$kid';
+ if (business == 'live') {
+ resKid = 'live_$kid';
+ } else if (business.contains('article')) {
+ resKid = 'article_$kid';
+ }
+
+ var res = await UserHttp.delHistory(resKid);
+ if (res['status']) {
+ historyList.removeWhere((e) => e.kid == kid);
+ SmartDialog.showToast(res['msg']);
+ }
+ }
+
+ // 删除已看历史记录
+ Future onDelHistory() async {
+ /// TODO 优化
+ List result =
+ historyList.where((e) => e.progress == -1).toList();
+ for (HisListItem i in result) {
+ String resKid = 'archive_${i.kid}';
+ await UserHttp.delHistory(resKid);
+ historyList.removeWhere((e) => e.kid == i.kid);
+ }
+ SmartDialog.showToast('操作完成');
+ }
+
+ // 删除选中的记录
+ Future onDelCheckedHistory() async {
+ SmartDialog.show(
+ useSystem: true,
+ animationType: SmartAnimationType.centerFade_otherSlide,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text('提示'),
+ content: const Text('确认删除所选历史记录吗?'),
+ actions: [
+ TextButton(
+ onPressed: () => SmartDialog.dismiss(),
+ child: Text(
+ '取消',
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ ),
+ ),
+ TextButton(
+ onPressed: () async {
+ /// TODO 优化
+ await SmartDialog.dismiss();
+ SmartDialog.showLoading(msg: '请求中');
+ List result =
+ historyList.where((e) => e.checked!).toList();
+ for (HisListItem i in result) {
+ String str = 'archive';
+ try {
+ str = i.history!.business!;
+ } catch (_) {}
+ String resKid = '${str}_${i.kid}';
+ await UserHttp.delHistory(resKid);
+ historyList.removeWhere((e) => e.kid == i.kid);
+ }
+ checkedCount.value = 0;
+ SmartDialog.dismiss();
+ enableMultiple.value = false;
+ },
+ child: const Text('确认'),
+ )
+ ],
+ );
+ },
+ );
+ }
}
diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart
index c0118819..d8fc60f0 100644
--- a/lib/pages/history/view.dart
+++ b/lib/pages/history/view.dart
@@ -37,6 +37,23 @@ class _HistoryPageState extends State {
}
},
);
+ _historyController.enableMultiple.listen((p0) {
+ setState(() {});
+ });
+ }
+
+ // 选中
+ onChoose(index) {
+ _historyController.historyList[index].checked =
+ !_historyController.historyList[index].checked!;
+ _historyController.checkedCount.value =
+ _historyController.historyList.where((item) => item.checked!).length;
+ _historyController.historyList.refresh();
+ }
+
+ // 更新多选状态
+ onUpdateMultiple() {
+ setState(() {});
}
@override
@@ -48,44 +65,112 @@ class _HistoryPageState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(
- titleSpacing: 0,
- centerTitle: false,
- title: Text(
- '观看记录',
- style: Theme.of(context).textTheme.titleMedium,
- ),
- actions: [
- PopupMenuButton(
- onSelected: (String type) {
- // 处理菜单项选择的逻辑
- switch (type) {
- case 'pause':
- _historyController.onPauseHistory();
- break;
- case 'clear':
- _historyController.onClearHistory();
- break;
- default:
- }
- },
- itemBuilder: (BuildContext context) => >[
- PopupMenuItem(
- value: 'pause',
- child: Obx(
- () => Text(!_historyController.pauseStatus.value
- ? '暂停观看记录'
- : '恢复观看记录'),
- ),
- ),
- const PopupMenuItem(
- value: 'clear',
- child: Text('清空观看记录'),
- ),
- ],
+ appBar: AppBarWidget(
+ visible: _historyController.enableMultiple.value,
+ child1: AppBar(
+ titleSpacing: 0,
+ centerTitle: false,
+ leading: IconButton(
+ onPressed: () => Get.back(),
+ icon: const Icon(Icons.arrow_back_outlined),
),
- const SizedBox(width: 6),
- ],
+ title: Text(
+ '观看记录',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ actions: [
+ IconButton(
+ onPressed: () => Get.toNamed('/historySearch'),
+ icon: const Icon(Icons.search_outlined),
+ ),
+ PopupMenuButton(
+ onSelected: (String type) {
+ // 处理菜单项选择的逻辑
+ switch (type) {
+ case 'pause':
+ _historyController.onPauseHistory();
+ break;
+ case 'clear':
+ _historyController.onClearHistory();
+ break;
+ case 'del':
+ _historyController.onDelHistory();
+ break;
+ case 'multiple':
+ _historyController.enableMultiple.value = true;
+ setState(() {});
+ break;
+ default:
+ }
+ },
+ itemBuilder: (BuildContext context) => >[
+ PopupMenuItem(
+ value: 'pause',
+ child: Obx(
+ () => Text(!_historyController.pauseStatus.value
+ ? '暂停观看记录'
+ : '恢复观看记录'),
+ ),
+ ),
+ const PopupMenuItem(
+ value: 'clear',
+ child: Text('清空观看记录'),
+ ),
+ const PopupMenuItem(
+ value: 'del',
+ child: Text('删除已看记录'),
+ ),
+ const PopupMenuItem(
+ value: 'multiple',
+ child: Text('多选删除'),
+ ),
+ ],
+ ),
+ const SizedBox(width: 6),
+ ],
+ ),
+ child2: AppBar(
+ titleSpacing: 0,
+ centerTitle: false,
+ leading: IconButton(
+ onPressed: () {
+ _historyController.enableMultiple.value = false;
+ for (var item in _historyController.historyList) {
+ item.checked = false;
+ }
+ _historyController.checkedCount.value = 0;
+ setState(() {});
+ },
+ icon: const Icon(Icons.close_outlined),
+ ),
+ title: Obx(
+ () => Text(
+ '已选择${_historyController.checkedCount.value}项',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ for (var item in _historyController.historyList) {
+ item.checked = true;
+ }
+ _historyController.checkedCount.value =
+ _historyController.historyList.length;
+ _historyController.historyList.refresh();
+ },
+ child: const Text('全选'),
+ ),
+ TextButton(
+ onPressed: () => _historyController.onDelCheckedHistory(),
+ child: Text(
+ '删除',
+ style: TextStyle(color: Theme.of(context).colorScheme.error),
+ ),
+ ),
+ const SizedBox(width: 6),
+ ],
+ ),
),
body: RefreshIndicator(
onRefresh: () async {
@@ -99,6 +184,9 @@ class _HistoryPageState extends State {
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
+ if (snapshot.data == null) {
+ return const SliverToBoxAdapter(child: SizedBox());
+ }
Map data = snapshot.data;
if (data['status']) {
return Obx(
@@ -109,6 +197,9 @@ class _HistoryPageState extends State {
return HistoryItem(
videoItem:
_historyController.historyList[index],
+ ctr: _historyController,
+ onChoose: () => onChoose(index),
+ onUpdateMultiple: () => onUpdateMultiple(),
);
},
childCount:
@@ -144,6 +235,36 @@ class _HistoryPageState extends State {
],
),
),
+ // bottomNavigationBar: BottomAppBar(),
+ );
+ }
+}
+
+class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
+ const AppBarWidget({
+ required this.child1,
+ required this.child2,
+ required this.visible,
+ Key? key,
+ }) : super(key: key);
+
+ final PreferredSizeWidget child1;
+ final PreferredSizeWidget child2;
+ final bool visible;
+ @override
+ Size get preferredSize => child1.preferredSize;
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedSwitcher(
+ duration: const Duration(milliseconds: 500),
+ transitionBuilder: (Widget child, Animation animation) {
+ return ScaleTransition(
+ scale: animation,
+ child: child,
+ );
+ },
+ child: !visible ? child1 : child2,
);
}
}
diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart
index 2d801668..b80affc8 100644
--- a/lib/pages/history/widgets/item.dart
+++ b/lib/pages/history/widgets/item.dart
@@ -11,12 +11,24 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/live/item.dart';
+import 'package:pilipala/pages/history/index.dart';
+import 'package:pilipala/pages/history_search/index.dart';
+import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart';
class HistoryItem extends StatelessWidget {
final dynamic videoItem;
- const HistoryItem({super.key, required this.videoItem});
+ final dynamic ctr;
+ final Function? onChoose;
+ final Function? onUpdateMultiple;
+ const HistoryItem({
+ super.key,
+ required this.videoItem,
+ this.ctr,
+ this.onChoose,
+ this.onUpdateMultiple,
+ });
@override
Widget build(BuildContext context) {
@@ -25,6 +37,11 @@ class HistoryItem extends StatelessWidget {
String heroTag = Utils.makeHeroTag(aid);
return InkWell(
onTap: () async {
+ if (ctr!.enableMultiple.value) {
+ feedBack();
+ onChoose!();
+ return;
+ }
if (videoItem.history.business.contains('article')) {
int cid = videoItem.history.cid ??
// videoItem.history.oid ??
@@ -115,6 +132,17 @@ class HistoryItem extends StatelessWidget {
arguments: {'heroTag': heroTag, 'pic': videoItem.cover});
}
},
+ onLongPress: () {
+ if (ctr is HistorySearchController) {
+ return;
+ }
+ if (!ctr!.enableMultiple.value) {
+ feedBack();
+ ctr!.enableMultiple.value = true;
+ onChoose!();
+ onUpdateMultiple!();
+ }
+ },
child: Column(
children: [
Padding(
@@ -130,53 +158,110 @@ class HistoryItem extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- AspectRatio(
- aspectRatio: StyleString.aspectRatio,
- child: LayoutBuilder(
- builder: (context, boxConstraints) {
- double maxWidth = boxConstraints.maxWidth;
- double maxHeight = boxConstraints.maxHeight;
- return Stack(
- children: [
- Hero(
- tag: heroTag,
- child: NetworkImgLayer(
- src: (videoItem.cover != ''
- ? videoItem.cover
- : videoItem.covers.first),
- width: maxWidth,
- height: maxHeight,
+ Stack(
+ children: [
+ AspectRatio(
+ aspectRatio: StyleString.aspectRatio,
+ child: LayoutBuilder(
+ builder: (context, boxConstraints) {
+ double maxWidth = boxConstraints.maxWidth;
+ double maxHeight = boxConstraints.maxHeight;
+ return Stack(
+ children: [
+ Hero(
+ tag: heroTag,
+ child: NetworkImgLayer(
+ src: (videoItem.cover != ''
+ ? videoItem.cover
+ : videoItem.covers.first),
+ width: maxWidth,
+ height: maxHeight,
+ ),
+ ),
+ if (!BusinessType
+ .hiddenDurationType.hiddenDurationType
+ .contains(videoItem.history.business))
+ PBadge(
+ text: videoItem.progress == -1
+ ? '已看完'
+ : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
+ right: 6.0,
+ bottom: 6.0,
+ type: 'gray',
+ ),
+ // 右上角
+ if (BusinessType.showBadge.showBadge
+ .contains(
+ videoItem.history.business) ||
+ videoItem.history.business ==
+ BusinessType.live.type)
+ PBadge(
+ text: videoItem.badge,
+ top: 6.0,
+ right: 6.0,
+ bottom: null,
+ left: null,
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ Obx(
+ () => Positioned.fill(
+ child: AnimatedOpacity(
+ opacity: ctr!.enableMultiple.value ? 1 : 0,
+ duration: const Duration(milliseconds: 200),
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ color: Colors.black.withOpacity(
+ ctr!.enableMultiple.value &&
+ videoItem.checked
+ ? 0.6
+ : 0),
+ ),
+ child: Center(
+ child: SizedBox(
+ width: 34,
+ height: 34,
+ child: AnimatedScale(
+ scale: videoItem.checked ? 1 : 0,
+ duration:
+ const Duration(milliseconds: 250),
+ curve: Curves.easeInOut,
+ child: IconButton(
+ style: ButtonStyle(
+ padding: MaterialStateProperty.all(
+ EdgeInsets.zero),
+ backgroundColor:
+ MaterialStateProperty
+ .resolveWith(
+ (states) {
+ return Colors.white
+ .withOpacity(0.8);
+ },
+ ),
+ ),
+ onPressed: () {
+ feedBack();
+ onChoose!();
+ },
+ icon: Icon(Icons.done_all_outlined,
+ color: Theme.of(context)
+ .colorScheme
+ .primary),
+ ),
+ ),
+ ),
),
),
- if (!BusinessType
- .hiddenDurationType.hiddenDurationType
- .contains(videoItem.history.business))
- PBadge(
- text: videoItem.progress == -1
- ? '已看完'
- : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
- right: 6.0,
- bottom: 6.0,
- type: 'gray',
- ),
- // 右上角
- if (BusinessType.showBadge.showBadge
- .contains(videoItem.history.business) ||
- videoItem.history.business ==
- BusinessType.live.type)
- PBadge(
- text: videoItem.badge,
- top: 6.0,
- right: 6.0,
- bottom: null,
- left: null,
- ),
- ],
- );
- },
- ),
+ ),
+ ),
+ ),
+ ],
),
- VideoContent(videoItem: videoItem)
+ VideoContent(videoItem: videoItem, ctr: ctr)
],
),
);
@@ -191,7 +276,8 @@ class HistoryItem extends StatelessWidget {
class VideoContent extends StatelessWidget {
final dynamic videoItem;
- const VideoContent({super.key, required this.videoItem});
+ final dynamic ctr;
+ const VideoContent({super.key, required this.videoItem, this.ctr});
@override
Widget build(BuildContext context) {
@@ -211,7 +297,8 @@ class VideoContent extends StatelessWidget {
maxLines: videoItem.videos > 1 ? 1 : 2,
overflow: TextOverflow.ellipsis,
),
- if (videoItem.showTitle != null)
+ if (videoItem.showTitle != null) ...[
+ const SizedBox(height: 2),
Text(
videoItem.showTitle,
textAlign: TextAlign.start,
@@ -219,21 +306,24 @@ class VideoContent extends StatelessWidget {
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.outline),
- maxLines: 2,
+ maxLines: 1,
overflow: TextOverflow.ellipsis,
),
+ ],
const Spacer(),
- Row(
- children: [
- Text(
- videoItem.authorName,
- style: TextStyle(
- fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
- color: Theme.of(context).colorScheme.outline,
+ if (videoItem.authorName != '')
+ Row(
+ children: [
+ Text(
+ videoItem.authorName,
+ style: TextStyle(
+ fontSize:
+ Theme.of(context).textTheme.labelMedium!.fontSize,
+ color: Theme.of(context).colorScheme.outline,
+ ),
),
- ),
- ],
- ),
+ ],
+ ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -244,26 +334,26 @@ class VideoContent extends StatelessWidget {
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline),
),
- if (videoItem.badge != '番剧' &&
- !videoItem.tagName.contains('动画') &&
- videoItem.history.business != 'live' &&
- !videoItem.history.business.contains('article'))
- SizedBox(
- width: 24,
- height: 24,
- child: PopupMenuButton(
- padding: EdgeInsets.zero,
- tooltip: '稍后再看',
- icon: Icon(
- Icons.more_vert_outlined,
- color: Theme.of(context).colorScheme.outline,
- size: 14,
- ),
- position: PopupMenuPosition.under,
- // constraints: const BoxConstraints(maxHeight: 35),
- onSelected: (String type) {},
- itemBuilder: (BuildContext context) =>
- >[
+ SizedBox(
+ width: 24,
+ height: 24,
+ child: PopupMenuButton(
+ padding: EdgeInsets.zero,
+ tooltip: '功能菜单',
+ icon: Icon(
+ Icons.more_vert_outlined,
+ color: Theme.of(context).colorScheme.outline,
+ size: 14,
+ ),
+ position: PopupMenuPosition.under,
+ // constraints: const BoxConstraints(maxHeight: 35),
+ onSelected: (String type) {},
+ itemBuilder: (BuildContext context) =>
+ >[
+ if (videoItem.badge != '番剧' &&
+ !videoItem.tagName.contains('动画') &&
+ videoItem.history.business != 'live' &&
+ !videoItem.history.business.contains('article'))
PopupMenuItem(
onTap: () async {
var res = await UserHttp.toViewLater(
@@ -280,9 +370,22 @@ class VideoContent extends StatelessWidget {
],
),
),
- ],
- ),
+ PopupMenuItem(
+ onTap: () => ctr!.delHistory(
+ videoItem.kid, videoItem.history.business),
+ value: 'pause',
+ height: 35,
+ child: const Row(
+ children: [
+ Icon(Icons.close_outlined, size: 16),
+ SizedBox(width: 6),
+ Text('删除记录', style: TextStyle(fontSize: 13))
+ ],
+ ),
+ ),
+ ],
),
+ ),
],
),
],
diff --git a/lib/pages/history_search/controller.dart b/lib/pages/history_search/controller.dart
new file mode 100644
index 00000000..90ac7a02
--- /dev/null
+++ b/lib/pages/history_search/controller.dart
@@ -0,0 +1,91 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/http/user.dart';
+import 'package:pilipala/models/user/history.dart';
+
+class HistorySearchController extends GetxController {
+ final ScrollController scrollController = ScrollController();
+ Rx controller = TextEditingController().obs;
+ final FocusNode searchFocusNode = FocusNode();
+ RxString searchKeyWord = ''.obs;
+ String hintText = '搜索';
+ RxString loadingStatus = 'init'.obs;
+ RxString loadingText = '加载中...'.obs;
+ bool hasRequest = false;
+ late int mid;
+ RxString uname = ''.obs;
+ int pn = 1;
+ int count = 0;
+ RxList historyList = [].obs;
+ RxBool enableMultiple = false.obs;
+
+ // 清空搜索
+ void onClear() {
+ if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
+ controller.value.clear();
+ searchKeyWord.value = '';
+ } else {
+ Get.back();
+ }
+ }
+
+ void onChange(value) {
+ searchKeyWord.value = value;
+ }
+
+ // 提交搜索内容
+ void submit() {
+ loadingStatus.value = 'loading';
+ if (hasRequest) {
+ pn = 1;
+ searchHistories();
+ }
+ }
+
+ // 搜索视频
+ Future searchHistories({type = 'init'}) async {
+ if (type == 'onLoad' && loadingText.value == '没有更多了') {
+ return;
+ }
+ var res = await UserHttp.searchHistory(
+ pn: pn,
+ keyword: controller.value.text,
+ );
+ if (res['status']) {
+ if (type == 'init' && pn == 1) {
+ historyList.value = res['data'].list;
+ } else {
+ historyList.addAll(res['data'].list);
+ }
+ count = res['data'].page['total'];
+ if (historyList.length == count) {
+ loadingText.value = '没有更多了';
+ }
+ pn += 1;
+ hasRequest = true;
+ }
+ loadingStatus.value = 'finish';
+ return res;
+ }
+
+ onLoad() {
+ searchHistories(type: 'onLoad');
+ }
+
+ Future delHistory(kid, business) async {
+ String resKid = 'archive_$kid';
+ if (business == 'live') {
+ resKid = 'live_$kid';
+ } else if (business.contains('article')) {
+ resKid = 'article_$kid';
+ }
+
+ var res = await UserHttp.delHistory(resKid);
+ if (res['status']) {
+ historyList.removeWhere((e) => e.kid == kid);
+ SmartDialog.showToast(res['msg']);
+ }
+ loadingStatus.value = 'finish';
+ }
+}
diff --git a/lib/pages/history_search/index.dart b/lib/pages/history_search/index.dart
new file mode 100644
index 00000000..a9db082b
--- /dev/null
+++ b/lib/pages/history_search/index.dart
@@ -0,0 +1,4 @@
+library history_search;
+
+export './controller.dart';
+export './view.dart';
diff --git a/lib/pages/history_search/view.dart b/lib/pages/history_search/view.dart
new file mode 100644
index 00000000..809e6d67
--- /dev/null
+++ b/lib/pages/history_search/view.dart
@@ -0,0 +1,174 @@
+import 'package:easy_debounce/easy_throttle.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/common/skeleton/video_card_h.dart';
+import 'package:pilipala/common/widgets/http_error.dart';
+import 'package:pilipala/common/widgets/no_data.dart';
+import 'package:pilipala/pages/history/widgets/item.dart';
+
+import 'controller.dart';
+
+class HistorySearchPage extends StatefulWidget {
+ const HistorySearchPage({super.key});
+
+ @override
+ State createState() => _HistorySearchPageState();
+}
+
+class _HistorySearchPageState extends State {
+ final HistorySearchController _historySearchCtr =
+ Get.put(HistorySearchController());
+ late ScrollController scrollController;
+
+ @override
+ void initState() {
+ super.initState();
+ scrollController = _historySearchCtr.scrollController;
+ scrollController.addListener(
+ () {
+ if (scrollController.position.pixels >=
+ scrollController.position.maxScrollExtent - 300) {
+ EasyThrottle.throttle('history', const Duration(seconds: 1), () {
+ _historySearchCtr.onLoad();
+ });
+ }
+ },
+ );
+ }
+
+ @override
+ void dispose() {
+ scrollController.removeListener(() {});
+ scrollController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ titleSpacing: 0,
+ actions: [
+ IconButton(
+ onPressed: () => _historySearchCtr.submit(),
+ icon: const Icon(Icons.search_outlined, size: 22)),
+ const SizedBox(width: 10)
+ ],
+ title: Obx(
+ () => TextField(
+ autofocus: true,
+ focusNode: _historySearchCtr.searchFocusNode,
+ controller: _historySearchCtr.controller.value,
+ textInputAction: TextInputAction.search,
+ onChanged: (value) => _historySearchCtr.onChange(value),
+ decoration: InputDecoration(
+ hintText: _historySearchCtr.hintText,
+ border: InputBorder.none,
+ suffixIcon: IconButton(
+ icon: Icon(
+ Icons.clear,
+ size: 22,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ onPressed: () => _historySearchCtr.onClear(),
+ ),
+ ),
+ onSubmitted: (String value) => _historySearchCtr.submit(),
+ ),
+ ),
+ ),
+ body: Obx(
+ () => Column(
+ children: _historySearchCtr.loadingStatus.value == 'init'
+ ? [const SizedBox()]
+ : [
+ Expanded(
+ child: FutureBuilder(
+ future: _historySearchCtr.searchHistories(),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ Map data = snapshot.data as Map;
+ if (data['status']) {
+ return Obx(
+ () => _historySearchCtr.historyList.isNotEmpty
+ ? ListView.builder(
+ controller: scrollController,
+ itemCount:
+ _historySearchCtr.historyList.length +
+ 1,
+ itemBuilder: (context, index) {
+ if (index ==
+ _historySearchCtr
+ .historyList.length) {
+ return Container(
+ height: MediaQuery.of(context)
+ .padding
+ .bottom +
+ 60,
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(context)
+ .padding
+ .bottom),
+ child: Center(
+ child: Obx(
+ () => Text(
+ _historySearchCtr
+ .loadingText.value,
+ style: TextStyle(
+ color: Theme.of(context)
+ .colorScheme
+ .outline,
+ fontSize: 13),
+ ),
+ ),
+ ),
+ );
+ } else {
+ return HistoryItem(
+ videoItem: _historySearchCtr
+ .historyList[index],
+ ctr: _historySearchCtr,
+ onChoose: null,
+ onUpdateMultiple: () => null,
+ );
+ ;
+ }
+ },
+ )
+ : _historySearchCtr.loadingStatus.value ==
+ 'loading'
+ ? const SizedBox(child: Text('加载中...'))
+ : const CustomScrollView(
+ slivers: [
+ NoData(),
+ ],
+ ),
+ );
+ } else {
+ return CustomScrollView(
+ slivers: [
+ HttpError(
+ errMsg: data['msg'],
+ fn: () => setState(() {}),
+ )
+ ],
+ );
+ }
+ } else {
+ // 骨架屏
+ return ListView.builder(
+ itemCount: 10,
+ itemBuilder: (context, index) {
+ return const VideoCardHSkeleton();
+ },
+ );
+ }
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/html/controller.dart b/lib/pages/html/controller.dart
new file mode 100644
index 00000000..f3187828
--- /dev/null
+++ b/lib/pages/html/controller.dart
@@ -0,0 +1,112 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/http/html.dart';
+import 'package:pilipala/http/reply.dart';
+import 'package:pilipala/models/common/reply_sort_type.dart';
+import 'package:pilipala/models/video/reply/item.dart';
+import 'package:pilipala/utils/feed_back.dart';
+import 'package:pilipala/utils/storage.dart';
+
+class HtmlRenderController extends GetxController {
+ late String id;
+ late String dynamicType;
+ late int type;
+ RxInt oid = (-1).obs;
+ late Map response;
+ int? floor;
+ int currentPage = 0;
+ bool isLoadingMore = false;
+ RxString noMore = ''.obs;
+ RxList replyList = [].obs;
+ RxInt acount = 0.obs;
+ final ScrollController scrollController = ScrollController();
+
+ ReplySortType _sortType = ReplySortType.time;
+ RxString sortTypeTitle = ReplySortType.time.titles.obs;
+ RxString sortTypeLabel = ReplySortType.time.labels.obs;
+ Box setting = GStrorage.setting;
+
+ @override
+ void onInit() {
+ super.onInit();
+ id = Get.parameters['id']!;
+ dynamicType = Get.parameters['dynamicType']!;
+ type = dynamicType == 'picture' ? 11 : 12;
+ }
+
+ // 请求动态内容
+ Future reqHtml(id) async {
+ late dynamic res;
+ if (dynamicType == 'opus' || dynamicType == 'picture') {
+ res = await HtmlHttp.reqHtml(id, dynamicType);
+ } else {
+ res = await HtmlHttp.reqReadHtml(id, dynamicType);
+ }
+ response = res;
+ oid.value = res['commentId'];
+ return res;
+ }
+
+ // 请求评论
+ Future queryReplyList({reqType = 'init'}) async {
+ var res = await ReplyHttp.replyList(
+ oid: oid.value,
+ pageNum: currentPage + 1,
+ type: type,
+ sort: _sortType.index,
+ );
+ if (res['status']) {
+ List replies = res['data'].replies;
+ acount.value = res['data'].page.acount;
+ if (replies.isNotEmpty) {
+ currentPage++;
+ noMore.value = '加载中...';
+ if (replies.length < 20) {
+ noMore.value = '没有更多了';
+ }
+ } else {
+ noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
+ }
+ if (reqType == 'init') {
+ // 添加置顶回复
+ if (res['data'].upper.top != null) {
+ bool flag = res['data']
+ .topReplies
+ .any((reply) => reply.rpid == res['data'].upper.top.rpid);
+ if (!flag) {
+ replies.insert(0, res['data'].upper.top);
+ }
+ }
+ replies.insertAll(0, res['data'].topReplies);
+ replyList.value = replies;
+ } else {
+ replyList.addAll(replies);
+ }
+ }
+ isLoadingMore = false;
+ return res;
+ }
+
+ // 排序搜索评论
+ queryBySort() {
+ feedBack();
+ switch (_sortType) {
+ case ReplySortType.time:
+ _sortType = ReplySortType.like;
+ break;
+ case ReplySortType.like:
+ _sortType = ReplySortType.reply;
+ break;
+ case ReplySortType.reply:
+ _sortType = ReplySortType.time;
+ break;
+ default:
+ }
+ sortTypeTitle.value = _sortType.titles;
+ sortTypeLabel.value = _sortType.labels;
+ currentPage = 0;
+ replyList.clear();
+ queryReplyList(reqType: 'init');
+ }
+}
diff --git a/lib/pages/html/index.dart b/lib/pages/html/index.dart
new file mode 100644
index 00000000..c62e60b7
--- /dev/null
+++ b/lib/pages/html/index.dart
@@ -0,0 +1,4 @@
+library html_render;
+
+export './controller.dart';
+export './view.dart';
diff --git a/lib/pages/html/view.dart b/lib/pages/html/view.dart
new file mode 100644
index 00000000..64fabff8
--- /dev/null
+++ b/lib/pages/html/view.dart
@@ -0,0 +1,457 @@
+import 'package:easy_debounce/easy_throttle.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/common/skeleton/video_reply.dart';
+import 'package:pilipala/common/widgets/html_render.dart';
+import 'package:pilipala/common/widgets/http_error.dart';
+import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/models/common/reply_type.dart';
+import 'package:pilipala/pages/mine/index.dart';
+import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
+import 'package:pilipala/pages/video/detail/replyNew/index.dart';
+import 'package:pilipala/pages/video/detail/replyReply/index.dart';
+import 'package:pilipala/utils/feed_back.dart';
+
+import 'controller.dart';
+
+class HtmlRenderPage extends StatefulWidget {
+ const HtmlRenderPage({super.key});
+
+ @override
+ State createState() => _HtmlRenderPageState();
+}
+
+class _HtmlRenderPageState extends State
+ with TickerProviderStateMixin {
+ final HtmlRenderController _htmlRenderCtr = Get.put(HtmlRenderController());
+ late String title;
+ late String id;
+ late String url;
+ late String dynamicType;
+ late int type;
+ bool _isFabVisible = true;
+ late Future _futureBuilderFuture;
+ late ScrollController scrollController;
+ late AnimationController fabAnimationCtr;
+
+ @override
+ void initState() {
+ super.initState();
+ title = Get.parameters['title']!;
+ id = Get.parameters['id']!;
+ url = Get.parameters['url']!;
+ dynamicType = Get.parameters['dynamicType']!;
+ type = dynamicType == 'picture' ? 11 : 12;
+ _futureBuilderFuture = _htmlRenderCtr.reqHtml(id);
+ fabAnimationCtr = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 300),
+ );
+ scrollListener();
+ }
+
+ void scrollListener() {
+ scrollController = _htmlRenderCtr.scrollController;
+ scrollController.addListener(
+ () {
+ // 分页加载
+ if (scrollController.position.pixels >=
+ scrollController.position.maxScrollExtent - 300) {
+ EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
+ _htmlRenderCtr.queryReplyList(reqType: 'onLoad');
+ });
+ }
+
+ // 标题
+ // if (scrollController.offset > 55 && !_visibleTitle) {
+ // _visibleTitle = true;
+ // titleStreamC.add(true);
+ // } else if (scrollController.offset <= 55 && _visibleTitle) {
+ // _visibleTitle = false;
+ // titleStreamC.add(false);
+ // }
+
+ // fab按钮
+ final ScrollDirection direction =
+ scrollController.position.userScrollDirection;
+ if (direction == ScrollDirection.forward) {
+ _showFab();
+ } else if (direction == ScrollDirection.reverse) {
+ _hideFab();
+ }
+ },
+ );
+ }
+
+ void _showFab() {
+ if (!_isFabVisible) {
+ _isFabVisible = true;
+ fabAnimationCtr.forward();
+ }
+ }
+
+ void _hideFab() {
+ if (_isFabVisible) {
+ _isFabVisible = false;
+ fabAnimationCtr.reverse();
+ }
+ }
+
+ void replyReply(replyItem) {
+ int oid = replyItem.oid;
+ int rpid = replyItem.rpid!;
+ Get.to(
+ () => Scaffold(
+ appBar: AppBar(
+ titleSpacing: 0,
+ centerTitle: false,
+ title: Text(
+ '评论详情',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ),
+ body: VideoReplyReplyPanel(
+ oid: oid,
+ rpid: rpid,
+ source: 'dynamic',
+ replyType: ReplyType.values[type],
+ firstFloor: replyItem,
+ ),
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ centerTitle: false,
+ titleSpacing: 0,
+ title: Text(
+ title,
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ actions: [
+ const SizedBox(width: 4),
+ IconButton(
+ onPressed: () {
+ Get.toNamed('/webview', parameters: {
+ 'url': url.startsWith('http') ? url : 'https:$url',
+ 'type': 'url',
+ 'pageTitle': title,
+ });
+ },
+ icon: const Icon(Icons.open_in_browser_outlined, size: 19),
+ ),
+ PopupMenuButton(
+ icon: const Icon(Icons.more_vert),
+ itemBuilder: (BuildContext context) => [
+ PopupMenuItem(
+ onTap: () => {
+ Clipboard.setData(ClipboardData(text: url)),
+ SmartDialog.showToast('已复制'),
+ },
+ child: const Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.copy_rounded, size: 19),
+ SizedBox(width: 10),
+ Text('复制链接'),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ onTap: () => {},
+ child: const Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.share_outlined, size: 19),
+ SizedBox(width: 10),
+ Text('分享'),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(width: 6)
+ ],
+ ),
+ body: Stack(
+ children: [
+ SingleChildScrollView(
+ controller: scrollController,
+ child: Column(
+ children: [
+ FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ var data = snapshot.data;
+ fabAnimationCtr.forward();
+ if (data['status']) {
+ return Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
+ child: Row(
+ children: [
+ NetworkImgLayer(
+ width: 40,
+ height: 40,
+ type: 'avatar',
+ src: _htmlRenderCtr.response['avatar']!,
+ ),
+ const SizedBox(width: 10),
+ Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ Text(_htmlRenderCtr.response['uname'],
+ style: TextStyle(
+ fontSize: Theme.of(context)
+ .textTheme
+ .titleSmall!
+ .fontSize,
+ )),
+ Text(
+ _htmlRenderCtr.response['updateTime'],
+ style: TextStyle(
+ color: Theme.of(context)
+ .colorScheme
+ .outline,
+ fontSize: Theme.of(context)
+ .textTheme
+ .labelSmall!
+ .fontSize,
+ ),
+ ),
+ ],
+ ),
+ const Spacer(),
+ ],
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
+ child: HtmlRender(
+ htmlContent: _htmlRenderCtr.response['content'],
+ ),
+ ),
+ Container(
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ width: 8,
+ color: Theme.of(context)
+ .dividerColor
+ .withOpacity(0.05),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ } else {
+ return const Text('error');
+ }
+ } else {
+ // 骨架屏
+ return const SizedBox();
+ }
+ },
+ ),
+ Obx(
+ () => _htmlRenderCtr.oid.value != -1
+ ? Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surface,
+ border: Border(
+ top: BorderSide(
+ width: 0.6,
+ color: Theme.of(context)
+ .dividerColor
+ .withOpacity(0.05),
+ ),
+ ),
+ ),
+ height: 45,
+ padding: const EdgeInsets.only(left: 12, right: 6),
+ child: Row(
+ children: [
+ const Text('回复'),
+ const Spacer(),
+ SizedBox(
+ height: 35,
+ child: TextButton.icon(
+ onPressed: () => _htmlRenderCtr.queryBySort(),
+ icon: const Icon(Icons.sort, size: 16),
+ label: Obx(
+ () => Text(
+ _htmlRenderCtr.sortTypeLabel.value,
+ style: const TextStyle(fontSize: 13),
+ ),
+ ),
+ ),
+ )
+ ],
+ ),
+ )
+ : const SizedBox(),
+ ),
+ Obx(
+ () => _htmlRenderCtr.oid.value != -1
+ ? FutureBuilder(
+ future: _htmlRenderCtr.queryReplyList(),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState ==
+ ConnectionState.done) {
+ Map data = snapshot.data as Map;
+ if (snapshot.data['status']) {
+ // 请求成功
+ return Obx(
+ () => _htmlRenderCtr.replyList.isEmpty &&
+ _htmlRenderCtr.isLoadingMore
+ ? ListView.builder(
+ itemCount: 5,
+ shrinkWrap: true,
+ physics:
+ const NeverScrollableScrollPhysics(),
+ itemBuilder: (context, index) {
+ return const VideoReplySkeleton();
+ },
+ )
+ : ListView.builder(
+ shrinkWrap: true,
+ physics:
+ const NeverScrollableScrollPhysics(),
+ itemCount:
+ _htmlRenderCtr.replyList.length +
+ 1,
+ itemBuilder: (context, index) {
+ if (index ==
+ _htmlRenderCtr
+ .replyList.length) {
+ return Container(
+ padding: EdgeInsets.only(
+ bottom:
+ MediaQuery.of(context)
+ .padding
+ .bottom),
+ height: MediaQuery.of(context)
+ .padding
+ .bottom +
+ 100,
+ child: Center(
+ child: Obx(
+ () => Text(
+ _htmlRenderCtr
+ .noMore.value,
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context)
+ .colorScheme
+ .outline,
+ ),
+ ),
+ ),
+ ),
+ );
+ } else {
+ return ReplyItem(
+ replyItem: _htmlRenderCtr
+ .replyList[index],
+ showReplyRow: true,
+ replyLevel: '1',
+ replyReply: (replyItem) =>
+ replyReply(replyItem),
+ replyType:
+ ReplyType.values[type],
+ addReply: (replyItem) {
+ _htmlRenderCtr
+ .replyList[index].replies!
+ .add(replyItem);
+ },
+ );
+ }
+ },
+ ),
+ );
+ } else {
+ // 请求错误
+ return CustomScrollView(
+ slivers: [
+ HttpError(
+ errMsg: data['msg'],
+ fn: () => setState(() {}),
+ )
+ ],
+ );
+ }
+ } else {
+ // 骨架屏
+ return ListView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: 5,
+ itemBuilder: (context, index) {
+ return const VideoReplySkeleton();
+ },
+ );
+ }
+ },
+ )
+ : const SizedBox(),
+ )
+ ],
+ ),
+ ),
+ Positioned(
+ bottom: MediaQuery.of(context).padding.bottom + 14,
+ right: 14,
+ child: SlideTransition(
+ position: Tween(
+ begin: const Offset(0, 2),
+ end: const Offset(0, 0),
+ ).animate(CurvedAnimation(
+ parent: fabAnimationCtr,
+ curve: Curves.easeInOut,
+ )),
+ child: FloatingActionButton(
+ heroTag: null,
+ onPressed: () {
+ feedBack();
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ builder: (BuildContext context) {
+ return VideoReplyNewDialog(
+ oid: _htmlRenderCtr.oid.value,
+ root: 0,
+ parent: 0,
+ replyType: ReplyType.values[type],
+ );
+ },
+ ).then(
+ (value) => {
+ // 完成评论,数据添加
+ if (value != null && value['data'] != null)
+ {
+ _htmlRenderCtr.replyList.add(value['data']),
+ _htmlRenderCtr.acount.value++
+ }
+ },
+ );
+ },
+ tooltip: '评论动态',
+ child: const Icon(Icons.reply),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/live/controller.dart b/lib/pages/live/controller.dart
index 8cdf53a7..6a26f0d2 100644
--- a/lib/pages/live/controller.dart
+++ b/lib/pages/live/controller.dart
@@ -20,14 +20,14 @@ class LiveController extends GetxController {
void onInit() {
super.onInit();
crossAxisCount.value =
- setting.get(SettingBoxKey.enableSingleRow, defaultValue: false) ? 1 : 2;
+ setting.get(SettingBoxKey.customRows, defaultValue: 2);
}
// 获取推荐
Future queryLiveList(type) async {
- if (type == 'init') {
- _currentPage = 1;
- }
+ // if (type == 'init') {
+ // _currentPage = 1;
+ // }
var res = await LiveHttp.liveList(
pn: _currentPage,
);
diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart
index 5e3e68a1..1fbff63c 100644
--- a/lib/pages/live/view.dart
+++ b/lib/pages/live/view.dart
@@ -10,6 +10,7 @@ import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/pages/main/index.dart';
+import 'package:pilipala/pages/rcmd/index.dart';
import 'controller.dart';
import 'widgets/live_item.dart';
@@ -21,11 +22,15 @@ class LivePage extends StatefulWidget {
State createState() => _LivePageState();
}
-class _LivePageState extends State {
+class _LivePageState extends State
+ with AutomaticKeepAliveClientMixin {
final LiveController _liveController = Get.put(LiveController());
late Future _futureBuilderFuture;
late ScrollController scrollController;
+ @override
+ bool get wantKeepAlive => true;
+
@override
void initState() {
super.initState();
@@ -37,7 +42,7 @@ class _LivePageState extends State {
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
- EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () {
+ EasyThrottle.throttle('liveList', const Duration(seconds: 1), () {
_liveController.isLoadingMore = true;
_liveController.onLoad();
});
@@ -84,6 +89,9 @@ class _LivePageState extends State {
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
+ if (snapshot.data == null) {
+ return const SliverToBoxAdapter(child: SizedBox());
+ }
Map data = snapshot.data as Map;
if (data['status']) {
return SliverLayoutBuilder(
@@ -111,7 +119,7 @@ class _LivePageState extends State {
},
),
),
- const LoadingMore()
+ LoadingMore(ctr: _liveController)
],
),
),
@@ -141,9 +149,9 @@ class _LivePageState extends State {
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
- mainAxisSpacing: StyleString.cardSpace + 4,
+ mainAxisSpacing: StyleString.safeSpace,
// 列间距
- crossAxisSpacing: StyleString.cardSpace + 4,
+ crossAxisSpacing: StyleString.safeSpace,
// 列数
crossAxisCount: crossAxisCount,
mainAxisExtent:
@@ -173,24 +181,3 @@ class _LivePageState extends State {
);
}
}
-
-class LoadingMore extends StatelessWidget {
- const LoadingMore({super.key});
-
- @override
- Widget build(BuildContext context) {
- return SliverToBoxAdapter(
- child: Container(
- height: MediaQuery.of(context).padding.bottom + 80,
- padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
- child: Center(
- child: Text(
- '加载中...',
- style: TextStyle(
- color: Theme.of(context).colorScheme.outline, fontSize: 13),
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/pages/live/widgets/live_item.dart b/lib/pages/live/widgets/live_item.dart
index 48a4356e..8fa797fb 100644
--- a/lib/pages/live/widgets/live_item.dart
+++ b/lib/pages/live/widgets/live_item.dart
@@ -24,7 +24,7 @@ class LiveCardV extends StatelessWidget {
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomId);
return Card(
- elevation: crossAxisCount == 1 ? 0 : 1,
+ elevation: 0,
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: GestureDetector(
@@ -102,7 +102,7 @@ class LiveContent extends StatelessWidget {
child: Padding(
padding: crossAxisCount == 1
? const EdgeInsets.fromLTRB(9, 9, 9, 4)
- : const EdgeInsets.fromLTRB(9, 8, 9, 8),
+ : const EdgeInsets.fromLTRB(5, 8, 5, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -120,15 +120,18 @@ class LiveContent extends StatelessWidget {
if (crossAxisCount == 1) const SizedBox(height: 4),
Row(
children: [
- Text(
- liveItem.uname,
- textAlign: TextAlign.start,
- style: TextStyle(
- fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
- color: Theme.of(context).colorScheme.outline,
+ Expanded(
+ child: Text(
+ liveItem.uname,
+ textAlign: TextAlign.start,
+ style: TextStyle(
+ fontSize:
+ Theme.of(context).textTheme.labelMedium!.fontSize,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
),
if (crossAxisCount == 1) ...[
Text(
@@ -169,7 +172,7 @@ class VideoStat extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
height: 50,
- padding: const EdgeInsets.only(top: 22, left: 10, right: 10),
+ padding: const EdgeInsets.only(top: 26, left: 10, right: 10),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
@@ -181,18 +184,17 @@ class VideoStat extends StatelessWidget {
tileMode: TileMode.mirror,
),
),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- liveItem!.areaName!,
- style: const TextStyle(fontSize: 11, color: Colors.white),
- ),
- Text(
- liveItem!.watchedShow!['text_small'],
- style: const TextStyle(fontSize: 11, color: Colors.white),
- ),
- ],
+ child: RichText(
+ maxLines: 1,
+ textAlign: TextAlign.justify,
+ softWrap: false,
+ text: TextSpan(
+ style: const TextStyle(fontSize: 11, color: Colors.white),
+ children: [
+ TextSpan(text: liveItem!.areaName!),
+ TextSpan(text: liveItem!.watchedShow!['text_small']),
+ ],
+ ),
),
);
}
diff --git a/lib/pages/liveRoom/view.dart b/lib/pages/liveRoom/view.dart
index fa881cb8..36b1f979 100644
--- a/lib/pages/liveRoom/view.dart
+++ b/lib/pages/liveRoom/view.dart
@@ -1,9 +1,13 @@
+import 'dart:io';
+
+import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'controller.dart';
+import 'widgets/bottom_control.dart';
class LiveRoomPage extends StatefulWidget {
const LiveRoomPage({super.key});
@@ -18,6 +22,7 @@ class _LiveRoomPageState extends State {
bool isShowCover = true;
bool isPlay = true;
+ Floating? floating;
@override
void initState() {
@@ -31,19 +36,24 @@ class _LiveRoomPageState extends State {
}
},
);
+ if (Platform.isAndroid) {
+ floating = Floating();
+ }
}
@override
void dispose() {
plPlayerController!.dispose();
+ if (floating != null) {
+ floating!.dispose();
+ }
super.dispose();
}
@override
Widget build(BuildContext context) {
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
-
- return Scaffold(
+ Widget childWhenDisabled = Scaffold(
primary: true,
appBar: AppBar(
centerTitle: false,
@@ -87,98 +97,61 @@ class _LiveRoomPageState extends State {
),
body: Column(
children: [
- Hero(
- tag: _liveRoomController.heroTag,
- child: Stack(
- children: [
- AspectRatio(
- aspectRatio: 16 / 9,
- child: plPlayerController!.videoPlayerController != null
- ? PLVideoPlayer(controller: plPlayerController!)
- : const SizedBox(),
- ),
- // if (_liveRoomController.liveItem != null &&
- // _liveRoomController.liveItem.cover != null)
- // Visibility(
- // visible: isShowCover,
- // child: Positioned(
- // top: 0,
- // left: 0,
- // right: 0,
- // child: NetworkImgLayer(
- // type: 'emote',
- // src: _liveRoomController.liveItem.cover,
- // width: Get.size.width,
- // height: videoHeight,
- // ),
- // ),
- // ),
- ],
- ),
+ Stack(
+ children: [
+ AspectRatio(
+ aspectRatio: 16 / 9,
+ child: plPlayerController!.videoPlayerController != null
+ ? PLVideoPlayer(
+ controller: plPlayerController!,
+ bottomControl: BottomControl(
+ controller: plPlayerController,
+ liveRoomCtr: _liveRoomController,
+ floating: floating,
+ ),
+ )
+ : const SizedBox(),
+ ),
+ // if (_liveRoomController.liveItem != null &&
+ // _liveRoomController.liveItem.cover != null)
+ // Visibility(
+ // visible: isShowCover,
+ // child: Positioned(
+ // top: 0,
+ // left: 0,
+ // right: 0,
+ // child: NetworkImgLayer(
+ // type: 'emote',
+ // src: _liveRoomController.liveItem.cover,
+ // width: Get.size.width,
+ // height: videoHeight,
+ // ),
+ // ),
+ // ),
+ ],
),
- // Container(
- // height: 45,
- // padding: const EdgeInsets.only(left: 12, right: 12),
- // decoration: BoxDecoration(
- // color: Theme.of(context).colorScheme.background,
- // border: Border(
- // bottom: BorderSide(
- // color: Theme.of(context).dividerColor.withOpacity(0.1)),
- // ),
- // ),
- // child: Row(children: [
- // SizedBox(
- // width: 38,
- // height: 38,
- // child: IconButton(
- // onPressed: () {},
- // icon: const Icon(
- // Icons.subtitles_outlined,
- // size: 21,
- // ),
- // ),
- // ),
- // const Spacer(),
- // SizedBox(
- // width: 38,
- // height: 38,
- // child: IconButton(
- // onPressed: () {},
- // icon: const Icon(
- // Icons.hd_outlined,
- // size: 20,
- // ),
- // ),
- // ),
- // SizedBox(
- // width: 38,
- // height: 38,
- // child: IconButton(
- // onPressed: () => _liveRoomController
- // .setVolumn(plPlayerController!.volume.value),
- // icon: Obx(() => Icon(
- // _liveRoomController.volumeOff.value
- // ? Icons.volume_off_outlined
- // : Icons.volume_up_outlined,
- // size: 21,
- // )),
- // ),
- // ),
- // SizedBox(
- // width: 38,
- // height: 38,
- // child: IconButton(
- // onPressed: () => {},
- // // plPlayerController!.goToFullscreen(context),
- // icon: const Icon(
- // Icons.fullscreen,
- // ),
- // ),
- // ),
- // ]),
- // ),
],
),
);
+ Widget childWhenEnabled = AspectRatio(
+ aspectRatio: 16 / 9,
+ child: plPlayerController!.videoPlayerController != null
+ ? PLVideoPlayer(
+ controller: plPlayerController!,
+ bottomControl: BottomControl(
+ controller: plPlayerController,
+ liveRoomCtr: _liveRoomController,
+ ),
+ )
+ : const SizedBox(),
+ );
+ if (Platform.isAndroid) {
+ return PiPSwitcher(
+ childWhenDisabled: childWhenDisabled,
+ childWhenEnabled: childWhenEnabled,
+ );
+ } else {
+ return childWhenDisabled;
+ }
}
}
diff --git a/lib/pages/liveRoom/widgets/bottom_control.dart b/lib/pages/liveRoom/widgets/bottom_control.dart
new file mode 100644
index 00000000..49343bb1
--- /dev/null
+++ b/lib/pages/liveRoom/widgets/bottom_control.dart
@@ -0,0 +1,151 @@
+import 'dart:io';
+
+import 'package:floating/floating.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:get/get.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/models/video/play/url.dart';
+import 'package:pilipala/pages/liveRoom/index.dart';
+import 'package:pilipala/plugin/pl_player/index.dart';
+import 'package:pilipala/utils/storage.dart';
+
+class BottomControl extends StatefulWidget implements PreferredSizeWidget {
+ final PlPlayerController? controller;
+ final LiveRoomController? liveRoomCtr;
+ final Floating? floating;
+ const BottomControl({
+ this.controller,
+ this.liveRoomCtr,
+ this.floating,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ State createState() => _BottomControlState();
+
+ @override
+ Size get preferredSize => throw UnimplementedError();
+}
+
+class _BottomControlState extends State {
+ late PlayUrlModel videoInfo;
+ List playSpeed = PlaySpeed.values;
+ TextStyle subTitleStyle = const TextStyle(fontSize: 12);
+ TextStyle titleStyle = const TextStyle(fontSize: 14);
+ Size get preferredSize => const Size(double.infinity, kToolbarHeight);
+ Box localCache = GStrorage.localCache;
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ const textStyle = TextStyle(
+ color: Colors.white,
+ fontSize: 12,
+ );
+ return AppBar(
+ backgroundColor: Colors.transparent,
+ foregroundColor: Colors.white,
+ elevation: 0,
+ scrolledUnderElevation: 0,
+ primary: false,
+ centerTitle: false,
+ automaticallyImplyLeading: false,
+ titleSpacing: 14,
+ title: Row(
+ children: [
+ // ComBtn(
+ // icon: const Icon(
+ // Icons.subtitles_outlined,
+ // size: 18,
+ // color: Colors.white,
+ // ),
+ // fuc: () => Get.back(),
+ // ),
+ const Spacer(),
+ // ComBtn(
+ // icon: const Icon(
+ // Icons.hd_outlined,
+ // size: 18,
+ // color: Colors.white,
+ // ),
+ // fuc: () => {},
+ // ),
+ // const SizedBox(width: 4),
+ // Obx(
+ // () => ComBtn(
+ // icon: Icon(
+ // widget.liveRoomCtr!.volumeOff.value
+ // ? Icons.volume_off_outlined
+ // : Icons.volume_up_outlined,
+ // size: 18,
+ // color: Colors.white,
+ // ),
+ // fuc: () => {},
+ // ),
+ // ),
+ // const SizedBox(width: 4),
+ if (Platform.isAndroid) ...[
+ SizedBox(
+ width: 34,
+ height: 34,
+ child: IconButton(
+ style: ButtonStyle(
+ padding: MaterialStateProperty.all(EdgeInsets.zero),
+ ),
+ onPressed: () async {
+ bool canUsePiP = false;
+ widget.controller!.hiddenControls(false);
+ try {
+ canUsePiP = await widget.floating!.isPipAvailable;
+ } on PlatformException catch (_) {
+ canUsePiP = false;
+ }
+ if (canUsePiP) {
+ await widget.floating!.enable();
+ } else {}
+ },
+ icon: const Icon(
+ Icons.picture_in_picture_outlined,
+ size: 18,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ const SizedBox(width: 4),
+ ],
+ ComBtn(
+ icon: const Icon(
+ Icons.fullscreen,
+ size: 20,
+ color: Colors.white,
+ ),
+ fuc: () => widget.controller!.triggerFullScreen(),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class MSliderTrackShape extends RoundedRectSliderTrackShape {
+ @override
+ Rect getPreferredRect({
+ required RenderBox parentBox,
+ Offset offset = Offset.zero,
+ SliderThemeData? sliderTheme,
+ bool isEnabled = false,
+ bool isDiscrete = false,
+ }) {
+ const double trackHeight = 3;
+ final double trackLeft = offset.dx;
+ final double trackTop =
+ offset.dy + (parentBox.size.height - trackHeight) / 2 + 4;
+ final double trackWidth = parentBox.size.width;
+ return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
+ }
+}
diff --git a/lib/pages/login/controller.dart b/lib/pages/login/controller.dart
new file mode 100644
index 00000000..c002fdf9
--- /dev/null
+++ b/lib/pages/login/controller.dart
@@ -0,0 +1,204 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/http/login.dart';
+import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';
+import 'package:pilipala/models/login/index.dart';
+
+class LoginPageController extends GetxController {
+ final GlobalKey mobFormKey = GlobalKey();
+ final GlobalKey passwordFormKey = GlobalKey();
+ final GlobalKey msgCodeFormKey = GlobalKey();
+
+ final TextEditingController mobTextController = TextEditingController();
+ final TextEditingController passwordTextController = TextEditingController();
+ final TextEditingController msgCodeTextController = TextEditingController();
+
+ final FocusNode mobTextFieldNode = FocusNode();
+ final FocusNode passwordTextFieldNode = FocusNode();
+ final FocusNode msgCodeTextFieldNode = FocusNode();
+
+ final PageController pageViewController = PageController();
+
+ RxInt currentIndex = 0.obs;
+
+ final Gt3FlutterPlugin captcha = Gt3FlutterPlugin();
+
+ // 默认密码登录
+ RxInt loginType = 0.obs;
+
+ // 监听pageView切换
+ void onPageChange(int index) {
+ currentIndex.value = index;
+ }
+
+ // 输入手机号 下一页
+ void nextStep() async {
+ if ((mobFormKey.currentState as FormState).validate()) {
+ await pageViewController.animateToPage(
+ 1,
+ duration: const Duration(microseconds: 3000),
+ curve: Curves.easeInOut,
+ );
+ passwordTextFieldNode.requestFocus();
+ }
+ }
+
+ // 上一页
+ void previousPage() async {
+ passwordTextFieldNode.unfocus();
+ await Future.delayed(const Duration(milliseconds: 200));
+ pageViewController.animateToPage(
+ 0,
+ duration: const Duration(microseconds: 300),
+ curve: Curves.easeInOut,
+ );
+ }
+
+ // 切换登录方式
+ void changeLoginType() {
+ loginType.value = loginType.value == 0 ? 1 : 0;
+ if (loginType.value == 0) {
+ passwordTextFieldNode.requestFocus();
+ } else {
+ msgCodeTextFieldNode.requestFocus();
+ }
+ }
+
+ // app端密码登录
+ void loginInByAppPassword() async {
+ if ((passwordFormKey.currentState as FormState).validate()) {
+ var webKeyRes = await LoginHttp.getWebKey();
+ if (webKeyRes['status']) {
+ String rhash = webKeyRes['data']['hash'];
+ String key = webKeyRes['data']['key'];
+ LoginHttp.loginInByMobPwd(
+ tel: mobTextController.text,
+ password: passwordTextController.text,
+ key: key,
+ rhash: rhash,
+ );
+ } else {
+ SmartDialog.showToast(webKeyRes['msg']);
+ }
+ }
+ }
+
+ // 验证码登录
+ void loginInByCode() {
+ if ((msgCodeFormKey.currentState as FormState).validate()) {}
+ }
+
+ // app端验证码
+ void getMsgCode() async {
+ getCaptcha((data) async {
+ CaptchaDataModel captchaData = data;
+ var res = await LoginHttp.sendAppSmsCode(
+ cid: 86,
+ tel: 13734077064,
+ token: captchaData.token!,
+ challenge: captchaData.geetest!.challenge!,
+ validate: captchaData.validate!,
+ seccode: captchaData.seccode!,
+ );
+ print(res);
+ });
+ }
+
+ // 申请极验验证码
+ Future getCaptcha(oncall) async {
+ SmartDialog.showLoading(msg: '请求中...');
+ var result = await LoginHttp.queryCaptcha();
+ if (result['status']) {
+ CaptchaDataModel captchaData = result['data'];
+ var registerData = Gt3RegisterData(
+ challenge: captchaData.geetest!.challenge,
+ gt: captchaData.geetest!.gt!,
+ success: true,
+ );
+ captcha.addEventHandler(onShow: (Map message) async {
+ SmartDialog.dismiss();
+ }, onClose: (Map message) async {
+ SmartDialog.showToast('关闭验证');
+ }, onResult: (Map message) async {
+ debugPrint("Captcha result: $message");
+ String code = message["code"];
+ if (code == "1") {
+ // 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询
+ SmartDialog.showToast('验证成功');
+ captchaData.validate = message['result']['geetest_validate'];
+ captchaData.seccode = message['result']['geetest_seccode'];
+ captchaData.geetest!.challenge =
+ message['result']['geetest_challenge'];
+ oncall(captchaData);
+ } else {
+ // 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried.
+ debugPrint("Captcha result code : $code");
+ }
+ }, onError: (Map message) async {
+ String code = message["code"];
+
+ // 处理验证中返回的错误 Handling errors returned in verification
+ if (Platform.isAndroid) {
+ // Android 平台
+ if (code == "-2") {
+ // Dart 调用异常 Call exception
+ } else if (code == "-1") {
+ // Gt3RegisterData 参数不合法 Parameter is invalid
+ } else if (code == "201") {
+ // 网络无法访问 Network inaccessible
+ } else if (code == "202") {
+ // Json 解析错误 Analysis error
+ } else if (code == "204") {
+ // WebView 加载超时,请检查是否混淆极验 SDK Load timed out
+ } else if (code == "204_1") {
+ // WebView 加载前端页面错误,请查看日志 Error loading front-end page, please check the log
+ } else if (code == "204_2") {
+ // WebView 加载 SSLError
+ } else if (code == "206") {
+ // gettype 接口错误或返回为 null API error or return null
+ } else if (code == "207") {
+ // getphp 接口错误或返回为 null API error or return null
+ } else if (code == "208") {
+ // ajax 接口错误或返回为 null API error or return null
+ } else {
+ // 更多错误码参考开发文档 More error codes refer to the development document
+ // https://docs.geetest.com/sensebot/apirefer/errorcode/android
+ }
+ }
+
+ if (Platform.isIOS) {
+ // iOS 平台
+ if (code == "-1009") {
+ // 网络无法访问 Network inaccessible
+ } else if (code == "-1004") {
+ // 无法查找到 HOST Unable to find HOST
+ } else if (code == "-1002") {
+ // 非法的 URL Illegal URL
+ } else if (code == "-1001") {
+ // 网络超时 Network timeout
+ } else if (code == "-999") {
+ // 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation
+ } else if (code == "-21") {
+ // 使用了重复的 challenge Duplicate challenges are used
+ // 检查获取 challenge 是否进行了缓存 Check if the fetch challenge is cached
+ } else if (code == "-20") {
+ // 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again
+ } else if (code == "-10") {
+ // 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification
+ } else if (code == "-2") {
+ // Dart 调用异常 Call exception
+ } else if (code == "-1") {
+ // Gt3RegisterData 参数不合法 Parameter is invalid
+ } else {
+ // 更多错误码参考开发文档 More error codes refer to the development document
+ // https://docs.geetest.com/sensebot/apirefer/errorcode/ios
+ }
+ }
+ });
+ captcha.startCaptcha(registerData);
+ } else {}
+ }
+}
diff --git a/lib/pages/login/index.dart b/lib/pages/login/index.dart
new file mode 100644
index 00000000..cdc05abd
--- /dev/null
+++ b/lib/pages/login/index.dart
@@ -0,0 +1,4 @@
+library login;
+
+export './controller.dart';
+export 'view.dart';
diff --git a/lib/pages/login/view.dart b/lib/pages/login/view.dart
new file mode 100644
index 00000000..6521e9d9
--- /dev/null
+++ b/lib/pages/login/view.dart
@@ -0,0 +1,362 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+import 'controller.dart';
+
+class LoginPage extends StatefulWidget {
+ const LoginPage({super.key});
+
+ @override
+ State createState() => _LoginPageState();
+}
+
+class _LoginPageState extends State {
+ final LoginPageController _loginPageCtr = Get.put(LoginPageController());
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: Obx(
+ () => _loginPageCtr.currentIndex.value == 0
+ ? IconButton(
+ onPressed: () async {
+ _loginPageCtr.mobTextFieldNode.unfocus();
+ await Future.delayed(const Duration(milliseconds: 200));
+ Get.back();
+ },
+ icon: const Icon(Icons.close_outlined),
+ )
+ : IconButton(
+ onPressed: () => _loginPageCtr.previousPage(),
+ icon: const Icon(Icons.arrow_back),
+ ),
+ ),
+ ),
+ body: PageView(
+ physics: const NeverScrollableScrollPhysics(),
+ controller: _loginPageCtr.pageViewController,
+ onPageChanged: (int index) => _loginPageCtr.onPageChange(index),
+ children: [
+ Padding(
+ padding: EdgeInsets.only(
+ left: 20,
+ right: 20,
+ top: 10,
+ bottom: MediaQuery.of(context).padding.bottom + 10,
+ ),
+ child: Form(
+ key: _loginPageCtr.mobFormKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Text(
+ '登录',
+ style: Theme.of(context).textTheme.titleLarge!.copyWith(
+ letterSpacing: 1,
+ height: 2.1,
+ fontSize: 34,
+ fontWeight: FontWeight.w500),
+ ),
+ Row(
+ children: [
+ Text(
+ '请使用您的 BiliBili 账号登录。',
+ style: Theme.of(context).textTheme.titleSmall!,
+ ),
+ GestureDetector(
+ onTap: () {},
+ child: const Icon(Icons.info_outline, size: 16),
+ )
+ ],
+ ),
+ Container(
+ margin: const EdgeInsets.only(top: 38, bottom: 15),
+ child: TextFormField(
+ controller: _loginPageCtr.mobTextController,
+ focusNode: _loginPageCtr.mobTextFieldNode,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(
+ isDense: true,
+ labelText: '输入手机号码',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(6.0),
+ ),
+ ),
+ // 校验用户名
+ validator: (v) {
+ return v!.trim().isNotEmpty ? null : "手机号码不能为空";
+ },
+ onSaved: (val) {
+ print(val);
+ },
+ onEditingComplete: () {
+ _loginPageCtr.nextStep();
+ },
+ ),
+ ),
+ GestureDetector(
+ onTap: () {
+ Get.offNamed(
+ '/webview',
+ parameters: {
+ 'url':
+ 'https://passport.bilibili.com/h5-app/passport/login',
+ 'type': 'login',
+ 'pageTitle': '登录bilibili',
+ },
+ );
+ },
+ child: Padding(
+ padding: const EdgeInsets.only(left: 2),
+ child: Text(
+ '使用网页端登录',
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.primary),
+ ),
+ ),
+ ),
+ const Spacer(),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ TextButton(onPressed: () {}, child: const Text('中国大陆')),
+ TextButton(
+ style: TextButton.styleFrom(
+ padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
+ foregroundColor:
+ Theme.of(context).colorScheme.onPrimary,
+ backgroundColor:
+ Theme.of(context).colorScheme.primary, // 设置按钮背景色
+ ),
+ onPressed: () => _loginPageCtr.nextStep(),
+ child: const Text('下一步'),
+ )
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.only(
+ left: 20,
+ right: 20,
+ top: 10,
+ bottom: MediaQuery.of(context).padding.bottom + 10,
+ ),
+ child: Obx(
+ () => _loginPageCtr.loginType.value == 0
+ ? Form(
+ key: _loginPageCtr.passwordFormKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Row(
+ children: [
+ Text(
+ '密码登录',
+ style: Theme.of(context)
+ .textTheme
+ .titleLarge!
+ .copyWith(
+ letterSpacing: 1,
+ height: 2.1,
+ fontSize: 34,
+ fontWeight: FontWeight.w500),
+ ),
+ const SizedBox(width: 4),
+ IconButton(
+ style: ButtonStyle(
+ backgroundColor:
+ MaterialStateProperty.resolveWith(
+ (states) {
+ return Theme.of(context)
+ .colorScheme
+ .primary
+ .withOpacity(0.1);
+ }),
+ ),
+ onPressed: () =>
+ _loginPageCtr.changeLoginType(),
+ icon: const Icon(Icons.swap_vert_outlined),
+ )
+ ],
+ ),
+ Text(
+ '请输入您的 BiliBili 密码。',
+ style: Theme.of(context).textTheme.titleSmall!,
+ ),
+ Container(
+ margin: const EdgeInsets.only(top: 38, bottom: 15),
+ child: TextFormField(
+ controller: _loginPageCtr.passwordTextController,
+ focusNode: _loginPageCtr.passwordTextFieldNode,
+ keyboardType: TextInputType.visiblePassword,
+ decoration: InputDecoration(
+ isDense: true,
+ labelText: '输入密码',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(6.0),
+ ),
+ ),
+ // 校验用户名
+ validator: (v) {
+ return v!.trim().isNotEmpty ? null : "密码不能为空";
+ },
+ onSaved: (val) {
+ print(val);
+ },
+ ),
+ ),
+ const Spacer(),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ TextButton(
+ onPressed: () => _loginPageCtr.previousPage(),
+ child: const Text('上一步'),
+ ),
+ const SizedBox(width: 15),
+ TextButton(
+ style: TextButton.styleFrom(
+ padding:
+ const EdgeInsets.fromLTRB(20, 0, 20, 0),
+ foregroundColor:
+ Theme.of(context).colorScheme.onPrimary,
+ backgroundColor: Theme.of(context)
+ .colorScheme
+ .primary, // 设置按钮背景色
+ ),
+ onPressed: () =>
+ _loginPageCtr.loginInByAppPassword(),
+ child: const Text('确认登录'),
+ )
+ ],
+ ),
+ ],
+ ),
+ )
+ : Form(
+ key: _loginPageCtr.msgCodeFormKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Row(
+ children: [
+ Text(
+ '验证码登录',
+ style: Theme.of(context)
+ .textTheme
+ .titleLarge!
+ .copyWith(
+ letterSpacing: 1,
+ height: 2.1,
+ fontSize: 34,
+ fontWeight: FontWeight.w500),
+ ),
+ const SizedBox(width: 4),
+ IconButton(
+ style: ButtonStyle(
+ backgroundColor:
+ MaterialStateProperty.resolveWith(
+ (states) {
+ return Theme.of(context)
+ .colorScheme
+ .primary
+ .withOpacity(0.1);
+ }),
+ ),
+ onPressed: () =>
+ _loginPageCtr.changeLoginType(),
+ icon: const Icon(Icons.swap_vert_outlined),
+ )
+ ],
+ ),
+ Text(
+ '请输入收到到验证码。',
+ style: Theme.of(context).textTheme.titleSmall!,
+ ),
+ Container(
+ margin: const EdgeInsets.only(top: 38, bottom: 15),
+ child: Stack(
+ children: [
+ TextFormField(
+ controller:
+ _loginPageCtr.msgCodeTextController,
+ focusNode: _loginPageCtr.msgCodeTextFieldNode,
+ maxLength: 6,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(
+ isDense: true,
+ labelText: '输入验证码',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(6.0),
+ ),
+ ),
+ // 校验用户名
+ validator: (v) {
+ return v!.trim().isNotEmpty
+ ? null
+ : "验证码不能为空";
+ },
+ onSaved: (val) {
+ print(val);
+ },
+ ),
+ Positioned(
+ right: 8,
+ top: 4,
+ child: Center(
+ child: TextButton(
+ onPressed: () =>
+ _loginPageCtr.getMsgCode(),
+ child: const Text('获取验证码'),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const Spacer(),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ TextButton(
+ onPressed: () => _loginPageCtr.previousPage(),
+ child: const Text('上一步'),
+ ),
+ const SizedBox(width: 15),
+ TextButton(
+ style: TextButton.styleFrom(
+ padding:
+ const EdgeInsets.fromLTRB(20, 0, 20, 0),
+ foregroundColor:
+ Theme.of(context).colorScheme.onPrimary,
+ backgroundColor: Theme.of(context)
+ .colorScheme
+ .primary, // 设置按钮背景色
+ ),
+ onPressed: () => _loginPageCtr.loginInByCode(),
+ child: const Text('确认登录'),
+ )
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart
index c744098e..ee8d3829 100644
--- a/lib/pages/main/view.dart
+++ b/lib/pages/main/view.dart
@@ -29,6 +29,8 @@ class _MainAppState extends State with SingleTickerProviderStateMixin {
late Animation? _slideAnimation;
int selectedIndex = 0;
int? _lastSelectTime; //上次点击时间
+ Box setting = GStrorage.setting;
+ late bool enableMYBar;
@override
void initState() {
@@ -45,6 +47,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin {
Tween(begin: 0.8, end: 1.0).animate(_animationController!);
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
_pageController = PageController(initialPage: selectedIndex);
+ enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
}
void setIndex(int value) async {
@@ -144,21 +147,38 @@ class _MainAppState extends State with SingleTickerProviderStateMixin {
builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide(
curve: Curves.easeInOutCubicEmphasized,
- duration: const Duration(milliseconds: 1000),
+ duration: const Duration(milliseconds: 500),
offset: Offset(0, snapshot.data ? 0 : 1),
- child: NavigationBar(
- onDestinationSelected: (value) => setIndex(value),
- selectedIndex: selectedIndex,
- destinations: [
- ..._mainController.navigationBars.map((e) {
- return NavigationDestination(
- icon: e['icon'],
- selectedIcon: e['selectIcon'],
- label: e['label'],
- );
- }).toList(),
- ],
- ),
+ child: enableMYBar
+ ? NavigationBar(
+ onDestinationSelected: (value) => setIndex(value),
+ selectedIndex: selectedIndex,
+ destinations: [
+ ..._mainController.navigationBars.map((e) {
+ return NavigationDestination(
+ icon: e['icon'],
+ selectedIcon: e['selectIcon'],
+ label: e['label'],
+ );
+ }).toList(),
+ ],
+ )
+ : BottomNavigationBar(
+ currentIndex: selectedIndex,
+ onTap: (value) => setIndex(value),
+ iconSize: 16,
+ selectedFontSize: 12,
+ unselectedFontSize: 12,
+ items: [
+ ..._mainController.navigationBars.map((e) {
+ return BottomNavigationBarItem(
+ icon: e['icon'],
+ activeIcon: e['selectIcon'],
+ label: e['label'],
+ );
+ }).toList(),
+ ],
+ ),
);
},
),
diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart
index 8ce33597..c621c2dc 100644
--- a/lib/pages/media/view.dart
+++ b/lib/pages/media/view.dart
@@ -39,45 +39,47 @@ class _MediaPageState extends State
Color primary = Theme.of(context).colorScheme.primary;
return Scaffold(
appBar: AppBar(toolbarHeight: 30),
- body: Column(
- children: [
- ListTile(
- leading: null,
- title: Padding(
- padding: const EdgeInsets.only(left: 20),
- child: Text(
- '媒体库',
- style: TextStyle(
- fontSize: Theme.of(context).textTheme.titleLarge!.fontSize,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ),
- for (var i in mediaController.list) ...[
+ body: SingleChildScrollView(
+ child: Column(
+ children: [
ListTile(
- onTap: () => i['onTap'](),
- dense: true,
- leading: Padding(
- padding: const EdgeInsets.only(left: 15),
- child: Icon(
- i['icon'],
- color: primary,
+ leading: null,
+ title: Padding(
+ padding: const EdgeInsets.only(left: 20),
+ child: Text(
+ '媒体库',
+ style: TextStyle(
+ fontSize: Theme.of(context).textTheme.titleLarge!.fontSize,
+ fontWeight: FontWeight.bold,
+ ),
),
),
- contentPadding:
- const EdgeInsets.only(left: 15, top: 2, bottom: 2),
- minLeadingWidth: 0,
- title: Text(
- i['title'],
- style: const TextStyle(fontSize: 15),
- ),
),
+ for (var i in mediaController.list) ...[
+ ListTile(
+ onTap: () => i['onTap'](),
+ dense: true,
+ leading: Padding(
+ padding: const EdgeInsets.only(left: 15),
+ child: Icon(
+ i['icon'],
+ color: primary,
+ ),
+ ),
+ contentPadding:
+ const EdgeInsets.only(left: 15, top: 2, bottom: 2),
+ minLeadingWidth: 0,
+ title: Text(
+ i['title'],
+ style: const TextStyle(fontSize: 15),
+ ),
+ ),
+ ],
+ Obx(() => mediaController.userLogin.value
+ ? favFolder(mediaController, context)
+ : const SizedBox())
],
- Obx(() => mediaController.userLogin.value
- ? favFolder(mediaController, context)
- : const SizedBox())
- ],
+ ),
),
);
}
@@ -136,11 +138,14 @@ class _MediaPageState extends State
// const SizedBox(height: 10),
SizedBox(
width: double.infinity,
- height: 170 * MediaQuery.of(context).textScaleFactor,
+ height: 200 * MediaQuery.of(context).textScaleFactor,
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
+ if (snapshot.data == null) {
+ return const SizedBox();
+ }
Map data = snapshot.data as Map;
if (data['status']) {
List favFolderList =
diff --git a/lib/pages/member/archive/controller.dart b/lib/pages/member/archive/controller.dart
index e893a07d..3fc90328 100644
--- a/lib/pages/member/archive/controller.dart
+++ b/lib/pages/member/archive/controller.dart
@@ -2,23 +2,43 @@ import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
class ArchiveController extends GetxController {
+ ArchiveController(this.mid);
int? mid;
int pn = 1;
int count = 0;
+ RxMap currentOrder = {}.obs;
+ List