Merge branch 'main' into fix

This commit is contained in:
guozhigq
2024-02-18 08:10:20 +08:00
57 changed files with 1566 additions and 605 deletions

View File

@@ -7,6 +7,7 @@ import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/github/latest.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../utils/cache_manage.dart';
class AboutPage extends StatefulWidget {
const AboutPage({super.key});
@@ -17,6 +18,19 @@ class AboutPage extends StatefulWidget {
class _AboutPageState extends State<AboutPage> {
final AboutController _aboutController = Get.put(AboutController());
String cacheSize = '';
@override
void initState() {
super.initState();
// 读取缓存占用
getCacheSize();
}
Future<void> getCacheSize() async {
final res = await CacheManage().loadApplicationCache();
setState(() => cacheSize = res);
}
@override
Widget build(BuildContext context) {
@@ -138,6 +152,17 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('错误日志'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
ListTile(
onTap: () async {
var cleanStatus = await CacheManage().clearCacheAll();
if (cleanStatus) {
getCacheSize();
}
},
title: const Text('清除缓存'),
subtitle: Text('图片及网络缓存 $cacheSize', style: subTitleStyle),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
],
),
),

View File

@@ -9,7 +9,6 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/view.dart';
import 'controller.dart';
import 'widgets/bangumu_card_v.dart';
@@ -199,7 +198,10 @@ class _BangumiPageState extends State<BangumiPage>
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
fn: () {
_futureBuilderFuture =
_bangumidController.queryBangumiListFeed();
},
);
}
} 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));
} else {
return const SliverToBoxAdapter(
child: SizedBox(height: 80));
child: SizedBox(height: 80),
);
}
} else {
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(
future: _futureBuilderFuture,
builder: (context, snapshot) {

View File

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

View File

@@ -31,6 +31,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
super.initState();
mediaId = Get.parameters['mediaId']!;
_futureBuilderFuture = _favDetailController.queryUserFavFolderDetail();
mediaId = Get.parameters['mediaId']!;
titleStreamC = StreamController<bool>();
_controller.addListener(
() {

View File

@@ -9,14 +9,20 @@ import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../../../common/widgets/badge.dart';
// 收藏视频卡片 - 水平布局
class FavVideoCardH extends StatelessWidget {
final dynamic videoItem;
final Function? callFn;
final int? searchType;
const FavVideoCardH({Key? key, required this.videoItem, this.callFn})
: super(key: key);
const FavVideoCardH({
Key? key,
required this.videoItem,
this.callFn,
this.searchType,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -27,7 +33,9 @@ class FavVideoCardH extends StatelessWidget {
onTap: () async {
// int? seasonId;
String? epId;
if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') {
if (videoItem.ogv != null &&
(videoItem.ogv['type_name'] == '番剧' ||
videoItem.ogv['type_name'] == '国创')) {
videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
// seasonId = videoItem.ogv['season_id'];
epId = videoItem.epId;
@@ -84,28 +92,31 @@ class FavVideoCardH extends StatelessWidget {
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),
),
PBadge(
text: Utils.timeFormat(videoItem.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
if (videoItem.ogv != null) ...[
PBadge(
text: videoItem.ogv['type_name'],
top: 6.0,
right: 6.0,
bottom: null,
left: null,
),
)
],
],
);
},
),
),
VideoContent(videoItem: videoItem, callFn: callFn)
VideoContent(
videoItem: videoItem,
callFn: callFn,
searchType: searchType,
)
],
),
);
@@ -121,93 +132,123 @@ class FavVideoCardH extends StatelessWidget {
class VideoContent extends StatelessWidget {
final dynamic videoItem;
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
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Stack(
children: [
Text(
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
Utils.dateFormat(videoItem.ctime!),
style: TextStyle(
fontSize: 11, color: Theme.of(context).colorScheme.outline),
),
Text(
videoItem.owner.name,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
Row(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatView(
theme: 'gray',
view: videoItem.cntInfo['play'],
Text(
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 8),
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,
if (videoItem.ogv != null) ...[
Text(
videoItem.intro,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
size: 18,
),
),
],
const Spacer(),
Text(
Utils.dateFormat(videoItem.favTime),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.outline),
),
if (videoItem.owner.name != '') ...[
Text(
videoItem.owner.name,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
],
Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
StatView(
theme: 'gray',
view: videoItem.cntInfo['play'],
),
const SizedBox(width: 8),
StatDanMu(
theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
const Spacer(),
],
),
),
],
),
searchType != 1
? Positioned(
right: 0,
bottom: -4,
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,
),
),
)
: const SizedBox(),
],
),
),

