Compare commits

..

31 Commits

Author SHA1 Message Date
3d2c6a122a feat: 充电视频试看 2024-02-16 21:30:29 +08:00
7a78729a44 fix: 合集切换推荐视频未刷新 2024-02-16 18:23:34 +08:00
03e5e22fef Merge pull request #458 from orz12/mod-add-time-in-rcmd-and-search
mod: 搜索和推荐页增加时间
2024-02-16 11:42:29 +08:00
aa93ce0b89 Merge branch 'main' into mod-add-time-in-rcmd-and-search 2024-02-16 11:42:01 +08:00
0c365ad049 Merge branch 'design' 2024-02-16 11:00:48 +08:00
3d5c578fef mod: 动态页面upPanel 2024-02-16 11:00:23 +08:00
0a22f0f543 Merge branch 'design' 2024-02-16 09:49:55 +08:00
5bf7b69d79 feat: 收藏搜索结果删除 2024-02-16 09:33:59 +08:00
d57f84a1d7 fix: 路由跳转传参丢失 2024-02-15 21:59:28 +08:00
32b2f0ceff Merge pull request #539 from orz12/fix-speed-dialog-cannot-dismiss
fix: 播放速度dialog无法关闭
2024-02-15 21:10:52 +08:00
bae871cfa1 Merge branch 'feature-replyItem' 2024-02-15 21:07:55 +08:00
d95fe9fe14 mod: MorePanel样式 2024-02-15 21:07:23 +08:00
eb006e4c55 Merge branch 'feature-replyItem' 2024-02-14 20:09:48 +08:00
cb88d0c9ae Merge branch 'feature-liveRoomRender' 2024-02-14 20:09:40 +08:00
3efad736ae fix: 直播闪退 issues #540 2024-02-14 19:38:55 +08:00
42ad959155 fix: 速度设置无法取消 2024-02-14 08:44:00 +08:00
cdf800c49f mod: 评论复制逻辑 issues #420 #331 #297 #152 2024-02-13 23:33:51 +08:00
569277572a Merge pull request #536 from KoolShow/fix_seekto_regexp
fix: 含有小时的时间无法跳转
2024-02-12 18:11:26 +08:00
19b84571c1 Merge branch 'main' into fix_seekto_regexp 2024-02-12 18:11:15 +08:00
0812b8339e Merge branch 'feature-replyItem' 2024-02-12 17:55:08 +08:00
b817a0c807 修正正则表达式以匹配含小时的时间 2024-02-12 17:20:18 +08:00
3da70d7e27 Merge branch 'fix-replyRepeat' 2024-02-12 16:55:56 +08:00
5e59db85be fix: 评论笔记跳转 issues #472 2024-02-12 16:51:05 +08:00
77477ff4dd mod: merge main 2024-02-12 10:30:18 +08:00
44a162762c fix: 评论页面路由跳转 issues #405 2024-02-09 23:24:26 +08:00
b0c56feef5 mod: 首页网络异常请求重试 2024-02-07 02:47:11 +08:00
191472d0c4 mod: 网络请求异常样式修改 2024-02-07 01:17:35 +08:00
40c666e3d1 mod: 网络异常组件样式修改 2024-02-07 00:52:25 +08:00
10d2995429 mod: 对齐搜索栏调整 2024-01-27 12:06:45 +08:00
23c8b34189 fix: app端推荐屏蔽时间显示,播放量与弹幕组件改为动态类型 2024-01-26 16:40:29 +08:00
932be48125 mod: 推荐、搜索页添加时间,修复视频搜索页无法筛选和回顶 2024-01-26 16:38:56 +08:00
32 changed files with 665 additions and 380 deletions

View File

@ -22,20 +22,27 @@ class HttpError extends StatelessWidget {
"assets/images/error.svg", "assets/images/error.svg",
height: 200, height: 200,
), ),
const SizedBox(height: 20), const SizedBox(height: 30),
Text( Text(
errMsg ?? '请求异常', errMsg ?? '请求异常',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
const SizedBox(height: 30), const SizedBox(height: 20),
OutlinedButton.icon( FilledButton.tonal(
onPressed: () { onPressed: () {
fn!(); fn!();
}, },
icon: const Icon(Icons.arrow_forward_outlined, size: 20), style: ButtonStyle(
label: Text(btnText ?? '点击重试'), backgroundColor: MaterialStateProperty.resolveWith((states) {
) return Theme.of(context).colorScheme.primary.withAlpha(20);
}),
),
child: Text(
btnText ?? '点击重试',
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
),
], ],
), ),
), ),

View File

@ -3,7 +3,7 @@ import 'package:pilipala/utils/utils.dart';
class StatDanMu extends StatelessWidget { class StatDanMu extends StatelessWidget {
final String? theme; final String? theme;
final int? danmu; final dynamic danmu;
final String? size; final String? size;
const StatDanMu({Key? key, this.theme, this.danmu, this.size}) const StatDanMu({Key? key, this.theme, this.danmu, this.size})

View File

@ -3,7 +3,7 @@ import 'package:pilipala/utils/utils.dart';
class StatView extends StatelessWidget { class StatView extends StatelessWidget {
final String? theme; final String? theme;
final int? view; final dynamic view;
final String? size; final String? size;
const StatView({Key? key, this.theme, this.view, this.size}) const StatView({Key? key, this.theme, this.view, this.size})

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../models/model_rec_video_item.dart';
import 'stat/danmu.dart';
import 'stat/view.dart';
import '../../http/dynamics.dart'; import '../../http/dynamics.dart';
import '../../http/search.dart'; import '../../http/search.dart';
import '../../http/user.dart'; import '../../http/user.dart';
@ -322,19 +325,31 @@ class VideoStat extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RichText( return Row(
maxLines: 1, children: [
text: TextSpan( StatView(
style: TextStyle( theme: 'gray',
fontSize: MediaQuery.textScalerOf(context) view: videoItem.stat.view,
.scale(Theme.of(context).textTheme.labelSmall!.fontSize!),
color: Theme.of(context).colorScheme.outline,
), ),
children: [ const SizedBox(width: 8),
TextSpan(text: '${Utils.numFormat(videoItem.stat.view)}观看'), StatDanMu(
TextSpan(text: '${Utils.numFormat(videoItem.stat.danmu)}弹幕'), theme: 'gray',
], danmu: videoItem.stat.danmu,
), ),
if (videoItem is RecVideoItemModel) ...<Widget>[
const Spacer(),
RichText(
maxLines: 1,
text: TextSpan(
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
text: Utils.formatTimestampToRelativeTime(videoItem.pubdate)),
),
const SizedBox(width: 4),
]
],
); );
} }
} }

