Merge branch 'main' into fix

This commit is contained in:
guozhigq
2024-04-10 23:42:49 +08:00
37 changed files with 1171 additions and 722 deletions

View File

@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: branches:
- "main" - "never"
paths-ignore: paths-ignore:
- "**.md" - "**.md"
- "**.txt" - "**.txt"
@ -12,6 +12,7 @@ on:
- ".idea/**" - ".idea/**"
- "!.github/workflows/**" - "!.github/workflows/**"
jobs: jobs:
update_version: update_version:
name: Read and update version name: Read and update version

View File

@ -26,13 +26,13 @@
Xcode 13.4 不支持**auto_orientation**,请注释相关代码 Xcode 13.4 不支持**auto_orientation**,请注释相关代码
```bash ```bash
[] Flutter (Channel stable, 3.16.4, on macOS 14.1.2 23B92 darwin-arm64, locale [] Flutter (Channel stable, 3.16.5, on macOS 14.1.2 23B92 darwin-arm64, locale
zh-Hans-CN) zh-Hans-CN)
[] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[] Xcode - develop for iOS and macOS (Xcode 15.1) [] Xcode - develop for iOS and macOS (Xcode 15.1)
[] Chrome - develop for the web [] Chrome - develop for the web
[] Android Studio (version 2022.3) [] Android Studio (version 2022.3)
[] VS Code (version 1.85.1) [] VS Code (version 1.87.2)
[] Connected device (3 available) [] Connected device (3 available)
[] Network resources [] Network resources
@ -44,6 +44,9 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
## 技术交流 ## 技术交流
Telegram: https://t.me/+lm_oOVmF0RJiODk1 Telegram: https://t.me/+lm_oOVmF0RJiODk1
Tg Beta版本@PiliPala_Beta
QQ频道: https://pd.qq.com/s/365esodk3 QQ频道: https://pd.qq.com/s/365esodk3

View File

@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../models/common/video_episode_type.dart';
class EpisodeBottomSheet {
final List<dynamic> episodes;
final int currentCid;
final dynamic dataType;
final BuildContext context;
final Function changeFucCall;
final int? cid;
final double? sheetHeight;
bool isFullScreen = false;
EpisodeBottomSheet({
required this.episodes,
required this.currentCid,
required this.dataType,
required this.context,
required this.changeFucCall,
this.cid,
this.sheetHeight,
this.isFullScreen = false,
});
Widget buildEpisodeListItem(
dynamic episode,
int index,
bool isCurrentIndex,
) {
Color primary = Theme.of(context).colorScheme.primary;
Color onSurface = Theme.of(context).colorScheme.onSurface;
String title = '';
switch (dataType) {
case VideoEpidoesType.videoEpisode:
title = episode.title;
break;
case VideoEpidoesType.videoPart:
title = episode.pagePart;
break;
case VideoEpidoesType.bangumiEpisode:
title = '${episode.title}${episode.longTitle!}';
break;
}
return ListTile(
onTap: () {
SmartDialog.showToast('切换至「$title');
changeFucCall.call(episode, index);
},
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.gif',
color: primary,
height: 12,
)
: null,
title: Text(
title,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex ? primary : onSurface,
),
),
);
}
Widget buildTitle() {
return AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
centerTitle: false,
title: Text(
'合集(${episodes.length}',
style: Theme.of(context).textTheme.titleMedium,
),
actions: !isFullScreen
? [
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 14),
]
: null,
);
}
Widget buildShowContent(BuildContext context) {
final ItemScrollController itemScrollController = ItemScrollController();
int currentIndex = episodes.indexWhere((dynamic e) => e.cid == currentCid);
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
itemScrollController.jumpTo(index: currentIndex);
});
return Container(
height: sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
buildTitle(),
Expanded(
child: Material(
child: PageStorage(
bucket: PageStorageBucket(),
child: ScrollablePositionedList.builder(
itemScrollController: itemScrollController,
itemCount: episodes.length + 1,
itemBuilder: (BuildContext context, int index) {
bool isLastItem = index == episodes.length;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(
height:
MediaQuery.of(context).padding.bottom + 20,
)
: buildEpisodeListItem(
episodes[index],
index,
isCurrentIndex,
);
},
),
),
),
),
],
),
);
});
}
/// The [BuildContext] of the widget that calls the bottom sheet.
PersistentBottomSheetController show(BuildContext context) {
final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
context: context,
builder: (BuildContext context) {
return buildShowContent(context);
},
);
return btmSheetCtr;
}
}

View File

@ -490,8 +490,11 @@ class Api {
/// 我的订阅 /// 我的订阅
static const userSubFolder = '/x/v3/fav/folder/collected/list'; static const userSubFolder = '/x/v3/fav/folder/collected/list';
/// 我的订阅详情 /// 我的订阅详情 type 21
static const userSubFolderDetail = '/x/space/fav/season/list'; static const userSeasonList = '/x/space/fav/season/list';
/// 我的订阅详情 type 11
static const userResourceList = '/x/v3/fav/resource/list';
/// 表情 /// 表情
static const emojiList = '/x/emote/user/panel/web'; static const emojiList = '/x/emote/user/panel/web';

View File

@ -330,12 +330,12 @@ class UserHttp {
} }
} }
static Future userSubFolderDetail({ static Future userSeasonList({
required int seasonId, required int seasonId,
required int pn, required int pn,
required int ps, required int ps,
}) async { }) async {
var res = await Request().get(Api.userSubFolderDetail, data: { var res = await Request().get(Api.userSeasonList, data: {
'season_id': seasonId, 'season_id': seasonId,
'ps': ps, 'ps': ps,
'pn': pn, 'pn': pn,
@ -350,6 +350,35 @@ class UserHttp {
} }
} }
static Future userResourceList({
required int seasonId,
required int pn,
required int ps,
}) async {
var res = await Request().get(Api.userResourceList, data: {
'media_id': seasonId,
'ps': ps,
'pn': pn,
'keyword': '',
'order': 'mtime',
'type': 0,
'tid': 0,
'platform': 'web',
});
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': SubDetailModelData.fromJson(res.data['data'])
};
} catch (err) {
return {'status': false, 'msg': err};
}
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 取消订阅 // 取消订阅
static Future cancelSub({required int seasonId}) async { static Future cancelSub({required int seasonId}) async {
var res = await Request().post( var res = await Request().post(

View File

@ -1,5 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../pages/dynamics/index.dart';
import '../../pages/home/index.dart';
import '../../pages/media/index.dart';
import '../../pages/rank/index.dart';
List defaultNavigationBars = [ List defaultNavigationBars = [
{ {
'id': 0, 'id': 0,
@ -13,6 +18,7 @@ List defaultNavigationBars = [
), ),
'label': "首页", 'label': "首页",
'count': 0, 'count': 0,
'page': const HomePage(),
}, },
{ {
'id': 1, 'id': 1,
@ -26,6 +32,7 @@ List defaultNavigationBars = [
), ),
'label': "排行榜", 'label': "排行榜",
'count': 0, 'count': 0,
'page': const RankPage(),
}, },
{ {
'id': 2, 'id': 2,
@ -39,6 +46,7 @@ List defaultNavigationBars = [
), ),
'label': "动态", 'label': "动态",
'count': 0, 'count': 0,
'page': const DynamicsPage(),
}, },
{ {
'id': 3, 'id': 3,
@ -52,5 +60,6 @@ List defaultNavigationBars = [
), ),
'label': "媒体库", 'label': "媒体库",
'count': 0, 'count': 0,
'page': const MediaPage(),
} }
]; ];

View File

@ -0,0 +1,5 @@
enum VideoEpidoesType {
videoEpisode,
videoPart,
bangumiEpisode,
}

View File

@ -15,6 +15,10 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../common/pages_bottom_sheet.dart';
import '../../../models/common/video_episode_type.dart';
import '../../../utils/drawer.dart';
class BangumiIntroController extends GetxController { class BangumiIntroController extends GetxController {
// 视频bvid // 视频bvid
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
@ -291,4 +295,29 @@ class BangumiIntroController extends GetxController {
int aid = episodes[nextIndex].aid!; int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid); changeSeasonOrbangu(bvid, cid, aid);
} }
// 播放器底栏 选集 回调
void showEposideHandler() {
late List episodes = bangumiDetail.value.episodes!;
VideoEpidoesType dataType = VideoEpidoesType.bangumiEpisode;
if (episodes.isEmpty) {
return;
}
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
DrawerUtils.showRightDialog(
child: EpisodeBottomSheet(
episodes: episodes,
currentCid: videoDetailCtr.cid.value,
dataType: dataType,
context: Get.context!,
sheetHeight: Get.size.height,
isFullScreen: true,
changeFucCall: (item, index) {
changeSeasonOrbangu(item.bvid, item.cid, item.aid);
SmartDialog.dismiss();
},
).buildShowContent(Get.context!),
);
}
} }

