merge main

This commit is contained in:
guozhigq
2023-08-12 22:18:44 +08:00
100 changed files with 2684 additions and 944 deletions

View File

@ -1,3 +1,68 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/bangumi.dart';
import 'package:pilipala/models/bangumi/list.dart';
import 'package:pilipala/utils/storage.dart';
class BangumiController extends GetxController {}
class BangumiController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<BangumiListItemModel> bangumiList = [BangumiListItemModel()].obs;
RxList<BangumiListItemModel> bangumiFollowList = [BangumiListItemModel()].obs;
int _currentPage = 1;
bool isLoadingMore = true;
Box user = GStrorage.user;
RxBool userLogin = false.obs;
late int mid;
@override
void onInit() {
super.onInit();
if (user.get(UserBoxKey.userMid) != null) {
mid = int.parse(user.get(UserBoxKey.userMid).toString());
}
userLogin.value = user.get(UserBoxKey.userLogin) != null;
}
Future queryBangumiListFeed({type = 'init'}) async {
if (type == 'init') {
_currentPage = 1;
}
var result = await BangumiHttp.bangumiList(page: _currentPage);
if (result['status']) {
if (type == 'init') {
bangumiList.value = result['data'].list;
} else {
bangumiList.addAll(result['data'].list);
}
_currentPage += 1;
} else {}
isLoadingMore = false;
return result;
}
// 上拉加载
Future onLoad() async {
queryBangumiListFeed(type: 'onLoad');
}
// 我的订阅
Future queryBangumiFollow() async {
var result = await BangumiHttp.bangumiFollow(mid: 17340771);
if (result['status']) {
bangumiFollowList.value = result['data'].list;
} else {}
return result;
}
// 返回顶部并刷新
void animateToTop() async {
if (scrollController.offset >=
MediaQuery.of(Get.context!).size.height * 5) {
scrollController.jumpTo(0);
} else {
await scrollController.animateTo(0,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
}

View File

@ -36,7 +36,6 @@ class BangumiIntroController extends GetxController {
RxBool isLoading = false.obs;
// 视频详情 请求返回
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
Rx<BangumiInfoModel> bangumiDetail = BangumiInfoModel().obs;
// 请求返回的信息
@ -89,11 +88,6 @@ class BangumiIntroController extends GetxController {
// 获取番剧简介&选集
Future queryBangumiIntro() async {
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
if (result['status']) {
bangumiDetail.value = result['data'];
epId = bangumiDetail.value.episodes!.first.id;
}
if (userLogin) {
// 获取点赞状态
queryHasLikeVideo();
@ -102,6 +96,11 @@ class BangumiIntroController extends GetxController {
// 获取收藏状态
queryHasFavVideo();
}
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
if (result['status']) {
bangumiDetail.value = result['data'];
epId = bangumiDetail.value.episodes!.first.id;
}
return result;
}
@ -132,15 +131,10 @@ class BangumiIntroController extends GetxController {
Future actionLikeVideo() async {
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
if (result['status']) {
if (!hasLike.value) {
SmartDialog.showToast('点赞成功 👍');
hasLike.value = true;
videoDetail.value.stat!.like = videoDetail.value.stat!.like! + 1;
} else if (hasLike.value) {
SmartDialog.showToast('取消赞');
hasLike.value = false;
videoDetail.value.stat!.like = videoDetail.value.stat!.like! - 1;
}
SmartDialog.showToast(!hasLike.value ? '点赞成功 👍' : '取消赞');
hasLike.value = !hasLike.value;
bangumiDetail.value.stat!['likes'] =
bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1);
hasLike.refresh();
} else {
SmartDialog.showToast(result['msg']);
@ -193,8 +187,8 @@ class BangumiIntroController extends GetxController {
if (res['status']) {
SmartDialog.showToast('投币成功 👏');
hasCoin.value = true;
videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + _tempThemeValue;
bangumiDetail.value.stat!['coins'] =
bangumiDetail.value.stat!['coins'] + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
@ -287,4 +281,13 @@ class BangumiIntroController extends GetxController {
await VideoHttp.bangumiDel(seasonId: bangumiDetail.value.seasonId);
SmartDialog.showToast(result['msg']);
}
Future queryVideoInFolder() async {
var result = await VideoHttp.videoInFolder(
mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid));
if (result['status']) {
favFolderData.value = result['data'];
}
return result;
}
}

View File

@ -33,6 +33,7 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
final BangumiIntroController bangumiIntroController =
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
BangumiInfoModel? bangumiDetail;
late Future _futureBuilderFuture;
// 添加页面缓存
@override
@ -44,13 +45,14 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
bangumiIntroController.bangumiDetail.listen((value) {
bangumiDetail = value;
});
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
}
@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
future: bangumiIntroController.queryBangumiIntro(),
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
@ -89,20 +91,19 @@ class BangumiInfo extends StatefulWidget {
}
class _BangumiInfoState extends State<BangumiInfo> {
late BangumiInfoModel? bangumiItem;
final BangumiIntroController bangumiIntroController =
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
late VideoDetailController? videoDetailCtr;
String heroTag = Get.arguments['heroTag'];
late final BangumiIntroController bangumiIntroController;
late final VideoDetailController videoDetailCtr;
Box localCache = GStrorage.localCache;
late final BangumiInfoModel? bangumiItem;
late double sheetHeight;
@override
void initState() {
super.initState();
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
bangumiItem = bangumiIntroController.bangumiItem;
videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
sheetHeight = localCache.get('sheetHeight');
}
@ -357,10 +358,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => bangumiIntroController.actionLikeVideo(),
selectStatus: bangumiIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['likes']!.toString()
: '-'),
: bangumiItem!.stat!['likes']!.toString()),
),
Obx(
() => ActionItem(
@ -368,10 +369,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => bangumiIntroController.actionCoinVideo(),
selectStatus: bangumiIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['coins']!.toString()
: '-'),
: bangumiItem!.stat!['coins']!.toString()),
),
Obx(
() => ActionItem(
@ -379,29 +380,29 @@ class _BangumiInfoState extends State<BangumiInfo> {
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(),
selectStatus: bangumiIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['favorite']!.toString()
: '-'),
: bangumiItem!.stat!['favorite']!.toString()),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.comment),
selectIcon: const Icon(FontAwesomeIcons.reply),
onTap: () => videoDetailCtr!.tabCtr!.animateTo(1),
onTap: () => videoDetailCtr.tabCtr!.animateTo(1),
selectStatus: false,
loadingStatus: widget.loadingStatus,
loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['reply']!.toString()
: '-',
: bangumiItem!.stat!['reply']!.toString(),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => bangumiIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['share']!.toString()
: '-'),
: bangumiItem!.stat!['share']!.toString()),
],
),
),
@ -465,9 +466,6 @@ class _BangumiInfoState extends State<BangumiInfo> {
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
// text: !widget.loadingStatus
// ? widget.videoDetail!.stat!.share!.toString()
// : '-',
text: '转发'),
]);
}

View File

@ -1,4 +1,17 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/view.dart';
import 'package:pilipala/utils/storage.dart';
import 'controller.dart';
import 'widgets/bangumu_card_v.dart';
class BangumiPage extends StatefulWidget {
const BangumiPage({super.key});
@ -7,12 +20,187 @@ class BangumiPage extends StatefulWidget {
State<BangumiPage> createState() => _BangumiPageState();
}
class _BangumiPageState extends State<BangumiPage> {
class _BangumiPageState extends State<BangumiPage>
with AutomaticKeepAliveClientMixin {
final BangumiController _bangumidController = Get.put(BangumiController());
late Future? _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
ScrollController scrollController = _bangumidController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
_futureBuilderFuture = _bangumidController.queryBangumiListFeed();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_bangumidController.isLoadingMore) {
_bangumidController.isLoadingMore = true;
await _bangumidController.onLoad();
}
}
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
},
);
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('还在开发中'),
super.build(context);
return Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius),
),
child: RefreshIndicator(
onRefresh: () async {
await _bangumidController.queryBangumiListFeed(type: 'init');
return _bangumidController.queryBangumiFollow();
},
child: CustomScrollView(
controller: _bangumidController.scrollController,
slivers: [
SliverToBoxAdapter(
child: Obx(
() => Visibility(
visible: _bangumidController.userLogin.value,
child: Column(
children: [
Padding(
padding:
const EdgeInsets.only(top: 10, bottom: 10, left: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'最近追番',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
SizedBox(
height: 254,
child: FutureBuilder(
future: _bangumidController.queryBangumiFollow(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _bangumidController
.bangumiFollowList.length,
itemBuilder: (context, index) {
return Container(
width: Get.size.width / 3,
height: 254,
margin: EdgeInsets.only(
right: index <
_bangumidController
.bangumiFollowList
.length -
1
? StyleString.safeSpace
: 0),
child: BangumiCardV(
bangumiItem: _bangumidController
.bangumiFollowList[index],
),
);
},
),
);
} else {
return SizedBox();
}
} else {
return SizedBox();
}
},
),
),
],
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 10, bottom: 10, left: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'推荐',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
),
SliverPadding(
padding: EdgeInsets.zero,
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() => contentGrid(_bangumidController,
_bangumidController.bangumiList));
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
}
} else {
return contentGrid(_bangumidController, []);
}
},
),
),
const LoadingMore()
],
),
),
);
}
Widget contentGrid(ctr, bangumiList) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
mainAxisSpacing: StyleString.cardSpace - 2,
// 列间距
crossAxisSpacing: StyleString.cardSpace,
// 列数
crossAxisCount: 3,
mainAxisExtent: Get.size.width / 3 / 0.65 + 30,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return bangumiList!.isNotEmpty
? BangumiCardV(bangumiItem: bangumiList[index])
: const SizedBox();
},
childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10,
),
);
}

View File

