Compare commits

..

1 Commits

Author SHA1 Message Date
f41bb02bae fix: 合集最后一p不显示 issues #620 2024-03-08 23:05:09 +08:00
33 changed files with 480 additions and 1197 deletions

View File

@ -38,10 +38,6 @@ class VideoCardH extends StatelessWidget {
Widget build(BuildContext context) {
final int aid = videoItem.aid;
final String bvid = videoItem.bvid;
String type = 'video';
try {
type = videoItem.type;
} catch (_) {}
final String heroTag = Utils.makeHeroTag(aid);
return GestureDetector(
onLongPress: () {
@ -57,10 +53,6 @@ class VideoCardH extends StatelessWidget {
child: InkWell(
onTap: () async {
try {
if (type == 'ketang') {
SmartDialog.showToast('课堂视频暂不支持播放');
return;
}
final int cid =
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
@ -103,20 +95,12 @@ class VideoCardH extends StatelessWidget {
height: maxHeight,
),
),
if (videoItem.duration != 0)
PBadge(
text: Utils.timeFormat(videoItem.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
if (type != 'video')
PBadge(
text: type,
left: 6.0,
bottom: 6.0,
type: 'primary',
),
// if (videoItem.rcmdReason != null &&
// videoItem.rcmdReason.content != '')
// pBadge(videoItem.rcmdReason.content, context,

View File

@ -499,8 +499,4 @@ class Api {
/// 发送私信
static const String sendMsg = '${HttpString.tUrl}/web_im/v1/web_im/send_msg';
/// 排行榜
static const String getRankApi = "/x/web-interface/ranking/v2";
}

View File

@ -475,27 +475,4 @@ class VideoHttp {
return {'status': false, 'data': []};
}
}
// 视频排行
static Future getRankVideoList(int rid) async {
try {
var rankApi = "${Api.getRankApi}?rid=$rid&type=all";
var res = await Request().get(rankApi);
if (res.data['code'] == 0) {
List<HotVideoItemModel> list = [];
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['list']) {
if (!blackMidsList.contains(i['owner']['mid'])) {
list.add(HotVideoItemModel.fromJson(i));
}
}
return {'status': true, 'data': list};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
} catch (err) {
return {'status': false, 'data': [], 'msg': err};
}
}
}

View File

@ -7,5 +7,5 @@ enum DynamicsType {
extension BusinessTypeExtension on DynamicsType {
String get values => ['all', 'video', 'pgc', 'article'][index];
String get labels => ['全部', '投稿', '', '专栏'][index];
String get labels => ['全部', '视频', '', '专栏'][index];
}

View File

@ -16,19 +16,6 @@ List defaultNavigationBars = [
},
{
'id': 1,
'icon': const Icon(
Icons.trending_up,
size: 21,
),
'selectIcon': const Icon(
Icons.trending_up_outlined,
size: 21,
),
'label': "排行榜",
'count': 0,
},
{
'id': 2,
'icon': const Icon(
Icons.motion_photos_on_outlined,
size: 21,
@ -41,7 +28,7 @@ List defaultNavigationBars = [
'count': 0,
},
{
'id': 3,
'id': 2,
'icon': const Icon(
Icons.video_collection_outlined,
size: 20,

View File

@ -1,240 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/rank/zone/index.dart';
enum RandType {
all,
creation,
animation,
music,
dance,
game,
knowledge,
technology,
sport,
car,
life,
food,
animal,
madness,
fashion,
entertainment,
film,
origin,
rookie
}
extension RankTypeDesc on RandType {
String get description => [
'全站',
'国创相关',
'动画',
'音乐',
'舞蹈',
'游戏',
'知识',
'科技',
'运动',
'汽车',
'生活',
'美食',
'动物圈',
'鬼畜',
'时尚',
'娱乐',
'影视'
][index];
String get id => [
'all',
'creation',
'animation',
'music',
'dance',
'game',
'knowledge',
'technology',
'sport',
'car',
'life',
'food',
'animal',
'madness',
'fashion',
'entertainment',
'film'
][index];
}
List tabsConfig = [
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '全站',
'type': RandType.all,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 0),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '国创相关',
'type': RandType.creation,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 168),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '动画',
'type': RandType.animation,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 1),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '音乐',
'type': RandType.music,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 3),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '舞蹈',
'type': RandType.dance,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 129),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '游戏',
'type': RandType.game,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 4),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '知识',
'type': RandType.knowledge,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 36),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '科技',
'type': RandType.technology,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 188),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '运动',
'type': RandType.sport,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 234),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '汽车',
'type': RandType.car,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 223),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '生活',
'type': RandType.life,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 160),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '美食',
'type': RandType.food,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 211),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '动物圈',
'type': RandType.animal,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 217),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '鬼畜',
'type': RandType.madness,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 119),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '时尚',
'type': RandType.fashion,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 155),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '娱乐',
'type': RandType.entertainment,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 5),
},
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '影视',
'type': RandType.film,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 181),
}
];

View File