View File

@ -130,7 +130,7 @@ class VideoHttp {
} }
return {'status': true, 'data': list}; return {'status': true, 'data': list};
} else { } else {
return {'status': false, 'data': []}; return {'status': false, 'data': [], 'msg': res.data['message']};
} }
} catch (err) { } catch (err) {
return {'status': false, 'data': [], 'msg': err}; return {'status': false, 'data': [], 'msg': err};

View File

@ -34,6 +34,7 @@ class PlayUrlModel {
String? seekParam; String? seekParam;
String? seekType; String? seekType;
Dash? dash; Dash? dash;
List<Durl>? durl;
List<FormatItem>? supportFormats; List<FormatItem>? supportFormats;
// String? highFormat; // String? highFormat;
int? lastPlayTime; int? lastPlayTime;
@ -52,7 +53,8 @@ class PlayUrlModel {
videoCodecid = json['video_codecid']; videoCodecid = json['video_codecid'];
seekParam = json['seek_param']; seekParam = json['seek_param'];
seekType = json['seek_type']; seekType = json['seek_type'];
dash = Dash.fromJson(json['dash']); dash = json['dash'] != null ? Dash.fromJson(json['dash']) : null;
durl = json['durl']?.map<Durl>((e) => Durl.fromJson(e)).toList();
supportFormats = json['support_formats'] != null supportFormats = json['support_formats'] != null
? json['support_formats'] ? json['support_formats']
.map<FormatItem>((e) => FormatItem.fromJson(e)) .map<FormatItem>((e) => FormatItem.fromJson(e))
@ -250,3 +252,30 @@ class Flac {
audio = json['audio'] != null ? AudioItem.fromJson(json['audio']) : null; audio = json['audio'] != null ? AudioItem.fromJson(json['audio']) : null;
} }
} }
class Durl {
Durl({
this.order,
this.length,
this.size,
this.ahead,
this.vhead,
this.url,
});
int? order;
int? length;
int? size;
String? ahead;
String? vhead;
String? url;
Durl.fromJson(Map<String, dynamic> json) {
order = json['order'];
length = json['length'];
size = json['size'];
ahead = json['ahead'];
vhead = json['vhead'];
url = json['url'];
}
}

View File

@ -9,7 +9,6 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/view.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/bangumu_card_v.dart'; import 'widgets/bangumu_card_v.dart';
@ -199,7 +198,10 @@ class _BangumiPageState extends State<BangumiPage>
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => {}, fn: () {
_futureBuilderFuture =
_bangumidController.queryBangumiListFeed();
},
); );
} }
} else { } else {
@ -208,7 +210,6 @@ class _BangumiPageState extends State<BangumiPage>
}, },
), ),
), ),
const LoadingMore()
], ],
), ),
); );

View File