@ -37,10 +37,10 @@ class _BangumiPanelState extends State<BangumiPanel> {
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
Container(
height: 45,
padding: const EdgeInsets.only(left: 14, right: 14),
child: Row(
AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
@ -53,10 +53,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
),
],
),
),
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
titleSpacing: 10,
),
Expanded(
child: Material(
@ -66,8 +63,15 @@ class _BangumiPanelState extends State<BangumiPanel> {
return ListTile(
onTap: () => changeFucCall(widget.pages[index], index),
dense: false,
leading: index == currentIndex
? Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
height: 12,
)
: null,
title: Text(
widget.pages[index].longTitle!,
'${index + 1}${widget.pages[index].longTitle!}',
style: TextStyle(
fontSize: 14,
color: index == currentIndex
@ -148,6 +152,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: widget.pages.length,
itemExtent: 150,
itemBuilder: ((context, i) {
return Container(
width: 150,

View File

@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class BangumiCardV extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final bangumiItem;
final Function()? longPress;
final Function()? longPressEnd;
const BangumiCardV({
Key? key,
required this.bangumiItem,
this.longPress,
this.longPressEnd,
}) : super(key: key);
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(bangumiItem.mediaId);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: GestureDetector(
// onLongPress: () {
// if (longPress != null) {
// longPress!();
// }
// },
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
int seasonId = bangumiItem.seasonId;
SmartDialog.showLoading(msg: '获取中...');
var res = await SearchHttp.bangumiInfo(seasonId: seasonId);
SmartDialog.dismiss().then((value) {
if (res['status']) {
if (res['data'].episodes.isEmpty) {
SmartDialog.showToast('资源加载失败');
return;
}
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=$seasonId',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
});
},
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: 0.65,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: bangumiItem.cover,
width: maxWidth,
height: maxHeight,
),
),
if (bangumiItem.badge != null)
pBadge(bangumiItem.badge, context, 6, 6, null, null),
if (bangumiItem.order != null)
pBadge(bangumiItem.order, context, null, null, 6, 6,
type: 'gray'),
],
);
}),
),
),
BangumiContent(bangumiItem: bangumiItem)
],
),
),
),
);
}
}
class BangumiContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final bangumiItem;
const BangumiContent({Key? key, required this.bangumiItem}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),
// 单列
// padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Expanded(
child: Text(
bangumiItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
],
),
if (bangumiItem.indexShow != null)
Text(
bangumiItem.indexShow,
maxLines: 1,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
if (bangumiItem.progress != null)
Text(
bangumiItem.progress,
maxLines: 1,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
],
),
),
);
}
}

View File

@ -29,7 +29,8 @@ class DynamicsPage extends StatefulWidget {
class _DynamicsPageState extends State<DynamicsPage>
with AutomaticKeepAliveClientMixin {
final DynamicsController _dynamicsController = Get.put(DynamicsController());
Future? _futureBuilderFuture;
late Future _futureBuilderFuture;
late Future _futureBuilderFutureUp;
bool _isLoadingMore = false;
Box user = GStrorage.user;
@ -40,6 +41,7 @@ class _DynamicsPageState extends State<DynamicsPage>
void initState() {
super.initState();
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
ScrollController scrollController = _dynamicsController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
@ -175,50 +177,6 @@ class _DynamicsPageState extends State<DynamicsPage>
icon: const Icon(Icons.history, size: 21),
),
),
Positioned(
left: 10,
top: 0,
bottom: 0,
child: Align(
alignment: Alignment.center,
child: user.get(UserBoxKey.userLogin) ?? false
? GestureDetector(
onTap: () {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
},
child: NetworkImgLayer(
type: 'avatar',
width: 30,
height: 30,
src: user.get(UserBoxKey.userFace),
),
)
: IconButton(
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
},
icon: const Icon(CupertinoIcons.person, size: 22),
),
),
),
],
),
),
@ -229,7 +187,7 @@ class _DynamicsPageState extends State<DynamicsPage>
controller: _dynamicsController.scrollController,
slivers: [
FutureBuilder(
future: _dynamicsController.queryFollowUp(),
future: _futureBuilderFutureUp,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;

View File

@ -40,7 +40,7 @@ class _UpPanelState extends State<UpPanel> {
1,
UpItem(
face: user.get(UserBoxKey.userFace),
uname: '',
uname: '',
mid: user.get(UserBoxKey.userMid),
),
);

View File

@ -13,6 +13,13 @@ class FavPage extends StatefulWidget {
class _FavPageState extends State<FavPage> {
final FavController _favController = Get.put(FavController());
late Future _futureBuilderFuture;
@override
void initState() {
super.initState();
_futureBuilderFuture = _favController.queryFavFolder();
}
@override
Widget build(BuildContext context) {
@ -26,7 +33,7 @@ class _FavPageState extends State<FavPage> {
),
),
body: FutureBuilder(
future: _favController.queryFavFolder(),
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;

View File

@ -1,77 +1,51 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/pages/bangumi/index.dart';
import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/live/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/utils/storage.dart';
class HomeController extends GetxController with GetTickerProviderStateMixin {
bool flag = false;
List tabs = [
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '直播',
'type': 'live'
},
{
'icon': const Icon(
Icons.thumb_up_off_alt_outlined,
size: 15,
),
'label': '推荐',
'type': 'rcm'
},
{
'icon': const Icon(
Icons.whatshot_outlined,
size: 15,
),
'label': '热门',
'type': 'hot'
},
{
'icon': const Icon(
Icons.play_circle_outlined,
size: 15,
),
'label': '番剧',
'type': 'bangumi'
},
];
late List tabs;
int initialIndex = 1;
late TabController tabController;
List ctrList = [
Get.find<LiveController>,
Get.find<RcmdController>,
Get.find<HotController>,
Get.find<BangumiController>,
];
late List tabsCtrList;
late List<Widget> tabsPageList;
RxString defaultSearch = '输入关键词搜索'.obs;
Box user = GStrorage.user;
RxBool userLogin = false.obs;
RxString userFace = ''.obs;
@override
void onInit() {
super.onInit();
searchDefault();
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
userFace.value = user.get(UserBoxKey.userFace) ?? '';
// 进行tabs配置
tabs = tabsConfig;
tabsCtrList = tabsConfig.map((e) => e['ctr']).toList();
tabsPageList = tabsConfig.map<Widget>((e) => e['page']).toList();
tabController = TabController(
initialIndex: initialIndex,
length: tabs.length,
vsync: this,
);
searchDefault();
}
void onRefresh() {
int index = tabController.index;
var ctr = ctrList[index];
var ctr = tabsCtrList[index];
ctr().onRefresh();
}
void animateToTop() {
int index = tabController.index;
var ctr = ctrList[index];
var ctr = tabsCtrList[index];
ctr().animateToTop();
}
@ -81,4 +55,10 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
defaultSearch.value = res.data['data']['name'];
}
}
// 更新登录状态
void updateLoginStatus(val) {
userLogin.value = val ?? false;
userFace.value = user.get(UserBoxKey.userFace) ?? '';
}
}

View File

@ -1,7 +1,5 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/bangumi/index.dart';
import 'package:pilipala/pages/hot/index.dart';
@ -10,7 +8,6 @@ import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import './controller.dart';
class HomePage extends StatefulWidget {
@ -29,6 +26,19 @@ class _HomePageState extends State<HomePage>
@override
bool get wantKeepAlive => true;
showUserBottonSheet() {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
}
@override
Widget build(BuildContext context) {
super.build(context);
@ -38,73 +48,68 @@ class _HomePageState extends State<HomePage>
appBar: AppBar(toolbarHeight: 0, elevation: 0),
body: Column(
children: [
CustomAppBar(stream: stream, ctr: _homeController),
Container(
CustomAppBar(
stream: stream,
ctr: _homeController,
callback: showUserBottonSheet,
),
Padding(
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 4),
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: Theme(
data: ThemeData(
splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
),
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: TabBar(
controller: _homeController.tabController,
tabs: [
for (var i in _homeController.tabs)
// Tab(text: i['label'])
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 0, vertical: 11),
child: Row(
children: [
i['icon'],
const SizedBox(width: 4),
Text(i['label'])
],
),
),
],
isScrollable: true,
indicatorWeight: 0,
indicatorPadding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 5),
indicator: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.8),
borderRadius:
const BorderRadius.all(Radius.circular(20)),
),
indicatorSize: TabBarIndicatorSize.tab,
labelColor: Theme.of(context).colorScheme.primary,
labelStyle: const TextStyle(fontSize: 13),
dividerColor: Colors.transparent,
unselectedLabelColor:
Theme.of(context).colorScheme.outline,
onTap: (value) =>
{feedBack(), _homeController.initialIndex = value},
),
),
child: Theme(
data: ThemeData(
splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
),
child: TabBar(
controller: _homeController.tabController,
tabs: [
for (var i in _homeController.tabs) Tab(text: i['label'])
],
isScrollable: true,
indicatorWeight: 0,
indicatorPadding: const EdgeInsets.only(
top: 37, left: 18, right: 18, bottom: 6),
indicatorColor: Colors.black,
indicator: BoxDecoration(
gradient: RadialGradient(
center: Alignment.centerLeft,
radius: 20.00,
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.background,
],
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(2),
bottomLeft: Radius.circular(2),
bottomRight: Radius.circular(4),
),
),
],
indicatorSize: TabBarIndicatorSize.tab,
labelColor: Theme.of(context).colorScheme.primary,
labelStyle:
const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
dividerColor: Colors.transparent,
unselectedLabelStyle: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontWeight: FontWeight.normal,
),
unselectedLabelColor: Theme.of(context).colorScheme.outline,
onTap: (value) {
feedBack();
if (_homeController.initialIndex == value) {
_homeController.tabsCtrList[value]().animateToTop();
}
_homeController.initialIndex = value;
},
),
),
),
Expanded(
child: TabBarView(
controller: _homeController.tabController,
children: const [
LivePage(),
RcmdPage(),
HotPage(),
BangumiPage(),
],
children: _homeController.tabsPageList,
),
),
],
@ -116,13 +121,15 @@ class _HomePageState extends State<HomePage>
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final double height;
final Stream<bool>? stream;
final ctr;
final HomeController? ctr;
final Function? callback;
const CustomAppBar({
super.key,
this.height = kToolbarHeight,
this.stream,
this.ctr,
this.callback,
});
@override
@ -130,8 +137,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
Box user = GStrorage.user;
return StreamBuilder(
stream: stream,
initialData: true,
@ -144,111 +149,100 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
child: AnimatedContainer(
curve: Curves.linear,
duration: const Duration(milliseconds: 300),
height: snapshot.data ? 94 : MediaQuery.of(context).padding.top,
height: snapshot.data
? MediaQuery.of(context).padding.top + 42
: MediaQuery.of(context).padding.top,
child: Container(
padding: EdgeInsets.only(
left: 12,
right: 12,
bottom: 4,
bottom: 0,
top: MediaQuery.of(context).padding.top,
),
child: Row(
children: [
const Text(
'PLPL',
style: TextStyle(
height: 2.8,
fontSize: 17,
fontWeight: FontWeight.bold,
fontFamily: 'Jura-Bold',
),
),
const SizedBox(width: 10),
Expanded(
child: GestureDetector(
onTap: () {
Get.toNamed('/search', parameters: {
'hintText': ctr.defaultSearch.value
});
},
child: Container(
width: 250,
height: 45,
clipBehavior: Clip.hardEdge,
padding: const EdgeInsets.only(left: 12, right: 22),
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(25)),
color:
Theme.of(context).colorScheme.onInverseSurface,
),
child: Row(
children: [
Icon(
Icons.search_outlined,
size: 23,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(width: 7),
Expanded(
child: Obx(
() => Text(
ctr.defaultSearch.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
),
child: Row(children: [
Image.asset(
'assets/images/logo/logo_android_2.png',
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 4),
Expanded(
child: GestureDetector(
onTap: () {
Get.toNamed('/search',
parameters: {'hintText': ctr!.defaultSearch.value});
},
child: Container(
width: 250,
height: 40,
clipBehavior: Clip.hardEdge,
padding: const EdgeInsets.only(left: 12, right: 22),
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(25)),
color: Theme.of(context).colorScheme.onInverseSurface,
),
child: Row(
children: [
Icon(
Icons.search_outlined,
size: 21,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(width: 6),
Expanded(
child: Obx(
() => Text(
ctr!.defaultSearch.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
),
),
],
),
),
],
),
),
),
const SizedBox(width: 12),
if (user.get(UserBoxKey.userLogin) ?? false) ...[
GestureDetector(
onTap: () {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
const SizedBox(width: 10),
Obx(
() => ctr!.userLogin.value
? GestureDetector(
onTap: () => callback!(),
child: NetworkImgLayer(
type: 'avatar',
width: 38,
height: 38,
src: ctr!.userFace.value,
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
},
child: NetworkImgLayer(
type: 'avatar',
width: 34,
height: 34,
src: user.get(UserBoxKey.userFace),
),
)
] else ...[
IconButton(
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
)
: SizedBox(
width: 38,
height: 38,
child: IconButton(
style: ButtonStyle(
padding:
MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith((states) {
return Theme.of(context)
.colorScheme
.onInverseSurface;
}),
),
onPressed: () => callback!(),
icon: Icon(
Icons.person_rounded,
size: 22,
color: Theme.of(context).colorScheme.primary,
),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
},
icon: const Icon(CupertinoIcons.person, size: 22),
)
],
],
),
),
),
]),
),
),
),

View File

@ -22,10 +22,12 @@ class LivePage extends StatefulWidget {
class _LivePageState extends State<LivePage> {
final LiveController _liveController = Get.put(LiveController());
late Future _futureBuilderFuture;
@override
void initState() {
super.initState();
_futureBuilderFuture = _liveController.queryLiveList('init');
ScrollController scrollController = _liveController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
@ -52,47 +54,54 @@ class _LivePageState extends State<LivePage> {
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
return await _liveController.onRefresh();
},
child: CustomScrollView(
controller: _liveController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 0, StyleString.safeSpace, 0),
sliver: FutureBuilder(
future: _liveController.queryLiveList('init'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() =>
contentGrid(_liveController, _liveController.liveList));
return Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius),
),
child: RefreshIndicator(
onRefresh: () async {
return await _liveController.onRefresh();
},
child: CustomScrollView(
controller: _liveController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: EdgeInsets.zero,
sliver: FutureBuilder(
future: _liveController.queryLiveList('init'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() => contentGrid(
_liveController, _liveController.liveList));
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
}
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
// 缓存数据
if (_liveController.liveList.length > 1) {
return contentGrid(
_liveController, _liveController.liveList);
}
// 骨架屏
else {
return contentGrid(_liveController, []);
}
}
} else {
// 缓存数据
if (_liveController.liveList.length > 1) {
return contentGrid(
_liveController, _liveController.liveList);
}
// 骨架屏
else {
return contentGrid(_liveController, []);
}
}
},
},
),
),
),
const LoadingMore()
],
const LoadingMore()
],
),
),
);
}