@ -85,9 +85,7 @@ class SearchVideoItemModel {
// title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']);
description = json['description'];
pic = json['pic'] != null && json['pic'].startsWith('//')
? 'https:${json['pic']}'
: json['pic'] ?? '';
pic = 'https:${json['pic']}';
videoReview = json['video_review'];
pubdate = json['pubdate'];
senddate = json['senddate'];

View File

@ -201,6 +201,7 @@ class _AboutPageState extends State<AboutPage> {
var cleanStatus = await CacheManage().clearCacheAll();
if (cleanStatus) {
getCacheSize();
SmartDialog.showToast('清除成功');
}
},
title: const Text('清除缓存'),
@ -253,16 +254,12 @@ class AboutController extends GetxController {
// 获取远程版本
Future getRemoteApp() async {
var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'});
isLoading.value = false;
if (result.data == null || result.data.isEmpty) {
SmartDialog.showToast('获取远程版本失败,请检查网络');
return;
}
data = LatestDataModel.fromJson(result.data);
remoteAppInfo = data;
remoteVersion.value = data.tagName!;
isUpdate.value =
Utils.needUpdate(currentVersion.value, remoteVersion.value);
isLoading.value = false;
}
// 跳转下载/本地更新
@ -280,7 +277,7 @@ class AboutController extends GetxController {
githubRelease() {
launchUrl(
Uri.parse('https://github.com/guozhigq/pilipala/releases'),
Uri.parse('https://github.com/guozhigq/pilipala/release'),
mode: LaunchMode.externalApplication,
);
}

View File

@ -144,9 +144,9 @@ class _BangumiPanelState extends State<BangumiPanel> {
Expanded(
child: Material(
child: ScrollablePositionedList.builder(
itemCount: widget.pages.length,
itemCount: widget.pages.length + 1,
itemBuilder: (BuildContext context, int index) {
bool isLastItem = index == widget.pages.length - 1;
bool isLastItem = index == widget.pages.length;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(

View File

@ -16,7 +16,7 @@ class FavDetailController extends GetxController {
RxMap favInfo = {}.obs;
RxList favList = [].obs;
RxString loadingText = '加载中...'.obs;
RxInt mediaCount = 0.obs;
int mediaCount = 0;
@override
void onInit() {
@ -29,7 +29,7 @@ class FavDetailController extends GetxController {
}
Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async {
if (type == 'onLoad' && favList.length >= mediaCount.value) {
if (type == 'onLoad' && favList.length >= mediaCount) {
loadingText.value = '没有更多了';
return;
}
@ -43,11 +43,11 @@ class FavDetailController extends GetxController {
favInfo.value = res['data'].info;
if (currentPage == 1 && type == 'init') {
favList.value = res['data'].medias;
mediaCount.value = res['data'].info['media_count'];
mediaCount = res['data'].info['media_count'];
} else if (type == 'onLoad') {
favList.addAll(res['data'].medias);
}
if (favList.length >= mediaCount.value) {
if (favList.length >= mediaCount) {
loadingText.value = '没有更多了';
}
}

View File

@ -84,7 +84,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
style: Theme.of(context).textTheme.titleMedium,
),
Text(
'${_favDetailController.mediaCount}条视频',
'${_favDetailController.item!.mediaCount!}条视频',
style: Theme.of(context).textTheme.labelMedium,
)
],
@ -175,7 +175,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx(
() => Text(
'${_favDetailController.mediaCount}条视频',
'${_favDetailController.favList.length}条视频',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,

View File

@ -29,6 +29,7 @@ class BottomControl extends StatefulWidget implements PreferredSizeWidget {
class _BottomControlState extends State<BottomControl> {
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);

View File

@ -9,7 +9,6 @@ import 'package:pilipala/http/common.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/rank/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
import '../../models/common/dynamic_badge_mode.dart';
@ -18,7 +17,6 @@ import '../../models/common/nav_bar_config.dart';
class MainController extends GetxController {
List<Widget> pages = <Widget>[
const HomePage(),
const RankPage(),
const DynamicsPage(),
const MediaPage(),
];

View File

@ -7,7 +7,6 @@ import 'package:pilipala/models/common/dynamic_badge_mode.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/rank/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
@ -23,7 +22,6 @@ class MainApp extends StatefulWidget {
class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
final MainController _mainController = Get.put(MainController());
final HomeController _homeController = Get.put(HomeController());
final RankController _rankController = Get.put(RankController());
final DynamicsController _dynamicController = Get.put(DynamicsController());
final MediaController _mediaController = Get.put(MediaController());
@ -59,21 +57,6 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
_homeController.flag = false;
}
if (currentPage is RankPage) {
if (_rankController.flag) {
// 单击返回顶部 双击并刷新
if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {
_rankController.onRefresh();
} else {
_rankController.animateToTop();
}
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
}
_rankController.flag = true;
} else {
_rankController.flag = false;
}
if (currentPage is DynamicsPage) {
if (_dynamicController.flag) {
// 单击返回顶部 双击并刷新

View File

@ -1,70 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/rank_type.dart';
import 'package:pilipala/utils/storage.dart';
class RankController extends GetxController with GetTickerProviderStateMixin {
bool flag = false;
late RxList tabs = [].obs;
RxInt initialIndex = 1.obs;
late TabController tabController;
late List tabsCtrList;
late List<Widget> tabsPageList;
Box setting = GStrorage.setting;
late final StreamController<bool> searchBarStream =
StreamController<bool>.broadcast();
late bool enableGradientBg;
@override
void onInit() {
super.onInit();
enableGradientBg =
setting.get(SettingBoxKey.enableGradientBg, defaultValue: true);
// 进行tabs配置
setTabConfig();
}
void onRefresh() {
int index = tabController.index;
var ctr = tabsCtrList[index];
ctr().onRefresh();
}
void animateToTop() {
int index = tabController.index;
var ctr = tabsCtrList[index];
ctr().animateToTop();
}
void setTabConfig() async {
tabs.value = tabsConfig;
initialIndex.value = 0;
tabsCtrList = tabs.map((e) => e['ctr']).toList();
tabsPageList = tabs.map<Widget>((e) => e['page']).toList();
tabController = TabController(
initialIndex: initialIndex.value,
length: tabs.length,
vsync: this,
);
// 监听 tabController 切换
if (enableGradientBg) {
tabController.animation!.addListener(() {
if (tabController.indexIsChanging) {
if (initialIndex.value != tabController.index) {
initialIndex.value = tabController.index;
}
} else {
final int temp = tabController.animation!.value.round();
if (initialIndex.value != temp) {
initialIndex.value = temp;
tabController.index = initialIndex.value;
}
}
});
}
}
}

View File

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

View File

@ -1,149 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:pilipala/utils/feed_back.dart';
import './controller.dart';
class RankPage extends StatefulWidget {
const RankPage({Key? key}) : super(key: key);
@override
State<RankPage> createState() => _RankPageState();
}
class _RankPageState extends State<RankPage>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
final RankController _rankController = Get.put(RankController());
List videoList = [];
late Stream<bool> stream;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
stream = _rankController.searchBarStream.stream;
}
@override
Widget build(BuildContext context) {
super.build(context);
Brightness currentBrightness = MediaQuery.of(context).platformBrightness;
// 设置状态栏图标的亮度
if (_rankController.enableGradientBg) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarIconBrightness: currentBrightness == Brightness.light
? Brightness.dark
: Brightness.light,
));
}
return Scaffold(
extendBody: true,
extendBodyBehindAppBar: false,
appBar: _rankController.enableGradientBg
? null
: AppBar(toolbarHeight: 0, elevation: 0),
body: Stack(
children: [
// gradient background
if (_rankController.enableGradientBg) ...[
Align(
alignment: Alignment.topLeft,
child: Opacity(
opacity: 0.6,
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context)
.colorScheme
.primary
.withOpacity(0.9),
Theme.of(context)
.colorScheme
.primary
.withOpacity(0.5),
Theme.of(context).colorScheme.surface
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: const [0, 0.0034, 0.34]),
),
),
),
),
],
Column(
children: [
const CustomAppBar(),
if (_rankController.tabs.length > 1) ...[
const SizedBox(height: 4),
SizedBox(
width: double.infinity,
height: 42,
child: Align(
alignment: Alignment.center,
child: TabBar(
controller: _rankController.tabController,
tabs: [
for (var i in _rankController.tabs)
Tab(text: i['label'])
],
isScrollable: true,
dividerColor: Colors.transparent,
enableFeedback: true,
splashBorderRadius: BorderRadius.circular(10),
tabAlignment: TabAlignment.center,
onTap: (value) {
feedBack();
if (_rankController.initialIndex.value == value) {
_rankController.tabsCtrList[value]().animateToTop();
}
_rankController.initialIndex.value = value;
},
),
),
),
] else ...[
const SizedBox(height: 6),
],
Expanded(
child: TabBarView(
controller: _rankController.tabController,
children: _rankController.tabsPageList,
),
),
],
),
],
),
);
}
}
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final double height;
const CustomAppBar({
super.key,
this.height = kToolbarHeight,
});
@override
Size get preferredSize => Size.fromHeight(height);
@override
Widget build(BuildContext context) {
final double top = MediaQuery.of(context).padding.top;
return Container(
width: MediaQuery.of(context).size.width,
height: top,
color: Colors.transparent,
);
}
}

View File

@ -1,53 +0,0 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/model_hot_video_item.dart';
class ZoneController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<HotVideoItemModel> videoList = <HotVideoItemModel>[].obs;
bool isLoadingMore = false;
bool flag = false;
OverlayEntry? popupDialog;
int zoneID = 0;
// 获取推荐
Future queryRankFeed(type, rid) async {
zoneID = rid;
var res = await VideoHttp.getRankVideoList(zoneID);
if (res['status']) {
if (type == 'init') {
videoList.value = res['data'];
} else if (type == 'onRefresh') {
videoList.clear();
videoList.addAll(res['data']);
} else if (type == 'onLoad') {
videoList.clear();
videoList.addAll(res['data']);
}
}
isLoadingMore = false;
return res;
}
// 下拉刷新
Future onRefresh() async {
queryRankFeed('onRefresh', zoneID);
}
// 上拉加载
Future onLoad() async {
queryRankFeed('onLoad', zoneID);
}
// 返回顶部并刷新
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

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

View File

@ -1,148 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rank/zone/index.dart';
class ZonePage extends StatefulWidget {
const ZonePage({Key? key, required this.rid}) : super(key: key);
final int rid;
@override
State<ZonePage> createState() => _ZonePageState();
}
class _ZonePageState extends State<ZonePage> {
final ZoneController _zoneController = Get.put(ZoneController());
List videoList = [];
Future? _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
super.initState();
_futureBuilderFuture = _zoneController.queryRankFeed('init', widget.rid);
scrollController = _zoneController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_zoneController.isLoadingMore) {
_zoneController.isLoadingMore = true;
_zoneController.onLoad();
}
}
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
searchBarStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
searchBarStream.add(false);
}
},
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
return await _zoneController.onRefresh();
},
child: CustomScrollView(
controller: _zoneController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding:
const EdgeInsets.fromLTRB(0, StyleString.safeSpace - 5, 0, 0),
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return VideoCardH(
videoItem: _zoneController.videoList[index],
showPubdate: true,
longPress: () {
_zoneController.popupDialog = _createPopupDialog(
_zoneController.videoList[index]);
Overlay.of(context)
.insert(_zoneController.popupDialog!);
},
longPressEnd: () {
_zoneController.popupDialog?.remove();
},
);
}, childCount: _zoneController.videoList.length),
),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_zoneController.queryRankFeed('init', widget.rid);
});
},
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
);
}
},
),
),
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).padding.bottom + 10,
),
)
],
),
);
}
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
closeFn: _zoneController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _zoneController.popupDialog?.remove),
),
);
}
}