View File

@@ -1,8 +1,11 @@
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/fav_detail.dart';
import '../../http/video.dart';
class FavSearchController extends GetxController {
final ScrollController scrollController = ScrollController();
Rx<TextEditingController> controller = TextEditingController().obs;
@@ -72,4 +75,21 @@ class FavSearchController extends GetxController {
if (!hasMore) return;
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';
class FavSearchPage extends StatefulWidget {
final int? sourceType;
final int? mediaId;
const FavSearchPage({super.key, this.sourceType, this.mediaId});
const FavSearchPage({super.key});
@override
State<FavSearchPage> createState() => _FavSearchPageState();
@@ -19,11 +17,12 @@ class FavSearchPage extends StatefulWidget {
class _FavSearchPageState extends State<FavSearchPage> {
final FavSearchController _favSearchCtr = Get.put(FavSearchController());
late ScrollController scrollController;
late int searchType;
@override
void initState() {
super.initState();
searchType = int.parse(Get.parameters['searchType']!);
scrollController = _favSearchCtr.scrollController;
scrollController.addListener(
() {
@@ -100,7 +99,11 @@ class _FavSearchPageState extends State<FavSearchPage> {
} else {
return FavVideoCardH(
videoItem: _favSearchCtr.favList[index],
callFn: () => null,
searchType: searchType,
callFn: () => searchType != 1
? _favSearchCtr
.onCancelFav(_favSearchCtr.favList[index].id!)
: {},
);
}
},

View File

@@ -37,6 +37,29 @@ class _FollowPageState extends State<FollowPage> {
: '${_followController.name}的关注',
style: Theme.of(context).textTheme.titleMedium,
),
actions: [
IconButton(
onPressed: () => Get.toNamed('/followSearch?mid=$mid'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
onTap: () => Get.toNamed('/blackListPage'),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.block, size: 19),
SizedBox(width: 10),
Text('黑名单管理'),
],
),
)
],
),
const SizedBox(width: 6),
],
),
body: Obx(
() => !_followController.isOwner.value
@@ -87,3 +110,22 @@ class _FollowPageState extends State<FollowPage> {
);
}
}
class _FakeAPI {
static const List<String> _kOptions = <String>[
'aardvark',
'bobcat',
'chameleon',
];
// Searches the options, but injects a fake "network" delay.
static Future<Iterable<String>> search(String query) async {
await Future<void>.delayed(
const Duration(seconds: 1)); // Fake 1 second delay.
if (query == '') {
return const Iterable<String>.empty();
}
return _kOptions.where((String option) {
return option.contains(query.toLowerCase());
});
}
}

View File

@@ -42,7 +42,7 @@ class FollowItem extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
dense: true,
trailing: ctr!.isOwner.value
trailing: ctr != null && ctr!.isOwner.value
? SizedBox(
height: 34,
child: TextButton(

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import '../../models/follow/result.dart';
class FollowSearchController extends GetxController {
Rx<TextEditingController> controller = TextEditingController().obs;
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
String hintText = '搜索';
RxString loadingStatus = 'init'.obs;
late int mid = 1;
RxString uname = ''.obs;
int ps = 20;
int pn = 1;
RxList<FollowItemModel> followList = <FollowItemModel>[].obs;
RxInt total = 0.obs;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
}
// 清空搜索
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';
searchFollow();
}
Future searchFollow({type = 'init'}) async {
if (controller.value.text == '') {
return {'status': true, 'data': <FollowItemModel>[].obs};
}
if (type == 'init') {
ps = 1;
}
var res = await MemberHttp.getfollowSearch(
mid: mid,
ps: ps,
pn: pn,
name: controller.value.text,
);
if (res['status']) {
if (type == 'init') {
followList.value = res['data'].list;
} else {
followList.addAll(res['data'].list);
}
total.value = res['data'].total;
}
return res;
}
void onLoad() {
searchFollow(type: 'onLoad');
}
}

View File

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

View File

@@ -0,0 +1,121 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/follow_search/index.dart';
import '../follow/widgets/follow_item.dart';
class FollowSearchPage extends StatefulWidget {
const FollowSearchPage({super.key});
@override
State<FollowSearchPage> createState() => _FollowSearchPageState();
}
class _FollowSearchPageState extends State<FollowSearchPage> {
final FollowSearchController _followSearchController =
Get.put(FollowSearchController());
late Future? _futureBuilder;
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_futureBuilder = _followSearchController.searchFollow();
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'my-throttler', const Duration(milliseconds: 500), () {
_followSearchController.onLoad();
});
}
},
);
}
void reRequest() {
setState(() {
_futureBuilder = _followSearchController.searchFollow();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
actions: [
IconButton(
onPressed: reRequest,
icon: const Icon(CupertinoIcons.search, size: 22),
),
const SizedBox(width: 6),
],
title: TextField(
autofocus: true,
focusNode: _followSearchController.searchFocusNode,
controller: _followSearchController.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _followSearchController.onChange(value),
decoration: InputDecoration(
hintText: _followSearchController.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _followSearchController.onClear(),
),
),
onSubmitted: (String value) => reRequest(),
),
),
body: FutureBuilder(
future: _futureBuilder,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data == null) {
return CustomScrollView(
slivers: [
HttpError(errMsg: snapshot.data['msg'], fn: reRequest)
],
);
}
if (data['status']) {
RxList followList = _followSearchController.followList;
return Obx(
() => followList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount: followList.length,
itemBuilder: ((context, index) {
return FollowItem(
item: followList[index],
);
}),
)
: CustomScrollView(
slivers: [HttpError(errMsg: '未搜索到结果', fn: reRequest)],
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(errMsg: snapshot.data['msg'], fn: reRequest)
],
);
}
} else {
return const SizedBox();
}
}),
);
}
}

View File

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

View File

@@ -10,8 +10,7 @@ class LiveController extends GetxController {
int count = 12;
int _currentPage = 1;
RxInt crossAxisCount = 2.obs;
RxList<LiveItemModel> liveList = [LiveItemModel()].obs;
bool isLoadingMore = false;
RxList<LiveItemModel> liveList = <LiveItemModel>[].obs;
bool flag = false;
OverlayEntry? popupDialog;
Box setting = GStrorage.setting;
@@ -39,7 +38,6 @@ class LiveController extends GetxController {
}
_currentPage += 1;
}
isLoadingMore = false;
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/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'controller.dart';
import 'widgets/live_item.dart';
@@ -45,8 +44,8 @@ class _LivePageState extends State<LivePage>
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('liveList', const Duration(seconds: 1), () {
_liveController.isLoadingMore = true;
EasyThrottle.throttle('liveList', const Duration(milliseconds: 200),
() {
_liveController.onLoad();
});
}
@@ -108,24 +107,20 @@ class _LivePageState extends State<LivePage>
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
fn: () {
setState(() {
_futureBuilderFuture =
_liveController.queryLiveList('init');
});
},
);
}
} else {
// 缓存数据
if (_liveController.liveList.length > 1) {
return contentGrid(
_liveController, _liveController.liveList);
}
// 骨架屏
else {
return contentGrid(_liveController, []);
}
return contentGrid(_liveController, []);
}
},
),
),
LoadingMore(ctr: _liveController)
],
),
),

View File

@@ -184,18 +184,32 @@ class VideoStat extends StatelessWidget {
tileMode: TileMode.mirror,
),
),
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']),
],
),
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']),
// ],
// ),
// ),
);
}
}

View File