View File

@ -31,6 +31,9 @@ class LiveRoomController extends GetxController {
liveItem = Get.arguments['liveItem'];
heroTag = Get.arguments['heroTag'] ?? '';
if (liveItem.pic != null && liveItem.pic != '') {
cover = liveItem.pic;
}
if (liveItem.cover != null && liveItem.cover != '') {
cover = liveItem.cover;
}
}

View File

@ -112,68 +112,67 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
],
),
),
if (_liveRoomController.liveItem.watchedShow != null)
Container(
height: 45,
padding: const EdgeInsets.only(left: 12, right: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor.withOpacity(0.1)),
Container(
height: 45,
padding: const EdgeInsets.only(left: 12, right: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor.withOpacity(0.1)),
),
),
child: Row(children: <Widget>[
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () {},
icon: const Icon(
Icons.subtitles_outlined,
size: 21,
),
),
),
child: Row(children: <Widget>[
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () {},
icon: const Icon(
Icons.subtitles_outlined,
size: 21,
),
const Spacer(),
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () {},
icon: const Icon(
Icons.hd_outlined,
size: 20,
),
),
const Spacer(),
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () {},
icon: const Icon(
Icons.hd_outlined,
size: 20,
),
),
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () => _liveRoomController
.setVolumn(plPlayerController!.volume.value),
icon: Obx(() => Icon(
_liveRoomController.volumeOff.value
? Icons.volume_off_outlined
: Icons.volume_up_outlined,
size: 21,
)),
),
),
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () => {},
// plPlayerController!.goToFullscreen(context),
icon: const Icon(
Icons.fullscreen,
),
),
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () => _liveRoomController
.setVolumn(plPlayerController!.volume.value),
icon: Obx(() => Icon(
_liveRoomController.volumeOff.value
? Icons.volume_off_outlined
: Icons.volume_up_outlined,
size: 21,
)),
),
),
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () => {},
// plPlayerController!.goToFullscreen(context),
icon: const Icon(
Icons.fullscreen,
),
),
),
]),
),
),
]),
),
],
),
);

View File

@ -143,7 +143,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
// type: BottomNavigationBarType.shifting,
selectedItemColor: Theme.of(context).colorScheme.primary,
unselectedItemColor:
Theme.of(context).colorScheme.onSurfaceVariant,
Theme.of(context).colorScheme.outline.withOpacity(0.5),
selectedFontSize: 12.4,
onTap: (value) => setIndex(value),
items: [

View File

@ -14,13 +14,22 @@ class MediaPage extends StatefulWidget {
class _MediaPageState extends State<MediaPage>
with AutomaticKeepAliveClientMixin {
late MediaController mediaController;
late Future _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder();
}
@override
Widget build(BuildContext context) {
super.build(context);
final MediaController mediaController = Get.put(MediaController());
Color primary = Theme.of(context).colorScheme.primary;
return Scaffold(
appBar: AppBar(toolbarHeight: 30),
@ -107,7 +116,7 @@ class _MediaPageState extends State<MediaPage>
),
),
trailing: IconButton(
onPressed: () => mediaController.queryFavFolder(),
onPressed: () => _futureBuilderFuture,
icon: const Icon(
Icons.refresh,
size: 20,
@ -119,7 +128,7 @@ class _MediaPageState extends State<MediaPage>
width: double.infinity,
height: 170,
child: FutureBuilder(
future: mediaController.queryFavFolder(),
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart';
import 'package:pilipala/utils/storage.dart';
@ -10,9 +12,11 @@ class MineController extends GetxController {
Rx<UserInfoData> userInfo = UserInfoData().obs;
// 用户状态 动态、关注、粉丝
Rx<UserStat> userStat = UserStat().obs;
Box user = GStrorage.user;
RxBool userLogin = false.obs;
Box user = GStrorage.user;
Box setting = GStrorage.setting;
Box userInfoCache = GStrorage.userInfo;
Rx<ThemeType> themeType = ThemeType.system.obs;
@override
onInit() {
@ -21,6 +25,9 @@ class MineController extends GetxController {
if (userInfoCache.get('userInfoCache') != null) {
userInfo.value = userInfoCache.get('userInfoCache');
}
themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
defaultValue: ThemeType.system.code)];
}
onLogin() async {
@ -90,4 +97,31 @@ class MineController extends GetxController {
userLogin.value = false;
// Get.find<MainController>().resetLast();
}
onChangeTheme() {
Brightness currentBrightness =
MediaQuery.of(Get.context!).platformBrightness;
ThemeType currentTheme = themeType.value;
switch (currentTheme) {
case ThemeType.dark:
setting.put(SettingBoxKey.themeMode, ThemeType.light.code);
themeType.value = ThemeType.light;
break;
case ThemeType.light:
setting.put(SettingBoxKey.themeMode, ThemeType.dark.code);
themeType.value = ThemeType.dark;
break;
case ThemeType.system:
// 判断当前的颜色模式
if (currentBrightness == Brightness.light) {
setting.put(SettingBoxKey.themeMode, ThemeType.dark.code);
themeType.value = ThemeType.dark;
} else {
setting.put(SettingBoxKey.themeMode, ThemeType.light.code);
themeType.value = ThemeType.light;
}
break;
}
Get.forceAppUpdate();
}
}

View File

@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/utils/storage.dart';
import 'controller.dart';
class MinePage extends StatelessWidget {
@ -21,16 +23,23 @@ class MinePage extends StatelessWidget {
elevation: 0,
toolbarHeight: kTextTabBarHeight + 20,
backgroundColor: Colors.transparent,
title: null,
centerTitle: false,
title: const Text(
'PLPL',
style: TextStyle(
height: 2.8,
fontSize: 17,
fontWeight: FontWeight.bold,
fontFamily: 'Jura-Bold',
),
),
actions: [
IconButton(
onPressed: () {
Get.changeThemeMode(ThemeMode.dark);
},
onPressed: () => mineController.onChangeTheme(),
icon: Icon(
Get.theme == ThemeData.light()
? CupertinoIcons.moon
: CupertinoIcons.sun_max,
mineController.themeType.value == ThemeType.dark
? CupertinoIcons.sun_max
: CupertinoIcons.moon,
size: 22,
),
),
@ -93,7 +102,7 @@ class MinePage extends StatelessWidget {
src: _mineController.userInfo.value.face,
width: 85,
height: 85)
: Image.asset('assets/images/loading.png'),
: Image.asset('assets/images/noface.jpeg'),
),
),
),

View File

@ -23,6 +23,7 @@ class RcmdPage extends StatefulWidget {
class _RcmdPageState extends State<RcmdPage>
with AutomaticKeepAliveClientMixin {
final RcmdController _rcmdController = Get.put(RcmdController());
late Future _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@ -30,6 +31,7 @@ class _RcmdPageState extends State<RcmdPage>
@override
void initState() {
super.initState();
_futureBuilderFuture = _rcmdController.queryRcmdFeed('init');
ScrollController scrollController = _rcmdController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
@ -57,49 +59,56 @@ class _RcmdPageState extends State<RcmdPage>
@override
Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator(
onRefresh: () async {
return await _rcmdController.onRefresh();
},
child: CustomScrollView(
controller: _rcmdController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: _rcmdController.crossAxisCount == 1
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 0, StyleString.safeSpace, 0),
sliver: FutureBuilder(
future: _rcmdController.queryRcmdFeed('init'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() => contentGrid(
_rcmdController, _rcmdController.videoList));
return Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius),
),
child: RefreshIndicator(
onRefresh: () async {
return await _rcmdController.onRefresh();
},
child: CustomScrollView(
controller: _rcmdController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: _rcmdController.crossAxisCount == 1
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(0, 0, 0, 0),
sliver: FutureBuilder(
future: _rcmdController.queryRcmdFeed('init'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() => contentGrid(
_rcmdController, _rcmdController.videoList));
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
}
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
// 缓存数据
if (_rcmdController.videoList.length > 1) {
return contentGrid(
_rcmdController, _rcmdController.videoList);
}
// 骨架屏
else {
return contentGrid(_rcmdController, []);
}
}
} else {
// 缓存数据
if (_rcmdController.videoList.length > 1) {
return contentGrid(
_rcmdController, _rcmdController.videoList);
}
// 骨架屏
else {
return contentGrid(_rcmdController, []);
}
}
},
},
),
),
),
const LoadingMore()
],
const LoadingMore()
],
),
),
);
}