View File

@ -115,7 +115,7 @@ class SSearchController extends GetxController {
onLongSelect(word) {
int index = historyList.indexOf(word);
historyList.removeAt(index);
historyList.value = historyList.removeAt(index);
historyList.refresh();
histiryWord.put('cacheList', historyList);
}

View File

@ -173,12 +173,6 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableAi,
defaultVal: true,
),
const SetSwitchItem(
title: '相关视频推荐',
subTitle: '视频详情页推荐相关视频',
setKey: SettingBoxKey.enableRelatedVideo,
defaultVal: true,
),
ListTile(
dense: false,
title: Text('评论展示', style: titleStyle),

View File

@ -17,7 +17,6 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
Box videoStorage = GStrorage.video;
Box settingStorage = GStrorage.setting;
late double playSpeedDefault;
late List<double> playSpeedSystem;
late double longPressSpeedDefault;
late List customSpeedsList;
late bool enableAutoLongPressSpeed;
@ -54,9 +53,6 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
@override
void initState() {
super.initState();
// 系统预设倍速
playSpeedSystem =
videoStorage.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed);
// 默认倍速
playSpeedDefault =
videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0);
@ -68,7 +64,6 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
videoStorage.get(VideoBoxKey.customSpeedsList, defaultValue: []);
enableAutoLongPressSpeed = settingStorage
.get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false);
// 开启动态长按倍速时不展示
if (enableAutoLongPressSpeed) {
Map newItem = sheetMenu[1];
newItem['show'] = false;
@ -128,7 +123,7 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
}
// 设定倍速弹窗
void showBottomSheet(String type, int i) {
void showBottomSheet(type, i) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
@ -164,11 +159,18 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
}
//
void menuAction(type, int index, id) async {
void menuAction(type, index, id) async {
double chooseSpeed = 1.0;
if (type == 'system' && id == -1) {
SmartDialog.showToast('系统预设倍速不支持删除');
return;
}
// 获取当前选中的倍速值
chooseSpeed =
type == 'system' ? playSpeedSystem[index] : customSpeedsList[index];
if (type == 'system') {
chooseSpeed = PlaySpeed.values[index].value;
} else {
chooseSpeed = customSpeedsList[index];
}
// 设置
if (id == 1) {
// 设置默认倍速
@ -180,22 +182,17 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
videoStorage.put(
VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault);
} else if (id == -1) {
late List speedsList =
type == 'system' ? playSpeedSystem : customSpeedsList;
if (speedsList[index] == playSpeedDefault) {
SmartDialog.showToast('默认倍速不可删除');
if (customSpeedsList[index] == playSpeedDefault) {
playSpeedDefault = 1.0;
videoStorage.put(VideoBoxKey.playSpeedDefault, playSpeedDefault);
}
if (speedsList[index] == longPressSpeedDefault) {
if (customSpeedsList[index] == longPressSpeedDefault) {
longPressSpeedDefault = 2.0;
videoStorage.put(
VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault);
}
speedsList.removeAt(index);
await videoStorage.put(
type == 'system'
? VideoBoxKey.playSpeedSystem
: VideoBoxKey.customSpeedsList,
speedsList);
customSpeedsList.removeAt(index);
await videoStorage.put(VideoBoxKey.customSpeedsList, customSpeedsList);
}
setState(() {});
SmartDialog.showToast('操作成功');
@ -252,7 +249,6 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
subtitle: Text(longPressSpeedDefault.toString()),
)
: const SizedBox(),
if (playSpeedSystem.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(
left: 14,
@ -276,16 +272,15 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
spacing: 8,
runSpacing: 2,
children: [
for (int i = 0; i < playSpeedSystem.length; i++) ...[
for (var i in PlaySpeed.values) ...[
FilledButton.tonal(
onPressed: () => showBottomSheet('system', i),
child: Text(playSpeedSystem[i].toString()),
onPressed: () => showBottomSheet('system', i.index),
child: Text(i.description),
),
]
],
),
)
],
),
Padding(
padding: const EdgeInsets.only(
left: 14,

View File

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/common/reply_type.dart';
@ -20,7 +19,6 @@ import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/video_utils.dart';
import 'package:screen_brightness/screen_brightness.dart';
import '../../../http/danmaku.dart';
import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart';
@ -93,7 +91,6 @@ class VideoDetailController extends GetxController
late int cacheAudioQa;
PersistentBottomSheetController? replyReplyBottomSheetCtr;
late bool enableRelatedVideo;
@override
void onInit() {
@ -116,8 +113,7 @@ class VideoDetailController extends GetxController
autoPlay.value =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true);
enableRelatedVideo =
setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true);
if (userInfo == null ||
localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false;
@ -387,86 +383,4 @@ class VideoDetailController extends GetxController
? replyReplyBottomSheetCtr!.close()
: print('replyReplyBottomSheetCtr is null');
}
/// 发送弹幕
void showShootDanmakuSheet() {
final TextEditingController textController = TextEditingController();
bool isSending = false; // 追踪是否正在发送
showDialog(
context: Get.context!,
builder: (BuildContext context) {
// TODO: 支持更多类型和颜色的弹幕
return AlertDialog(
title: const Text('发送弹幕'),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return TextField(
controller: textController,
);
}),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return TextButton(
onPressed: isSending
? null
: () async {
final String msg = textController.text;
if (msg.isEmpty) {
SmartDialog.showToast('弹幕内容不能为空');
return;
} else if (msg.length > 100) {
SmartDialog.showToast('弹幕内容不能超过100个字符');
return;
}
setState(() {
isSending = true; // 开始发送,更新状态
});
//修改按钮文字
// SmartDialog.showToast('弹幕发送中,\n$msg');
final dynamic res = await DanmakaHttp.shootDanmaku(
oid: cid.value,
msg: textController.text,
bvid: bvid,
progress:
plPlayerController.position.value.inMilliseconds,
type: 1,
);
setState(() {
isSending = false; // 发送结束,更新状态
});
if (res['status']) {
SmartDialog.showToast('发送成功');
// 发送成功,自动预览该弹幕,避免重新请求
// TODO: 暂停状态下预览弹幕仍会移动与计时可考虑添加到dmSegList或其他方式实现
plPlayerController.danmakuController?.addItems([
DanmakuItem(
msg,
color: Colors.white,
time: plPlayerController
.position.value.inMilliseconds,
type: DanmakuItemType.scroll,
isSend: true,
)
]);
Get.back();
} else {
SmartDialog.showToast('发送失败,错误信息为${res['msg']}');
}
},
child: Text(isSending ? '发送中...' : '发送'),
);
})
],
);
},
);
}
}