View File

@ -138,6 +138,9 @@ class _BangumiInfoState extends State<BangumiInfo> {
cid = widget.cid!; cid = widget.cid!;
videoDetailCtr.cid.listen((p0) { videoDetailCtr.cid.listen((p0) {
cid = p0; cid = p0;
if (!mounted) {
return;
}
setState(() {}); setState(() {});
}); });
} }
@ -317,7 +320,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
if (widget.bangumiDetail!.episodes!.isNotEmpty) ...[ if (widget.bangumiDetail!.episodes!.isNotEmpty) ...[
BangumiPanel( BangumiPanel(
pages: widget.bangumiDetail!.episodes!, pages: widget.bangumiDetail!.episodes!,
cid: cid ?? widget.bangumiDetail!.episodes!.first.cid, cid: cid! ?? widget.bangumiDetail!.episodes!.first.cid!,
sheetHeight: sheetHeight, sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => changeFuc: (bvid, cid, aid) =>
bangumiIntroController.changeSeasonOrbangu(bvid, cid, aid), bangumiIntroController.changeSeasonOrbangu(bvid, cid, aid),

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -6,19 +8,21 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../../../common/pages_bottom_sheet.dart';
import '../../../models/common/video_episode_type.dart';
class BangumiPanel extends StatefulWidget { class BangumiPanel extends StatefulWidget {
const BangumiPanel({ const BangumiPanel({
super.key, super.key,
required this.pages, required this.pages,
this.cid, required this.cid,
this.sheetHeight, this.sheetHeight,
this.changeFuc, this.changeFuc,
this.bangumiDetail, this.bangumiDetail,
}); });
final List<EpisodeItem> pages; final List<EpisodeItem> pages;
final int? cid; final int cid;
final double? sheetHeight; final double? sheetHeight;
final Function? changeFuc; final Function? changeFuc;
final BangumiInfoModel? bangumiDetail; final BangumiInfoModel? bangumiDetail;
@ -28,9 +32,8 @@ class BangumiPanel extends StatefulWidget {
} }
class _BangumiPanelState extends State<BangumiPanel> { class _BangumiPanelState extends State<BangumiPanel> {
late int currentIndex; late RxInt currentIndex = (-1).obs;
final ScrollController listViewScrollCtr = ScrollController(); final ScrollController listViewScrollCtr = ScrollController();
final ScrollController listViewScrollCtr_2 = ScrollController();
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
dynamic userInfo; dynamic userInfo;
// 默认未开通 // 默认未开通
@ -39,169 +42,68 @@ class _BangumiPanelState extends State<BangumiPanel> {
String heroTag = Get.arguments['heroTag']; String heroTag = Get.arguments['heroTag'];
late final VideoDetailController videoDetailCtr; late final VideoDetailController videoDetailCtr;
final ItemScrollController itemScrollController = ItemScrollController(); final ItemScrollController itemScrollController = ItemScrollController();
late PersistentBottomSheetController? _bottomSheetController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
cid = widget.cid!; cid = widget.cid;
currentIndex = widget.pages.indexWhere((e) => e.cid == cid); videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
currentIndex.value =
widget.pages.indexWhere((EpisodeItem e) => e.cid == cid);
scrollToIndex(); scrollToIndex();
videoDetailCtr.cid.listen((int p0) {
cid = p0;
currentIndex.value =
widget.pages.indexWhere((EpisodeItem e) => e.cid == cid);
scrollToIndex();
});
/// 获取大会员状态
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) { if (userInfo != null) {
vipStatus = userInfo.vipStatus; vipStatus = userInfo.vipStatus;
} }
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
videoDetailCtr.cid.listen((int p0) {
cid = p0;
setState(() {});
currentIndex = widget.pages.indexWhere((EpisodeItem e) => e.cid == cid);
scrollToIndex();
});
} }
@override @override
void dispose() { void dispose() {
listViewScrollCtr.dispose(); listViewScrollCtr.dispose();
listViewScrollCtr_2.dispose();
super.dispose(); super.dispose();
} }
Widget buildPageListItem(
EpisodeItem page,
int index,
bool isCurrentIndex,
) {
Color primary = Theme.of(context).colorScheme.primary;
return ListTile(
onTap: () {
Get.back();
setState(() {
changeFucCall(page, index);
});
},
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.gif',
color: primary,
height: 12,
)
: null,
title: Text(
'${page.title}${page.longTitle!}',
style: TextStyle(
fontSize: 14,
color: isCurrentIndex
? primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: page.badge != null
? Text(
page.badge!,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
)
: const SizedBox(),
);
}
void showBangumiPanel() {
showBottomSheet(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
// await Future.delayed(const Duration(milliseconds: 200));
// listViewScrollCtr_2.animateTo(currentIndex * 56,
// duration: const Duration(milliseconds: 500),
// curve: Curves.easeInOut);
itemScrollController.jumpTo(index: currentIndex);
});
// 在这里使用 setState 更新状态
return Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'合集(${widget.pages.length}',
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
titleSpacing: 10,
),
Expanded(
child: Material(
child: ScrollablePositionedList.builder(
itemCount: widget.pages.length + 1,
itemBuilder: (BuildContext context, int index) {
bool isLastItem = index == widget.pages.length;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(
height:
MediaQuery.of(context).padding.bottom +
20,
)
: buildPageListItem(
widget.pages[index],
index,
isCurrentIndex,
);
},
itemScrollController: itemScrollController,
),
),
),
],
),
);
},
);
},
);
}
void changeFucCall(item, i) async { void changeFucCall(item, i) async {
if (item.badge != null && item.badge == '会员' && vipStatus != 1) { if (item.badge != null && item.badge == '会员' && vipStatus != 1) {
SmartDialog.showToast('需要大会员'); SmartDialog.showToast('需要大会员');
return; return;
} }
await widget.changeFuc!( widget.changeFuc?.call(
item.bvid, item.bvid,
item.cid, item.cid,
item.aid, item.aid,
); );
_bottomSheetController?.close();
currentIndex = i; currentIndex = i;
setState(() {});
scrollToIndex(); scrollToIndex();
} }
void scrollToIndex() { void scrollToIndex() {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// 在回调函数中获取更新后的状态 // 在回调函数中获取更新后的状态
listViewScrollCtr.animateTo(currentIndex * 150, final double offset = min((currentIndex * 150) - 75,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); listViewScrollCtr.position.maxScrollExtent);
listViewScrollCtr.animateTo(
offset,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color primary = Theme.of(context).colorScheme.primary;
Color onSurface = Theme.of(context).colorScheme.onSurface;
return Column( return Column(
children: [ children: [
Padding( Padding(
@ -211,12 +113,14 @@ class _BangumiPanelState extends State<BangumiPanel> {
children: [ children: [
const Text('选集 '), const Text('选集 '),
Expanded( Expanded(
child: Text( child: Obx(
' 正在播放:${widget.pages[currentIndex].longTitle}', () => Text(
overflow: TextOverflow.ellipsis, ' 正在播放:${widget.pages[currentIndex.value].longTitle}',
style: TextStyle( overflow: TextOverflow.ellipsis,
fontSize: 12, style: TextStyle(
color: Theme.of(context).colorScheme.outline, fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
), ),
), ),
), ),
@ -227,7 +131,16 @@ class _BangumiPanelState extends State<BangumiPanel> {
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
onPressed: () => showBangumiPanel(), onPressed: () {
_bottomSheetController = EpisodeBottomSheet(
currentCid: cid,
episodes: widget.pages,
changeFucCall: changeFucCall,
sheetHeight: widget.sheetHeight,
dataType: VideoEpidoesType.bangumiEpisode,
context: context,
).show(context);
},
child: Text( child: Text(
'${widget.bangumiDetail!.newEp!['desc']}', '${widget.bangumiDetail!.newEp!['desc']}',
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
@ -245,6 +158,8 @@ class _BangumiPanelState extends State<BangumiPanel> {
itemCount: widget.pages.length, itemCount: widget.pages.length,
itemExtent: 150, itemExtent: 150,
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
var page = widget.pages[i];
bool isSelected = i == currentIndex.value;
return Container( return Container(
width: 150, width: 150,
margin: const EdgeInsets.only(right: 10), margin: const EdgeInsets.only(right: 10),
@ -253,42 +168,37 @@ class _BangumiPanelState extends State<BangumiPanel> {
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: InkWell( child: InkWell(
onTap: () => changeFucCall(widget.pages[i], i), onTap: () => changeFucCall(page, i),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 10), vertical: 8,
horizontal: 10,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Row( Row(
children: [ children: [
if (i == currentIndex) ...<Widget>[ if (isSelected) ...<Widget>[
Image.asset( Image.asset('assets/images/live.png',
'assets/images/live.png', color: primary, height: 12),
color: Theme.of(context).colorScheme.primary,
height: 12,
),
const SizedBox(width: 6) const SizedBox(width: 6)
], ],
Text( Text(
'${i + 1}', '${i + 1}',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: i == currentIndex color: isSelected ? primary : onSurface,
? Theme.of(context).colorScheme.primary ),
: Theme.of(context)
.colorScheme
.onSurface),
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
if (widget.pages[i].badge != null) ...[ if (page.badge != null) ...[
const Spacer(), const Spacer(),
Text( Text(
widget.pages[i].badge!, page.badge!,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: color: primary,
Theme.of(context).colorScheme.primary,
), ),
), ),
] ]
@ -296,13 +206,12 @@ class _BangumiPanelState extends State<BangumiPanel> {
), ),
const SizedBox(height: 3), const SizedBox(height: 3),
Text( Text(
widget.pages[i].longTitle!, page.longTitle!,
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: i == currentIndex color: isSelected ? primary : onSurface,
? Theme.of(context).colorScheme.primary ),
: Theme.of(context).colorScheme.onSurface),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
) )
], ],

111
lib/pages/dlna/index.dart Normal file
View File

@ -0,0 +1,111 @@
import 'dart:async';
import 'package:dlna_dart/dlna.dart';
import 'package:flutter/material.dart';
class LiveDlnaPage extends StatefulWidget {
final String datasource;
const LiveDlnaPage({Key? key, required this.datasource}) : super(key: key);
@override
State<LiveDlnaPage> createState() => _LiveDlnaPageState();
}
class _LiveDlnaPageState extends State<LiveDlnaPage> {
final Map<String, DLNADevice> _deviceList = {};
final DLNAManager searcher = DLNAManager();
late final Timer stopSearchTimer;
String selectDeviceKey = '';
bool isSearching = true;
DLNADevice? get device => _deviceList[selectDeviceKey];
@override
void initState() {
stopSearchTimer = Timer(const Duration(seconds: 20), () {
setState(() => isSearching = false);
searcher.stop();
});
searcher.stop();
startSearch();
super.initState();
}
@override
void dispose() {
super.dispose();
searcher.stop();
stopSearchTimer.cancel();
}
void startSearch() async {
// clear old devices
isSearching = true;
selectDeviceKey = '';
_deviceList.clear();
setState(() {});
// start search server
final m = await searcher.start();
m.devices.stream.listen((deviceList) {
deviceList.forEach((key, value) {
_deviceList[key] = value;
});
setState(() {});
});
// close the server, the closed server can be start by call searcher.start()
}
void selectDevice(String key) {
if (selectDeviceKey.isNotEmpty) device?.pause();
selectDeviceKey = key;
device?.setUrl(widget.datasource);
device?.play();
setState(() {});
}
@override
Widget build(BuildContext context) {
Widget cur;
if (isSearching && _deviceList.isEmpty) {
cur = const Center(child: CircularProgressIndicator());
} else if (_deviceList.isEmpty) {
cur = Center(
child: Text(
'没有找到设备',
style: Theme.of(context).textTheme.bodyLarge,
),
);
} else {
cur = ListView(
children: _deviceList.keys
.map<Widget>((key) => ListTile(
contentPadding: const EdgeInsets.all(2),
title: Text(_deviceList[key]!.info.friendlyName),
subtitle: Text(key),
onTap: () => selectDevice(key),
))
.toList(),
);
}
return AlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('查找设备'),
IconButton(
onPressed: startSearch,
icon: const Icon(Icons.refresh_rounded),
),
],
),
content: SizedBox(
height: 200,
width: 200,
child: cur,
),
);
}
}

View File

@ -62,6 +62,11 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
controller: plPlayerController, controller: plPlayerController,
liveRoomCtr: _liveRoomController, liveRoomCtr: _liveRoomController,
floating: floating, floating: floating,
onRefresh: () {
setState(() {
_futureBuilderFuture = _liveRoomController.queryLiveInfo();
});
},
), ),
); );
} else { } else {

View File

@ -14,10 +14,12 @@ class BottomControl extends StatefulWidget implements PreferredSizeWidget {
final PlPlayerController? controller; final PlPlayerController? controller;
final LiveRoomController? liveRoomCtr; final LiveRoomController? liveRoomCtr;
final Floating? floating; final Floating? floating;
final Function? onRefresh;
const BottomControl({ const BottomControl({
this.controller, this.controller,
this.liveRoomCtr, this.liveRoomCtr,
this.floating, this.floating,
this.onRefresh,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -61,6 +63,14 @@ class _BottomControlState extends State<BottomControl> {
// ), // ),
// fuc: () => Get.back(), // fuc: () => Get.back(),
// ), // ),
ComBtn(
icon: const Icon(
Icons.refresh_outlined,
size: 18,
color: Colors.white,
),
fuc: widget.onRefresh,
),
const Spacer(), const Spacer(),
// ComBtn( // ComBtn(
// icon: const Icon( // icon: const Icon(
@ -150,21 +160,3 @@ class _BottomControlState extends State<BottomControl> {
); );
} }
} }
class MSliderTrackShape extends RoundedRectSliderTrackShape {
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
SliderThemeData? sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
const double trackHeight = 3;
final double trackLeft = offset.dx;
final double trackTop =
offset.dy + (parentBox.size.height - trackHeight) / 2 + 4;
final double trackWidth = parentBox.size.width;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
}

View File

@ -6,23 +6,16 @@ import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/common.dart'; 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/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../../models/common/dynamic_badge_mode.dart'; import '../../models/common/dynamic_badge_mode.dart';
import '../../models/common/nav_bar_config.dart'; import '../../models/common/nav_bar_config.dart';
class MainController extends GetxController { class MainController extends GetxController {
List<Widget> pages = <Widget>[ List<Widget> pages = <Widget>[];
const HomePage(), RxList navigationBars = [].obs;
const RankPage(), late List defaultNavTabs;
const DynamicsPage(), late List<int> navBarSort;
const MediaPage(),
];
RxList navigationBars = defaultNavigationBars.obs;
final StreamController<bool> bottomBarStream = final StreamController<bool> bottomBarStream =
StreamController<bool>.broadcast(); StreamController<bool>.broadcast();
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
@ -41,10 +34,7 @@ class MainController extends GetxController {
Utils.checkUpdata(); Utils.checkUpdata();
} }
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true); hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
int defaultHomePage =
setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int;
selectedIndex = defaultNavigationBars
.indexWhere((item) => item['id'] == defaultHomePage);
var userInfo = userInfoCache.get('userInfoCache'); var userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null; userLogin.value = userInfo != null;
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
@ -53,6 +43,7 @@ class MainController extends GetxController {
if (dynamicBadgeType.value != DynamicBadgeMode.hidden) { if (dynamicBadgeType.value != DynamicBadgeMode.hidden) {
getUnreadDynamic(); getUnreadDynamic();
} }
setNavBarConfig();
} }
void onBackPressed(BuildContext context) { void onBackPressed(BuildContext context) {
@ -93,4 +84,21 @@ class MainController extends GetxController {
} }
navigationBars.refresh(); navigationBars.refresh();
} }
void setNavBarConfig() async {
defaultNavTabs = [...defaultNavigationBars];
navBarSort =
setting.get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2, 3]);
defaultNavTabs.retainWhere((item) => navBarSort.contains(item['id']));
defaultNavTabs.sort((a, b) =>
navBarSort.indexOf(a['id']).compareTo(navBarSort.indexOf(b['id'])));
navigationBars.value = defaultNavTabs;
int defaultHomePage =
setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int;
int defaultIndex =
navigationBars.indexWhere((item) => item['id'] == defaultHomePage);
// 如果找不到匹配项默认索引设置为0或其他合适的值
selectedIndex = defaultIndex != -1 ? defaultIndex : 0;
pages = navigationBars.map<Widget>((e) => e['page']).toList();
}
} }

View File

@ -25,7 +25,7 @@ class MemberSeasonsItem extends StatelessWidget {
child: InkWell( child: InkWell(
onTap: () async { onTap: () async {
int cid = int cid =
await SearchHttp.ab2c(aid: seasonItem.aid, bvid: seasonItem.bvid); await SearchHttp.ab2c(aid: seasonItem.aid, bvid: seasonItem.bvid);
Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid', Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid',
arguments: {'videoItem': seasonItem, 'heroTag': heroTag}); arguments: {'videoItem': seasonItem, 'heroTag': heroTag});
}, },
@ -51,8 +51,7 @@ class MemberSeasonsItem extends StatelessWidget {
bottom: 6, bottom: 6,
right: 6, right: 6,
type: 'gray', type: 'gray',
text: Utils.CustomStamp_str( text: Utils.timeFormat(seasonItem.duration),
timestamp: seasonItem.pubdate, date: 'YY-MM-DD'),
) )
], ],
); );

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/utils/storage.dart';
import '../../../models/common/nav_bar_config.dart';
class NavigationBarSetPage extends StatefulWidget {
const NavigationBarSetPage({super.key});
@override
State<NavigationBarSetPage> createState() => _NavigationbarSetPageState();
}
class _NavigationbarSetPageState extends State<NavigationBarSetPage> {
Box settingStorage = GStrorage.setting;
late List defaultNavTabs;
late List<int> navBarSort;
@override
void initState() {
super.initState();
defaultNavTabs = defaultNavigationBars;
navBarSort = settingStorage
.get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2, 3]);
// 对 tabData 进行排序
defaultNavTabs.sort((a, b) {
int indexA = navBarSort.indexOf(a['id']);
int indexB = navBarSort.indexOf(b['id']);
// 如果类型在 sortOrder 中不存在,则放在末尾
if (indexA == -1) indexA = navBarSort.length;
if (indexB == -1) indexB = navBarSort.length;
return indexA.compareTo(indexB);
});
}
void saveEdit() {
List<int> sortedTabbar = defaultNavTabs
.where((i) => navBarSort.contains(i['id']))
.map<int>((i) => i['id'])
.toList();
settingStorage.put(SettingBoxKey.navBarSort, sortedTabbar);
SmartDialog.showToast('保存成功,下次启动时生效');
}
void onReorder(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final tabsItem = defaultNavTabs.removeAt(oldIndex);
defaultNavTabs.insert(newIndex, tabsItem);
});
}
@override
Widget build(BuildContext context) {
final listTiles = [
for (int i = 0; i < defaultNavTabs.length; i++) ...[
CheckboxListTile(
key: Key(defaultNavTabs[i]['label']),
value: navBarSort.contains(defaultNavTabs[i]['id']),
onChanged: (bool? newValue) {
int tabTypeId = defaultNavTabs[i]['id'];
if (!newValue!) {
navBarSort.remove(tabTypeId);
} else {
navBarSort.add(tabTypeId);
}
setState(() {});
},
title: Text(defaultNavTabs[i]['label']),
secondary: const Icon(Icons.drag_indicator_rounded),
enabled: defaultNavTabs[i]['id'] != 0,
)
]
];
return Scaffold(
appBar: AppBar(
title: const Text('Navbar编辑'),
actions: [
TextButton(onPressed: () => saveEdit(), child: const Text('保存')),
const SizedBox(width: 12)
],
),
body: ReorderableListView(
onReorder: onReorder,
physics: const NeverScrollableScrollPhysics(),
footer: SizedBox(
height: MediaQuery.of(context).padding.bottom + 30,
),
children: listTiles,
),
);
}
}