View File

@ -56,7 +56,7 @@ class SSearchController extends GetxController {
}
void onClear() {
if (searchKeyWord.value.isNotEmpty) {
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
controller.value.clear();
searchKeyWord.value = '';
searchSuggestList.value = [];

View File

@ -211,13 +211,13 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
return Obx(
() => Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(10, 25, 4, 0),
padding: const EdgeInsets.fromLTRB(10, 25, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_searchController.historyList.isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 1, 2),
padding: const EdgeInsets.fromLTRB(6, 0, 0, 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart';
class SearchPanelController extends GetxController {
SearchPanelController({this.keyword, this.searchType});
@ -21,6 +23,7 @@ class SearchPanelController extends GetxController {
} else if (type == 'onRefresh') {
resultList.value = result['data'].list;
}
onPushDetail(keyword, resultList);
}
return result;
}
@ -40,4 +43,24 @@ class SearchPanelController extends GetxController {
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
void onPushDetail(keyword, resultList) async {
// 匹配输入内容如果是AV、BV号且有结果 直接跳转详情页
Map matchRes = IdUtils.matchAvorBv(input: keyword);
List matchKeys = matchRes.keys.toList();
if (matchKeys.isNotEmpty && searchType == SearchType.video) {
String bvid = resultList.first.bvid;
int aid = resultList.first.aid;
String heroTag = Utils.makeHeroTag(bvid);
int cid = await SearchHttp.ab2c(aid: aid, bvid: bvid);
if (matchKeys.first == 'BV' && matchRes[matchKeys.first] == bvid ||
matchKeys.first == 'AV' && matchRes[matchKeys.first] == aid) {
Get.toNamed(
'/video?bvid=$bvid&cid=$cid',
arguments: {'videoItem': resultList.first, 'heroTag': heroTag},
);
}
}
}
}

View File

@ -25,7 +25,7 @@ class SearchPanel extends StatefulWidget {
class _SearchPanelState extends State<SearchPanel>
with AutomaticKeepAliveClientMixin {
late SearchPanelController? _searchPanelController;
late SearchPanelController _searchPanelController;
bool _isLoadingMore = false;
late Future _futureBuilderFuture;
@ -41,10 +41,9 @@ class _SearchPanelState extends State<SearchPanel>
keyword: widget.keyword,
searchType: widget.searchType,
),
tag: widget.searchType!.type + widget.tag!,
tag: widget.searchType!.type,
);
ScrollController scrollController =
_searchPanelController!.scrollController;
ScrollController scrollController = _searchPanelController.scrollController;
scrollController.addListener(() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 100) {
@ -55,7 +54,7 @@ class _SearchPanelState extends State<SearchPanel>
}
}
});
_futureBuilderFuture = _searchPanelController!.onSearch();
_futureBuilderFuture = _searchPanelController.onSearch();
}
@override
@ -63,7 +62,7 @@ class _SearchPanelState extends State<SearchPanel>
super.build(context);
return RefreshIndicator(
onRefresh: () async {
await _searchPanelController!.onRefresh();
await _searchPanelController.onRefresh();
},
child: FutureBuilder(
future: _futureBuilderFuture,
@ -71,7 +70,7 @@ class _SearchPanelState extends State<SearchPanel>
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
var ctr = _searchPanelController;
List list = ctr!.resultList;
List list = ctr.resultList;
if (data['status']) {
return Obx(() {
switch (widget.searchType) {

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart';
@ -32,6 +33,7 @@ class LiveItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomid);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
@ -40,7 +42,10 @@ class LiveItem extends StatelessWidget {
),
margin: EdgeInsets.zero,
child: InkWell(
onTap: () {},
onTap: () async {
Get.toNamed('/liveRoom?roomid=${liveItem.roomid}',
arguments: {'liveItem': liveItem, 'heroTag': heroTag});
},
child: Column(
children: [
ClipRRect(
@ -58,7 +63,7 @@ class LiveItem extends StatelessWidget {
return Stack(
children: [
Hero(
tag: Utils.makeHeroTag(liveItem.roomid),
tag: heroTag,
child: NetworkImgLayer(
src: liveItem.cover,
type: 'emote',

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'package:pilipala/pages/searchPanel/index.dart';
import 'controller.dart';
@ -88,6 +89,7 @@ class _SearchResultPageState extends State<SearchResultPage>
tag: SearchType.values[index].type)
.animateToTop();
}
_searchResultController!.tabIndex = index;
},
),

View File

@ -1,16 +1,21 @@
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/mine/controller.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class SettingController extends GetxController {
Box user = GStrorage.user;
RxBool userLogin = false.obs;
Box userInfoCache = GStrorage.userInfo;
Box setting = GStrorage.setting;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
RxBool feedBackEnable = false.obs;
RxInt picQuality = 10.obs;
Rx<ThemeType> themeType = ThemeType.system.obs;
@override
void onInit() {
@ -18,6 +23,10 @@ class SettingController extends GetxController {
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
feedBackEnable.value =
setting.get(SettingBoxKey.feedBackEnable, defaultValue: false);
picQuality.value =
setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
defaultValue: ThemeType.system.code)];
}
loginOut() async {
@ -25,6 +34,8 @@ class SettingController extends GetxController {
await Get.find<MineController>().resetUserInfo();
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
userInfoCache.put('userInfoCache', null);
HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(false);
}
// 开启关闭震动反馈

View File

@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart';
class PlaySetting extends StatefulWidget {
const PlaySetting({super.key});
@override
State<PlaySetting> createState() => _PlaySettingState();
}
class _PlaySettingState extends State<PlaySetting> {
Box setting = GStrorage.setting;
late dynamic defaultVideoQa;
late dynamic defaultAudioQa;
late dynamic defaultDecode;
@override
void initState() {
super.initState();
defaultVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
defaultValue: VideoQuality.values.last.code);
defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.values.last.code);
defaultDecode = setting.get(SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code);
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
'播放设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: ListView(
children: [
const SetSwitchItem(
title: '自动播放',
subTitle: '进入详情页自动播放',
setKey: SettingBoxKey.autoPlayEnable,
defaultVal: true,
),
const SetSwitchItem(
title: '开启硬解',
subTitle: '以较低功耗播放视频',
setKey: SettingBoxKey.enableHA,
defaultVal: true,
),
ListTile(
dense: false,
title: Text('默认画质', style: titleStyle),
subtitle: Text(
'当前画质' + VideoQualityCode.fromCode(defaultVideoQa)!.description!,
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: defaultVideoQa,
icon: const Icon(Icons.arrow_forward_rounded, size: 22),
onSelected: (item) {
defaultVideoQa = item;
setting.put(SettingBoxKey.defaultVideoQa, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in VideoQuality.values.reversed) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
),
ListTile(
dense: false,
title: Text('默认音质', style: titleStyle),
subtitle: Text(
'当前音质' + AudioQualityCode.fromCode(defaultAudioQa)!.description!,
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: defaultAudioQa,
icon: const Icon(Icons.arrow_forward_rounded, size: 22),
onSelected: (item) {
defaultAudioQa = item;
setting.put(SettingBoxKey.defaultAudioQa, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in AudioQuality.values.reversed) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
),
ListTile(
dense: false,
title: Text('默认解码格式', style: titleStyle),
subtitle: Text(
'当前解码格式' +
VideoDecodeFormatsCode.fromCode(defaultDecode)!.description!,
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: defaultDecode,
icon: const Icon(Icons.arrow_forward_rounded, size: 22),
onSelected: (item) {
defaultDecode = item;
setting.put(SettingBoxKey.defaultDecode, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in VideoDecodeFormats.values) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/utils/storage.dart';
import 'controller.dart';
class StyleSetting extends StatefulWidget {
const StyleSetting({super.key});
@override
State<StyleSetting> createState() => _StyleSettingState();
}
class _StyleSettingState extends State<StyleSetting> {
final SettingController settingController = Get.put(SettingController());
Box setting = GStrorage.setting;
late int picQuality;
late ThemeType _tempThemeValue;
@override
void initState() {
super.initState();
picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
_tempThemeValue = settingController.themeType.value;
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
'外观设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: ListView(
children: [
Obx(
() => ListTile(
enableFeedback: true,
onTap: () => settingController.onOpenFeedBack(),
title: const Text('震动反馈'),
subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle),
trailing: Transform.scale(
scale: 0.8,
child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
if (states.isNotEmpty &&
states.first == MaterialState.selected) {
return const Icon(Icons.done);
}
return null; // All other states will use the default thumbIcon.
}),
value: settingController.feedBackEnable.value,
onChanged: (value) => settingController.onOpenFeedBack()),
),
),
),
ListTile(
dense: false,
onTap: () {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setState) {
final SettingController settingController =
Get.put(SettingController());
return AlertDialog(
title: const Text('图片质量'),
contentPadding: const EdgeInsets.only(
top: 20, left: 8, right: 8, bottom: 8),
content: SizedBox(
height: 40,
child: Slider(
value: picQuality.toDouble(),
min: 10,
max: 100,
divisions: 9,
label: '$picQuality%',
onChanged: (double val) {
picQuality = val.toInt();
setState(() {});
},
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline))),
TextButton(
onPressed: () {
setting.put(
SettingBoxKey.defaultPicQa, picQuality);
Get.back();
settingController.picQuality.value = picQuality;
},
child: const Text('确定'),
)
],
);
},
);
},
);
},
title: Text('图片质量', style: titleStyle),
subtitle: Text('选择合适的图片清晰度上限100%', style: subTitleStyle),
trailing: Obx(
() => Text(
'${settingController.picQuality.value}%',
style: Theme.of(context).textTheme.titleSmall,
),
),
),
ListTile(
dense: false,
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('主题模式'),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
content: StatefulBuilder(
builder: (context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (var i in ThemeType.values) ...[
RadioListTile(
value: i,
title: Text(i.description, style: titleStyle),
groupValue: _tempThemeValue,
onChanged: (ThemeType? value) {
setState(() {
_tempThemeValue = i;
});
},
),
]
],
);
}),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
)),
TextButton(
onPressed: () {
settingController.themeType.value = _tempThemeValue;
setting.put(
SettingBoxKey.themeMode, _tempThemeValue.code);
Get.forceAppUpdate();
Get.back();
},
child: const Text('确定'))
],
);
},
);
},
title: Text('主题模式', style: titleStyle),
subtitle: Obx(() => Text(
'当前模式:${settingController.themeType.value.description}',
style: subTitleStyle)),
trailing: const Icon(Icons.arrow_right_alt_outlined),
),
],
),
);
}
}