View File

@ -268,10 +268,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
final Color outline = t.colorScheme.outline;
return SliverPadding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
top: 16,
),
left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10),
sliver: SliverToBoxAdapter(
child: !loadingStatus
? Column(

View File

@ -161,9 +161,9 @@ class _SeasonPanelState extends State<SeasonPanel> {
Expanded(
child: Material(
child: ScrollablePositionedList.builder(
itemCount: episodes.length,
itemCount: episodes.length + 1,
itemBuilder: (BuildContext context, int index) {
bool isLastItem = index == episodes.length - 1;
bool isLastItem = index == episodes.length;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(

View File

@ -9,6 +9,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:nil/nil.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart';
@ -24,7 +25,7 @@ import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/storage.dart';
import '../../../services/shutdown_timer_service.dart';
import 'widgets/app_bar.dart';
import 'widgets/header_control.dart';
class VideoDetailPage extends StatefulWidget {
const VideoDetailPage({Key? key}) : super(key: key);
@ -37,7 +38,7 @@ class VideoDetailPage extends StatefulWidget {
class _VideoDetailPageState extends State<VideoDetailPage>
with TickerProviderStateMixin, RouteAware {
late VideoDetailController vdCtr;
late VideoDetailController videoDetailController;
PlPlayerController? plPlayerController;
final ScrollController _extendNestCtr = ScrollController();
late StreamController<double> appbarStream;
@ -64,18 +65,20 @@ class _VideoDetailPageState extends State<VideoDetailPage>
void initState() {
super.initState();
heroTag = Get.arguments['heroTag'];
vdCtr = Get.put(VideoDetailController(), tag: heroTag);
videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
videoIntroController = Get.put(
VideoIntroController(bvid: Get.parameters['bvid']!),
tag: heroTag);
videoIntroController.videoDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);
videoPlayerServiceHandler.onVideoDetailChange(
value, videoDetailController.cid.value);
});
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
bangumiIntroController.bangumiDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);
videoPlayerServiceHandler.onVideoDetailChange(
value, videoDetailController.cid.value);
});
vdCtr.cid.listen((p0) {
videoDetailController.cid.listen((p0) {
videoPlayerServiceHandler.onVideoDetailChange(
bangumiIntroController.bangumiDetail.value, p0);
});
@ -90,16 +93,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
appbarStreamListen();
fullScreenStatusListener();
if (Platform.isAndroid) {
floating = vdCtr.floating!;
floating = videoDetailController.floating!;
autoEnterPip();
}
}
// 获取视频资源,初始化播放器
Future<void> videoSourceInit() async {
_futureBuilderFuture = vdCtr.queryVideoUrl();
if (vdCtr.autoPlay.value) {
plPlayerController = vdCtr.plPlayerController;
_futureBuilderFuture = videoDetailController.queryVideoUrl();
if (videoDetailController.autoPlay.value) {
plPlayerController = videoDetailController.plPlayerController;
plPlayerController!.addStatusLister(playerListener);
}
}
@ -128,10 +131,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 顺序播放 列表循环
if (plPlayerController!.playRepeat != PlayRepeat.pause &&
plPlayerController!.playRepeat != PlayRepeat.singleCycle) {
if (vdCtr.videoType == SearchType.video) {
if (videoDetailController.videoType == SearchType.video) {
videoIntroController.nextPlay();
}
if (vdCtr.videoType == SearchType.media_bangumi) {
if (videoDetailController.videoType == SearchType.media_bangumi) {
bangumiIntroController.nextPlay();
}
}
@ -143,7 +146,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
// 播放完展示控制栏
try {
PiPStatus currentStatus = await vdCtr.floating!.pipStatus;
PiPStatus currentStatus =
await videoDetailController.floating!.pipStatus;
if (currentStatus == PiPStatus.disabled) {
plPlayerController!.onLockControl(false);
}
@ -164,17 +168,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 未开启自动播放时触发播放
Future<void> handlePlay() async {
await vdCtr.playerInit();
plPlayerController = vdCtr.plPlayerController;
await videoDetailController.playerInit();
plPlayerController = videoDetailController.plPlayerController;
plPlayerController!.addStatusLister(playerListener);
vdCtr.autoPlay.value = true;
vdCtr.isShowCover.value = false;
videoDetailController.autoPlay.value = true;
videoDetailController.isShowCover.value = false;
}
void fullScreenStatusListener() {
plPlayerController?.isFullScreen.listen((bool isFullScreen) {
if (isFullScreen) {
vdCtr.hiddenReplyReplyPanel();
videoDetailController.hiddenReplyReplyPanel();
}
});
}
@ -186,8 +190,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.dispose();
}
if (vdCtr.floating != null) {
vdCtr.floating!.dispose();
if (videoDetailController.floating != null) {
videoDetailController.floating!.dispose();
}
videoPlayerServiceHandler.onVideoDetailDispose();
if (Platform.isAndroid) {
@ -203,10 +207,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 开启
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)
as bool) {
vdCtr.brightness = plPlayerController!.brightness.value;
videoDetailController.brightness = plPlayerController!.brightness.value;
}
if (plPlayerController != null) {
vdCtr.defaultST = plPlayerController!.position.value;
videoDetailController.defaultST = plPlayerController!.position.value;
videoIntroController.isPaused = true;
plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause();
@ -222,16 +226,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.videoPlayerController != null) {
setState(() => isShowing = true);
}
vdCtr.isFirstTime = false;
videoDetailController.isFirstTime = false;
final bool autoplay = autoPlayEnable;
vdCtr.playerInit(autoplay: autoplay);
videoDetailController.playerInit(autoplay: autoplay);
/// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回
vdCtr.autoPlay.value = !vdCtr.isShowCover.value;
videoDetailController.autoPlay.value =
!videoDetailController.isShowCover.value;
videoIntroController.isPaused = false;
if (_extendNestCtr.position.pixels == 0 && autoplay) {
await Future.delayed(const Duration(milliseconds: 300));
plPlayerController?.seekTo(vdCtr.defaultST);
plPlayerController?.seekTo(videoDetailController.defaultST);
plPlayerController?.play();
}
plPlayerController?.addStatusLister(playerListener);
@ -254,166 +259,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override
Widget build(BuildContext context) {
// final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16;
final sizeContext = MediaQuery.sizeOf(context);
final _context = MediaQuery.of(context);
late double defaultVideoHeight = sizeContext.width * 9 / 16;
late RxDouble videoHeight = defaultVideoHeight.obs;
final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16;
final double pinnedHeaderHeight =
statusBarHeight + kToolbarHeight + videoHeight.value;
// ignore: no_leading_underscores_for_local_identifiers
// 竖屏
final bool isPortrait = _context.orientation == Orientation.portrait;
// 横屏
final bool isLandscape = _context.orientation == Orientation.landscape;
final Rx<bool> isFullScreen = plPlayerController?.isFullScreen ?? false.obs;
// 全屏时高度撑满
if (isLandscape || isFullScreen.value == true) {
videoHeight.value = Get.size.height;
enterFullScreen();
} else {
videoHeight.value = defaultVideoHeight;
exitFullScreen();
}
/// 播放器面板
Widget videoPlayerPanel = FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData && snapshot.data['status']) {
return Obx(
() {
return !vdCtr.autoPlay.value
? const SizedBox()
: PLVideoPlayer(
controller: plPlayerController!,
headerControl: vdCtr.headerControl,
danmuWidget: Obx(
() => PlDanmaku(
key: Key(vdCtr.danmakuCid.value.toString()),
cid: vdCtr.danmakuCid.value,
playerController: plPlayerController!,
),
),
);
},
);
} else {
// 加载失败异常处理
return const SizedBox();
}
},
);
/// tabbar
Widget tabbarBuild = Container(
width: double.infinity,
height: 45,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
),
),
child: Row(
children: [
const SizedBox(width: 20),
Expanded(
child: TabBar(
controller: vdCtr.tabCtr,
dividerColor: Colors.transparent,
tabs: vdCtr.tabs.map((String name) => Tab(text: name)).toList(),
),
),
SizedBox(
width: 220,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: 32,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => vdCtr.showShootDanmakuSheet(),
child: const Text('发弹幕', style: TextStyle(fontSize: 12)),
),
),
const SizedBox(width: 4),
SizedBox(
width: 34,
height: 32,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
plPlayerController?.isOpenDanmu.value =
!(plPlayerController?.isOpenDanmu.value ?? false);
},
child: Obx(() => Text(
'',
style: TextStyle(
fontSize: 12,
color: (plPlayerController?.isOpenDanmu.value ??
false)
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
)),
),
),
const SizedBox(width: 14),
],
),
),
),
],
),
);
/// 手动播放
Widget handlePlayPanel() {
return Stack(
children: [
GestureDetector(
onTap: () {
handlePlay();
},
child: NetworkImgLayer(
type: 'emote',
src: vdCtr.videoItem['pic'],
width: Get.width,
height: videoHeight.value,
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: buildCustomAppBar(),
),
Positioned(
right: 12,
bottom: 10,
child: IconButton(
tooltip: '播放',
onPressed: () => handlePlay(),
icon: Image.asset(
'assets/images/play.png',
width: 60,
height: 60,
)),
),
],
);
}
statusBarHeight + kToolbarHeight + videoHeight;
Widget childWhenDisabled = SafeArea(
top: MediaQuery.of(context).orientation == Orientation.portrait &&
plPlayerController?.isFullScreen.value == true,
@ -425,7 +273,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
children: [
Scaffold(
resizeToAvoidBottomInset: false,
key: vdCtr.scaffoldKey,
key: videoDetailController.scaffoldKey,
backgroundColor: Colors.black,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(0),
@ -451,19 +299,21 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return SliverAppBar(
automaticallyImplyLeading: false,
// 假装使用一个非空变量避免Obx检测不到而罢工
pinned: vdCtr.autoPlay.value,
pinned: videoDetailController.autoPlay.value ^
false ^
videoDetailController.autoPlay.value,
elevation: 0,
scrolledUnderElevation: 0,
forceElevated: innerBoxIsScrolled,
expandedHeight: MediaQuery.of(context).orientation ==
Orientation.landscape ||
plPlayerController?.isFullScreen.value == true
? (MediaQuery.sizeOf(context).height -
? MediaQuery.sizeOf(context).height -
(MediaQuery.of(context).orientation ==
Orientation.landscape
? 0
: MediaQuery.of(context).padding.top))
: videoHeight.value,
: MediaQuery.of(context).padding.top)
: videoHeight,
backgroundColor: Colors.black,
flexibleSpace: FlexibleSpaceBar(
background: PopScope(
@ -483,27 +333,108 @@ class _VideoDetailPageState extends State<VideoDetailPage>
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
// final double maxWidth =
// boxConstraints.maxWidth;
// final double maxHeight =
// boxConstraints.maxHeight;
final double maxWidth =
boxConstraints.maxWidth;
final double maxHeight =
boxConstraints.maxHeight;
return Stack(
children: <Widget>[
if (isShowing) videoPlayerPanel,
if (isShowing)
FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context,
AsyncSnapshot snapshot) {
if (snapshot.hasData &&
snapshot.data['status']) {
return Obx(
() =>
!videoDetailController
.autoPlay.value
? nil
: PLVideoPlayer(
controller:
plPlayerController!,
headerControl:
videoDetailController
.headerControl,
danmuWidget: Obx(
() => PlDanmaku(
key: Key(videoDetailController
.danmakuCid
.value
.toString()),
cid: videoDetailController
.danmakuCid
.value,
playerController:
plPlayerController!,
),
),
),
);
} else {
return buildCustomAppBar();
}
},
),
/// 关闭自动播放时 手动播放
if (!videoDetailController
.autoPlay.value) ...<Widget>[
Obx(
() => Visibility(
visible: !vdCtr.autoPlay.value &&
vdCtr.isShowCover.value,
visible: videoDetailController
.isShowCover.value,
child: Positioned(
top: 0,
left: 0,
right: 0,
child: handlePlayPanel(),
child: GestureDetector(
onTap: () {
handlePlay();
},
child: NetworkImgLayer(
type: 'emote',
src: videoDetailController
.videoItem['pic'],
width: maxWidth,
height: maxHeight,
),
),
),
),
),
Obx(
() => Visibility(
visible: videoDetailController
.isShowCover.value &&
videoDetailController
.isEffective.value,
child: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
child: buildCustomAppBar(),
),
Positioned(
right: 12,
bottom: 10,
child: IconButton(
tooltip: '播放',
onPressed: () =>
handlePlay(),
icon: Image.asset(
'assets/images/play.png',
width: 60,
height: 60,
)),
),
],
)),
),
]
],
);
},
@ -514,15 +445,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
];
},
// pinnedHeaderSliverHeightBuilder: () {
// return playerStatus != PlayerStatus.playing
// ? statusBarHeight + kToolbarHeight
// : pinnedHeaderHeight;
// },
/// 不收回
pinnedHeaderSliverHeightBuilder: () {
return MediaQuery.of(context).orientation ==
Orientation.landscape ||
plPlayerController?.isFullScreen.value == true
? MediaQuery.sizeOf(context).height
: playerStatus != PlayerStatus.playing
? kToolbarHeight
: pinnedHeaderHeight;
},
onlyOneScrollInBody: true,
@ -531,23 +464,54 @@ class _VideoDetailPageState extends State<VideoDetailPage>
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
tabbarBuild,
Opacity(
opacity: 0,
child: SizedBox(
width: double.infinity,
height: 0,
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(
child: TabBarView(
controller: vdCtr.tabCtr,
controller: videoDetailController.tabCtr,
children: <Widget>[
Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: const PageStorageKey<String>('简介'),
slivers: <Widget>[
if (vdCtr.videoType == SearchType.video) ...[
VideoIntroPanel(bvid: vdCtr.bvid),
] else if (vdCtr.videoType ==
if (videoDetailController.videoType ==
SearchType.video) ...[
VideoIntroPanel(
bvid: videoDetailController.bvid),
] else if (videoDetailController.videoType ==
SearchType.media_bangumi) ...[
Obx(() => BangumiIntroPanel(
cid: vdCtr.cid.value)),
cid: videoDetailController.cid.value)),
],
// if (videoDetailController.videoType ==
// SearchType.video) ...[
// SliverPersistentHeader(
// floating: true,
// pinned: true,
// delegate: SliverHeaderDelegate(
// height: 50,
// child:
// const MenuRow(loadingStatus: false),
// ),
// ),
// ],
SliverToBoxAdapter(
child: Divider(
indent: 12,
@ -557,8 +521,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
.withOpacity(0.06),
),
),
if (vdCtr.videoType == SearchType.video &&
vdCtr.enableRelatedVideo)
const RelatedVideoPanel(),
],
);
@ -566,8 +528,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
Obx(
() => VideoReplyPanel(
bvid: vdCtr.bvid,
oid: vdCtr.oid.value,
bvid: videoDetailController.bvid,
oid: videoDetailController.oid.value,
),
)
],
@ -581,26 +543,56 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 重新进入会刷新
// 播放完成/暂停播放
StreamBuilder(
stream: appbarStream.stream,
initialData: 0,
builder: ((context, snapshot) {
return ScrollAppBar(
snapshot.data!.toDouble(),
() => continuePlay(),
playerStatus,
null,
);
}),
)
// StreamBuilder(
// stream: appbarStream.stream,
// initialData: 0,
// builder: ((context, snapshot) {
// return ScrollAppBar(
// snapshot.data!.toDouble(),
// () => continuePlay(),
// playerStatus,
// null,
// );
// }),
// )
],
),
);
Widget childWhenEnabled = FutureBuilder(
key: Key(heroTag),
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData && snapshot.data['status']) {
return Obx(
() => !videoDetailController.autoPlay.value
? const SizedBox()
: PLVideoPlayer(
controller: plPlayerController!,
headerControl: HeaderControl(
controller: plPlayerController,
videoDetailCtr: videoDetailController,
bvid: videoDetailController.bvid,
videoType: videoDetailController.videoType,
),
danmuWidget: Obx(
() => PlDanmaku(
key: Key(
videoDetailController.danmakuCid.value.toString()),
cid: videoDetailController.danmakuCid.value,
playerController: plPlayerController!,
),
),
),
);
} else {
return nil;
}
},
);
if (Platform.isAndroid) {
return PiPSwitcher(
childWhenDisabled: childWhenDisabled,
childWhenEnabled: videoPlayerPanel,
childWhenEnabled: childWhenEnabled,
floating: floating,
);
} else {
@ -641,7 +633,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
ComBtn(
icon: const Icon(Icons.history_outlined, size: 22),
fuc: () async {
var res = await UserHttp.toViewLater(bvid: vdCtr.bvid);
var res = await UserHttp.toViewLater(
bvid: videoDetailController.bvid);
SmartDialog.showToast(res['msg']);
},
),

View File

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
@ -16,6 +17,7 @@ import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/http/danmaku.dart';
import 'package:pilipala/services/shutdown_timer_service.dart';
import '../../../../models/common/search_type.dart';
import '../../../../models/video_detail_res.dart';
@ -45,6 +47,7 @@ class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
class _HeaderControlState extends State<HeaderControl> {
late PlayUrlModel videoInfo;
List<PlaySpeed> playSpeed = PlaySpeed.values;
static const TextStyle subTitleStyle = TextStyle(fontSize: 12);
static const TextStyle titleStyle = TextStyle(fontSize: 14);
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@ -218,6 +221,88 @@ class _HeaderControlState extends State<HeaderControl> {
);
}
/// 发送弹幕
void showShootDanmakuSheet() {
final TextEditingController textController = TextEditingController();
bool isSending = false; // 追踪是否正在发送
showDialog(
context: Get.context!,
builder: (BuildContext context) {
// TODO: 支持更多类型和颜色的弹幕
return AlertDialog(
title: const Text('发送弹幕(测试)'),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return TextField(
controller: textController,
);
}),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return TextButton(
onPressed: isSending
? null
: () async {
final String msg = textController.text;
if (msg.isEmpty) {
SmartDialog.showToast('弹幕内容不能为空');
return;
} else if (msg.length > 100) {
SmartDialog.showToast('弹幕内容不能超过100个字符');
return;
}
setState(() {
isSending = true; // 开始发送,更新状态
});
//修改按钮文字
// SmartDialog.showToast('弹幕发送中,\n$msg');
final dynamic res = await DanmakaHttp.shootDanmaku(
oid: widget.videoDetailCtr!.cid.value,
msg: textController.text,
bvid: widget.videoDetailCtr!.bvid,
progress:
widget.controller!.position.value.inMilliseconds,
type: 1,
);
setState(() {
isSending = false; // 发送结束,更新状态
});
if (res['status']) {
SmartDialog.showToast('发送成功');
// 发送成功,自动预览该弹幕,避免重新请求
// TODO: 暂停状态下预览弹幕仍会移动与计时可考虑添加到dmSegList或其他方式实现
widget.controller!.danmakuController!.addItems([
DanmakuItem(
msg,
color: Colors.white,
time: widget
.controller!.position.value.inMilliseconds,
type: DanmakuItemType.scroll,
isSend: true,
)
]);
Get.back();
} else {
SmartDialog.showToast('发送失败,错误信息为${res['msg']}');
}
},
child: Text(isSending ? '发送中...' : '发送'),
);
})
],
);
},
);
}
/// 定时关闭
void scheduleExit() async {
const List<int> scheduleTimeChoices = [
@ -1081,6 +1166,41 @@ class _HeaderControlState extends State<HeaderControl> {
// ),
// fuc: () => _.screenshot(),
// ),
SizedBox(
width: 56,
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => showShootDanmakuSheet(),
child: const Text(
'发弹幕',
style: textStyle,
),
),
),
SizedBox(
width: 34,
height: 34,
child: Obx(
() => IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
_.isOpenDanmu.value = !_.isOpenDanmu.value;
},
icon: Icon(
_.isOpenDanmu.value
? Icons.subtitles_outlined
: Icons.subtitles_off_outlined,
size: 19,
color: Colors.white,
),
),
),
),
SizedBox(width: buttonSpace),
if (Platform.isAndroid) ...<Widget>[
SizedBox(

View File

@ -292,19 +292,11 @@ class PlPlayerController {
_longPressSpeed.value = videoStorage
.get(VideoBoxKey.longPressSpeedDefault, defaultValue: 2.0);
}
// 自定义倍速集合
speedsList = List<double>.from(videoStorage
.get(VideoBoxKey.customSpeedsList, defaultValue: <double>[]));
// 默认倍速
speedsList = List<double>.from(videoStorage
.get(VideoBoxKey.customSpeedsList, defaultValue: <double>[]));
//playSpeedSystem
final List<double> playSpeedSystem =
videoStorage.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed);
// for (final PlaySpeed i in PlaySpeed.values) {
speedsList.addAll(playSpeedSystem);
// }
for (final PlaySpeed i in PlaySpeed.values) {
speedsList.add(i.value);
}
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
// if (status == PlayerStatus.playing) {
@ -684,6 +676,18 @@ class PlPlayerController {
_playbackSpeed.value = speed;
}
/// 设置倍速
// Future<void> togglePlaybackSpeed() async {
// List<double> allowedSpeeds =
// PlaySpeed.values.map<double>((e) => e.value).toList();
// int index = allowedSpeeds.indexOf(_playbackSpeed.value);
// if (index < allowedSpeeds.length - 1) {
// setPlaybackSpeed(allowedSpeeds[index + 1]);
// } else {
// setPlaybackSpeed(allowedSpeeds[0]);
// }
// }
/// 播放视频
/// TODO _duration.value丢失
Future<void> play(

View File

@ -1,15 +1,39 @@
List<double> generatePlaySpeedList() {
List<double> playSpeed = [];
double startSpeed = 0.25;
double endSpeed = 2.0;
double increment = 0.25;
enum PlaySpeed {
pointTwoFive,
pointFive,
pointSevenFive,
for (double speed = startSpeed; speed <= endSpeed; speed += increment) {
playSpeed.add(speed);
one,
onePointTwoFive,
onePointFive,
onePointSevenFive,
two,
}
return playSpeed;
}
extension PlaySpeedExtension on PlaySpeed {
static final List<String> _descList = [
'0.25',
'0.5',
'0.75',
'正常',
'1.25',
'1.5',
'1.75',
'2.0',
];
String get description => _descList[index];
// 导出 playSpeed 列表
List<double> playSpeed = generatePlaySpeedList();
static final List<double> _valueList = [
0.25,
0.5,
0.75,
1.0,
1.25,
1.5,
1.75,
2.0,
];
double get value => _valueList[index];
double get defaultValue => _valueList[3];
}

View File

@ -99,9 +99,11 @@ class CacheManage {
try {
// 清除缓存 图片缓存
await clearLibraryCache();
Timer(const Duration(milliseconds: 500), () {
SmartDialog.dismiss().then((res) {
SmartDialog.showToast('清除完成');
});
});
} catch (err) {
SmartDialog.dismiss();
SmartDialog.showToast(err.toString());

View File

@ -131,8 +131,7 @@ class SettingBoxKey {
enableSearchWord = 'enableSearchWord',
enableSystemProxy = 'enableSystemProxy',
enableAi = 'enableAi',
defaultHomePage = 'defaultHomePage',
enableRelatedVideo = 'enableRelatedVideo';
defaultHomePage = 'defaultHomePage';
/// 外观
static const String themeMode = 'themeMode',
@ -182,8 +181,6 @@ class VideoBoxKey {
videoSpeed = 'videoSpeed',
// 播放顺序
playRepeat = 'playRepeat',
// 系统预设倍速
playSpeedSystem = 'playSpeedSystem',
// 默认倍速
playSpeedDefault = 'playSpeedDefault',
// 默认长按倍速

View File

@ -50,9 +50,6 @@ class Utils {
return time;
}
if (time < 3600) {
if (time == 0) {
return time;
}
final int minute = time ~/ 60;
final double res = time / 60;
if (minute != res) {
@ -90,9 +87,6 @@ class Utils {
// 时间显示刚刚x分钟前
static String dateFormat(timeStamp, {formatType = 'list'}) {
if (timeStamp == 0 || timeStamp == null || timeStamp == '') {
return '';
}
// 当前时间
int time = (DateTime.now().millisecondsSinceEpoch / 1000).round();
// 对比
@ -109,7 +103,6 @@ class Utils {
toInt: false,
formatType: formatType);
}
print('distance: $distance');
if (distance <= 60) {
return '刚刚';
} else if (distance <= 3600) {
@ -243,10 +236,6 @@ class Utils {
SmartDialog.dismiss();
var currentInfo = await PackageInfo.fromPlatform();
var result = await Request().get(Api.latestApp, extra: {'ua': 'mob'});
if (result.data == null || result.data.isEmpty) {
SmartDialog.showToast('获取远程版本失败,请检查网络');
return false;
}
LatestDataModel data = LatestDataModel.fromJson(result.data);
bool isUpdate = Utils.needUpdate(currentInfo.version, data.tagName!);
if (isUpdate) {
@ -355,8 +344,9 @@ class Utils {
}
static List<int> generateRandomBytes(int minLength, int maxLength) {
return List<int>.generate(random.nextInt(maxLength - minLength + 1),
(_) => random.nextInt(0x60) + 0x20);
return List<int>.generate(
random.nextInt(maxLength-minLength+1), (_) => random.nextInt(0x60) + 0x20
);
}
static String base64EncodeRandomString(int minLength, int maxLength) {