@@ -3,6 +3,7 @@ import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/room_info.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import '../../models/live/room_info_h5.dart';
class LiveRoomController extends GetxController {
String cover = '';
@@ -14,13 +15,7 @@ class LiveRoomController extends GetxController {
RxBool volumeOff = false.obs;
PlPlayerController plPlayerController =
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;
@override
void onInit() {
@@ -36,11 +31,10 @@ class LiveRoomController extends GetxController {
cover = liveItem.cover;
}
}
queryLiveInfo();
}
playerInit(source) {
plPlayerController.setDataSource(
playerInit(source) async {
await plPlayerController.setDataSource(
DataSource(
videoSource: source,
audioSource: null,
@@ -66,7 +60,8 @@ class LiveRoomController extends GetxController {
String videoUrl = (item.urlInfo?.first.host)! +
item.baseUrl! +
item.urlInfo!.first.extra!;
playerInit(videoUrl);
await playerInit(videoUrl);
return res;
}
}
@@ -80,4 +75,12 @@ class LiveRoomController extends GetxController {
volumeOff.value = true;
}
}
Future queryLiveInfoH5() async {
var res = await LiveHttp.liveRoomInfoH5(roomId: roomId);
if (res['status']) {
roomInfoH5.value = res['data'];
}
return res;
}
}

View File

@@ -19,6 +19,8 @@ class LiveRoomPage extends StatefulWidget {
class _LiveRoomPageState extends State<LiveRoomPage> {
final LiveRoomController _liveRoomController = Get.put(LiveRoomController());
PlPlayerController? plPlayerController;
late Future? _futureBuilder;
late Future? _futureBuilderFuture;
bool isShowCover = true;
bool isPlay = true;
@@ -27,18 +29,16 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override
void initState() {
super.initState();
plPlayerController = _liveRoomController.plPlayerController;
plPlayerController!.onPlayerStatusChanged.listen(
(PlayerStatus status) {
if (status == PlayerStatus.playing) {
isShowCover = false;
setState(() {});
}
},
);
if (Platform.isAndroid) {
floating = Floating();
}
videoSourceInit();
_futureBuilderFuture = _liveRoomController.queryLiveInfo();
}
Future<void> videoSourceInit() async {
_futureBuilder = _liveRoomController.queryLiveInfoH5();
plPlayerController = _liveRoomController.plPlayerController;
}
@override
@@ -52,57 +52,123 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override
Widget build(BuildContext context) {
Widget videoPlayerPanel = FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData && snapshot.data['status']) {
return PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
);
} else {
return const SizedBox();
}
},
);
Widget childWhenDisabled = Scaffold(
primary: true,
appBar: PreferredSize(
preferredSize: Size.fromHeight(
MediaQuery.of(context).orientation == Orientation.portrait ? 56 : 0,
),
child: AppBar(
centerTitle: false,
titleSpacing: 0,
title: _liveRoomController.liveItem != null
? Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController.liveItem.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.liveItem.uname,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
if (_liveRoomController.liveItem.watchedShow != null)
Text(
_liveRoomController
.liveItem.watchedShow['text_large'] ??
'',
style: const TextStyle(fontSize: 12)),
],
),
],
)
: const SizedBox(),
// actions: [
// SizedBox(
// height: 34,
// child: ElevatedButton(onPressed: () {}, child: const Text('关注')),
// ),
// const SizedBox(width: 12),
// ],
),
),
body: Column(
backgroundColor: Colors.black,
body: Stack(
children: [
Stack(
// Obx(
// () => Positioned.fill(
// child: Opacity(
// opacity: 0.8,
// child: _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground !=
// '' &&
// _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground !=
// null
// ? NetworkImgLayer(
// width: Get.width,
// height: Get.height,
// src: _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground ??
// '',
// )
// : Image.asset(
// 'assets/images/live/default_bg.webp',
// width: Get.width,
// height: Get.height,
// ),
// ),
// ),
// ),
Positioned.fill(
child: Opacity(
opacity: 0.8,
child: Image.asset(
'assets/images/live/default_bg.webp',
width: Get.width,
height: Get.height,
),
),
),
Column(
children: [
AppBar(
centerTitle: false,
titleSpacing: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
toolbarHeight:
MediaQuery.of(context).orientation == Orientation.portrait
? 56
: 0,
title: FutureBuilder(
future: _futureBuilder,
builder: (context, snapshot) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController
.roomInfoH5.value.anchorInfo!.baseInfo!.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.roomInfoH5.value
.anchorInfo!.baseInfo!.uname!,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
if (_liveRoomController
.roomInfoH5.value.watchedShow !=
null)
Text(
_liveRoomController.roomInfoH5.value
.watchedShow!['text_large'] ??
'',
style: const TextStyle(fontSize: 12),
),
],
),
],
),
);
} else {
return const SizedBox();
}
},
),
),
PopScope(
canPop: plPlayerController?.isFullScreen.value != true,
onPopInvoked: (bool didPop) {
@@ -120,55 +186,19 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
Orientation.landscape
? Get.size.height
: Get.size.width * 9 / 16,
child: plPlayerController!.videoPlayerController != null
? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
)
: const SizedBox(),
child: videoPlayerPanel,
),
),
// 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,
// ),
// ),
// ),
],
),
],
),
);
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,
childWhenEnabled: videoPlayerPanel,
floating: floating,
);
} else {
return childWhenDisabled;

View File

@@ -25,7 +25,7 @@ class MemberArchiveController extends GetxController {
// 获取用户投稿
Future getMemberArchive(type) async {
if (type == 'onRefresh') {
if (type == 'init') {
pn = 1;
}
var res = await MemberHttp.memberArchive(
@@ -34,7 +34,12 @@ class MemberArchiveController extends GetxController {
order: currentOrder['type']!,
);
if (res['status']) {
archivesList.addAll(res['data'].list.vlist);
if (type == 'init') {
archivesList.value = res['data'].list.vlist;
}
if (type == 'onLoad') {
archivesList.addAll(res['data'].list.vlist);
}
count = res['data'].page['count'];
pn += 1;
}
@@ -42,13 +47,14 @@ class MemberArchiveController extends GetxController {
}
toggleSort() async {
pn = 1;
int index = orderList.indexOf(currentOrder);
List<String> typeList = orderList.map((e) => e['type']!).toList();
int index = typeList.indexOf(currentOrder['type']!);
if (index == orderList.length - 1) {
currentOrder.value = orderList.first;
} else {
currentOrder.value = orderList[index + 1];
}
getMemberArchive('init');
}
// 上拉加载

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/utils/utils.dart';
import '../../common/widgets/http_error.dart';
import 'controller.dart';
class MemberArchivePage extends StatefulWidget {
@@ -25,8 +26,7 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
final String heroTag = Utils.makeHeroTag(mid);
_memberArchivesController =
Get.put(MemberArchiveController(), tag: heroTag);
_futureBuilderFuture =
_memberArchivesController.getMemberArchive('onRefresh');
_futureBuilderFuture = _memberArchivesController.getMemberArchive('init');
scrollController = _memberArchivesController.scrollController;
scrollController.addListener(
() {
@@ -48,39 +48,16 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
titleSpacing: 0,
centerTitle: false,
title: Text('他的投稿', style: Theme.of(context).textTheme.titleMedium),
// actions: [
// Obx(
// () => PopupMenuButton<String>(
// padding: EdgeInsets.zero,
// tooltip: '投稿排序',
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// ),
// position: PopupMenuPosition.under,
// onSelected: (String type) {},
// itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
// for (var i in _memberArchivesController.orderList) ...[
// PopupMenuItem<String>(
// onTap: () {},
// value: _memberArchivesController.currentOrder['label'],
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Text(i['label']!),
// if (_memberArchivesController.currentOrder['label'] ==
// i['label']) ...[
// const SizedBox(width: 10),
// const Icon(Icons.done, size: 20),
// ],
// ],
// ),
// ),
// ]
// ],
// ),
// ),
// ],
actions: [
Obx(
() => TextButton.icon(
icon: const Icon(Icons.sort, size: 20),
onPressed: _memberArchivesController.toggleSort,
label: Text(_memberArchivesController.currentOrder['label']!),
),
),
const SizedBox(width: 6),
],
),
body: CustomScrollView(
controller: _memberArchivesController.scrollController,
@@ -110,10 +87,16 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
: const SliverToBoxAdapter(),
);
} else {
return const SliverToBoxAdapter();
return HttpError(
errMsg: snapshot.data['msg'],
fn: () {},
);
}
} else {
return const SliverToBoxAdapter();
return HttpError(
errMsg: snapshot.data['msg'],
fn: () {},
);
}
} else {
return const SliverToBoxAdapter();

View File

@@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:pilipala/pages/member_dynamics/index.dart';
import 'package:pilipala/utils/utils.dart';
import '../../common/widgets/http_error.dart';
import '../dynamics/widgets/dynamic_panel.dart';
class MemberDynamicsPage extends StatefulWidget {
@@ -80,10 +81,16 @@ class _MemberDynamicsPageState extends State<MemberDynamicsPage> {
: const SliverToBoxAdapter(),
);
} else {
return const SliverToBoxAdapter();
return HttpError(
errMsg: snapshot.data['msg'],
fn: () {},
);
}
} else {
return const SliverToBoxAdapter();
return HttpError(
errMsg: snapshot.data['msg'],
fn: () {},
);
}
} else {
return const SliverToBoxAdapter();

View File

@@ -119,7 +119,7 @@ class MineController extends GetxController {
SmartDialog.showToast('账号未登录');
return;
}
Get.toNamed('/follow?mid=${userInfo.value.mid}');
Get.toNamed('/follow?mid=${userInfo.value.mid}', preventDuplicates: false);
}
pushFans() {
@@ -127,7 +127,7 @@ class MineController extends GetxController {
SmartDialog.showToast('账号未登录');
return;
}
Get.toNamed('/fan?mid=${userInfo.value.mid}');
Get.toNamed('/fan?mid=${userInfo.value.mid}', preventDuplicates: false);
}
pushDynamic() {
@@ -135,6 +135,7 @@ class MineController extends GetxController {
SmartDialog.showToast('账号未登录');
return;
}
Get.toNamed('/memberDynamics?mid=${userInfo.value.mid}');
Get.toNamed('/memberDynamics?mid=${userInfo.value.mid}',
preventDuplicates: false);
}
}

View File

@@ -102,15 +102,12 @@ class _ImagePreviewState extends State<ImagePreview>
);
}
// 设置状态栏图标透明
// 隐藏状态栏,避免遮挡图片内容
setStatusBar() async {
if (Platform.isIOS) {
if (Platform.isIOS || Platform.isAndroid) {
await StatusBarControl.setHidden(true,
animation: StatusBarAnimation.SLIDE);
}
if (Platform.isAndroid) {
await StatusBarControl.setColor(Colors.transparent);
}
}
@override

View File

@@ -44,7 +44,7 @@ class _RcmdPageState extends State<RcmdPage>
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'my-throttler', const Duration(milliseconds: 500), () {
'my-throttler', const Duration(milliseconds: 200), () {
_rcmdController.isLoadingMore = true;
_rcmdController.onLoad();
});
@@ -113,6 +113,7 @@ class _RcmdPageState extends State<RcmdPage>
errMsg: data['msg'],
fn: () {
setState(() {
_rcmdController.isLoadingMore = true;
_futureBuilderFuture =
_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 {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
);
}
} else {

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/video_utils.dart';
import 'package:screen_brightness/screen_brightness.dart';
import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart';
class VideoDetailController extends GetxController
@@ -61,7 +62,7 @@ class VideoDetailController extends GetxController
Box localCache = GStrorage.localCache;
Box setting = GStrorage.setting;
int oid = 0;
RxInt oid = 0.obs;
// 评论id 请求楼中楼评论使用
int fRpid = 0;
@@ -135,13 +136,14 @@ class VideoDetailController extends GetxController
defaultValue: VideoDecodeFormats.values.last.code);
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code);
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
}
showReplyReplyPanel() {
PersistentBottomSheetController? ctr =
scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
return VideoReplyReplyPanel(
oid: oid,
oid: oid.value,
rpid: fRpid,
closePanel: () => {
fRpid = 0,

View File

@@ -18,6 +18,7 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart';
import '../related/index.dart';
import 'widgets/group_panel.dart';
class VideoIntroController extends GetxController {
@@ -298,7 +299,6 @@ class VideoIntroController extends GetxController {
await queryVideoInFolder();
int defaultFolderId = favFolderData.value.list!.first.id!;
int favStatus = favFolderData.value.list!.first.favState!;
print('favStatus: $favStatus');
var result = await VideoHttp.favVideo(
aid: IdUtils.bv2av(bvid),
addIds: favStatus == 0 ? '$defaultFolderId' : '',
@@ -310,6 +310,8 @@ class VideoIntroController extends GetxController {
await queryHasFavVideo();
SmartDialog.showToast('✅ 操作成功');
}
} else {
SmartDialog.showToast(result['msg']);
}
return;
}
@@ -340,6 +342,8 @@ class VideoIntroController extends GetxController {
await queryHasFavVideo();
SmartDialog.showToast('✅ 操作成功');
}
} else {
SmartDialog.showToast(result['msg']);
}
}
@@ -475,10 +479,15 @@ class VideoIntroController extends GetxController {
// 重新获取视频资源
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
final ReleatedController releatedCtr =
Get.find<ReleatedController>(tag: heroTag);
videoDetailCtr.bvid = bvid;
videoDetailCtr.oid.value = aid;
videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl();
releatedCtr.bvid = bvid;
releatedCtr.queryRelatedVideo();
// 重新请求评论
try {
/// 未渲染回复组件时可能异常

View File

@@ -1,14 +1,22 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/video.dart';
import '../../../../models/model_hot_video_item.dart';
class ReleatedController extends GetxController {
// 视频aid
String bvid = Get.parameters['bvid'] ?? "";
// 推荐视频列表
List relatedVideoList = [];
RxList relatedVideoList = <HotVideoItemModel>[].obs;
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 './controller.dart';
class RelatedVideoPanel extends StatelessWidget {
final ReleatedController _releatedController =
Get.put(ReleatedController(), tag: Get.arguments?['heroTag']);
RelatedVideoPanel({super.key});
class RelatedVideoPanel extends StatefulWidget {
const 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
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
future: _releatedController.queryRelatedVideo(),
future: _futureBuilder,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
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) {
if (index == snapshot.data['data'].length) {
return SizedBox(height: MediaQuery.of(context).padding.bottom);
} else {
return Material(
child: VideoCardH(
videoItem: snapshot.data['data'][index],
showPubdate: true,
longPress: () {
try {
_releatedController.popupDialog =
_createPopupDialog(snapshot.data['data'][index]);
Overlay.of(context)
.insert(_releatedController.popupDialog!);
} catch (err) {
return {};
}
},
longPressEnd: () {
_releatedController.popupDialog?.remove();
},
),
);
}
}, childCount: snapshot.data['data'].length + 1));
if (index == relatedVideoList.length) {
return SizedBox(
height: MediaQuery.of(context).padding.bottom);
} else {
return Material(
child: VideoCardH(
videoItem: relatedVideoList[index],
showPubdate: true,
longPress: () {
try {
_releatedController.popupDialog =
_createPopupDialog(_releatedController
.relatedVideoList[index]);
Overlay.of(context)
.insert(_releatedController.popupDialog!);
} catch (err) {
return {};
}
},
longPressEnd: () {
_releatedController.popupDialog?.remove();
},
),
);
}
}, childCount: relatedVideoList.length + 1),
),
);
} else {
// 请求错误
return HttpError(errMsg: '出错了', fn: () {});

View File

@@ -53,9 +53,13 @@ class VideoReplyController extends GetxController {
}
Future queryReplyList({type = 'init'}) async {
if (isLoadingMore) {
return;
}
isLoadingMore = true;
if (type == 'init') {
currentPage = 0;
noMore.value = '';
}
if (noMore.value == '没有更多了') {
return;

View File

@@ -16,11 +16,13 @@ import 'widgets/reply_item.dart';
class VideoReplyPanel extends StatefulWidget {
final String? bvid;
final int? oid;
final int rpid;
final String? replyLevel;
const VideoReplyPanel({
this.bvid,
this.oid,
this.rpid = 0,
this.replyLevel,
super.key,
@@ -48,16 +50,17 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
@override
void initState() {
super.initState();
int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
// int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
heroTag = Get.arguments['heroTag'];
replyLevel = widget.replyLevel ?? '1';
if (replyLevel == '2') {
_videoReplyController = Get.put(
VideoReplyController(oid, widget.rpid.toString(), replyLevel),
VideoReplyController(widget.oid, widget.rpid.toString(), replyLevel),
tag: widget.rpid.toString());
} else {
_videoReplyController =
Get.put(VideoReplyController(oid, '', replyLevel), tag: heroTag);
_videoReplyController = Get.put(
VideoReplyController(widget.oid, '', replyLevel),
tag: heroTag);
}
fabAnimationCtr = AnimationController(
@@ -75,7 +78,8 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
EasyThrottle.throttle('replylist', const Duration(milliseconds: 200),
() {
_videoReplyController.onLoad();
});
}
@@ -110,7 +114,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
if (replyItem != null) {
videoDetailCtr.oid = replyItem.oid;
videoDetailCtr.oid.value = replyItem.oid;
videoDetailCtr.fRpid = replyItem.rpid!;
videoDetailCtr.firstFloor = replyItem;
videoDetailCtr.showReplyReplyPanel();

View File

@@ -1,7 +1,7 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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:hive/hive.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/reply_new/index.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/url_utils.dart';
import 'package:pilipala/utils/utils.dart';
import 'zan.dart';
Box setting = GStrorage.setting;
@@ -48,6 +47,17 @@ class ReplyItem extends StatelessWidget {
replyReply!(replyItem);
}
},
onLongPress: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: replyItem);
},
);
},
child: Column(
children: [
Padding(
@@ -123,98 +133,6 @@ class ReplyItem extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
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无效
GestureDetector(
behavior: HitTestBehavior.opaque,
@@ -291,30 +209,26 @@ class ReplyItem extends StatelessWidget {
// title
Container(
margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4),
child: SelectableRegion(
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Text.rich(
style: const TextStyle(height: 1.75),
maxLines:
replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999,
overflow: TextOverflow.ellipsis,
TextSpan(
children: [
if (replyItem!.isTop!)
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'TOP',
size: 'small',
stack: 'normal',
type: 'line',
fs: 9,
),
child: Text.rich(
style: const TextStyle(height: 1.75),
maxLines:
replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999,
overflow: TextOverflow.ellipsis,
TextSpan(
children: [
if (replyItem!.isTop!)
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'TOP',
size: 'small',
stack: 'normal',
type: 'line',
fs: 9,
),
buildContent(context, replyItem!, replyReply, null),
],
),
),
buildContent(context, replyItem!, replyReply, null),
],
),
),
),
@@ -447,6 +361,17 @@ class ReplyItemRow extends StatelessWidget {
InkWell(
// 一楼点击评论展开评论详情
onTap: () => replyReply!(replyItem),
onLongPress: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: replies![i]);
},
);
},
child: Container(
width: double.infinity,
padding: EdgeInsets.fromLTRB(
@@ -541,7 +466,6 @@ InlineSpan buildContent(
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
final content = replyItem.content;
final List<InlineSpan> spanChilds = <InlineSpan>[];
bool hasMatchMember = false;
// 投票
if (content.vote.isNotEmpty) {
@@ -591,7 +515,7 @@ InlineSpan buildContent(
if (patternStr.isNotEmpty) {
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);
List<String> matchedStrs = [];
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(
TextSpan(
text: ' $matchStr ',
@@ -650,7 +576,6 @@ InlineSpan buildContent(
..onTap = () {
// 跳转到指定位置
try {
matchStr = matchStr.replaceAll('', ':');
SmartDialog.showToast('跳转至:$matchStr');
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.plPlayerController
@@ -692,16 +617,54 @@ InlineSpan buildContent(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
..onTap = () async {
final String title = content.jumpUrl[matchStr]['title'];
if (appUrlSchema == '') {
final String str = Uri.parse(matchStr).pathSegments[0];
final Map matchRes = IdUtils.matchAvorBv(input: str);
final List matchKeys = matchRes.keys.toList();
if (matchKeys.isNotEmpty) {
if (matchKeys.first == 'BV') {
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(
'/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(
'/searchResult',
parameters: {'keyword': matchRes['BV']},
'/webview',
parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title
},
);
}
} else {
@@ -710,16 +673,10 @@ InlineSpan buildContent(
parameters: {
'url': matchStr,
'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) {
final List<String> picList = <String>[];
@@ -753,11 +751,15 @@ InlineSpan buildContent(
builder: (BuildContext context, BoxConstraints box) {
double maxHeight = box.maxWidth * 0.6; // 设置最大高度
// double width = (box.maxWidth / 2).truncateToDouble();
double height = ((box.maxWidth /
2 *
pictureItem['img_height'] /
pictureItem['img_width']))
.truncateToDouble();
double height = 100;
try {
height = ((box.maxWidth /
2 *
pictureItem['img_height'] /
pictureItem['img_width']))
.truncateToDouble();
} catch (_) {}
return GestureDetector(
onTap: () {
showDialog(
@@ -797,8 +799,7 @@ InlineSpan buildContent(
),
),
);
}
if (len > 1) {
} else if (len > 1) {
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(content.pictures[i]['img_src']);
@@ -816,10 +817,11 @@ InlineSpan buildContent(
);
},
child: NetworkImgLayer(
src: content.pictures[i]['img_src'],
width: box.maxWidth,
height: box.maxWidth,
),
src: content.pictures[i]['img_src'],
width: box.maxWidth,
height: box.maxWidth,
origAspectRatio: content.pictures[i]['img_width'] /
content.pictures[i]['img_height']),
);
},
),
@@ -880,3 +882,100 @@ InlineSpan buildContent(
// spanChilds.add(TextSpan(text: matchMember));
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

@@ -570,8 +570,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
);
},
),
VideoReplyPanel(
bvid: videoDetailController.bvid,
Obx(
() => VideoReplyPanel(
bvid: videoDetailController.bvid,
oid: videoDetailController.oid.value,
),
)
],
),

View File

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