View File

@ -14,32 +14,30 @@ class SettingPage extends StatelessWidget {
final SettingController settingController = Get.put(SettingController());
return Scaffold(
appBar: AppBar(
title: const Text('设置'),
centerTitle: false,
titleSpacing: 0,
title: Text(
'设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: Column(
children: [
Obx(
() => ListTile(
enableFeedback: true,
onTap: () => settingController.onOpenFeedBack(),
title: const Text('震动反馈'),
subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle),
trailing: Transform.scale(
scale: 0.8,
child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
if (states.isNotEmpty &&
states.first == MaterialState.selected) {
return const Icon(Icons.done);
}
return null; // All other states will use the default thumbIcon.
}),
value: settingController.feedBackEnable.value,
onChanged: (value) => settingController.onOpenFeedBack()),
),
),
ListTile(
onTap: () => Get.toNamed('/playSetting'),
dense: false,
title: const Text('播放设置'),
),
ListTile(
onTap: () => Get.toNamed('/styleSetting'),
dense: false,
title: const Text('外观设置'),
),
// ListTile(
// onTap: () {},
// dense: false,
// title: const Text('其他设置'),
// ),
Obx(
() => Visibility(
visible: settingController.userLogin.value,

View File

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/utils/storage.dart';
class SetSelectItem extends StatefulWidget {
final String? title;
final String? subTitle;
final String? setKey;
const SetSelectItem({
this.title,
this.subTitle,
this.setKey,
Key? key,
}) : super(key: key);
@override
State<SetSelectItem> createState() => _SetSelectItemState();
}
class _SetSelectItemState extends State<SetSelectItem> {
Box Setting = GStrorage.setting;
late var currentVal;
late int currentIndex;
late List menus;
late List<PopupMenuEntry> popMenuItems;
@override
void initState() {
super.initState();
late String defaultVal;
switch (widget.setKey) {
case 'defaultVideoQa':
defaultVal = VideoQuality.values.last.description;
List<VideoQuality> list = menus = VideoQuality.values.reversed.toList();
currentVal = Setting.get(widget.setKey, defaultValue: defaultVal);
currentIndex =
list.firstWhere((i) => i.description == currentVal).index;
popMenuItems = [
for (var i in list) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
)
]
];
break;
case 'defaultAudioQa':
defaultVal = AudioQuality.values.last.description;
List<AudioQuality> list = menus = AudioQuality.values.reversed.toList();
currentVal = Setting.get(widget.setKey, defaultValue: defaultVal);
currentIndex =
list.firstWhere((i) => i.description == currentVal).index;
popMenuItems = [
for (var i in list) ...[
PopupMenuItem(
value: i.index,
child: Text(i.description),
),
]
];
break;
case 'defaultDecode':
defaultVal = VideoDecodeFormats.values[0].description;
currentVal = Setting.get(widget.setKey, defaultValue: defaultVal);
List<VideoDecodeFormats> list = menus = VideoDecodeFormats.values;
currentIndex =
list.firstWhere((i) => i.description == currentVal).index;
popMenuItems = [
for (var i in list) ...[
PopupMenuItem(
value: i.index,
child: Text(i.description),
),
]
];
break;
case 'defaultVideoSpeed':
defaultVal = '1.0';
currentVal = Setting.get(widget.setKey, defaultValue: defaultVal);
break;
}
}
@override
Widget build(BuildContext context) {
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return ListTile(
onTap: () {},
dense: false,
title: Text(widget.title!),
subtitle: Text(
'当前${widget.title!} $currentVal',
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: currentIndex,
icon: const Icon(
Icons.arrow_forward_rounded,
size: 22,
),
onSelected: (item) {
currentVal = menus.firstWhere((e) => e.code == item).first;
setState(() {});
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry>[...popMenuItems],
),
);
}
}

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart';
class SetSwitchItem extends StatefulWidget {
final String? title;
final String? subTitle;
final String? setKey;
final bool? defaultVal;
const SetSwitchItem({
this.title,
this.subTitle,
this.setKey,
this.defaultVal,
Key? key,
}) : super(key: key);
@override
State<SetSwitchItem> createState() => _SetSwitchItemState();
}
class _SetSwitchItemState extends State<SetSwitchItem> {
// ignore: non_constant_identifier_names
Box Setting = GStrorage.setting;
late bool val;
@override
void initState() {
super.initState();
val = Setting.get(widget.setKey, defaultValue: widget.defaultVal ?? false);
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return ListTile(
enableFeedback: true,
onTap: () {
Setting.put(widget.setKey, !val);
},
title: Text(widget.title!, style: titleStyle),
subtitle: widget.subTitle != null
? Text(widget.subTitle!, style: subTitleStyle)
: null,
trailing: Transform.scale(
scale: 0.8,
child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
if (states.isNotEmpty && states.first == MaterialState.selected) {
return const Icon(Icons.done);
}
return null; // All other states will use the default thumbIcon.
}),
value: val,
onChanged: (value) {
val = value;
Setting.put(widget.setKey, value);
setState(() {});
}),
),
);
}
}

View File