View File

@ -284,12 +284,17 @@ class _StyleSettingState extends State<StyleSetting> {
onTap: () => Get.toNamed('/tabbarSetting'), onTap: () => Get.toNamed('/tabbarSetting'),
title: Text('首页tabbar', style: titleStyle), title: Text('首页tabbar', style: titleStyle),
), ),
ListTile(
dense: false,
onTap: () => Get.toNamed('/navbarSetting'),
title: Text('navbar设置', style: titleStyle),
),
if (Platform.isAndroid) if (Platform.isAndroid)
ListTile( ListTile(
dense: false, dense: false,
onTap: () => Get.toNamed('/displayModeSetting'), onTap: () => Get.toNamed('/displayModeSetting'),
title: Text('屏幕帧率', style: titleStyle), title: Text('屏幕帧率', style: titleStyle),
) ),
], ],
), ),
); );

View File

@ -44,6 +44,7 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
setState(() { setState(() {
_tempValue = value as T; _tempValue = value as T;
}); });
Navigator.pop(context, _tempValue);
}, },
), ),
] ]
@ -51,19 +52,6 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
), ),
); );
}), }),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () => Navigator.pop(context, _tempValue),
child: const Text('确定'),
)
],
); );
} }
} }

View File

@ -6,7 +6,6 @@ import '../../models/user/sub_folder.dart';
class SubDetailController extends GetxController { class SubDetailController extends GetxController {
late SubFolderItemData item; late SubFolderItemData item;
late int seasonId; late int seasonId;
late String heroTag; late String heroTag;
int currentPage = 1; int currentPage = 1;
@ -26,17 +25,23 @@ class SubDetailController extends GetxController {
super.onInit(); super.onInit();
} }
Future<dynamic> queryUserSubFolderDetail({type = 'init'}) async { Future<dynamic> queryUserSeasonList({type = 'init'}) async {
if (type == 'onLoad' && subList.length >= mediaCount) { if (type == 'onLoad' && subList.length >= mediaCount) {
loadingText.value = '没有更多了'; loadingText.value = '没有更多了';
return; return;
} }
isLoadingMore = true; isLoadingMore = true;
var res = await UserHttp.userSubFolderDetail( var res = type == 21
seasonId: seasonId, ? await UserHttp.userSeasonList(
ps: 20, seasonId: seasonId,
pn: currentPage, ps: 20,
); pn: currentPage,
)
: await UserHttp.userResourceList(
seasonId: seasonId,
ps: 20,
pn: currentPage,
);
if (res['status']) { if (res['status']) {
subInfo.value = res['data'].info; subInfo.value = res['data'].info;
if (currentPage == 1 && type == 'init') { if (currentPage == 1 && type == 'init') {
@ -55,6 +60,6 @@ class SubDetailController extends GetxController {
} }
onLoad() { onLoad() {
queryUserSubFolderDetail(type: 'onLoad'); queryUserSeasonList(type: 'onLoad');
} }
} }

View File

@ -26,13 +26,11 @@ class _SubDetailPageState extends State<SubDetailPage> {
Get.put(SubDetailController()); Get.put(SubDetailController());
late StreamController<bool> titleStreamC; // a late StreamController<bool> titleStreamC; // a
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
late String seasonId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
seasonId = Get.parameters['seasonId']!; _futureBuilderFuture = _subDetailController.queryUserSeasonList();
_futureBuilderFuture = _subDetailController.queryUserSubFolderDetail();
titleStreamC = StreamController<bool>(); titleStreamC = StreamController<bool>();
_controller.addListener( _controller.addListener(
() { () {
@ -161,15 +159,18 @@ class _SubDetailPageState extends State<SubDetailPage> {
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Obx(
'${Utils.numFormat(_subDetailController.item.viewCount)}次播放', () => Text(
style: TextStyle( '${Utils.numFormat(_subDetailController.subInfo.value.cntInfo?['play'])}次播放',
fontSize: Theme.of(context) style: TextStyle(
.textTheme fontSize: Theme.of(context)
.labelSmall! .textTheme
.fontSize, .labelSmall!
color: Theme.of(context).colorScheme.outline), .fontSize,
), color:
Theme.of(context).colorScheme.outline),
),
)
], ],
), ),
), ),
@ -182,14 +183,12 @@ class _SubDetailPageState extends State<SubDetailPage> {
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx( child: Text(
() => Text( '${_subDetailController.item.mediaCount}条视频',
'${_subDetailController.subList.length}条视频', style: TextStyle(
style: TextStyle( fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
fontSize: color: Theme.of(context).colorScheme.outline,
Theme.of(context).textTheme.labelMedium!.fontSize, letterSpacing: 1,
color: Theme.of(context).colorScheme.outline,
letterSpacing: 1),
), ),
), ),
), ),

View File

@ -22,6 +22,7 @@ import 'package:screen_brightness/screen_brightness.dart';
import '../../../models/video/subTitile/content.dart'; import '../../../models/video/subTitile/content.dart';
import '../../../http/danmaku.dart'; import '../../../http/danmaku.dart';
import '../../../plugin/pl_player/models/bottom_control_type.dart';
import '../../../utils/id_utils.dart'; import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart'; import 'widgets/header_control.dart';
@ -98,6 +99,13 @@ class VideoDetailController extends GetxController
<SubTitileContentModel>[].obs; <SubTitileContentModel>[].obs;
late bool enableRelatedVideo; late bool enableRelatedVideo;
List subtitles = []; List subtitles = [];
RxList<BottomControlType> bottomList = [
BottomControlType.playOrPause,
BottomControlType.time,
BottomControlType.space,
BottomControlType.fit,
BottomControlType.fullscreen,
].obs;
@override @override
void onInit() { void onInit() {

View File

@ -18,6 +18,9 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../../common/pages_bottom_sheet.dart';
import '../../../../models/common/video_episode_type.dart';
import '../../../../utils/drawer.dart';
import '../related/index.dart'; import '../related/index.dart';
import 'widgets/group_panel.dart'; import 'widgets/group_panel.dart';
@ -25,15 +28,10 @@ class VideoIntroController extends GetxController {
VideoIntroController({required this.bvid}); VideoIntroController({required this.bvid});
// 视频bvid // 视频bvid
String bvid; String bvid;
// 请求状态
RxBool isLoading = false.obs;
// 视频详情 请求返回 // 视频详情 请求返回
Rx<VideoDetailData> videoDetail = VideoDetailData().obs; Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
// up主粉丝数 // up主粉丝数
Map userStat = {'follower': '-'}; Map userStat = {'follower': '-'};
// 是否点赞 // 是否点赞
RxBool hasLike = false.obs; RxBool hasLike = false.obs;
// 是否投币 // 是否投币
@ -59,6 +57,7 @@ class VideoIntroController extends GetxController {
bool isPaused = false; bool isPaused = false;
String heroTag = ''; String heroTag = '';
late ModelResult modelResult; late ModelResult modelResult;
PersistentBottomSheetController? bottomSheetController;
@override @override
void onInit() { void onInit() {
@ -562,4 +561,52 @@ class VideoIntroController extends GetxController {
} }
return res; return res;
} }
hiddenEpisodeBottomSheet() {
bottomSheetController?.close();
}
// 播放器底栏 选集 回调
void showEposideHandler() {
late List episodes;
VideoEpidoesType dataType = VideoEpidoesType.videoEpisode;
if (videoDetail.value.ugcSeason != null) {
dataType = VideoEpidoesType.videoEpisode;
final List<SectionItem> sections = videoDetail.value.ugcSeason!.sections!;
for (int i = 0; i < sections.length; i++) {
final List<EpisodeItem> episodesList = sections[i].episodes!;
for (int j = 0; j < episodesList.length; j++) {
if (episodesList[j].cid == lastPlayCid.value) {
episodes = episodesList;
continue;
}
}
}
}
if (videoDetail.value.pages != null &&
videoDetail.value.pages!.length > 1) {
dataType = VideoEpidoesType.videoPart;
episodes = videoDetail.value.pages!;
}
DrawerUtils.showRightDialog(
child: EpisodeBottomSheet(
episodes: episodes,
currentCid: lastPlayCid.value,
dataType: dataType,
context: Get.context!,
sheetHeight: Get.size.height,
isFullScreen: true,
changeFucCall: (item, index) {
if (dataType == VideoEpidoesType.videoEpisode) {
changeSeasonOrbangu(IdUtils.av2bv(item.aid), item.cid, item.aid);
}
if (dataType == VideoEpidoesType.videoPart) {
changeSeasonOrbangu(bvid, item.cid, null);
}
SmartDialog.dismiss();
},
).buildShowContent(Get.context!),
);
}
} }

View File

@ -20,8 +20,8 @@ import '../../../../http/user.dart';
import 'widgets/action_item.dart'; import 'widgets/action_item.dart';
import 'widgets/fav_panel.dart'; import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart'; import 'widgets/intro_detail.dart';
import 'widgets/page.dart'; import 'widgets/page_panel.dart';
import 'widgets/season.dart'; import 'widgets/season_panel.dart';
class VideoIntroPanel extends StatefulWidget { class VideoIntroPanel extends StatefulWidget {
final String bvid; final String bvid;
@ -373,7 +373,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
/// 点赞收藏转发 /// 点赞收藏转发
actionGrid(context, videoIntroController), actionGrid(context, videoIntroController),
// 合集 // 合集 videoPart 简洁
if (widget.videoDetail!.ugcSeason != null) ...[ if (widget.videoDetail!.ugcSeason != null) ...[
Obx( Obx(
() => SeasonPanel( () => SeasonPanel(
@ -383,19 +383,31 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
: widget.videoDetail!.pages!.first.cid, : widget.videoDetail!.pages!.first.cid,
sheetHeight: sheetHeight, sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => changeFuc: (bvid, cid, aid) =>
videoIntroController.changeSeasonOrbangu(bvid, cid, aid), videoIntroController.changeSeasonOrbangu(
bvid,
cid,
aid,
),
videoIntroCtr: videoIntroController,
), ),
) )
], ],
// 合集 videoEpisode
if (widget.videoDetail!.pages != null && if (widget.videoDetail!.pages != null &&
widget.videoDetail!.pages!.length > 1) ...[ widget.videoDetail!.pages!.length > 1) ...[
Obx(() => PagesPanel( Obx(
pages: widget.videoDetail!.pages!, () => PagesPanel(
cid: videoIntroController.lastPlayCid.value, pages: widget.videoDetail!.pages!,
sheetHeight: sheetHeight, cid: videoIntroController.lastPlayCid.value,
changeFuc: (cid) => videoIntroController.changeSeasonOrbangu( sheetHeight: sheetHeight,
videoIntroController.bvid, cid, null), changeFuc: (cid) => videoIntroController.changeSeasonOrbangu(
)) videoIntroController.bvid,
cid,
null,
),
videoIntroCtr: videoIntroController,
),
)
], ],
GestureDetector( GestureDetector(
onTap: onPushMember, onTap: onPushMember,

View File

@ -1,258 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
class PagesPanel extends StatefulWidget {
const PagesPanel({
super.key,
required this.pages,
this.cid,
this.sheetHeight,
this.changeFuc,
});
final List<Part> pages;
final int? cid;
final double? sheetHeight;
final Function? changeFuc;
@override
State<PagesPanel> createState() => _PagesPanelState();
}
class _PagesPanelState extends State<PagesPanel> {
late List<Part> episodes;
late int cid;
late int currentIndex;
final String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
cid = widget.cid!;
episodes = widget.pages;
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
currentIndex = episodes.indexWhere((Part e) => e.cid == cid);
_videoDetailController.cid.listen((int p0) {
cid = p0;
setState(() {});
currentIndex = episodes.indexWhere((Part e) => e.cid == cid);
});
}
void changeFucCall(item, i) async {
await widget.changeFuc!(
item.cid,
);
currentIndex = i;
setState(() {});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Widget buildEpisodeListItem(
Part episode,
int index,
bool isCurrentIndex,
) {
Color primary = Theme.of(context).colorScheme.primary;
return ListTile(
onTap: () {
changeFucCall(episode, index);
Get.back();
},
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.gif',
color: primary,
height: 12,
)
: null,
title: Text(
episode.pagePart!,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex
? primary
: Theme.of(context).colorScheme.onSurface,
),
),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
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: (BuildContext context) {
return StatefulBuilder(builder:
(BuildContext context, StateSetter setState) {
WidgetsBinding.instance
.addPostFrameCallback((_) async {
await Future.delayed(
const Duration(milliseconds: 200));
_scrollController.jumpTo(currentIndex * 56);
});
return 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(
controller: _scrollController,
itemCount: episodes.length + 1,
itemBuilder:
(BuildContext context, int index) {
bool isLastItem =
index == episodes.length;
bool isCurrentIndex =
currentIndex == index;
return isLastItem
? SizedBox(
height: MediaQuery.of(context)
.padding
.bottom +
20,
)
: buildEpisodeListItem(
episodes[index],
index,
isCurrentIndex,
);
},
),
),
),
],
),
);
});
},
);
},
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: (BuildContext context, int i) {
bool isCurrentIndex = currentIndex == 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: <Widget>[
if (isCurrentIndex) ...<Widget>[
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: isCurrentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
overflow: TextOverflow.ellipsis,
))
],
),
),
),
),
);
},
),
)
],
);
}
}