@ -192,22 +192,6 @@ class _DynamicsPageState extends State<DynamicsPage>
) )
], ],
), ),
// Obx(
// () => Visibility(
// visible: _dynamicsController.userLogin.value,
// child: Positioned(
// right: 4,
// top: 0,
// bottom: 0,
// child: IconButton(
// padding: EdgeInsets.zero,
// onPressed: () =>
// {feedBack(), _dynamicsController.resetSearch()},
// icon: const Icon(Icons.history, size: 21),
// ),
// ),
// ),
// ),
], ],
), ),
), ),
@ -229,7 +213,8 @@ class _DynamicsPageState extends State<DynamicsPage>
return Obx(() => UpPanel(_dynamicsController.upData.value)); return Obx(() => UpPanel(_dynamicsController.upData.value));
} else { } else {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(
child: SizedBox(height: 80)); child: SizedBox(height: 80),
);
} }
} else { } else {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(
@ -240,15 +225,6 @@ class _DynamicsPageState extends State<DynamicsPage>
} }
}, },
), ),
SliverToBoxAdapter(
child: Container(
height: 6,
color: Theme.of(context)
.colorScheme
.onInverseSurface
.withOpacity(0.5),
),
),
FutureBuilder( FutureBuilder(
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {

View File

@ -36,8 +36,7 @@ class _UpPanelState extends State<UpPanel> {
} }
upList.insert( upList.insert(
0, 0,
UpItem( UpItem(face: '', uname: '全部动态', mid: -1),
face: 'https://files.catbox.moe/8uc48f.png', uname: '全部动态', mid: -1),
); );
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
upList.insert( upList.insert(
@ -56,7 +55,7 @@ class _UpPanelState extends State<UpPanel> {
floating: true, floating: true,
pinned: false, pinned: false,
delegate: _SliverHeaderDelegate( delegate: _SliverHeaderDelegate(
height: 124, height: 126,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -121,6 +120,13 @@ class _UpPanelState extends State<UpPanel> {
], ],
), ),
), ),
Container(
height: 6,
color: Theme.of(context)
.colorScheme
.onInverseSurface
.withOpacity(0.5),
),
], ],
)), )),
); );
@ -171,6 +177,9 @@ class _UpPanelState extends State<UpPanel> {
}, },
onLongPress: () { onLongPress: () {
feedBack(); feedBack();
if (data.mid == -1) {
return;
}
String heroTag = Utils.makeHeroTag(data.mid); String heroTag = Utils.makeHeroTag(data.mid);
Get.toNamed('/member?mid=${data.mid}', Get.toNamed('/member?mid=${data.mid}',
arguments: {'face': data.face, 'heroTag': heroTag}); arguments: {'face': data.face, 'heroTag': heroTag});
@ -198,12 +207,19 @@ class _UpPanelState extends State<UpPanel> {
backgroundColor: data.type == 'live' backgroundColor: data.type == 'live'
? Theme.of(context).colorScheme.secondaryContainer ? Theme.of(context).colorScheme.secondaryContainer
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
child: NetworkImgLayer( child: data.face != ''
width: 49, ? NetworkImgLayer(
height: 49, width: 50,
src: data.face, height: 50,
type: 'avatar', src: data.face,
), type: 'avatar',
)
: const CircleAvatar(
radius: 25,
backgroundImage: AssetImage(
'assets/images/noface.jpeg',
),
),
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: 4),
@ -271,13 +287,11 @@ class UpPanelSkeleton extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( Container(
width: 49, width: 50,
height: 49, height: 50,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface, color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(50),
Radius.circular(24),
),
), ),
), ),
Container( Container(

View File

@ -24,11 +24,13 @@ class _FavDetailPageState extends State<FavDetailPage> {
Get.put(FavDetailController()); Get.put(FavDetailController());
late StreamController<bool> titleStreamC; // a late StreamController<bool> titleStreamC; // a
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
late String mediaId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_futureBuilderFuture = _favDetailController.queryUserFavFolderDetail(); _futureBuilderFuture = _favDetailController.queryUserFavFolderDetail();
mediaId = Get.parameters['mediaId']!;
titleStreamC = StreamController<bool>(); titleStreamC = StreamController<bool>();
_controller.addListener( _controller.addListener(
() { () {
@ -94,8 +96,8 @@ class _FavDetailPageState extends State<FavDetailPage> {
), ),
actions: [ actions: [
IconButton( IconButton(
onPressed: () => Get.toNamed( onPressed: () =>
'/favSearch?searchType=0&mediaId=${Get.parameters['mediaId']!}'), Get.toNamed('/favSearch?searchType=0&mediaId=$mediaId'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
), ),
// IconButton( // IconButton(

View File

@ -15,9 +15,14 @@ import '../../../common/widgets/badge.dart';
class FavVideoCardH extends StatelessWidget { class FavVideoCardH extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final Function? callFn; final Function? callFn;
final int? searchType;
const FavVideoCardH({Key? key, required this.videoItem, this.callFn}) const FavVideoCardH({
: super(key: key); Key? key,
required this.videoItem,
this.callFn,
this.searchType,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -107,7 +112,11 @@ class FavVideoCardH extends StatelessWidget {
}, },
), ),
), ),
VideoContent(videoItem: videoItem, callFn: callFn) VideoContent(
videoItem: videoItem,
callFn: callFn,
searchType: searchType,
)
], ],
), ),
); );
@ -123,7 +132,13 @@ class FavVideoCardH extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final Function? callFn; final Function? callFn;
const VideoContent({super.key, required this.videoItem, this.callFn}); final int? searchType;
const VideoContent({
super.key,
required this.videoItem,
this.callFn,
this.searchType,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -189,48 +204,51 @@ class VideoContent extends StatelessWidget {
), ),
], ],
), ),
Positioned( searchType != 1
right: 0, ? Positioned(
bottom: -4, right: 0,
child: IconButton( bottom: -4,
style: ButtonStyle( child: IconButton(
padding: MaterialStateProperty.all(EdgeInsets.zero), style: ButtonStyle(
), padding: MaterialStateProperty.all(EdgeInsets.zero),
onPressed: () { ),
showDialog( onPressed: () {
context: Get.context!, showDialog(
builder: (context) { context: Get.context!,
return AlertDialog( builder: (context) {
title: const Text('提示'), return AlertDialog(
content: const Text('要取消收藏吗?'), title: const Text('提示'),
actions: [ content: const Text('要取消收藏吗?'),
TextButton( actions: [
onPressed: () => Get.back(), TextButton(
child: Text( onPressed: () => Get.back(),
'取消', child: Text(
style: TextStyle( '取消',
color: style: TextStyle(
Theme.of(context).colorScheme.outline), color: Theme.of(context)
)), .colorScheme
TextButton( .outline),
onPressed: () async { )),
await callFn!(); TextButton(
Get.back(); onPressed: () async {
}, await callFn!();
child: const Text('确定取消'), Get.back();
) },
], child: const Text('确定取消'),
); )
}, ],
); );
}, },
icon: Icon( );
Icons.clear_outlined, },
color: Theme.of(context).colorScheme.outline, icon: Icon(
size: 18, Icons.clear_outlined,
), color: Theme.of(context).colorScheme.outline,
), size: 18,
), ),
),
)
: const SizedBox(),
], ],
), ),
), ),

View File

@ -1,8 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/fav_detail.dart'; import 'package:pilipala/models/user/fav_detail.dart';
import '../../http/video.dart';
class FavSearchController extends GetxController { class FavSearchController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Rx<TextEditingController> controller = TextEditingController().obs; Rx<TextEditingController> controller = TextEditingController().obs;
@ -72,4 +75,21 @@ class FavSearchController extends GetxController {
if (!hasMore) return; if (!hasMore) return;
searchFav(type: 'onLoad'); searchFav(type: 'onLoad');
} }
onCancelFav(int id) async {
var result = await VideoHttp.favVideo(
aid: id, addIds: '', delIds: mediaId.toString());
if (result['status']) {
if (result['data']['prompt']) {
List dataList = favList;
for (var i in dataList) {
if (i.id == id) {
dataList.remove(i);
break;
}
}
SmartDialog.showToast('取消收藏');
}
}
}
} }

View File

@ -8,9 +8,7 @@ import 'package:pilipala/pages/fav_detail/widget/fav_video_card.dart';
import 'controller.dart'; import 'controller.dart';
class FavSearchPage extends StatefulWidget { class FavSearchPage extends StatefulWidget {
final int? sourceType; const FavSearchPage({super.key});
final int? mediaId;
const FavSearchPage({super.key, this.sourceType, this.mediaId});
@override @override
State<FavSearchPage> createState() => _FavSearchPageState(); State<FavSearchPage> createState() => _FavSearchPageState();
@ -19,11 +17,12 @@ class FavSearchPage extends StatefulWidget {
class _FavSearchPageState extends State<FavSearchPage> { class _FavSearchPageState extends State<FavSearchPage> {
final FavSearchController _favSearchCtr = Get.put(FavSearchController()); final FavSearchController _favSearchCtr = Get.put(FavSearchController());
late ScrollController scrollController; late ScrollController scrollController;
late int searchType;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
searchType = int.parse(Get.parameters['searchType']!);
scrollController = _favSearchCtr.scrollController; scrollController = _favSearchCtr.scrollController;
scrollController.addListener( scrollController.addListener(
() { () {
@ -100,7 +99,11 @@ class _FavSearchPageState extends State<FavSearchPage> {
} else { } else {
return FavVideoCardH( return FavVideoCardH(
videoItem: _favSearchCtr.favList[index], videoItem: _favSearchCtr.favList[index],
callFn: () => null, searchType: searchType,
callFn: () => searchType != 1
? _favSearchCtr
.onCancelFav(_favSearchCtr.favList[index].id!)
: {},
); );
} }
}, },

View File

@ -89,8 +89,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
if (data['status']) { if (data['status']) {
return Obx( return Obx(
() => SliverList( () => SliverList(
delegate: delegate: SliverChildBuilderDelegate((context, index) {
SliverChildBuilderDelegate((context, index) {
return VideoCardH( return VideoCardH(
videoItem: _hotController.videoList[index], videoItem: _hotController.videoList[index],
showPubdate: true, showPubdate: true,
@ -110,7 +109,12 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => setState(() {}), fn: () {
setState(() {
_futureBuilderFuture =
_hotController.queryHotFeed('init');
});
},
); );
} }
} else { } else {

View File

@ -10,8 +10,7 @@ class LiveController extends GetxController {
int count = 12; int count = 12;
int _currentPage = 1; int _currentPage = 1;
RxInt crossAxisCount = 2.obs; RxInt crossAxisCount = 2.obs;
RxList<LiveItemModel> liveList = [LiveItemModel()].obs; RxList<LiveItemModel> liveList = <LiveItemModel>[].obs;
bool isLoadingMore = false;
bool flag = false; bool flag = false;
OverlayEntry? popupDialog; OverlayEntry? popupDialog;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
@ -39,7 +38,6 @@ class LiveController extends GetxController {
} }
_currentPage += 1; _currentPage += 1;
} }
isLoadingMore = false;
return res; return res;
} }

View File

@ -11,7 +11,6 @@ import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/live_item.dart'; import 'widgets/live_item.dart';
@ -45,8 +44,8 @@ class _LivePageState extends State<LivePage>
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) { scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('liveList', const Duration(seconds: 1), () { EasyThrottle.throttle('liveList', const Duration(milliseconds: 200),
_liveController.isLoadingMore = true; () {
_liveController.onLoad(); _liveController.onLoad();
}); });
} }
@ -108,24 +107,20 @@ class _LivePageState extends State<LivePage>
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => {}, fn: () {
setState(() {
_futureBuilderFuture =
_liveController.queryLiveList('init');
});
},
); );
} }
} else { } else {
// 缓存数据 return contentGrid(_liveController, []);
if (_liveController.liveList.length > 1) {
return contentGrid(
_liveController, _liveController.liveList);
}
// 骨架屏
else {
return contentGrid(_liveController, []);
}
} }
}, },
), ),
), ),
LoadingMore(ctr: _liveController)
], ],
), ),
), ),

View File

@ -3,7 +3,6 @@ import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/live.dart'; import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/room_info.dart'; import 'package:pilipala/models/live/room_info.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import '../../models/live/room_info_h5.dart'; import '../../models/live/room_info_h5.dart';
class LiveRoomController extends GetxController { class LiveRoomController extends GetxController {
@ -16,13 +15,6 @@ class LiveRoomController extends GetxController {
RxBool volumeOff = false.obs; RxBool volumeOff = false.obs;
PlPlayerController plPlayerController = PlPlayerController plPlayerController =
PlPlayerController.getInstance(videoType: 'live'); PlPlayerController.getInstance(videoType: 'live');
// MeeduPlayerController meeduPlayerController = MeeduPlayerController(
// colorTheme: Theme.of(Get.context!).colorScheme.primary,
// pipEnabled: true,
// controlsStyle: ControlsStyle.live,
// enabledButtons: const EnabledButtons(pip: true),
// );
Rx<RoomInfoH5Model> roomInfoH5 = RoomInfoH5Model().obs; Rx<RoomInfoH5Model> roomInfoH5 = RoomInfoH5Model().obs;
@override @override
@ -39,8 +31,6 @@ class LiveRoomController extends GetxController {
cover = liveItem.cover; cover = liveItem.cover;
} }
} }
queryLiveInfo();
queryLiveInfoH5();
} }
playerInit(source) async { playerInit(source) async {

View File

@ -29,22 +29,18 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
plPlayerController = _liveRoomController.plPlayerController;
plPlayerController!.onPlayerStatusChanged.listen(
(PlayerStatus status) {
if (status == PlayerStatus.playing) {
isShowCover = false;
setState(() {});
}
},
);
if (Platform.isAndroid) { if (Platform.isAndroid) {
floating = Floating(); floating = Floating();
} }
_futureBuilder = _liveRoomController.queryLiveInfoH5(); videoSourceInit();
_futureBuilderFuture = _liveRoomController.queryLiveInfo(); _futureBuilderFuture = _liveRoomController.queryLiveInfo();
} }
Future<void> videoSourceInit() async {
_futureBuilder = _liveRoomController.queryLiveInfoH5();
plPlayerController = _liveRoomController.plPlayerController;
}
@override @override
void dispose() { void dispose() {
plPlayerController!.dispose(); plPlayerController!.dispose();

View File

@ -105,7 +105,7 @@ class _MemberPageState extends State<MemberPage>
actions: [ actions: [
IconButton( IconButton(
onPressed: () => Get.toNamed( onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'), '/memberSearch?mid=$mid&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
), ),
PopupMenuButton( PopupMenuButton(

View File

@ -44,7 +44,7 @@ class _RcmdPageState extends State<RcmdPage>
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) { scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle( EasyThrottle.throttle(
'my-throttler', const Duration(milliseconds: 500), () { 'my-throttler', const Duration(milliseconds: 200), () {
_rcmdController.isLoadingMore = true; _rcmdController.isLoadingMore = true;
_rcmdController.onLoad(); _rcmdController.onLoad();
}); });
@ -113,6 +113,7 @@ class _RcmdPageState extends State<RcmdPage>
errMsg: data['msg'], errMsg: data['msg'],
fn: () { fn: () {
setState(() { setState(() {
_rcmdController.isLoadingMore = true;
_futureBuilderFuture = _futureBuilderFuture =
_rcmdController.queryRcmdFeed('init'); _rcmdController.queryRcmdFeed('init');
}); });
@ -125,7 +126,6 @@ class _RcmdPageState extends State<RcmdPage>
}, },
), ),
), ),
LoadingMore(ctr: _rcmdController),
], ],
), ),
), ),
@ -188,33 +188,3 @@ class _RcmdPageState extends State<RcmdPage>
); );
} }
} }
class LoadingMore extends StatelessWidget {
final dynamic ctr;
const LoadingMore({super.key, this.ctr});
@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: GestureDetector(
onTap: () {
if (ctr != null) {
ctr!.isLoadingMore = true;
ctr!.onLoad();
}
},
child: Center(
child: Text(
'点击加载更多 👇',
style: TextStyle(
color: Theme.of(context).colorScheme.outline, fontSize: 13),
),
),
),
),
);
}
}

View File

@ -187,9 +187,13 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
), ),
); );
} else { } else {
return HttpError( return CustomScrollView(
errMsg: data['msg'], slivers: [
fn: () => setState(() {}), HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
); );
} }
} else { } else {

View File

@ -105,7 +105,11 @@ class _SearchPanelState extends State<SearchPanel>
slivers: [ slivers: [
HttpError( HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => setState(() {}), fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
), ),
], ],
); );
@ -116,7 +120,11 @@ class _SearchPanelState extends State<SearchPanel>
slivers: [ slivers: [
HttpError( HttpError(
errMsg: '没有相关数据', errMsg: '没有相关数据',
fn: () => setState(() {}), fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
), ),
], ],
); );

View File

@ -35,7 +35,7 @@ class SearchVideoPanel extends StatelessWidget {
padding: index == 0 padding: index == 0
? const EdgeInsets.only(top: 2) ? const EdgeInsets.only(top: 2)
: EdgeInsets.zero, : EdgeInsets.zero,
child: VideoCardH(videoItem: i), child: VideoCardH(videoItem: i, showPubdate: true),
); );
}, },
), ),
@ -70,7 +70,7 @@ class SearchVideoPanel extends StatelessWidget {
controller.selectedType.value = i['type']; controller.selectedType.value = i['type'];
ctr!.order.value = ctr!.order.value =
i['type'].toString().split('.').last; i['type'].toString().split('.').last;
SmartDialog.showLoading(msg: 'loooad'); SmartDialog.showLoading(msg: 'loading');
await ctr!.onRefresh(); await ctr!.onRefresh();
SmartDialog.dismiss(); SmartDialog.dismiss();
}, },
@ -202,7 +202,7 @@ class VideoPanelController extends GetxController {
Get.find<SearchPanelController>( Get.find<SearchPanelController>(
tag: 'video${searchPanelCtr.keyword!}'); tag: 'video${searchPanelCtr.keyword!}');
ctr.duration.value = i['value']; ctr.duration.value = i['value'];
SmartDialog.showLoading(msg: 'loooad'); SmartDialog.showLoading(msg: 'loading');
await ctr.onRefresh(); await ctr.onRefresh();
SmartDialog.dismiss(); SmartDialog.dismiss();
}, },

View File

@ -229,9 +229,11 @@ class VideoDetailController extends GetxController
seekTo: seekToTime ?? defaultST, seekTo: seekToTime ?? defaultST,
duration: duration ?? Duration(milliseconds: data.timeLength ?? 0), duration: duration ?? Duration(milliseconds: data.timeLength ?? 0),
// 宽>高 水平 否则 垂直 // 宽>高 水平 否则 垂直
direction: (firstVideo.width! - firstVideo.height!) > 0 direction: firstVideo.width != null && firstVideo.height != null
? 'horizontal' ? ((firstVideo.width! - firstVideo.height!) > 0
: 'vertical', ? 'horizontal'
: 'vertical')
: null,
bvid: bvid, bvid: bvid,
cid: cid.value, cid: cid.value,
enableHeart: enableHeart, enableHeart: enableHeart,
@ -248,6 +250,21 @@ class VideoDetailController extends GetxController
var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid); var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
if (result['status']) { if (result['status']) {
data = result['data']; data = result['data'];
if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) {
SmartDialog.showToast(
'该视频为专属视频,仅提供试看',
displayTime: const Duration(seconds: 3),
);
videoUrl = data.durl!.first.url!;
audioUrl = '';
defaultST = Duration.zero;
firstVideo = VideoItem();
if (autoPlay.value) {
await playerInit();
isShowCover.value = false;
}
return result;
}
final List<VideoItem> allVideosList = data.dash!.video!; final List<VideoItem> allVideosList = data.dash!.video!;
try { try {
// 当前可播放的最高质量视频 // 当前可播放的最高质量视频

View File

@ -18,6 +18,7 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../related/index.dart';
import 'widgets/group_panel.dart'; import 'widgets/group_panel.dart';
class VideoIntroController extends GetxController { class VideoIntroController extends GetxController {
@ -478,11 +479,15 @@ class VideoIntroController extends GetxController {
// 重新获取视频资源 // 重新获取视频资源
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
final ReleatedController releatedCtr =
Get.find<ReleatedController>(tag: heroTag);
videoDetailCtr.bvid = bvid; videoDetailCtr.bvid = bvid;
videoDetailCtr.oid.value = aid; videoDetailCtr.oid.value = aid;
videoDetailCtr.cid.value = cid; videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();
releatedCtr.bvid = bvid;
releatedCtr.queryRelatedVideo();
// 重新请求评论 // 重新请求评论
try { try {
/// 未渲染回复组件时可能异常 /// 未渲染回复组件时可能异常

View File

@ -1,14 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import '../../../../models/model_hot_video_item.dart';
class ReleatedController extends GetxController { class ReleatedController extends GetxController {
// 视频aid // 视频aid
String bvid = Get.parameters['bvid'] ?? ""; String bvid = Get.parameters['bvid'] ?? "";
// 推荐视频列表 // 推荐视频列表
List relatedVideoList = []; RxList relatedVideoList = <HotVideoItemModel>[].obs;
OverlayEntry? popupDialog; OverlayEntry? popupDialog;
Future<dynamic> queryRelatedVideo() => VideoHttp.relatedVideoList(bvid: bvid); Future<dynamic> queryRelatedVideo() async {
return VideoHttp.relatedVideoList(bvid: bvid).then((value) {
if (value['status']) {
relatedVideoList.value = value['data'];
}
return value;
});
}
} }

View File

@ -7,48 +7,73 @@ import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
import './controller.dart'; import './controller.dart';
class RelatedVideoPanel extends StatelessWidget { class RelatedVideoPanel extends StatefulWidget {
final ReleatedController _releatedController = const RelatedVideoPanel({super.key});
Get.put(ReleatedController(), tag: Get.arguments?['heroTag']);
RelatedVideoPanel({super.key}); @override
State<RelatedVideoPanel> createState() => _RelatedVideoPanelState();
}
class _RelatedVideoPanelState extends State<RelatedVideoPanel>
with AutomaticKeepAliveClientMixin {
late ReleatedController _releatedController;
late Future _futureBuilder;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_releatedController =
Get.put(ReleatedController(), tag: Get.arguments?['heroTag']);
_futureBuilder = _releatedController.queryRelatedVideo();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return FutureBuilder( return FutureBuilder(
future: _releatedController.queryRelatedVideo(), future: _futureBuilder,
builder: (BuildContext context, AsyncSnapshot snapshot) { builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) { if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox()); return const SliverToBoxAdapter(child: SizedBox());
} }
if (snapshot.data!['status']) { if (snapshot.data!['status'] && snapshot.data != null) {
RxList relatedVideoList = _releatedController.relatedVideoList;
// 请求成功 // 请求成功
return SliverList( return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate((context, index) { delegate: SliverChildBuilderDelegate((context, index) {
if (index == snapshot.data['data'].length) { if (index == relatedVideoList.length) {
return SizedBox(height: MediaQuery.of(context).padding.bottom); return SizedBox(
} else { height: MediaQuery.of(context).padding.bottom);
return Material( } else {
child: VideoCardH( return Material(
videoItem: snapshot.data['data'][index], child: VideoCardH(
showPubdate: true, videoItem: relatedVideoList[index],
longPress: () { showPubdate: true,
try { longPress: () {
_releatedController.popupDialog = try {
_createPopupDialog(snapshot.data['data'][index]); _releatedController.popupDialog =
Overlay.of(context) _createPopupDialog(_releatedController
.insert(_releatedController.popupDialog!); .relatedVideoList[index]);
} catch (err) { Overlay.of(context)
return {}; .insert(_releatedController.popupDialog!);
} } catch (err) {
}, return {};
longPressEnd: () { }
_releatedController.popupDialog?.remove(); },
}, longPressEnd: () {
), _releatedController.popupDialog?.remove();
); },
} ),
}, childCount: snapshot.data['data'].length + 1)); );
}
}, childCount: relatedVideoList.length + 1),
),
);
} else { } else {
// 请求错误 // 请求错误
return HttpError(errMsg: '出错了', fn: () {}); return HttpError(errMsg: '出错了', fn: () {});

View File

@ -114,7 +114,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
if (replyItem != null) { if (replyItem != null) {
videoDetailCtr.oid = replyItem.oid; videoDetailCtr.oid.value = replyItem.oid;
videoDetailCtr.fRpid = replyItem.rpid!; videoDetailCtr.fRpid = replyItem.rpid!;
videoDetailCtr.firstFloor = replyItem; videoDetailCtr.firstFloor = replyItem;
videoDetailCtr.showReplyReplyPanel(); videoDetailCtr.showReplyReplyPanel();

View File

@ -1,7 +1,7 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
@ -12,10 +12,9 @@ import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply_new/index.dart'; import 'package:pilipala/pages/video/detail/reply_new/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/url_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'zan.dart'; import 'zan.dart';
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
@ -48,6 +47,17 @@ class ReplyItem extends StatelessWidget {
replyReply!(replyItem); replyReply!(replyItem);
} }
}, },
onLongPress: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: replyItem);
},
);
},
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -123,98 +133,6 @@ class ReplyItem extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
// 头像、昵称
// SizedBox(
// width: double.infinity,
// child: Stack(
// children: [
// GestureDetector(
// behavior: HitTestBehavior.opaque,
// onTap: () {
// feedBack();
// Get.toNamed('/member?mid=${replyItem!.mid}', arguments: {
// 'face': replyItem!.member!.avatar!,
// 'heroTag': heroTag
// });
// },
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisSize: MainAxisSize.min,
// children: <Widget>[
// lfAvtar(context, heroTag),
// const SizedBox(width: 12),
// Text(
// replyItem!.member!.uname!,
// style: TextStyle(
// color: replyItem!.member!.vip!['vipStatus'] > 0
// ? const Color.fromARGB(255, 251, 100, 163)
// : Theme.of(context).colorScheme.outline,
// fontSize: 13,
// ),
// ),
// const SizedBox(width: 6),
// Image.asset(
// 'assets/images/lv/lv${replyItem!.member!.level}.png',
// height: 11,
// ),
// const SizedBox(width: 6),
// if (replyItem!.isUp!)
// const PBadge(
// text: 'UP',
// size: 'small',
// stack: 'normal',
// fs: 9,
// ),
// ],
// ),
// ),
// Positioned(
// top: 0,
// left: 0,
// right: 0,
// child: Container(
// width: double.infinity,
// height: 45,
// decoration: BoxDecoration(
// image: replyItem!.member!.userSailing!.cardbg != null
// ? DecorationImage(
// alignment: Alignment.centerRight,
// fit: BoxFit.fitHeight,
// image: NetworkImage(
// replyItem!.member!.userSailing!.cardbg!['image'],
// ),
// )
// : null,
// ),
// ),
// ),
// if (replyItem!.member!.userSailing!.cardbg != null &&
// replyItem!.member!.userSailing!.cardbg!['fan']['number'] > 0)
// Positioned(
// top: 10,
// left: Get.size.width / 7 * 5.8,
// child: DefaultTextStyle(
// style: TextStyle(
// fontFamily: 'fansCard',
// fontSize: 9,
// color: Theme.of(context).colorScheme.primary,
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// const Text('NO.'),
// Text(
// replyItem!.member!.userSailing!.cardbg!['fan']
// ['num_desc'],
// ),
// ],
// ),
// ),
// ),
// ],
// ),
// ),
/// fix Stack内GestureDetector onTap无效 /// fix Stack内GestureDetector onTap无效
GestureDetector( GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
@ -291,30 +209,26 @@ class ReplyItem extends StatelessWidget {
// title // title
Container( Container(
margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4), margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4),
child: SelectableRegion( child: Text.rich(
focusNode: FocusNode(), style: const TextStyle(height: 1.75),
selectionControls: MaterialTextSelectionControls(), maxLines:
child: Text.rich( replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999,
style: const TextStyle(height: 1.75), overflow: TextOverflow.ellipsis,
maxLines: TextSpan(
replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999, children: [
overflow: TextOverflow.ellipsis, if (replyItem!.isTop!)
TextSpan( const WidgetSpan(
children: [ alignment: PlaceholderAlignment.top,
if (replyItem!.isTop!) child: PBadge(
const WidgetSpan( text: 'TOP',
alignment: PlaceholderAlignment.top, size: 'small',
child: PBadge( stack: 'normal',
text: 'TOP', type: 'line',
size: 'small', fs: 9,
stack: 'normal',
type: 'line',
fs: 9,
),
), ),
buildContent(context, replyItem!, replyReply, null), ),
], buildContent(context, replyItem!, replyReply, null),
), ],
), ),
), ),
), ),
@ -447,6 +361,17 @@ class ReplyItemRow extends StatelessWidget {
InkWell( InkWell(
// 一楼点击评论展开评论详情 // 一楼点击评论展开评论详情
onTap: () => replyReply!(replyItem), onTap: () => replyReply!(replyItem),
onLongPress: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: replies![i]);
},
);
},
child: Container( child: Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
@ -541,7 +466,6 @@ InlineSpan buildContent(
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示 // fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
final content = replyItem.content; final content = replyItem.content;
final List<InlineSpan> spanChilds = <InlineSpan>[]; final List<InlineSpan> spanChilds = <InlineSpan>[];
bool hasMatchMember = false;
// 投票 // 投票
if (content.vote.isNotEmpty) { if (content.vote.isNotEmpty) {
@ -591,7 +515,7 @@ InlineSpan buildContent(
if (patternStr.isNotEmpty) { if (patternStr.isNotEmpty) {
patternStr += "|"; patternStr += "|";
} }
patternStr += r'(\b\d{1,2}[:]\d{2}\b)'; patternStr += r'(\b(?:\d+[:])?[0-5]?[0-9][:][0-5]?[0-9]\b)';
final RegExp pattern = RegExp(patternStr); final RegExp pattern = RegExp(patternStr);
List<String> matchedStrs = []; List<String> matchedStrs = [];
void addPlainTextSpan(str) { void addPlainTextSpan(str) {
@ -639,7 +563,9 @@ InlineSpan buildContent(
}, },
), ),
); );
} else if (RegExp(r'^\b[0-9]{1,2}[:][0-9]{2}\b$').hasMatch(matchStr)) { } else if (RegExp(r'^\b(?:\d+[:])?[0-5]?[0-9][:][0-5]?[0-9]\b$')
.hasMatch(matchStr)) {
matchStr = matchStr.replaceAll('', ':');
spanChilds.add( spanChilds.add(
TextSpan( TextSpan(
text: ' $matchStr ', text: ' $matchStr ',
@ -650,7 +576,6 @@ InlineSpan buildContent(
..onTap = () { ..onTap = () {
// 跳转到指定位置 // 跳转到指定位置
try { try {
matchStr = matchStr.replaceAll('', ':');
SmartDialog.showToast('跳转至:$matchStr'); SmartDialog.showToast('跳转至:$matchStr');
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']) Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.plPlayerController .plPlayerController
@ -692,16 +617,54 @@ InlineSpan buildContent(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () async {
final String title = content.jumpUrl[matchStr]['title'];
if (appUrlSchema == '') { if (appUrlSchema == '') {
final String str = Uri.parse(matchStr).pathSegments[0]; final String redirectUrl =
final Map matchRes = IdUtils.matchAvorBv(input: str); await UrlUtils.parseRedirectUrl(matchStr);
final List matchKeys = matchRes.keys.toList(); final String pathSegment = Uri.parse(redirectUrl).path;
if (matchKeys.isNotEmpty) { final String lastPathSegment =
if (matchKeys.first == 'BV') { pathSegment.split('/').last;
if (lastPathSegment.startsWith('BV')) {
UrlUtils.matchUrlPush(
lastPathSegment,
title,
redirectUrl,
);
} else {
Get.toNamed(
'/webview',
parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title
},
);
}
} else {
if (appUrlSchema.startsWith('bilibili://search')) {
Get.toNamed('/searchResult',
parameters: {'keyword': title});
} else if (matchStr.startsWith('https://b23.tv')) {
final String redirectUrl =
await UrlUtils.parseRedirectUrl(matchStr);
final String pathSegment = Uri.parse(redirectUrl).path;
final String lastPathSegment =
pathSegment.split('/').last;
if (lastPathSegment.startsWith('BV')) {
UrlUtils.matchUrlPush(
lastPathSegment,
title,
redirectUrl,
);
} else {
Get.toNamed( Get.toNamed(
'/searchResult', '/webview',
parameters: {'keyword': matchRes['BV']}, parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title
},
); );
} }
} else { } else {
@ -710,16 +673,10 @@ InlineSpan buildContent(
parameters: { parameters: {
'url': matchStr, 'url': matchStr,
'type': 'url', 'type': 'url',
'pageTitle': '' 'pageTitle': title
}, },
); );
} }
} else {
if (appUrlSchema.startsWith('bilibili://search')) {
Get.toNamed('/searchResult', parameters: {
'keyword': content.jumpUrl[matchStr]['title']
});
}
} }
}, },
) )
@ -739,6 +696,47 @@ InlineSpan buildContent(
}, },
); );
if (content.jumpUrl.keys.isNotEmpty) {
List<String> unmatchedItems = content.jumpUrl.keys
.toList()
.where((item) => !content.message.contains(item))
.toList();
if (unmatchedItems.isNotEmpty) {
for (int i = 0; i < unmatchedItems.length; i++) {
String patternStr = unmatchedItems[i];
spanChilds.addAll(
[
if (content.jumpUrl[patternStr]?['prefix_icon'] != null) ...[
WidgetSpan(
child: Image.network(
content.jumpUrl[patternStr]['prefix_icon'],
height: 19,
color: Theme.of(context).colorScheme.primary,
),
)
],
TextSpan(
text: content.jumpUrl[patternStr]['title'],
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/webview',
parameters: {
'url': patternStr,
'type': 'url',
'pageTitle': content.jumpUrl[patternStr]['title']
},
);
},
)
],
);
}
}
}
// 图片渲染 // 图片渲染
if (content.pictures.isNotEmpty) { if (content.pictures.isNotEmpty) {
final List<String> picList = <String>[]; final List<String> picList = <String>[];
@ -753,11 +751,15 @@ InlineSpan buildContent(
builder: (BuildContext context, BoxConstraints box) { builder: (BuildContext context, BoxConstraints box) {
double maxHeight = box.maxWidth * 0.6; // 设置最大高度 double maxHeight = box.maxWidth * 0.6; // 设置最大高度
// double width = (box.maxWidth / 2).truncateToDouble(); // double width = (box.maxWidth / 2).truncateToDouble();
double height = ((box.maxWidth / double height = 100;
2 * try {
pictureItem['img_height'] / height = ((box.maxWidth /
pictureItem['img_width'])) 2 *
.truncateToDouble(); pictureItem['img_height'] /
pictureItem['img_width']))
.truncateToDouble();
} catch (_) {}
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
showDialog( showDialog(
@ -880,3 +882,100 @@ InlineSpan buildContent(
// spanChilds.add(TextSpan(text: matchMember)); // spanChilds.add(TextSpan(text: matchMember));
return TextSpan(children: spanChilds); return TextSpan(children: spanChilds);
} }
class MorePanel extends StatelessWidget {
final dynamic item;
const MorePanel({super.key, required this.item});
Future<dynamic> menuActionHandler(String type) async {
String message = item.content.message ?? item.content;
switch (type) {
case 'copyAll':
await Clipboard.setData(ClipboardData(text: message));
SmartDialog.showToast('已复制');
Get.back();
break;
case 'copyFreedom':
Get.back();
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('自由复制'),
content: SelectableText(message),
);
},
);
break;
// case 'block':
// SmartDialog.showToast('加入黑名单');
// break;
// case 'report':
// SmartDialog.showToast('举报');
// break;
// case 'delete':
// SmartDialog.showToast('删除');
// break;
default:
}
}
@override
Widget build(BuildContext context) {
Color errorColor = Theme.of(context).colorScheme.error;
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
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))),
),
),
),
),
ListTile(
onTap: () async => await menuActionHandler('copyAll'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_all_outlined, size: 19),
title: Text('复制全部', style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
onTap: () async => await menuActionHandler('copyFreedom'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_outlined, size: 19),
title: Text('自由复制', style: Theme.of(context).textTheme.titleSmall),
),
// ListTile(
// onTap: () async => await menuActionHandler('block'),
// minLeadingWidth: 0,
// leading: Icon(Icons.block_outlined, color: errorColor),
// title: Text('加入黑名单', style: TextStyle(color: errorColor)),
// ),
// ListTile(
// onTap: () async => await menuActionHandler('report'),
// minLeadingWidth: 0,
// leading: Icon(Icons.report_outlined, color: errorColor),
// title: Text('举报', style: TextStyle(color: errorColor)),
// ),
// ListTile(
// onTap: () async => await menuActionHandler('del'),
// minLeadingWidth: 0,
// leading: Icon(Icons.delete_outline, color: errorColor),
// title: Text('删除', style: TextStyle(color: errorColor)),
// ),
],
),
);
}
}

View File

@ -438,7 +438,7 @@ class _HeaderControlState extends State<HeaderControl> {
}), }),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: () => Get.back(),
child: Text( child: Text(
'取消', '取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline), style: TextStyle(color: Theme.of(context).colorScheme.outline),

61
lib/utils/url_utils.dart Normal file
View File

@ -0,0 +1,61 @@
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import '../http/search.dart';
import 'id_utils.dart';
import 'utils.dart';
class UrlUtils {
// 302重定向路由截取
static Future<String> parseRedirectUrl(String url) async {
late String redirectUrl;
final dio = Dio();
dio.options.followRedirects = false;
dio.options.validateStatus = (status) {
return status == 200 || status == 301 || status == 302;
};
final response = await dio.get(url);
if (response.statusCode == 302) {
redirectUrl = response.headers['location']?.first as String;
if (redirectUrl.endsWith('/')) {
redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1);
}
} else {
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
}
return url;
}
return redirectUrl;
}
// 匹配url路由跳转
static matchUrlPush(
String pathSegment,
String title,
String redirectUrl,
) async {
final Map matchRes = IdUtils.matchAvorBv(input: pathSegment);
if (matchRes.containsKey('BV')) {
final String bv = matchRes['BV'];
final int cid = await SearchHttp.ab2c(bvid: bv);
final String heroTag = Utils.makeHeroTag(bv);
await Get.toNamed(
'/video?bvid=$bv&cid=$cid',
arguments: <String, String?>{
'pic': '',
'heroTag': heroTag,
},
);
} else {
await Get.toNamed(
'/webview',
parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title,
},
);
}
}
}

View File

@ -28,7 +28,7 @@ class Utils {
} }
static String numFormat(dynamic number) { static String numFormat(dynamic number) {
if (number == null){ if (number == null) {
return '0'; return '0';
} }
if (number is String) { if (number is String) {
@ -63,6 +63,26 @@ class Utils {
} }
} }
// 完全相对时间显示
static String formatTimestampToRelativeTime(timeStamp) {
var difference = DateTime.now()
.difference(DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000));
if (difference.inDays > 365) {
return '${difference.inDays ~/ 365}年前';
} else if (difference.inDays > 30) {
return '${difference.inDays ~/ 30}个月前';
} else if (difference.inDays > 0) {
return '${difference.inDays}天前';
} else if (difference.inHours > 0) {
return '${difference.inHours}小时前';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}分钟前';
} else {
return '刚刚';
}
}
// 时间显示刚刚x分钟前 // 时间显示刚刚x分钟前
static String dateFormat(timeStamp, {formatType = 'list'}) { static String dateFormat(timeStamp, {formatType = 'list'}) {
// 当前时间 // 当前时间