@ -13,36 +13,47 @@ import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin {
int tabInitialIndex = 0;
TabController? tabCtr;
// tabs
RxList<String> tabs = <String>['简介', '评论'].obs;
// 视频aid
/// 路由传参
String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!);
// 视频类型 默认投稿视频
SearchType videoType = SearchType.video;
late PlayUrlModel data;
// 当前画质
late VideoQuality currentVideoQa;
// 当前音质
late AudioQuality currentAudioQa;
// 是否预渲染 骨架屏
bool preRender = false;
// 视频详情 上个页面传入
String heroTag = Get.arguments['heroTag'];
// 视频详情
Map videoItem = {};
// 视频类型 默认投稿视频
SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;
/// tabs相关配置
int tabInitialIndex = 0;
late TabController tabCtr;
RxList<String> tabs = <String>['简介', '评论'].obs;
// 请求返回的视频信息
late PlayUrlModel data;
// 请求状态
RxBool isLoading = false.obs;
String heroTag = '';
/// 播放器配置 画质 音质 解码格式
late VideoQuality currentVideoQa;
late AudioQuality currentAudioQa;
late VideoDecodeFormats currentDecodeFormats;
// PlPlayerController plPlayerController = PlPlayerController();
// 是否开始自动播放 存在多p的情况下第二p需要为true
RxBool autoPlay = true.obs;
// 视频资源是否有效
RxBool isEffective = true.obs;
// 封面图的展示
RxBool isShowCover = true.obs;
// 硬解
RxBool enableHA = true.obs;
/// 本地存储
Box user = GStrorage.user;
Box localCache = GStrorage.localCache;
Box setting = GStrorage.setting;
int oid = 0;
// 评论id 请求楼中楼评论使用
@ -52,15 +63,7 @@ class VideoDetailController extends GetxController
final scaffoldKey = GlobalKey<ScaffoldState>();
Timer? timer;
RxString bgCover = ''.obs;
Box user = GStrorage.user;
Box localCache = GStrorage.localCache;
PlPlayerController plPlayerController = PlPlayerController.getInstance();
// 是否开始自动播放 存在多p的情况下第二p需要为true
RxBool autoPlay = true.obs;
// 视频资源是否有效
RxBool isEffective = true.obs;
// 封面图的展示
RxBool isShowCover = true.obs;
late VideoItem firstVideo;
late String videoUrl;
@ -70,24 +73,23 @@ class VideoDetailController extends GetxController
@override
void onInit() {
super.onInit();
if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('videoItem')) {
preRender = true;
var args = Get.arguments['videoItem'];
Map argMap = Get.arguments;
var keys = argMap.keys.toList();
if (keys.isNotEmpty) {
if (keys.contains('videoItem')) {
var args = argMap['videoItem'];
if (args.pic != null && args.pic != '') {
videoItem['pic'] = args.pic;
bgCover.value = args.pic;
}
}
if (Get.arguments.containsKey('pic')) {
videoItem['pic'] = Get.arguments['pic'];
bgCover.value = Get.arguments['pic'];
if (keys.contains('pic')) {
videoItem['pic'] = argMap['pic'];
}
heroTag = Get.arguments['heroTag'];
videoType = Get.arguments['videoType'] ?? SearchType.video;
}
tabCtr = TabController(length: 2, vsync: this);
// queryVideoUrl();
autoPlay.value =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true);
}
showReplyReplyPanel() {
@ -120,8 +122,15 @@ class VideoDetailController extends GetxController
/// 暂不匹配解码规则
/// 根据currentVideoQa 重新设置videoUrl
firstVideo =
data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
// firstVideo =
// data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
// videoUrl = firstVideo.baseUrl!;
/// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
List<VideoItem> videoList =
data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList();
firstVideo = videoList
.firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code));
videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl
@ -133,6 +142,7 @@ class VideoDetailController extends GetxController
}
Future playerInit({video, audio, seekToTime, duration}) async {
print('data.timeLength:${data.timeLength}');
await plPlayerController.setDataSource(
DataSource(
videoSource: video ?? videoUrl,
@ -145,7 +155,7 @@ class VideoDetailController extends GetxController
},
),
// 硬解
enableHA: true,
enableHA: enableHA.value,
autoplay: autoPlay.value,
seekTo: seekToTime ?? defaultST,
duration: duration ?? Duration(milliseconds: data.timeLength ?? 0),
@ -170,14 +180,73 @@ class VideoDetailController extends GetxController
data = result['data'];
/// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量
firstVideo = data.dash!.video!.first;
videoUrl = firstVideo.baseUrl!;
//
currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
// firstVideo = data.dash!.video!.first;
// videoUrl = firstVideo.baseUrl!;
// //
// currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
// /// 优先顺序 设置中指定质量 -> 当前可选的最高质量
// AudioItem firstAudio =
// data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
// audioUrl = firstAudio.baseUrl ?? '';
List<VideoItem> allVideosList = data.dash!.video!;
try {
// 当前可播放的最高质量视频
int currentHighVideoQa = allVideosList.first.quality!.code;
//
int cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
defaultValue: currentHighVideoQa);
int resVideoQa = currentHighVideoQa;
if (cacheVideoQa <= currentHighVideoQa) {
List<int> numbers = data.acceptQuality!
.where((e) => e <= currentHighVideoQa)
.toList();
resVideoQa = Utils.findClosestNumber(cacheVideoQa, numbers);
}
currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!;
/// 取出符合当前画质的videoList
List<VideoItem> videosList =
allVideosList.where((e) => e.quality!.code == resVideoQa).toList();
/// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式
List<FormatItem> supportFormats = data.supportFormats!;
// 根据画质选编码格式
List supportDecodeFormats =
supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!;
try {
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
SettingBoxKey.defaultDecode,
defaultValue: supportDecodeFormats.first))!;
} catch (_) {}
/// 取出符合当前解码格式的videoItem
firstVideo = videosList
.firstWhere((e) => e.codecs!.startsWith(currentDecodeFormats.code));
videoUrl = firstVideo.baseUrl!;
} catch (_) {}
/// 优先顺序 设置中指定质量 -> 当前可选的最高质量
AudioItem firstAudio =
data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
late AudioItem firstAudio;
List audiosList = data.dash!.audio!;
try {
if (audiosList.isNotEmpty) {
firstAudio = audiosList.first;
int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: firstAudio.id);
// 选择最接近的那个音轨
firstAudio = audiosList.firstWhere(
(e) => e.id == resultAudioQa,
orElse: () => AudioItem(),
);
} else {
firstAudio = AudioItem();
}
} catch (_) {}
audioUrl = firstAudio.baseUrl ?? '';
//
if (firstAudio.id != null) {
@ -185,6 +254,13 @@ class VideoDetailController extends GetxController
}
defaultST = Duration(milliseconds: data.lastPlayTime!);
await playerInit();
// await playerInit(
// firstVideo,
// audioUrl,
// defaultST: Duration(milliseconds: data.lastPlayTime!),
// duration: data.timeLength ?? 0,
// );
} else {
SmartDialog.showToast(result['msg'].toString());
}

View File

@ -51,6 +51,8 @@ class VideoIntroController extends GetxController {
RxMap followStatus = {}.obs;
int _tempThemeValue = -1;
RxInt lastPlayCid = 0.obs;
@override
void onInit() {
super.onInit();
@ -76,6 +78,7 @@ class VideoIntroController extends GetxController {
}
}
userLogin = user.get(UserBoxKey.userLogin) != null;
lastPlayCid.value = int.parse(Get.parameters['cid']!);
}
// 获取视频简介&分p
@ -83,6 +86,9 @@ class VideoIntroController extends GetxController {
var result = await VideoHttp.videoIntro(bvid: bvid);
if (result['status']) {
videoDetail.value = result['data']!;
if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {
lastPlayCid.value = videoDetail.value.pages!.first.cid!;
}
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.tabs
.value = ['简介', '评论 ${result['data']!.stat!.reply}'];

View File

@ -1,4 +1,3 @@
import 'package:flutter/gestures.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
@ -20,6 +19,7 @@ import 'widgets/action_item.dart';
import 'widgets/action_row_item.dart';
import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart';
import 'widgets/page.dart';
import 'widgets/season.dart';
class VideoIntroPanel extends StatefulWidget {
@ -62,7 +62,6 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
// 请求成功
// return _buildView(context, false, videoDetail);
return Obx(
() => VideoInfo(
loadingStatus: false,
@ -95,22 +94,35 @@ class VideoInfo extends StatefulWidget {
}
class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Map videoItem = Get.put(VideoIntroController()).videoItem!;
final VideoIntroController videoIntroController =
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
bool isExpand = false;
final String heroTag = Get.arguments['heroTag'];
late final VideoIntroController videoIntroController;
late final VideoDetailController videoDetailCtr;
late final Map<dynamic, dynamic> videoItem;
late VideoDetailController? videoDetailCtr;
Box localCache = GStrorage.localCache;
late double sheetHeight;
late final bool loadingStatus; // 加载状态
late final dynamic owner;
late final dynamic follower;
late final dynamic followStatus;
@override
void initState() {
super.initState();
videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
videoItem = videoIntroController.videoItem!;
sheetHeight = localCache.get('sheetHeight');
loadingStatus = widget.loadingStatus;
owner = loadingStatus ? videoItem['owner'] : widget.videoDetail!.owner;
follower = loadingStatus
? '-'
: Utils.numFormat(videoIntroController.userStat['follower']);
followStatus = videoIntroController.followStatus;
}
// 收藏
@ -141,24 +153,39 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
);
}
// 用户主页
onPushMember() {
feedBack();
int mid = !loadingStatus
? widget.videoDetail!.owner!.mid
: videoItem['owner'].mid;
String face = !loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face;
Get.toNamed('/member?mid=$mid',
arguments: {'face': face, 'heroTag': (mid + 99).toString()});
}
@override
Widget build(BuildContext context) {
ThemeData t = Theme.of(context);
Color outline = t.colorScheme.outline;
return SliverPadding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10),
sliver: SliverToBoxAdapter(
child: !widget.loadingStatus || videoItem.isNotEmpty
child: !loadingStatus || videoItem.isNotEmpty
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Row(
children: [
Expanded(
child: Text(
!widget.loadingStatus
!loadingStatus
? widget.videoDetail!.title
: videoItem['title'],
style: const TextStyle(
@ -182,14 +209,18 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
return t.highlightColor.withOpacity(0.2);
}),
),
onPressed: () => showIntroDetail(),
icon: const Icon(Icons.more_horiz),
onPressed: showIntroDetail,
icon: Icon(
Icons.more_horiz,
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
),
InkWell(
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Row(
children: [
@ -237,7 +268,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// 点赞收藏转发 布局样式2
// actionGrid(context, videoIntroController),
// 合集
if (!widget.loadingStatus &&
if (!loadingStatus &&
widget.videoDetail!.ugcSeason != null) ...[
SeasonPanel(
ugcSeason: widget.videoDetail!.ugcSeason!,
@ -247,97 +278,86 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
.changeSeasonOrbangu(bvid, cid, aid),
)
],
if (!loadingStatus &&
widget.videoDetail!.pages != null &&
widget.videoDetail!.pages!.length > 1) ...[
Obx(() => PagesPanel(
pages: widget.videoDetail!.pages!,
cid: videoIntroController.lastPlayCid.value,
sheetHeight: sheetHeight,
changeFuc: (cid) =>
videoIntroController.changeSeasonOrbangu(
videoIntroController.bvid, cid, null),
))
],
GestureDetector(
onTap: () {
feedBack();
int mid = !widget.loadingStatus
? widget.videoDetail!.owner!.mid
: videoItem['owner'].mid;
String face = !widget.loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face;
Get.toNamed('/member?mid=$mid', arguments: {
'face': face,
'heroTag': (mid + 99).toString()
});
},
child: Padding(
padding: const EdgeInsets.only(
top: 12, bottom: 12, left: 4, right: 4),
onTap: onPushMember,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 4),
child: Row(
children: [
NetworkImgLayer(
type: 'avatar',
src: !widget.loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face,
src: loadingStatus
? owner.face
: widget.videoDetail!.owner!.face,
width: 34,
height: 34,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 10),
Text(
!widget.loadingStatus
? widget.videoDetail!.owner!.name
: videoItem['owner'].name,
style: const TextStyle(fontSize: 13),
),
Text(owner.name,
style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
widget.loadingStatus
? '-'
: Utils.numFormat(
videoIntroController.userStat['follower']),
follower,
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: t.colorScheme.outline),
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
),
),
const Spacer(),
AnimatedOpacity(
opacity: widget.loadingStatus ? 0 : 1,
opacity: loadingStatus ? 0 : 1,
duration: const Duration(milliseconds: 150),
child: SizedBox(
height: 32,
child: Obx(
() => videoIntroController
.followStatus.isNotEmpty
? TextButton(
onPressed: () => videoIntroController
.actionRelationMod(),
style: TextButton.styleFrom(
padding: const EdgeInsets.only(
left: 8, right: 8),
foregroundColor:
videoIntroController.followStatus[
'attribute'] !=
0
? t.colorScheme.outline
: t.colorScheme.onPrimary,
backgroundColor: videoIntroController
.followStatus[
'attribute'] !=
0
? t.colorScheme.onInverseSurface
: t.colorScheme
.primary, // 设置按钮背景色
),
child: Text(
videoIntroController.followStatus[
'attribute'] !=
0
? '已关注'
: '关注',
style: TextStyle(
fontSize: t.textTheme.labelMedium!
.fontSize),
),
)
: ElevatedButton(
onPressed: () => videoIntroController
.actionRelationMod(),
child: const Text('关注'),
),
() =>
videoIntroController.followStatus.isNotEmpty
? TextButton(
onPressed: videoIntroController
.actionRelationMod,
style: TextButton.styleFrom(
padding: const EdgeInsets.only(
left: 8, right: 8),
foregroundColor:
followStatus['attribute'] != 0
? outline
: t.colorScheme.onPrimary,
backgroundColor:
followStatus['attribute'] != 0
? t.colorScheme
.onInverseSurface
: t.colorScheme
.primary, // 设置按钮背景色
),
child: Text(
followStatus['attribute'] != 0
? '已关注'
: '关注',
style: TextStyle(
fontSize: t.textTheme
.labelMedium!.fontSize),
),
)
: ElevatedButton(
onPressed: videoIntroController
.actionRelationMod,
child: const Text('关注'),
),
),
),
),
@ -359,66 +379,64 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Widget actionGrid(BuildContext context, videoIntroController) {
return LayoutBuilder(builder: (context, constraints) {
return Padding(
return Container(
padding: const EdgeInsets.only(top: 6, bottom: 10),
child: SizedBox(
height: constraints.maxWidth / 5 * 0.8,
child: GridView.count(
primary: false,
padding: const EdgeInsets.all(0),
crossAxisCount: 5,
childAspectRatio: 1.25,
children: <Widget>[
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-'),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.clock),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: '稍后再看'),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-'),
),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
// onTap: () => videoIntroController.actionFavVideo(),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-'),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.share!.toString()
height: constraints.maxWidth / 5 * 0.8,
child: GridView.count(
primary: false,
padding: const EdgeInsets.all(0),
crossAxisCount: 5,
childAspectRatio: 1.25,
children: <Widget>[
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-'),
],
),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.clock),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: loadingStatus,
text: '稍后再看'),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-'),
),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
// onTap: () => videoIntroController.actionFavVideo(),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-'),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.share!.toString()
: '-'),
],
),
);
});
@ -431,10 +449,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-',
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-',
),
),
const SizedBox(width: 8),
@ -443,10 +460,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-',
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-',
),
),
const SizedBox(width: 8),
@ -455,8 +471,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-',
),
@ -468,57 +484,20 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
videoDetailCtr.tabCtr.animateTo(1);
},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.reply!.toString()
: '-',
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-',
),
const SizedBox(width: 8),
ActionRowItem(
icon: const Icon(FontAwesomeIcons.share),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
// text: !widget.loadingStatus
loadingStatus: loadingStatus,
// text: !loadingStatus
// ? widget.videoDetail!.stat!.share!.toString()
// : '-',
text: '转发'),
]);
}
InlineSpan buildContent(BuildContext context, content) {
String desc = content.desc;
List descV2 = content.descV2;
// type
// 1 普通文本
// 2 @用户
List<InlineSpan> spanChilds = [];
if (descV2.isNotEmpty) {
for (var i = 0; i < descV2.length; i++) {
if (descV2[i].type == 1) {
spanChilds.add(TextSpan(text: descV2[i].rawText));
} else if (descV2[i].type == 2) {
spanChilds.add(
TextSpan(
text: '@${descV2[i].rawText}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
String heroTag = Utils.makeHeroTag(descV2[i].bizId);
Get.toNamed(
'/member?mid=${descV2[i].bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
),
);
}
}
} else {
spanChilds.add(TextSpan(text: desc));
}
return TextSpan(children: spanChilds);
}
}

View File

@ -33,24 +33,13 @@ class _FavPanelState extends State<FavPanel> {
child: Column(
children: [
AppBar(
toolbarHeight: 50,
automaticallyImplyLeading: false,
centerTitle: false,
elevation: 1,
title: Text(
'选择文件夹',
style: Theme.of(context).textTheme.titleMedium,
),
actions: [
TextButton(
onPressed: () async {
feedBack();
await widget.ctr!.actionFavVideo();
},
child: const Text('完成'),
),
const SizedBox(width: 6),
],
elevation: 0,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close_outlined)),
title:
Text('添加到收藏夹', style: Theme.of(context).textTheme.titleMedium),
),
Expanded(
child: Material(
@ -63,45 +52,33 @@ class _FavPanelState extends State<FavPanel> {
return Obx(
() => ListView.builder(
itemCount:
widget.ctr!.favFolderData.value.list!.length + 1,
widget.ctr!.favFolderData.value.list!.length,
itemBuilder: (context, index) {
if (index == 0) {
return const SizedBox(height: 10);
} else {
return ListTile(
onTap: () => widget.ctr!.onChoose(
widget.ctr!.favFolderData.value
.list![index - 1].favState !=
1,
index - 1),
dense: true,
leading:
const Icon(Icons.folder_special_outlined),
minLeadingWidth: 0,
title: Text(widget.ctr!.favFolderData.value
.list![index - 1].title!),
subtitle: Text(
'${widget.ctr!.favFolderData.value.list![index - 1].mediaCount}个内容',
style: TextStyle(
color:
Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize),
return ListTile(
onTap: () => widget.ctr!.onChoose(
widget.ctr!.favFolderData.value.list![index]
.favState !=
1,
index),
dense: true,
leading: const Icon(Icons.folder_outlined),
minLeadingWidth: 0,
title: Text(widget.ctr!.favFolderData.value
.list![index].title!),
subtitle: Text(
'${widget.ctr!.favFolderData.value.list![index].mediaCount}个内容',
),
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: widget.ctr!.favFolderData.value
.list![index].favState ==
1,
onChanged: (bool? checkValue) =>
widget.ctr!.onChoose(checkValue!, index),
),
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: widget.ctr!.favFolderData.value
.list![index - 1].favState ==
1,
onChanged: (bool? checkValue) => widget.ctr!
.onChoose(checkValue!, index - 1),
),
),
);
}
),
);
},
),
);
@ -119,6 +96,46 @@ class _FavPanelState extends State<FavPanel> {
),
),
),
Divider(
height: 1,
color: Theme.of(context).disabledColor.withOpacity(0.08),
),
Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 12,
bottom: MediaQuery.of(context).padding.bottom),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 30, right: 30),
backgroundColor: Theme.of(context)
.colorScheme
.onInverseSurface, // 设置按钮背景色
),
child: const Text('取消'),
),
const SizedBox(width: 10),
TextButton(
onPressed: () async {
feedBack();
await widget.ctr!.actionFavVideo();
},
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 30, right: 30),
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor:
Theme.of(context).colorScheme.primary, // 设置按钮背景色
),
child: const Text('完成'),
),
],
),
),
],
),
);

View File

@ -27,19 +27,20 @@ class IntroDetail extends StatelessWidget {
height: sheetHeight,
child: Column(
children: [
Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer
.withOpacity(0.5),
borderRadius: const BorderRadius.all(Radius.circular(3))),
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius:
const BorderRadius.all(Radius.circular(3))),
),
),
),
),
@ -125,33 +126,29 @@ class IntroDetail extends StatelessWidget {
// type
// 1 普通文本
// 2 @用户
List<InlineSpan> spanChilds = [];
if (descV2.isNotEmpty) {
for (var i = 0; i < descV2.length; i++) {
if (descV2[i].type == 1) {
spanChilds.add(TextSpan(text: descV2[i].rawText));
} else if (descV2[i].type == 2) {
spanChilds.add(
TextSpan(
text: '@${descV2[i].rawText}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
String heroTag = Utils.makeHeroTag(descV2[i].bizId);
Get.toNamed(
'/member?mid=${descV2[i].bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
),
List<TextSpan> spanChilds = List.generate(descV2.length, (index) {
final currentDesc = descV2[index];
switch (currentDesc.type) {
case 1:
return TextSpan(text: currentDesc.rawText);
case 2:
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
return TextSpan(
text: '@${currentDesc.rawText}',
style: TextStyle(color: colorSchemePrimary),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/member?mid=${currentDesc.bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
);
}
default:
return TextSpan();
}
} else {
spanChilds.add(TextSpan(text: desc));
}
});
return TextSpan(children: spanChilds);
}
}

View File

@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart';
class PagesPanel extends StatefulWidget {
final List<Part> pages;
final int? cid;
final double? sheetHeight;
final Function? changeFuc;
const PagesPanel({
super.key,
required this.pages,
this.cid,
this.sheetHeight,
this.changeFuc,
});
@override
State<PagesPanel> createState() => _PagesPanelState();
}
class _PagesPanelState extends State<PagesPanel> {
late List<Part> episodes;
late int currentIndex;
@override
void initState() {
super.initState();
episodes = widget.pages;
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
}
void changeFucCall(item, i) async {
await widget.changeFuc!(
item.cid,
);
currentIndex = i;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 10, bottom: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('视频选集 '),
Expanded(
child: Text(
' 正在播放:${widget.pages[currentIndex].pagePart}',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
),
const SizedBox(width: 10),
SizedBox(
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showBottomSheet(
context: context,
builder: (_) => Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
Container(
height: 45,
padding:
const EdgeInsets.only(left: 14, right: 14),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'合集(${episodes.length}',
style:
Theme.of(context).textTheme.titleMedium,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
Divider(
height: 1,
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
Expanded(
child: Material(
child: ListView.builder(
itemCount: episodes.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
changeFucCall(episodes[index], index);
Get.back();
},
child: Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
left: 15,
right: 15),
child: Text(
episodes[index].pagePart!,
style: TextStyle(
color: index == currentIndex
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.onSurface),
),
),
);
},
),
),
),
],
),
),
);
},
child: Text(
'${widget.pages.length}',
style: const TextStyle(fontSize: 13),
),
),
),
],
),
),
Container(
height: 35,
margin: const EdgeInsets.only(bottom: 8),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: widget.pages.length,
itemExtent: 150,
itemBuilder: ((context, i) {
return Container(
width: 150,
margin: const EdgeInsets.only(right: 10),
child: Material(
color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => changeFucCall(widget.pages[i], i),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 8),
child: Row(
children: [
if (i == currentIndex) ...[
Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
height: 12,
),
const SizedBox(width: 6)
],
Expanded(
child: Text(
widget.pages[i].pagePart!,
maxLines: 1,
style: TextStyle(
fontSize: 13,
color: i == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
overflow: TextOverflow.ellipsis,
))
],
),
),
),
),
);
}),
),
)
],
);
}
}

View File

@ -391,6 +391,7 @@ class ReplyItemRow extends StatelessWidget {
),
if (replies![i].isUp)
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: UpTag(),
),
buildContent(

View File

@ -118,7 +118,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
@override
Widget build(BuildContext context) {
return Container(
height: 400,
height: 500,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(

View File

@ -1,11 +1,13 @@
import 'dart:async';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/sliver_header.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/bangumi/introduction/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
@ -163,8 +165,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
scrolledUnderElevation: 0,
forceElevated: innerBoxIsScrolled,
expandedHeight: videoHeight,
// backgroundColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
backgroundColor:
MediaQuery.of(Get.context!).platformBrightness ==
Brightness.dark
? Colors.black
: Theme.of(context).colorScheme.background,
flexibleSpace: FlexibleSpaceBar(
background: Padding(
padding: EdgeInsets.only(top: statusBarHeight),
@ -233,10 +238,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
backgroundColor:
Colors.transparent,
actions: [
/// TODO
IconButton(
tooltip: '稍后再看',
onPressed: () {},
onPressed: () async {
var res = await UserHttp
.toViewLater(
bvid:
videoDetailController
.bvid);
SmartDialog.showToast(
res['msg']);
},
icon: const Icon(Icons
.history_outlined))
],
@ -291,39 +303,20 @@ class _VideoDetailPageState extends State<VideoDetailPage>
children: [
Opacity(
opacity: 0,
child: Container(
child: SizedBox(
width: double.infinity,
height: 0,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
child: Obx(
() => TabBar(
controller: videoDetailController.tabCtr,
dividerColor: Colors.transparent,
indicatorColor:
Theme.of(context).colorScheme.background,
tabs: videoDetailController.tabs
.map((String name) => Tab(text: name))
.toList(),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Container(
width: 280,
margin: const EdgeInsets.only(left: 20),
child: Obx(
() => TabBar(
controller: videoDetailController.tabCtr,
dividerColor: Colors.transparent,
indicatorColor:
Theme.of(context).colorScheme.background,
tabs: videoDetailController.tabs
.map((String name) => Tab(text: name))
.toList(),
),
),
),
],
),
),
),
Expanded(

View File

@ -28,6 +28,7 @@ class _HeaderControlState extends State<HeaderControl> {
late PlayUrlModel videoInfo;
List<PlaySpeed> playSpeed = PlaySpeed.values;
TextStyle subTitleStyle = const TextStyle(fontSize: 12);
TextStyle titleStyle = const TextStyle(fontSize: 14);
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@override
@ -81,7 +82,7 @@ class _HeaderControlState extends State<HeaderControl> {
enabled: false,
leading:
const Icon(Icons.network_cell_outlined, size: 20),
title: const Text('省流模式'),
title: Text('省流模式', style: titleStyle),
subtitle: Text('低画质 减少视频缓存', style: subTitleStyle),
trailing: Transform.scale(
scale: 0.75,
@ -99,22 +100,22 @@ class _HeaderControlState extends State<HeaderControl> {
),
),
),
Obx(
() => ListTile(
onTap: () => {Get.back(), showSetSpeedSheet()},
dense: true,
leading: const Icon(Icons.speed_outlined, size: 20),
title: const Text('播放速度'),
subtitle: Text(
'当前倍速 x${widget.controller!.playbackSpeed}',
style: subTitleStyle),
),
),
// Obx(
// () => ListTile(
// onTap: () => {Get.back(), showSetSpeedSheet()},
// dense: true,
// leading: const Icon(Icons.speed_outlined, size: 20),
// title: Text('播放速度', style: titleStyle),
// subtitle: Text(
// '当前倍速 x${widget.controller!.playbackSpeed}',
// style: subTitleStyle),
// ),
// ),
ListTile(
onTap: () => {Get.back(), showSetVideoQa()},
dense: true,
leading: const Icon(Icons.play_circle_outline, size: 20),
title: const Text('选择画质'),
title: Text('选择画质', style: titleStyle),
subtitle: Text(
'当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}',
style: subTitleStyle),
@ -123,24 +124,33 @@ class _HeaderControlState extends State<HeaderControl> {
onTap: () => {Get.back(), showSetAudioQa()},
dense: true,
leading: const Icon(Icons.album_outlined, size: 20),
title: const Text('选择音质'),
title: Text('选择音质', style: titleStyle),
subtitle: Text(
'当前音质 ${widget.videoDetailCtr!.currentAudioQa.description}',
style: subTitleStyle),
),
ListTile(
onTap: () {},
onTap: () => {Get.back(), showSetDecodeFormats()},
dense: true,
enabled: false,
leading: const Icon(Icons.play_circle_outline, size: 20),
title: const Text('播放设置'),
leading: const Icon(Icons.av_timer_outlined, size: 20),
title: Text('解码格式', style: titleStyle),
subtitle: Text(
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
style: subTitleStyle),
),
// ListTile(
// onTap: () {},
// dense: true,
// enabled: false,
// leading: const Icon(Icons.play_circle_outline, size: 20),
// title: Text('播放设置', style: titleStyle),
// ),
ListTile(
onTap: () {},
dense: true,
enabled: false,
leading: const Icon(Icons.subtitles_outlined, size: 20),
title: const Text('弹幕设置'),
title: Text('弹幕设置', style: titleStyle),
),
],
),
@ -250,7 +260,7 @@ class _HeaderControlState extends State<HeaderControl> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('选择画质'),
Text('选择画质', style: titleStyle),
const SizedBox(width: 4),
Icon(
Icons.info_outline,
@ -329,7 +339,9 @@ class _HeaderControlState extends State<HeaderControl> {
margin: const EdgeInsets.all(12),
child: Column(
children: [
const SizedBox(height: 45, child: Center(child: Text('选择音质'))),
SizedBox(
height: 45,
child: Center(child: Text('选择音质', style: titleStyle))),
Expanded(
child: Material(
child: ListView(
@ -370,6 +382,74 @@ class _HeaderControlState extends State<HeaderControl> {
);
}
// 选择解码格式
void showSetDecodeFormats() {
// 当前选中的解码格式
VideoDecodeFormats currentDecodeFormats =
widget.videoDetailCtr!.currentDecodeFormats;
// 当前视频可用的解码格式
List<FormatItem> videoFormat = videoInfo.supportFormats!;
List list = videoFormat.first.codecs!;
showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 250,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: const EdgeInsets.all(12),
child: Column(
children: [
SizedBox(
height: 45,
child: Center(child: Text('选择解码格式', style: titleStyle))),
Expanded(
child: Material(
child: ListView(
children: [
for (var i in list) ...[
ListTile(
onTap: () {
widget.videoDetailCtr!.currentDecodeFormats =
VideoDecodeFormatsCode.fromString(i)!;
widget.videoDetailCtr!.updatePlayer();
Get.back();
},
dense: true,
contentPadding:
const EdgeInsets.only(left: 20, right: 20),
title: Text(VideoDecodeFormatsCode.fromString(i)!
.description!),
subtitle: Text(
i!,
style: subTitleStyle,
),
trailing: i.startsWith(currentDecodeFormats.code)
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: const SizedBox(),
),
]
],
),
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final _ = widget.controller!;

View File

@ -7,6 +7,7 @@ import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/rcmd/controller.dart';
import 'package:pilipala/utils/cookie.dart';
@ -71,17 +72,24 @@ class WebviewController extends GetxController {
print('网页登录: $result');
if (result['status'] && result['data'].isLogin) {
SmartDialog.showToast('登录成功');
Box user = GStrorage.user;
user.put(UserBoxKey.userLogin, true);
user.put(UserBoxKey.userName, result['data'].uname);
user.put(UserBoxKey.userFace, result['data'].face);
user.put(UserBoxKey.userMid, result['data'].mid);
Box userInfoCache = GStrorage.userInfo;
userInfoCache.put('userInfoCache', result['data']);
Get.find<MineController>().userInfo.value = result['data'];
Get.find<MineController>().onInit();
Get.find<RcmdController>().queryRcmdFeed('onRefresh');
Get.find<DynamicsController>().queryFollowDynamic();
try {
Box user = GStrorage.user;
user.put(UserBoxKey.userLogin, true);
user.put(UserBoxKey.userName, result['data'].uname);
user.put(UserBoxKey.userFace, result['data'].face);
user.put(UserBoxKey.userMid, result['data'].mid);
Box userInfoCache = GStrorage.userInfo;
userInfoCache.put('userInfoCache', result['data']);
Get.find<MineController>().userInfo.value = result['data'];
Get.find<MineController>().onInit();
Get.find<RcmdController>().queryRcmdFeed('onRefresh');
Get.find<DynamicsController>().queryFollowDynamic();
HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(true);
} catch (_) {}
Get.back();
}
} catch (e) {