View File

@ -0,0 +1,185 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/introduction/index.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../../../../../common/pages_bottom_sheet.dart';
import '../../../../../models/common/video_episode_type.dart';
class PagesPanel extends StatefulWidget {
const PagesPanel({
super.key,
required this.pages,
required this.cid,
this.sheetHeight,
this.changeFuc,
required this.videoIntroCtr,
});
final List<Part> pages;
final int cid;
final double? sheetHeight;
final Function? changeFuc;
final VideoIntroController videoIntroCtr;
@override
State<PagesPanel> createState() => _PagesPanelState();
}
class _PagesPanelState extends State<PagesPanel> {
late List<Part> episodes;
late int cid;
late RxInt currentIndex = (-1).obs;
final String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
final ScrollController listViewScrollCtr = ScrollController();
final ItemScrollController itemScrollController = ItemScrollController();
late PersistentBottomSheetController? _bottomSheetController;
@override
void initState() {
super.initState();
cid = widget.cid;
episodes = widget.pages;
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
currentIndex.value = episodes.indexWhere((Part e) => e.cid == cid);
scrollToIndex();
_videoDetailController.cid.listen((int p0) {
cid = p0;
currentIndex.value = episodes.indexWhere((Part e) => e.cid == cid);
scrollToIndex();
});
}
@override
void dispose() {
listViewScrollCtr.dispose();
super.dispose();
}
void changeFucCall(item, i) async {
print('pages changeFucCall');
widget.changeFuc?.call(item.cid);
currentIndex.value = i;
_bottomSheetController?.close();
scrollToIndex();
}
void scrollToIndex() {
WidgetsBinding.instance.addPostFrameCallback((_) {
// 在回调函数中获取更新后的状态
final double offset = min((currentIndex * 150) - 75,
listViewScrollCtr.position.maxScrollExtent);
listViewScrollCtr.animateTo(
offset,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 10, bottom: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('视频选集 '),
Expanded(
child: Obx(() => Text(
' 正在播放:${widget.pages[currentIndex.value].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: () {
widget.videoIntroCtr.bottomSheetController =
_bottomSheetController = EpisodeBottomSheet(
currentCid: cid,
episodes: episodes,
changeFucCall: changeFucCall,
sheetHeight: widget.sheetHeight,
dataType: VideoEpidoesType.videoPart,
context: context,
).show(context);
},
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,
controller: listViewScrollCtr,
itemCount: widget.pages.length,
itemExtent: 150,
itemBuilder: (BuildContext context, int i) {
bool isCurrentIndex = currentIndex.value == 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: <Widget>[
if (isCurrentIndex) ...<Widget>[
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: isCurrentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
overflow: TextOverflow.ellipsis,
))
],
),
),
),
),
);
},
),
)
],
);
}
}

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/pages_bottom_sheet.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../../../../../models/common/video_episode_type.dart';
import '../controller.dart';
class SeasonPanel extends StatefulWidget { class SeasonPanel extends StatefulWidget {
const SeasonPanel({ const SeasonPanel({
@ -12,11 +15,13 @@ class SeasonPanel extends StatefulWidget {
this.cid, this.cid,
this.sheetHeight, this.sheetHeight,
this.changeFuc, this.changeFuc,
required this.videoIntroCtr,
}); });
final UgcSeason ugcSeason; final UgcSeason ugcSeason;
final int? cid; final int? cid;
final double? sheetHeight; final double? sheetHeight;
final Function? changeFuc; final Function? changeFuc;
final VideoIntroController videoIntroCtr;
@override @override
State<SeasonPanel> createState() => _SeasonPanelState(); State<SeasonPanel> createState() => _SeasonPanelState();
@ -25,11 +30,11 @@ class SeasonPanel extends StatefulWidget {
class _SeasonPanelState extends State<SeasonPanel> { class _SeasonPanelState extends State<SeasonPanel> {
late List<EpisodeItem> episodes; late List<EpisodeItem> episodes;
late int cid; late int cid;
late int currentIndex; late RxInt currentIndex = (-1).obs;
final String heroTag = Get.arguments['heroTag']; final String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController; late VideoDetailController _videoDetailController;
final ScrollController _scrollController = ScrollController();
final ItemScrollController itemScrollController = ItemScrollController(); final ItemScrollController itemScrollController = ItemScrollController();
late PersistentBottomSheetController? _bottomSheetController;
@override @override
void initState() { void initState() {
@ -52,32 +57,21 @@ class _SeasonPanelState extends State<SeasonPanel> {
} }
/// season_id episodes /// season_id episodes
// episodes = widget.ugcSeason.sections! currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
// .firstWhere((e) => e.seasonId == widget.ugcSeason.id)
// .episodes!;
currentIndex = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
_videoDetailController.cid.listen((int p0) { _videoDetailController.cid.listen((int p0) {
cid = p0; cid = p0;
setState(() {}); currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
currentIndex = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
}); });
} }
void changeFucCall(item, int i) async { void changeFucCall(item, int i) async {
await widget.changeFuc!( widget.changeFuc?.call(
IdUtils.av2bv(item.aid), IdUtils.av2bv(item.aid),
item.cid, item.cid,
item.aid, item.aid,
); );
currentIndex = i; currentIndex.value = i;
Get.back(); _bottomSheetController?.close();
setState(() {});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
} }
Widget buildEpisodeListItem( Widget buildEpisodeListItem(
@ -123,71 +117,17 @@ class _SeasonPanelState extends State<SeasonPanel> {
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: InkWell( child: InkWell(
onTap: () => showBottomSheet( onTap: () {
context: context, widget.videoIntroCtr.bottomSheetController =
builder: (BuildContext context) { _bottomSheetController = EpisodeBottomSheet(
return StatefulBuilder( currentCid: cid,
builder: (BuildContext context, StateSetter setState) { episodes: episodes,
WidgetsBinding.instance.addPostFrameCallback((_) async { changeFucCall: changeFucCall,
itemScrollController.jumpTo(index: currentIndex); sheetHeight: widget.sheetHeight,
}); dataType: VideoEpidoesType.videoEpisode,
return Container( context: context,
height: widget.sheetHeight, ).show(context);
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: ScrollablePositionedList.builder(
itemCount: episodes.length + 1,
itemBuilder: (BuildContext context, int index) {
bool isLastItem = index == episodes.length;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(
height: MediaQuery.of(context)
.padding
.bottom +
20,
)
: buildEpisodeListItem(
episodes[index],
index,
isCurrentIndex,
);
},
itemScrollController: itemScrollController,
),
),
),
],
),
);
});
},
),
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
child: Row( child: Row(
@ -206,10 +146,10 @@ class _SeasonPanelState extends State<SeasonPanel> {
height: 12, height: 12,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Obx(() => Text(
'${currentIndex + 1}/${episodes.length}', '${currentIndex.value + 1}/${episodes.length}',
style: Theme.of(context).textTheme.labelMedium, style: Theme.of(context).textTheme.labelMedium,
), )),
const SizedBox(width: 6), const SizedBox(width: 6),
const Icon( const Icon(
Icons.arrow_forward_ios_outlined, Icons.arrow_forward_ios_outlined,

View File

@ -24,6 +24,7 @@ import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/services/service_locator.dart'; import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../../../plugin/pl_player/models/bottom_control_type.dart';
import '../../../services/shutdown_timer_service.dart'; import '../../../services/shutdown_timer_service.dart';
import 'widgets/app_bar.dart'; import 'widgets/app_bar.dart';
@ -176,6 +177,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController?.isFullScreen.listen((bool isFullScreen) { plPlayerController?.isFullScreen.listen((bool isFullScreen) {
if (isFullScreen) { if (isFullScreen) {
vdCtr.hiddenReplyReplyPanel(); vdCtr.hiddenReplyReplyPanel();
videoIntroController.hiddenEpisodeBottomSheet();
if (videoIntroController.videoDetail.value.ugcSeason != null ||
(videoIntroController.videoDetail.value.pages != null &&
videoIntroController.videoDetail.value.pages!.length > 1)) {
vdCtr.bottomList.insert(3, BottomControlType.episode);
}
} else {
if (vdCtr.bottomList.contains(BottomControlType.episode)) {
vdCtr.bottomList.removeAt(3);
}
} }
}); });
} }
@ -292,15 +303,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
() { () {
return !vdCtr.autoPlay.value return !vdCtr.autoPlay.value
? const SizedBox() ? const SizedBox()
: PLVideoPlayer( : Obx(
controller: plPlayerController!, () => PLVideoPlayer(
headerControl: vdCtr.headerControl, controller: plPlayerController!,
danmuWidget: Obx( headerControl: vdCtr.headerControl,
() => PlDanmaku( danmuWidget: PlDanmaku(
key: Key(vdCtr.danmakuCid.value.toString()), key: Key(vdCtr.danmakuCid.value.toString()),
cid: vdCtr.danmakuCid.value, cid: vdCtr.danmakuCid.value,
playerController: plPlayerController!, playerController: plPlayerController!,
), ),
bottomList: vdCtr.bottomList,
showEposideCb: () => vdCtr.videoType == SearchType.video
? videoIntroController.showEposideHandler()
: bangumiIntroController.showEposideHandler(),
), ),
); );
}, },

View File

@ -11,12 +11,14 @@ import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/pages/dlna/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; 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/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/services/shutdown_timer_service.dart'; import 'package:pilipala/services/shutdown_timer_service.dart';
import '../../../../http/danmaku.dart';
import '../../../../models/common/search_type.dart'; import '../../../../models/common/search_type.dart';
import '../../../../models/video_detail_res.dart'; import '../../../../models/video_detail_res.dart';
import '../introduction/index.dart'; import '../introduction/index.dart';
@ -52,7 +54,7 @@ class _HeaderControlState extends State<HeaderControl> {
final Box<dynamic> videoStorage = GStrorage.video; final Box<dynamic> videoStorage = GStrorage.video;
late List<double> speedsList; late List<double> speedsList;
double buttonSpace = 8; double buttonSpace = 8;
bool showTitle = false; RxBool isFullScreen = false.obs;
late String heroTag; late String heroTag;
late VideoIntroController videoIntroController; late VideoIntroController videoIntroController;
late VideoDetailData videoDetail; late VideoDetailData videoDetail;
@ -69,13 +71,8 @@ class _HeaderControlState extends State<HeaderControl> {
} }
void fullScreenStatusListener() { void fullScreenStatusListener() {
widget.videoDetailCtr!.plPlayerController.isFullScreen widget.videoDetailCtr!.plPlayerController.isFullScreen.listen((bool val) {
.listen((bool isFullScreen) { isFullScreen.value = val;
if (isFullScreen) {
showTitle = true;
} else {
showTitle = false;
}
/// TODO setState() called after dispose() /// TODO setState() called after dispose()
if (mounted) { if (mounted) {
@ -162,7 +159,7 @@ class _HeaderControlState extends State<HeaderControl> {
dense: true, dense: true,
leading: leading:
const Icon(Icons.hourglass_top_outlined, size: 20), const Icon(Icons.hourglass_top_outlined, size: 20),
title: const Text('定时关闭(测试)', style: titleStyle), title: const Text('定时关闭', style: titleStyle),
), ),
ListTile( ListTile(
onTap: () => {Get.back(), showSetVideoQa()}, onTap: () => {Get.back(), showSetVideoQa()},
@ -218,6 +215,87 @@ 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; // 开始发送,更新状态
});
//修改按钮文字
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 { void scheduleExit() async {
const List<int> scheduleTimeChoices = [ const List<int> scheduleTimeChoices = [
@ -653,9 +731,12 @@ class _HeaderControlState extends State<HeaderControl> {
margin: const EdgeInsets.all(12), margin: const EdgeInsets.all(12),
child: Column( child: Column(
children: [ children: [
SizedBox( const SizedBox(
height: 45, height: 45,
child: Center(child: Text('选择解码格式', style: titleStyle))), child: Center(
child: Text('选择解码格式', style: titleStyle),
),
),
Expanded( Expanded(
child: Material( child: Material(
child: ListView( child: ListView(
@ -1000,9 +1081,12 @@ class _HeaderControlState extends State<HeaderControl> {
margin: const EdgeInsets.all(12), margin: const EdgeInsets.all(12),
child: Column( child: Column(
children: [ children: [
SizedBox( const SizedBox(
height: 45, height: 45,
child: Center(child: Text('选择播放顺序', style: titleStyle))), child: Center(
child: Text('选择播放顺序', style: titleStyle),
),
),
Expanded( Expanded(
child: Material( child: Material(
child: ListView( child: ListView(
@ -1079,7 +1163,7 @@ class _HeaderControlState extends State<HeaderControl> {
}, },
), ),
SizedBox(width: buttonSpace), SizedBox(width: buttonSpace),
if (showTitle && if (isFullScreen.value &&
isLandscape && isLandscape &&
widget.videoType == SearchType.video) ...[ widget.videoType == SearchType.video) ...[
Column( Column(
@ -1087,11 +1171,13 @@ class _HeaderControlState extends State<HeaderControl> {
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200), constraints: const BoxConstraints(maxWidth: 200),
child: Text( child: Obx(
videoIntroController.videoDetail.value.title ?? '', () => Text(
style: const TextStyle( videoIntroController.videoDetail.value.title ?? '',
color: Colors.white, style: const TextStyle(
fontSize: 16, color: Colors.white,
fontSize: 16,
),
), ),
), ),
), ),
@ -1131,6 +1217,59 @@ class _HeaderControlState extends State<HeaderControl> {
// ), // ),
// fuc: () => _.screenshot(), // fuc: () => _.screenshot(),
// ), // ),
ComBtn(
icon: const Icon(
Icons.cast,
size: 19,
color: Colors.white,
),
fuc: () async {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return LiveDlnaPage(
datasource: widget.videoDetailCtr!.videoUrl);
},
);
},
),
if (isFullScreen.value) ...[
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), SizedBox(width: buttonSpace),
if (Platform.isAndroid) ...<Widget>[ if (Platform.isAndroid) ...<Widget>[
SizedBox( SizedBox(

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
class RightDrawer extends StatefulWidget {
const RightDrawer({super.key});
@override
State<RightDrawer> createState() => _RightDrawerState();
}
class _RightDrawerState extends State<RightDrawer> {
@override
Widget build(BuildContext context) {
return Drawer(
shadowColor: Colors.transparent,
elevation: 0,
backgroundColor:
Theme.of(context).colorScheme.surface.withOpacity(0.8));
}
}

View File

@ -4,6 +4,7 @@ enum BottomControlType {
next, next,
time, time,
space, space,
episode,
fit, fit,
speed, speed,
fullscreen, fullscreen,

View File

@ -37,6 +37,7 @@ class PLVideoPlayer extends StatefulWidget {
this.bottomList, this.bottomList,
this.customWidget, this.customWidget,
this.customWidgets, this.customWidgets,
this.showEposideCb,
super.key, super.key,
}); });
@ -49,6 +50,7 @@ class PLVideoPlayer extends StatefulWidget {
final Widget? customWidget; final Widget? customWidget;
final List<Widget>? customWidgets; final List<Widget>? customWidgets;
final Function? showEposideCb;
@override @override
State<PLVideoPlayer> createState() => _PLVideoPlayerState(); State<PLVideoPlayer> createState() => _PLVideoPlayerState();
@ -214,8 +216,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 上一集 /// 上一集
BottomControlType.pre: ComBtn( BottomControlType.pre: ComBtn(
icon: const Icon( icon: const Icon(
Icons.skip_previous_outlined, Icons.skip_previous_rounded,
size: 15, size: 21,
color: Colors.white, color: Colors.white,
), ),
fuc: () {}, fuc: () {},
@ -229,8 +231,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 下一集 /// 下一集
BottomControlType.next: ComBtn( BottomControlType.next: ComBtn(
icon: const Icon( icon: const Icon(
Icons.last_page_outlined, Icons.skip_next_rounded,
size: 15, size: 21,
color: Colors.white, color: Colors.white,
), ),
fuc: () {}, fuc: () {},
@ -239,6 +241,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 时间进度 /// 时间进度
BottomControlType.time: Row( BottomControlType.time: Row(
children: [ children: [
const SizedBox(width: 8),
Obx(() { Obx(() {
return Text( return Text(
_.durationSeconds.value >= 3600 _.durationSeconds.value >= 3600
@ -266,6 +269,24 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 空白占位 /// 空白占位
BottomControlType.space: const Spacer(), BottomControlType.space: const Spacer(),
/// 选集
BottomControlType.episode: SizedBox(
height: 30,
width: 30,
child: TextButton(
onPressed: () {
widget.showEposideCb?.call();
},
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
child: const Text(
'选集',
style: TextStyle(color: Colors.white, fontSize: 13),
),
),
),
/// 画面比例 /// 画面比例
BottomControlType.fit: SizedBox( BottomControlType.fit: SizedBox(
height: 30, height: 30,

View File

@ -68,91 +68,8 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
); );
}, },
), ),
Row( Row(children: [...buildBottomControl!]),
children: [...buildBottomControl!], const SizedBox(height: 10),
),
// Row(
// children: [
// PlayOrPauseButton(
// controller: _,
// ),
// const SizedBox(width: 4),
// // 播放时间
// Obx(() {
// return Text(
// _.durationSeconds.value >= 3600
// ? printDurationWithHours(
// Duration(seconds: _.positionSeconds.value))
// : printDuration(
// Duration(seconds: _.positionSeconds.value)),
// style: textStyle,
// );
// }),
// const SizedBox(width: 2),
// const Text('/', style: textStyle),
// const SizedBox(width: 2),
// Obx(
// () => Text(
// _.durationSeconds.value >= 3600
// ? printDurationWithHours(
// Duration(seconds: _.durationSeconds.value))
// : printDuration(
// Duration(seconds: _.durationSeconds.value)),
// style: textStyle,
// ),
// ),
// const Spacer(),
// // 倍速
// // Obx(
// // () => SizedBox(
// // width: 45,
// // height: 34,
// // child: TextButton(
// // style: ButtonStyle(
// // padding: MaterialStateProperty.all(EdgeInsets.zero),
// // ),
// // onPressed: () {
// // _.togglePlaybackSpeed();
// // },
// // child: Text(
// // '${_.playbackSpeed.toString()}X',
// // style: textStyle,
// // ),
// // ),
// // ),
// // ),
// SizedBox(
// height: 30,
// child: TextButton(
// onPressed: () => _.toggleVideoFit(),
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// child: Obx(
// () => Text(
// _.videoFitDEsc.value,
// style: const TextStyle(color: Colors.white, fontSize: 13),
// ),
// ),
// ),
// ),
// const SizedBox(width: 10),
// // 全屏
// Obx(
// () => ComBtn(
// icon: Icon(
// _.isFullScreen.value
// ? FontAwesomeIcons.compress
// : FontAwesomeIcons.expand,
// size: 15,
// color: Colors.white,
// ),
// fuc: () => triggerFullScreen!(),
// ),
// ),
// ],
// ),
const SizedBox(height: 12),
], ],
), ),
); );

View File

@ -39,6 +39,7 @@ import '../pages/setting/pages/color_select.dart';
import '../pages/setting/pages/display_mode.dart'; import '../pages/setting/pages/display_mode.dart';
import '../pages/setting/pages/font_size_select.dart'; import '../pages/setting/pages/font_size_select.dart';
import '../pages/setting/pages/home_tabbar_set.dart'; import '../pages/setting/pages/home_tabbar_set.dart';
import '../pages/setting/pages/navigation_bar_set.dart';
import '../pages/setting/pages/play_gesture_set.dart'; import '../pages/setting/pages/play_gesture_set.dart';
import '../pages/setting/pages/play_speed_set.dart'; import '../pages/setting/pages/play_speed_set.dart';
import '../pages/setting/recommend_setting.dart'; import '../pages/setting/recommend_setting.dart';
@ -170,6 +171,9 @@ class Routes {
// 播放器手势 // 播放器手势
CustomGetPage( CustomGetPage(
name: '/playerGestureSet', page: () => const PlayGesturePage()), name: '/playerGestureSet', page: () => const PlayGesturePage()),
// navigation bar
CustomGetPage(
name: '/navbarSetting', page: () => const NavigationBarSetPage()),
]; ];
} }

39
lib/utils/drawer.dart Normal file
View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class DrawerUtils {
static void showRightDialog({
required Widget child,
double width = 400,
bool useSystem = false,
}) {
SmartDialog.show(
alignment: Alignment.topRight,
animationBuilder: (controller, child, animationParam) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(controller.view),
child: child,
);
},
useSystem: useSystem,
maskColor: Colors.black.withOpacity(0.5),
animationTime: const Duration(milliseconds: 200),
builder: (context) => Container(
width: width,
color: Theme.of(context).scaffoldBackgroundColor,
child: SafeArea(
left: false,
right: false,
bottom: false,
child: MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.zero),
child: child,
),
),
),
);
}
}

View File

@ -148,7 +148,8 @@ class SettingBoxKey {
hideTabBar = 'hideTabBar', // 收起底栏 hideTabBar = 'hideTabBar', // 收起底栏
tabbarSort = 'tabbarSort', // 首页tabbar tabbarSort = 'tabbarSort', // 首页tabbar
dynamicBadgeMode = 'dynamicBadgeMode', dynamicBadgeMode = 'dynamicBadgeMode',
enableGradientBg = 'enableGradientBg'; enableGradientBg = 'enableGradientBg',
navBarSort = 'navBarSort';
} }
class LocalCacheKey { class LocalCacheKey {

View File

@ -409,6 +409,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
dlna_dart:
dependency: "direct main"
description:
name: dlna_dart
sha256: ae07c1c53077bbf58756fa589f936968719b0085441981d33e74f82f89d1d281
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.8"
dynamic_color: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -144,6 +144,8 @@ dependencies:
disable_battery_optimization: ^1.1.1 disable_battery_optimization: ^1.1.1
# 展开/收起 # 展开/收起
expandable: ^5.0.1 expandable: ^5.0.1
# 投屏
dlna_dart: ^0.0.8
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: