Merge branch 'main' into feature-updateVideoDetailStructure

This commit is contained in:
guozhigq
2024-04-13 13:31:49 +08:00
76 changed files with 1959 additions and 1080 deletions

View File

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

View File

@ -26,13 +26,13 @@
Xcode 13.4 不支持**auto_orientation**,请注释相关代码
```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)
[] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[] Xcode - develop for iOS and macOS (Xcode 15.1)
[] Chrome - develop for the web
[] Android Studio (version 2022.3)
[] VS Code (version 1.85.1)
[] VS Code (version 1.87.2)
[] Connected device (3 available)
[] Network resources
@ -44,6 +44,9 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
## 技术交流
Telegram: https://t.me/+lm_oOVmF0RJiODk1
Tg Beta版本@PiliPala_Beta
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

@ -484,11 +484,17 @@ class Api {
/// 激活buvid3
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';
/// 获取字幕配置
static const getSubtitleConfig = '/x/player/v2';
/// 我的订阅
static const userSubFolder = '/x/v3/fav/folder/collected/list';
/// 我的订阅详情
static const userSubFolderDetail = '/x/space/fav/season/list';
/// 我的订阅详情 type 21
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';
@ -503,4 +509,6 @@ class Api {
/// 排行榜
static const String getRankApi = "/x/web-interface/ranking/v2";
/// 取消订阅
static const String cancelSub = '/x/v3/fav/season/unfav';
}

View File

@ -330,12 +330,12 @@ class UserHttp {
}
}
static Future userSubFolderDetail({
static Future userSeasonList({
required int seasonId,
required int pn,
required int ps,
}) async {
var res = await Request().get(Api.userSubFolderDetail, data: {
var res = await Request().get(Api.userSeasonList, data: {
'season_id': seasonId,
'ps': ps,
'pn': pn,
@ -349,4 +349,50 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
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 {
var res = await Request().post(
Api.cancelSub,
queryParameters: {
'platform': 'web',
'season_id': seasonId,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
}

View File

@ -8,9 +8,11 @@ import '../models/model_rec_video_item.dart';
import '../models/user/fav_folder.dart';
import '../models/video/ai.dart';
import '../models/video/play/url.dart';
import '../models/video/subTitile/result.dart';
import '../models/video_detail_res.dart';
import '../utils/recommend_filter.dart';
import '../utils/storage.dart';
import '../utils/subtitle.dart';
import '../utils/wbi_sign.dart';
import 'api.dart';
import 'init.dart';
@ -476,6 +478,25 @@ class VideoHttp {
}
}
static Future getSubtitle({int? cid, String? bvid}) async {
var res = await Request().get(Api.getSubtitleConfig, data: {
'cid': cid,
'bvid': bvid,
});
try {
if (res.data['code'] == 0) {
return {
'status': true,
'data': SubTitlteModel.fromJson(res.data['data']),
};
} else {
return {'status': false, 'data': [], 'msg': res.data['msg']};
}
} catch (err) {
print(err);
}
}
// 视频排行
static Future getRankVideoList(int rid) async {
try {
@ -498,4 +519,12 @@ class VideoHttp {
return {'status': false, 'data': [], 'msg': err};
}
}
// 获取字幕内容
static Future<Map<String, dynamic>> getSubtitleContent(url) async {
var res = await Request().get('https:$url');
final String content = SubTitleUtils.convertToWebVTT(res.data['body']);
final List body = res.data['body'];
return {'content': content, 'body': body};
}
}

View File

@ -20,6 +20,7 @@ import 'package:pilipala/services/disable_battery_opt.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/global_data.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
import 'package:pilipala/utils/recommend_filter.dart';
@ -34,6 +35,7 @@ void main() async {
.then((_) async {
await GStrorage.init();
await setupServiceLocator();
clearLogs();
Request();
await Request.setCookie();
RecommendFilter();
@ -63,14 +65,8 @@ void main() async {
},
);
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
statusBarColor: Colors.transparent,
));
Data.init();
GlobalData();
PiliSchame.init();
DisableBatteryOpt();
});
@ -133,45 +129,51 @@ class MyApp extends StatelessWidget {
brightness: Brightness.dark,
);
}
final SnackBarThemeData snackBarThemeData = SnackBarThemeData(
actionTextColor: darkColorScheme.primary,
backgroundColor: darkColorScheme.secondaryContainer,
closeIconColor: darkColorScheme.secondary,
contentTextStyle: TextStyle(color: darkColorScheme.secondary),
elevation: 20,
);
ThemeData themeData = ThemeData(
// fontFamily: 'HarmonyOS',
colorScheme: currentThemeValue == ThemeType.dark
? darkColorScheme
: lightColorScheme,
snackBarTheme: snackBarThemeData,
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: ZoomPageTransitionsBuilder(
allowEnterRouteSnapshotting: false,
),
},
),
);
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: GlobalData().enableMYBar
? const Color(0x00010000)
: themeData.canvasColor,
systemNavigationBarDividerColor: GlobalData().enableMYBar
? const Color(0x00010000)
: themeData.canvasColor,
systemNavigationBarIconBrightness: currentThemeValue == ThemeType.dark
? Brightness.light
: Brightness.dark,
statusBarColor: Colors.transparent,
));
// 图片缓存
// PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20;
return GetMaterialApp(
title: 'PiLiPaLa',
theme: ThemeData(
// fontFamily: 'HarmonyOS',
colorScheme: currentThemeValue == ThemeType.dark
? darkColorScheme
: lightColorScheme,
useMaterial3: true,
snackBarTheme: SnackBarThemeData(
actionTextColor: lightColorScheme.primary,
backgroundColor: lightColorScheme.secondaryContainer,
closeIconColor: lightColorScheme.secondary,
contentTextStyle: TextStyle(color: lightColorScheme.secondary),
elevation: 20,
),
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: ZoomPageTransitionsBuilder(
allowEnterRouteSnapshotting: false,
),
},
),
),
darkTheme: ThemeData(
// fontFamily: 'HarmonyOS',
colorScheme: currentThemeValue == ThemeType.light
? lightColorScheme
: darkColorScheme,
useMaterial3: true,
snackBarTheme: SnackBarThemeData(
actionTextColor: darkColorScheme.primary,
backgroundColor: darkColorScheme.secondaryContainer,
closeIconColor: darkColorScheme.secondary,
contentTextStyle: TextStyle(color: darkColorScheme.secondary),
elevation: 20,
),
),
theme: themeData,
darkTheme: themeData,
localizationsDelegates: const [
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,

View File

@ -1,5 +1,10 @@
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 = [
{
'id': 0,
@ -13,6 +18,7 @@ List defaultNavigationBars = [
),
'label': "首页",
'count': 0,
'page': const HomePage(),
},
{
'id': 1,
@ -26,6 +32,7 @@ List defaultNavigationBars = [
),
'label': "排行榜",
'count': 0,
'page': const RankPage(),
},
{
'id': 2,
@ -39,6 +46,7 @@ List defaultNavigationBars = [
),
'label': "动态",
'count': 0,
'page': const DynamicsPage(),
},
{
'id': 3,
@ -52,5 +60,6 @@ List defaultNavigationBars = [
),
'label': "媒体库",
'count': 0,
'page': const MediaPage(),
}
];

View File

@ -0,0 +1,47 @@
enum SubtitleType {
// 中文(中国)
zhCN,
// 中文(自动翻译)
aizh,
// 英语(自动生成)
aien,
}
extension SubtitleTypeExtension on SubtitleType {
String get description {
switch (this) {
case SubtitleType.zhCN:
return '中文(中国)';
case SubtitleType.aizh:
return '中文(自动翻译)';
case SubtitleType.aien:
return '英语(自动生成)';
}
}
}
extension SubtitleIdExtension on SubtitleType {
String get id {
switch (this) {
case SubtitleType.zhCN:
return 'zh-CN';
case SubtitleType.aizh:
return 'ai-zh';
case SubtitleType.aien:
return 'ai-en';
}
}
}
extension SubtitleCodeExtension on SubtitleType {
int get code {
switch (this) {
case SubtitleType.zhCN:
return 1;
case SubtitleType.aizh:
return 2;
case SubtitleType.aien:
return 3;
}
}
}

View File

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

View File

@ -47,18 +47,23 @@ class Vip {
this.status,
this.dueDate,
this.label,
this.nicknameColor,
});
int? type;
int? status;
int? dueDate;
Map? label;
int? nicknameColor;
Vip.fromJson(Map<String, dynamic> json) {
type = json['type'];
status = json['status'];
dueDate = json['due_date'];
label = json['label'];
nicknameColor = json['nickname_color'] == ''
? null
: int.parse("0xFF${json['nickname_color'].replaceAll('#', '')}");
}
}

View File

@ -437,7 +437,8 @@ class SearchArticleItemModel {
pubTime = json['pub_time'];
like = json['like'];
title = Em.regTitle(json['title']);
subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
subTitle =
Em.decodeHtmlEntities(json['title'].replaceAll(RegExp(r'<[^>]*>'), ''));
rankOffset = json['rank_offset'];
mid = json['mid'];
imageUrls = json['image_urls'];

View File

@ -0,0 +1,20 @@
class SubTitileContentModel {
double? from;
double? to;
int? location;
String? content;
SubTitileContentModel({
this.from,
this.to,
this.location,
this.content,
});
SubTitileContentModel.fromJson(Map<String, dynamic> json) {
from = json['from'];
to = json['to'];
location = json['location'];
content = json['content'];
}
}

View File

@ -0,0 +1,89 @@
import 'package:get/get.dart';
import '../../common/subtitle_type.dart';
class SubTitlteModel {
SubTitlteModel({
this.aid,
this.bvid,
this.cid,
this.loginMid,
this.loginMidHash,
this.isOwner,
this.name,
this.subtitles,
});
int? aid;
String? bvid;
int? cid;
int? loginMid;
String? loginMidHash;
bool? isOwner;
String? name;
List<SubTitlteItemModel>? subtitles;
factory SubTitlteModel.fromJson(Map<String, dynamic> json) => SubTitlteModel(
aid: json["aid"],
bvid: json["bvid"],
cid: json["cid"],
loginMid: json["login_mid"],
loginMidHash: json["login_mid_hash"],
isOwner: json["is_owner"],
name: json["name"],
subtitles: json["subtitle"] != null
? json["subtitle"]["subtitles"]
.map<SubTitlteItemModel>((x) => SubTitlteItemModel.fromJson(x))
.toList()
: [],
);
}
class SubTitlteItemModel {
SubTitlteItemModel({
this.id,
this.lan,
this.lanDoc,
this.isLock,
this.subtitleUrl,
this.type,
this.aiType,
this.aiStatus,
this.title,
this.code,
this.content,
this.body,
});
int? id;
String? lan;
String? lanDoc;
bool? isLock;
String? subtitleUrl;
int? type;
int? aiType;
int? aiStatus;
String? title;
int? code;
String? content;
List? body;
factory SubTitlteItemModel.fromJson(Map<String, dynamic> json) =>
SubTitlteItemModel(
id: json["id"],
lan: json["lan"].replaceAll('-', ''),
lanDoc: json["lan_doc"],
isLock: json["is_lock"],
subtitleUrl: json["subtitle_url"],
type: json["type"],
aiType: json["ai_type"],
aiStatus: json["ai_status"],
title: json["lan_doc"],
code: SubtitleType.values
.firstWhereOrNull(
(element) => element.id.toString() == json["lan"])
?.index ??
-1,
content: '',
body: [],
);
}

View File

@ -15,6 +15,10 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.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 {
// 视频bvid
String bvid = Get.parameters['bvid']!;
@ -291,4 +295,29 @@ class BangumiIntroController extends GetxController {
int aid = episodes[nextIndex].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!;
videoDetailCtr.cid.listen((p0) {
cid = p0;
if (!mounted) {
return;
}
setState(() {});
});
}
@ -317,7 +320,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
if (widget.bangumiDetail!.episodes!.isNotEmpty) ...[
BangumiPanel(
pages: widget.bangumiDetail!.episodes!,
cid: cid ?? widget.bangumiDetail!.episodes!.first.cid,
cid: cid! ?? widget.bangumiDetail!.episodes!.first.cid!,
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) =>
bangumiIntroController.changeSeasonOrbangu(bvid, cid, aid),

View File

@ -2,13 +2,11 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:nil/nil.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'controller.dart';
import 'widgets/bangumu_card_v.dart';
@ -34,10 +32,6 @@ class _BangumiPageState extends State<BangumiPage>
void initState() {
super.initState();
scrollController = _bangumidController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
_futureBuilderFuture = _bangumidController.queryBangumiListFeed();
_futureBuilderFutureFollow = _bangumidController.queryBangumiFollow();
scrollController.addListener(
@ -49,16 +43,7 @@ class _BangumiPageState extends State<BangumiPage>
_bangumidController.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);
}
handleScrollEvent(scrollController);
},
);
}

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.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/utils/storage.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 {
const BangumiPanel({
super.key,
required this.pages,
this.cid,
required this.cid,
this.sheetHeight,
this.changeFuc,
this.bangumiDetail,
});
final List<EpisodeItem> pages;
final int? cid;
final int cid;
final double? sheetHeight;
final Function? changeFuc;
final BangumiInfoModel? bangumiDetail;
@ -28,9 +32,8 @@ class BangumiPanel extends StatefulWidget {
}
class _BangumiPanelState extends State<BangumiPanel> {
late int currentIndex;
late RxInt currentIndex = (-1).obs;
final ScrollController listViewScrollCtr = ScrollController();
final ScrollController listViewScrollCtr_2 = ScrollController();
Box userInfoCache = GStrorage.userInfo;
dynamic userInfo;
// 默认未开通
@ -39,169 +42,68 @@ class _BangumiPanelState extends State<BangumiPanel> {
String heroTag = Get.arguments['heroTag'];
late final VideoDetailController videoDetailCtr;
final ItemScrollController itemScrollController = ItemScrollController();
late PersistentBottomSheetController? _bottomSheetController;
@override
void initState() {
super.initState();
cid = widget.cid!;
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
cid = widget.cid;
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
currentIndex.value =
widget.pages.indexWhere((EpisodeItem e) => e.cid == cid);
scrollToIndex();
videoDetailCtr.cid.listen((int p0) {
cid = p0;
currentIndex.value =
widget.pages.indexWhere((EpisodeItem e) => e.cid == cid);
scrollToIndex();
});
/// 获取大会员状态
userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) {
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
void dispose() {
listViewScrollCtr.dispose();
listViewScrollCtr_2.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 {
if (item.badge != null && item.badge == '会员' && vipStatus != 1) {
SmartDialog.showToast('需要大会员');
return;
}
await widget.changeFuc!(
widget.changeFuc?.call(
item.bvid,
item.cid,
item.aid,
);
_bottomSheetController?.close();
currentIndex = i;
setState(() {});
scrollToIndex();
}
void scrollToIndex() {
WidgetsBinding.instance.addPostFrameCallback((_) {
// 在回调函数中获取更新后的状态
listViewScrollCtr.animateTo(currentIndex * 150,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
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) {
Color primary = Theme.of(context).colorScheme.primary;
Color onSurface = Theme.of(context).colorScheme.onSurface;
return Column(
children: [
Padding(
@ -211,12 +113,14 @@ class _BangumiPanelState extends State<BangumiPanel> {
children: [
const Text('选集 '),
Expanded(
child: Text(
' 正在播放:${widget.pages[currentIndex].longTitle}',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
child: Obx(
() => Text(
' 正在播放:${widget.pages[currentIndex.value].longTitle}',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
),
),
@ -227,7 +131,16 @@ class _BangumiPanelState extends State<BangumiPanel> {
style: ButtonStyle(
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(
'${widget.bangumiDetail!.newEp!['desc']}',
style: const TextStyle(fontSize: 13),
@ -245,6 +158,8 @@ class _BangumiPanelState extends State<BangumiPanel> {
itemCount: widget.pages.length,
itemExtent: 150,
itemBuilder: (BuildContext context, int i) {
var page = widget.pages[i];
bool isSelected = i == currentIndex.value;
return Container(
width: 150,
margin: const EdgeInsets.only(right: 10),
@ -253,42 +168,37 @@ class _BangumiPanelState extends State<BangumiPanel> {
borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => changeFucCall(widget.pages[i], i),
onTap: () => changeFucCall(page, i),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 10),
vertical: 8,
horizontal: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: [
if (i == currentIndex) ...<Widget>[
Image.asset(
'assets/images/live.png',
color: Theme.of(context).colorScheme.primary,
height: 12,
),
if (isSelected) ...<Widget>[
Image.asset('assets/images/live.png',
color: primary, height: 12),
const SizedBox(width: 6)
],
Text(
'${i + 1}',
style: TextStyle(
fontSize: 13,
color: i == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context)
.colorScheme
.onSurface),
fontSize: 13,
color: isSelected ? primary : onSurface,
),
),
const SizedBox(width: 2),
if (widget.pages[i].badge != null) ...[
if (page.badge != null) ...[
const Spacer(),
Text(
widget.pages[i].badge!,
page.badge!,
style: TextStyle(
fontSize: 12,
color:
Theme.of(context).colorScheme.primary,
color: primary,
),
),
]
@ -296,13 +206,12 @@ class _BangumiPanelState extends State<BangumiPanel> {
),
const SizedBox(height: 3),
Text(
widget.pages[i].longTitle!,
page.longTitle!,
maxLines: 1,
style: TextStyle(
fontSize: 13,
color: i == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
fontSize: 13,
color: isSelected ? primary : onSurface,
),
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

@ -194,7 +194,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
centerTitle: false,
titleSpacing: 0,
title: StreamBuilder(
stream: titleStreamC.stream,
stream: titleStreamC.stream.distinct(),
initialData: false,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity(

View File

@ -3,15 +3,14 @@ import 'dart:async';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'package:pilipala/utils/storage.dart';
import '../mine/controller.dart';
@ -44,8 +43,6 @@ class _DynamicsPageState extends State<DynamicsPage>
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
scrollController = _dynamicsController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
@ -55,14 +52,7 @@ class _DynamicsPageState extends State<DynamicsPage>
_dynamicsController.queryFollowDynamic(type: 'onLoad');
});
}
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
handleScrollEvent(scrollController);
},
);

View File

@ -67,7 +67,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
pinned: true,
titleSpacing: 0,
title: StreamBuilder(
stream: titleStreamC.stream,
stream: titleStreamC.stream.distinct(),
initialData: false,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity(

View File

@ -185,7 +185,7 @@ class HistoryItem extends StatelessWidget {
? '已看完'
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
right: 6.0,
bottom: 6.0,
bottom: 8.0,
type: 'gray',
),
// 右上角
@ -258,6 +258,27 @@ class HistoryItem extends StatelessWidget {
),
),
),
videoItem.progress != 0
? Positioned(
left: 3,
right: 3,
bottom: 0,
child: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(
StyleString.imgRadius.x),
bottomRight: Radius.circular(
StyleString.imgRadius.x),
),
child: LinearProgressIndicator(
value: videoItem.progress == -1
? 100
: videoItem.progress /
videoItem.duration,
),
),
)
: const SizedBox()
],
),
VideoContent(videoItem: videoItem, ctr: ctr)

View File

@ -35,7 +35,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
userLogin.value = userInfo != null;
userFace.value = userInfo != null ? userInfo.face : '';
hideSearchBar =
setting.get(SettingBoxKey.hideSearchBar, defaultValue: true);
setting.get(SettingBoxKey.hideSearchBar, defaultValue: false);
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
searchDefault();
}

View File

@ -171,7 +171,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: stream,
stream: stream!.distinct(),
initialData: true,
builder: (BuildContext context, AsyncSnapshot snapshot) {
final RxBool isUserLoggedIn = ctr!.userLogin;

View File

@ -1,7 +1,6 @@
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';
@ -9,9 +8,8 @@ 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/hot/controller.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/utils/main_stream.dart';
class HotPage extends StatefulWidget {
const HotPage({Key? key}) : super(key: key);
@ -34,10 +32,6 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
super.initState();
_futureBuilderFuture = _hotController.queryHotFeed('init');
scrollController = _hotController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
@ -47,16 +41,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
_hotController.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);
}
handleScrollEvent(scrollController);
},
);
}

View File

@ -2,15 +2,13 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
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/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'controller.dart';
import 'widgets/live_item.dart';
@ -36,10 +34,6 @@ class _LivePageState extends State<LivePage>
super.initState();
_futureBuilderFuture = _liveController.queryLiveList('init');
scrollController = _liveController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
@ -49,16 +43,7 @@ class _LivePageState extends State<LivePage>
_liveController.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);
}
handleScrollEvent(scrollController);
},
);
}

View File

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

View File

@ -14,10 +14,12 @@ class BottomControl extends StatefulWidget implements PreferredSizeWidget {
final PlPlayerController? controller;
final LiveRoomController? liveRoomCtr;
final Floating? floating;
final Function? onRefresh;
const BottomControl({
this.controller,
this.liveRoomCtr,
this.floating,
this.onRefresh,
Key? key,
}) : super(key: key);
@ -61,6 +63,14 @@ class _BottomControlState extends State<BottomControl> {
// ),
// fuc: () => Get.back(),
// ),
ComBtn(
icon: const Icon(
Icons.refresh_outlined,
size: 18,
color: Colors.white,
),
fuc: widget.onRefresh,
),
const Spacer(),
// ComBtn(
// 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:hive/hive.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/utils.dart';
import '../../models/common/dynamic_badge_mode.dart';
import '../../models/common/nav_bar_config.dart';
class MainController extends GetxController {
List<Widget> pages = <Widget>[
const HomePage(),
const RankPage(),
const DynamicsPage(),
const MediaPage(),
];
RxList navigationBars = defaultNavigationBars.obs;
List<Widget> pages = <Widget>[];
RxList navigationBars = [].obs;
late List defaultNavTabs;
late List<int> navBarSort;
final StreamController<bool> bottomBarStream =
StreamController<bool>.broadcast();
Box setting = GStrorage.setting;
@ -40,11 +33,8 @@ class MainController extends GetxController {
if (setting.get(SettingBoxKey.autoUpdate, defaultValue: false)) {
Utils.checkUpdata();
}
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
int defaultHomePage =
setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int;
selectedIndex = defaultNavigationBars
.indexWhere((item) => item['id'] == defaultHomePage);
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: false);
var userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
@ -53,6 +43,7 @@ class MainController extends GetxController {
if (dynamicBadgeType.value != DynamicBadgeMode.hidden) {
getUnreadDynamic();
}
setNavBarConfig();
}
void onBackPressed(BuildContext context) {
@ -93,4 +84,21 @@ class MainController extends GetxController {
}
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

@ -10,6 +10,7 @@ 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/global_data.dart';
import 'package:pilipala/utils/storage.dart';
import './controller.dart';
@ -29,7 +30,6 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
int? _lastSelectTime; //上次点击时间
Box setting = GStrorage.setting;
late bool enableMYBar;
@override
void initState() {
@ -37,7 +37,6 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
_mainController.pageController =
PageController(initialPage: _mainController.selectedIndex);
enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
}
void setIndex(int value) async {
@ -127,81 +126,82 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
},
children: _mainController.pages,
),
bottomNavigationBar: StreamBuilder(
stream: _mainController.hideTabBar
? _mainController.bottomBarStream.stream
: StreamController<bool>.broadcast().stream,
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide(
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 500),
offset: Offset(0, snapshot.data ? 0 : 1),
child: Obx(
() => enableMYBar
? NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: _mainController.selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: Obx(
() => Badge(
label:
_mainController.dynamicBadgeType.value ==
bottomNavigationBar: _mainController.navigationBars.length > 1
? StreamBuilder(
stream: _mainController.hideTabBar
? _mainController.bottomBarStream.stream.distinct()
: StreamController<bool>.broadcast().stream,
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide(
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 500),
offset: Offset(0, snapshot.data ? 0 : 1),
child: GlobalData().enableMYBar
? NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: _mainController.selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: Obx(
() => Badge(
label: _mainController
.dynamicBadgeType.value ==
DynamicBadgeMode.number
? Text(e['count'].toString())
: null,
padding:
const EdgeInsets.fromLTRB(6, 0, 6, 0),
isLabelVisible:
_mainController.dynamicBadgeType.value !=
padding:
const EdgeInsets.fromLTRB(6, 0, 6, 0),
isLabelVisible: _mainController
.dynamicBadgeType.value !=
DynamicBadgeMode.hidden &&
e['count'] > 0,
child: e['icon'],
),
),
selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
)
: BottomNavigationBar(
currentIndex: _mainController.selectedIndex,
onTap: (value) => setIndex(value),
iconSize: 16,
selectedFontSize: 12,
unselectedFontSize: 12,
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
icon: Obx(
() => Badge(
label:
_mainController.dynamicBadgeType.value ==
child: e['icon'],
),
),
selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
)
: BottomNavigationBar(
currentIndex: _mainController.selectedIndex,
type: BottomNavigationBarType.fixed,
onTap: (value) => setIndex(value),
iconSize: 16,
selectedFontSize: 12,
unselectedFontSize: 12,
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
icon: Obx(
() => Badge(
label: _mainController
.dynamicBadgeType.value ==
DynamicBadgeMode.number
? Text(e['count'].toString())
: null,
padding:
const EdgeInsets.fromLTRB(6, 0, 6, 0),
isLabelVisible:
_mainController.dynamicBadgeType.value !=
padding:
const EdgeInsets.fromLTRB(6, 0, 6, 0),
isLabelVisible: _mainController
.dynamicBadgeType.value !=
DynamicBadgeMode.hidden &&
e['count'] > 0,
child: e['icon'],
),
),
activeIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
),
);
},
),
child: e['icon'],
),
),
activeIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
);
},
)
: null,
),
);
}

View File

@ -1,13 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:media_kit/media_kit.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'package:pilipala/utils/utils.dart';
class MediaPage extends StatefulWidget {
@ -31,25 +29,12 @@ class _MediaPageState extends State<MediaPage>
mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder();
ScrollController scrollController = mediaController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
mediaController.userLogin.listen((status) {
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
});
scrollController.addListener(
() {
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
},
);
handleScrollEvent(scrollController);
}
@override

View File

@ -65,7 +65,7 @@ class _MemberPageState extends State<MemberPage>
children: [
AppBar(
title: StreamBuilder(
stream: appbarStream.stream,
stream: appbarStream.stream.distinct(),
initialData: false,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
@ -281,8 +281,8 @@ class _MemberPageState extends State<MemberPage>
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data!;
if (data['status']) {
Map? data = snapshot.data;
if (data != null && data['status']) {
return Obx(
() => Stack(
alignment: AlignmentDirectional.center,
@ -302,7 +302,14 @@ class _MemberPageState extends State<MemberPage>
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
.copyWith(
fontWeight: FontWeight.bold,
color: _memberController.memberInfo.value
.vip!.nicknameColor !=
null
? Color(_memberController.memberInfo
.value.vip!.nicknameColor!)
: null),
)),
const SizedBox(width: 2),
if (_memberController.memberInfo.value.sex == '')

View File

@ -180,7 +180,9 @@ class ProfilePanel extends StatelessWidget {
Obx(
() => Expanded(
child: TextButton(
onPressed: () => ctr.actionRelationMod(),
onPressed: () => loadingStatus
? null
: ctr.actionRelationMod(),
style: TextButton.styleFrom(
foregroundColor: ctr.attribute.value == -1
? Colors.transparent

View File

@ -18,45 +18,32 @@ class MemberSeasonsPanel extends StatelessWidget {
itemBuilder: (context, index) {
MemberSeasonsList item = data!.seasonsList![index];
return Padding(
padding: const EdgeInsets.only(bottom: 12, right: 4),
padding: const EdgeInsets.only(bottom: 12),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 12, left: 4),
child: Row(
children: [
Text(
item.meta!.name!,
maxLines: 1,
style: Theme.of(context).textTheme.titleSmall!,
),
const SizedBox(width: 10),
PBadge(
stack: 'relative',
size: 'small',
text: item.meta!.total.toString(),
),
const Spacer(),
SizedBox(
width: 35,
height: 35,
child: IconButton(
onPressed: () => Get.toNamed(
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'),
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
icon: const Icon(
Icons.arrow_forward,
size: 20,
),
),
)
],
ListTile(
onTap: () => Get.toNamed(
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'),
title: Text(
item.meta!.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall!,
),
dense: true,
leading: PBadge(
stack: 'relative',
size: 'small',
text: item.meta!.total.toString(),
),
trailing: const Icon(
Icons.arrow_forward,
size: 20,
),
),
const SizedBox(height: 10),
LayoutBuilder(
builder: (context, boxConstraints) {
return GridView.builder(

View File

@ -59,6 +59,7 @@ class MemberCoinsItem extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(5, 6, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
coinItem.title!,

View File

@ -25,7 +25,7 @@ class MemberSeasonsItem extends StatelessWidget {
child: InkWell(
onTap: () async {
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',
arguments: {'videoItem': seasonItem, 'heroTag': heroTag});
},
@ -51,8 +51,7 @@ class MemberSeasonsItem extends StatelessWidget {
bottom: 6,
right: 6,
type: 'gray',
text: Utils.CustomStamp_str(
timestamp: seasonItem.pubdate, date: 'YY-MM-DD'),
text: Utils.timeFormat(seasonItem.duration),
)
],
);

View File

@ -9,7 +9,7 @@ import 'package:pilipala/utils/storage.dart';
class RankController extends GetxController with GetTickerProviderStateMixin {
bool flag = false;
late RxList tabs = [].obs;
RxInt initialIndex = 1.obs;
RxInt initialIndex = 0.obs;
late TabController tabController;
late List tabsCtrList;
late List<Widget> tabsPageList;
@ -50,21 +50,5 @@ class RankController extends GetxController with GetTickerProviderStateMixin {
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,7 +1,6 @@
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';
@ -9,9 +8,8 @@ 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';
import 'package:pilipala/utils/main_stream.dart';
class ZonePage extends StatefulWidget {
const ZonePage({Key? key, required this.rid}) : super(key: key);
@ -22,21 +20,22 @@ class ZonePage extends StatefulWidget {
State<ZonePage> createState() => _ZonePageState();
}
class _ZonePageState extends State<ZonePage> {
final ZoneController _zoneController = Get.put(ZoneController());
class _ZonePageState extends State<ZonePage>
with AutomaticKeepAliveClientMixin {
late ZoneController _zoneController;
List videoList = [];
Future? _futureBuilderFuture;
late ScrollController scrollController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_zoneController = Get.put(ZoneController(), tag: widget.rid.toString());
_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 >=
@ -46,16 +45,7 @@ class _ZonePageState extends State<ZonePage> {
_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);
}
handleScrollEvent(scrollController);
},
);
}
@ -68,6 +58,7 @@ class _ZonePageState extends State<ZonePage> {
@override
Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator(
onRefresh: () async {
return await _zoneController.onRefresh();

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
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/skeleton/video_card_v.dart';
@ -10,8 +9,7 @@ import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_v.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'controller.dart';
@ -35,10 +33,6 @@ class _RcmdPageState extends State<RcmdPage>
super.initState();
_futureBuilderFuture = _rcmdController.queryRcmdFeed('init');
ScrollController scrollController = _rcmdController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
@ -49,15 +43,7 @@ class _RcmdPageState extends State<RcmdPage>
_rcmdController.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);
}
handleScrollEvent(scrollController);
},
);
}

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'),
title: Text('首页tabbar', style: titleStyle),
),
ListTile(
dense: false,
onTap: () => Get.toNamed('/navbarSetting'),
title: Text('底部导航栏设置', style: titleStyle),
),
if (Platform.isAndroid)
ListTile(
dense: false,
onTap: () => Get.toNamed('/displayModeSetting'),
title: Text('屏幕帧率', style: titleStyle),
)
),
],
),
);

View File

@ -44,6 +44,7 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
setState(() {
_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

@ -46,4 +46,40 @@ class SubController extends GetxController {
Future onLoad() async {
querySubFolder(type: 'onload');
}
// 取消订阅
Future<void> cancelSub(SubFolderItemData subFolderItem) async {
showDialog(
context: Get.context!,
builder: (context) => AlertDialog(
title: const Text('提示'),
content: const Text('确定取消订阅吗?'),
actions: [
TextButton(
onPressed: () {
Get.back();
},
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
var res = await UserHttp.cancelSub(seasonId: subFolderItem.id!);
if (res['status']) {
subFolderData.value.list!.remove(subFolderItem);
subFolderData.update((val) {});
SmartDialog.showToast('取消订阅成功');
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
child: const Text('确定'),
),
],
),
);
}
}

View File

@ -58,7 +58,8 @@ class _SubPageState extends State<SubPage> {
itemBuilder: (context, index) {
return SubItem(
subFolderItem:
_subController.subFolderData.value.list![index]);
_subController.subFolderData.value.list![index],
cancelSub: _subController.cancelSub);
},
),
);

View File

@ -8,7 +8,12 @@ import '../../../models/user/sub_folder.dart';
class SubItem extends StatelessWidget {
final SubFolderItemData subFolderItem;
const SubItem({super.key, required this.subFolderItem});
final Function(SubFolderItemData) cancelSub;
const SubItem({
super.key,
required this.subFolderItem,
required this.cancelSub,
});
@override
Widget build(BuildContext context) {
@ -51,7 +56,10 @@ class SubItem extends StatelessWidget {
},
),
),
VideoContent(subFolderItem: subFolderItem)
VideoContent(
subFolderItem: subFolderItem,
cancelSub: cancelSub,
)
],
),
);
@ -64,7 +72,8 @@ class SubItem extends StatelessWidget {
class VideoContent extends StatelessWidget {
final SubFolderItemData subFolderItem;
const VideoContent({super.key, required this.subFolderItem});
final Function(SubFolderItemData)? cancelSub;
const VideoContent({super.key, required this.subFolderItem, this.cancelSub});
@override
Widget build(BuildContext context) {
@ -100,6 +109,24 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline,
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: 35,
width: 35,
child: IconButton(
onPressed: () => cancelSub?.call(subFolderItem),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.outline,
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
),
icon: const Icon(Icons.delete_outline, size: 18),
),
)
],
)
],
),
),

View File

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

View File

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

View File

@ -20,7 +20,9 @@ import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/video_utils.dart';
import 'package:screen_brightness/screen_brightness.dart';
import '../../../models/video/subTitile/content.dart';
import '../../../http/danmaku.dart';
import '../../../plugin/pl_player/models/bottom_control_type.dart';
import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart';
@ -90,10 +92,20 @@ class VideoDetailController extends GetxController
late bool enableCDN;
late int? cacheVideoQa;
late String cacheDecode;
late int cacheAudioQa;
late int defaultAudioQa;
PersistentBottomSheetController? replyReplyBottomSheetCtr;
RxList<SubTitileContentModel> subtitleContents =
<SubTitileContentModel>[].obs;
late bool enableRelatedVideo;
List subtitles = [];
RxList<BottomControlType> bottomList = [
BottomControlType.playOrPause,
BottomControlType.time,
BottomControlType.space,
BottomControlType.fit,
BottomControlType.fullscreen,
].obs;
@override
void onInit() {
@ -142,9 +154,10 @@ class VideoDetailController extends GetxController
// 预设的解码格式
cacheDecode = setting.get(SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code);
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code);
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
getSubtitle();
}
showReplyReplyPanel() {
@ -251,6 +264,8 @@ class VideoDetailController extends GetxController
/// 开启自动全屏时在player初始化完成后立即传入headerControl
plPlayerController.headerControl = headerControl;
plPlayerController.subtitles.value = subtitles;
}
// 视频链接
@ -346,9 +361,9 @@ class VideoDetailController extends GetxController
if (audiosList.isNotEmpty) {
final List<int> numbers = audiosList.map((map) => map.id!).toList();
int closestNumber = Utils.findClosestNumber(cacheAudioQa, numbers);
if (!numbers.contains(cacheAudioQa) &&
numbers.any((e) => e > cacheAudioQa)) {
int closestNumber = Utils.findClosestNumber(defaultAudioQa, numbers);
if (!numbers.contains(defaultAudioQa) &&
numbers.any((e) => e > defaultAudioQa)) {
closestNumber = 30280;
}
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);
@ -388,6 +403,45 @@ class VideoDetailController extends GetxController
: print('replyReplyBottomSheetCtr is null');
}
// 获取字幕配置
Future getSubtitle() async {
var result = await VideoHttp.getSubtitle(bvid: bvid, cid: cid.value);
if (result['status']) {
if (result['data'].subtitles.isNotEmpty) {
subtitles = result['data'].subtitles;
if (subtitles.isNotEmpty) {
for (var i in subtitles) {
final Map<String, dynamic> res = await VideoHttp.getSubtitleContent(
i.subtitleUrl,
);
i.content = res['content'];
i.body = res['body'];
}
}
}
return result['data'];
}
}
// 获取字幕内容
// Future getSubtitleContent(String url) async {
// var res = await Request().get('https:$url');
// subtitleContents.value = res.data['body'].map<SubTitileContentModel>((e) {
// return SubTitileContentModel.fromJson(e);
// }).toList();
// setSubtitleContent();
// }
setSubtitleContent() {
plPlayerController.subtitleContent.value = '';
plPlayerController.subtitles.value = subtitles;
}
clearSubtitleContent() {
plPlayerController.subtitleContent.value = '';
plPlayerController.subtitles.value = [];
}
/// 发送弹幕
void showShootDanmakuSheet() {
final TextEditingController textController = TextEditingController();

View File

@ -18,6 +18,9 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.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 'widgets/group_panel.dart';
@ -25,15 +28,10 @@ class VideoIntroController extends GetxController {
VideoIntroController({required this.bvid});
// 视频bvid
String bvid;
// 请求状态
RxBool isLoading = false.obs;
// 视频详情 请求返回
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
// up主粉丝数
Map userStat = {'follower': '-'};
// 是否点赞
RxBool hasLike = false.obs;
// 是否投币
@ -59,6 +57,7 @@ class VideoIntroController extends GetxController {
bool isPaused = false;
String heroTag = '';
late ModelResult modelResult;
PersistentBottomSheetController? bottomSheetController;
@override
void onInit() {
@ -562,4 +561,52 @@ class VideoIntroController extends GetxController {
}
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/fav_panel.dart';
import 'widgets/intro_detail.dart';
import 'widgets/page.dart';
import 'widgets/season.dart';
import 'widgets/page_panel.dart';
import 'widgets/season_panel.dart';
class VideoIntroPanel extends StatefulWidget {
final String bvid;
@ -373,7 +373,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
/// 点赞收藏转发
actionGrid(context, videoIntroController),
// 合集
// 合集 videoPart 简洁
if (widget.videoDetail!.ugcSeason != null) ...[
Obx(
() => SeasonPanel(
@ -383,19 +383,31 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
: widget.videoDetail!.pages!.first.cid,
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) =>
videoIntroController.changeSeasonOrbangu(bvid, cid, aid),
videoIntroController.changeSeasonOrbangu(
bvid,
cid,
aid,
),
videoIntroCtr: videoIntroController,
),
)
],
// 合集 videoEpisode
if (widget.videoDetail!.pages != null &&
widget.videoDetail!.pages!.length > 1) ...[
Obx(() => PagesPanel(
pages: widget.videoDetail!.pages!,
cid: videoIntroController.lastPlayCid.value,
sheetHeight: sheetHeight,
changeFuc: (cid) => videoIntroController.changeSeasonOrbangu(
videoIntroController.bvid, cid, null),
))
Obx(
() => PagesPanel(
pages: widget.videoDetail!.pages!,
cid: videoIntroController.lastPlayCid.value,
sheetHeight: sheetHeight,
changeFuc: (cid) => videoIntroController.changeSeasonOrbangu(
videoIntroController.bvid,
cid,
null,
),
videoIntroCtr: videoIntroController,
),
)
],
GestureDetector(
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:get/get.dart';
import 'package:pilipala/common/pages_bottom_sheet.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../../../../../models/common/video_episode_type.dart';
import '../controller.dart';
class SeasonPanel extends StatefulWidget {
const SeasonPanel({
@ -12,11 +15,13 @@ class SeasonPanel extends StatefulWidget {
this.cid,
this.sheetHeight,
this.changeFuc,
required this.videoIntroCtr,
});
final UgcSeason ugcSeason;
final int? cid;
final double? sheetHeight;
final Function? changeFuc;
final VideoIntroController videoIntroCtr;
@override
State<SeasonPanel> createState() => _SeasonPanelState();
@ -25,11 +30,11 @@ class SeasonPanel extends StatefulWidget {
class _SeasonPanelState extends State<SeasonPanel> {
late List<EpisodeItem> episodes;
late int cid;
late int currentIndex;
late RxInt currentIndex = (-1).obs;
final String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
final ScrollController _scrollController = ScrollController();
final ItemScrollController itemScrollController = ItemScrollController();
late PersistentBottomSheetController? _bottomSheetController;
@override
void initState() {
@ -52,32 +57,21 @@ class _SeasonPanelState extends State<SeasonPanel> {
}
/// season_id episodes
// episodes = widget.ugcSeason.sections!
// .firstWhere((e) => e.seasonId == widget.ugcSeason.id)
// .episodes!;
currentIndex = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
_videoDetailController.cid.listen((int p0) {
cid = p0;
setState(() {});
currentIndex = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
});
}
void changeFucCall(item, int i) async {
await widget.changeFuc!(
widget.changeFuc?.call(
IdUtils.av2bv(item.aid),
item.cid,
item.aid,
);
currentIndex = i;
Get.back();
setState(() {});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
currentIndex.value = i;
_bottomSheetController?.close();
}
Widget buildEpisodeListItem(
@ -123,71 +117,17 @@ class _SeasonPanelState extends State<SeasonPanel> {
borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => showBottomSheet(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
itemScrollController.jumpTo(index: currentIndex);
});
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: 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,
),
),
),
],
),
);
});
},
),
onTap: () {
widget.videoIntroCtr.bottomSheetController =
_bottomSheetController = EpisodeBottomSheet(
currentCid: cid,
episodes: episodes,
changeFucCall: changeFucCall,
sheetHeight: widget.sheetHeight,
dataType: VideoEpidoesType.videoEpisode,
context: context,
).show(context);
},
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
child: Row(
@ -206,10 +146,10 @@ class _SeasonPanelState extends State<SeasonPanel> {
height: 12,
),
const SizedBox(width: 10),
Text(
'${currentIndex + 1}/${episodes.length}',
style: Theme.of(context).textTheme.labelMedium,
),
Obx(() => Text(
'${currentIndex.value + 1}/${episodes.length}',
style: Theme.of(context).textTheme.labelMedium,
)),
const SizedBox(width: 6),
const Icon(
Icons.arrow_forward_ios_outlined,

View File

@ -148,34 +148,16 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
floating: true,
delegate: _MySliverPersistentHeaderDelegate(
child: Container(
height: 45,
height: 40,
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
border: Border(
bottom: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withOpacity(0.1)),
),
),
color: Theme.of(context).colorScheme.surface,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() => AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
'${_videoReplyController.count.value}条回复',
key: ValueKey<int>(
_videoReplyController.count.value),
),
() => Text(
'${_videoReplyController.sortTypeLabel.value}评论',
style: const TextStyle(fontSize: 13),
),
),
SizedBox(
@ -184,10 +166,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
onPressed: () =>
_videoReplyController.queryBySort(),
icon: const Icon(Icons.sort, size: 16),
label: Obx(() => Text(
_videoReplyController.sortTypeLabel.value,
style: const TextStyle(fontSize: 13),
)),
label: Obx(
() => Text(
_videoReplyController.sortTypeLabel.value,
style: const TextStyle(fontSize: 13),
),
),
),
)
],
@ -329,8 +313,8 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
class _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
_MySliverPersistentHeaderDelegate({required this.child});
final double _minExtent = 45;
final double _maxExtent = 45;
final double _minExtent = 40;
final double _maxExtent = 40;
final Widget child;
@override

View File

@ -498,7 +498,7 @@ InlineSpan buildContent(
return str;
});
}
// content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
content.message = content.message
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
@ -525,14 +525,18 @@ InlineSpan buildContent(
if (jumpUrlKeysList.isNotEmpty) {
patternStr += '|${jumpUrlKeysList.join('|')}';
}
RegExp bv23Regex = RegExp(r'https://b23\.tv/[a-zA-Z0-9]{7}');
final RegExp pattern = RegExp(patternStr);
List<String> matchedStrs = [];
void addPlainTextSpan(str) {
spanChilds.add(TextSpan(
spanChilds.add(
TextSpan(
text: str,
recognizer: TapGestureRecognizer()
..onTap = () =>
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem)));
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem),
),
);
}
// 分割文本并处理每个部分
@ -734,8 +738,36 @@ InlineSpan buildContent(
return '';
},
onNonMatch: (String nonMatchStr) {
addPlainTextSpan(nonMatchStr);
return nonMatchStr;
return nonMatchStr.splitMapJoin(
bv23Regex,
onMatch: (Match match) {
String matchStr = match[0]!;
spanChilds.add(
TextSpan(
text: ' $matchStr ',
style: isVideoPage
? TextStyle(
color: Theme.of(context).colorScheme.primary,
)
: null,
recognizer: TapGestureRecognizer()
..onTap = () => Get.toNamed(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': matchStr
},
),
),
);
return '';
},
onNonMatch: (String nonMatchOtherStr) {
addPlainTextSpan(nonMatchOtherStr);
return nonMatchOtherStr;
},
);
},
);

View File

@ -142,9 +142,10 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
@override
Widget build(BuildContext context) {
double keyboardHeight = EdgeInsets.fromViewPadding(
double _keyboardHeight = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio)
.bottom;
print('_keyboardHeight: $_keyboardHeight');
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
@ -235,7 +236,11 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
duration: const Duration(milliseconds: 300),
child: SizedBox(
width: double.infinity,
height: toolbarType == 'input' ? keyboardHeight : emoteHeight,
height: toolbarType == 'input'
? (_keyboardHeight > keyboardHeight
? _keyboardHeight
: keyboardHeight)
: emoteHeight,
child: EmotePanel(
onChoose: (package, emote) => onChooseEmote(package, emote),
),

View File

@ -140,8 +140,8 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
future: _futureBuilderFuture,
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
final Map data = snapshot.data as Map;
if (data['status']) {
Map? data = snapshot.data;
if (data != null && data['status']) {
// 请求成功
return Obx(
() => SliverList(
@ -199,7 +199,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
} else {
// 请求错误
return HttpError(
errMsg: data['msg'],
errMsg: data?['msg'] ?? '请求错误',
fn: () => setState(() {}),
);
}

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/utils/storage.dart';
import '../../../plugin/pl_player/models/bottom_control_type.dart';
import '../../../services/shutdown_timer_service.dart';
import 'widgets/app_bar.dart';
@ -176,6 +177,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController?.isFullScreen.listen((bool isFullScreen) {
if (isFullScreen) {
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);
}
}
});
}
@ -212,6 +223,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoIntroController.isPaused = true;
plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause();
vdCtr.clearSubtitleContent();
}
setState(() => isShowing = false);
super.didPushNext();
@ -222,7 +234,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
void didPopNext() async {
if (plPlayerController != null &&
plPlayerController!.videoPlayerController != null) {
setState(() => isShowing = true);
setState(() {
vdCtr.setSubtitleContent();
isShowing = true;
});
}
vdCtr.isFirstTime = false;
final bool autoplay = autoPlayEnable;
@ -288,15 +303,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
() {
return !vdCtr.autoPlay.value
? const SizedBox()
: PLVideoPlayer(
controller: plPlayerController!,
headerControl: vdCtr.headerControl,
danmuWidget: Obx(
() => PlDanmaku(
: Obx(
() => PLVideoPlayer(
controller: plPlayerController!,
headerControl: vdCtr.headerControl,
danmuWidget: PlDanmaku(
key: Key(vdCtr.danmakuCid.value.toString()),
cid: vdCtr.danmakuCid.value,
playerController: plPlayerController!,
),
bottomList: vdCtr.bottomList,
showEposideCb: () => vdCtr.videoType == SearchType.video
? videoIntroController.showEposideHandler()
: bangumiIntroController.showEposideHandler(),
),
);
},
@ -599,7 +618,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 重新进入会刷新
// 播放完成/暂停播放
StreamBuilder(
stream: appbarStream.stream,
stream: appbarStream.stream.distinct(),
initialData: 0,
builder: ((context, snapshot) {
return ScrollAppBar(

View File

@ -11,6 +11,7 @@ import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/video/play/quality.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/introduction/widgets/menu_row.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
@ -158,7 +159,7 @@ class _HeaderControlState extends State<HeaderControl> {
dense: true,
leading:
const Icon(Icons.hourglass_top_outlined, size: 20),
title: const Text('定时关闭(测试)', style: titleStyle),
title: const Text('定时关闭', style: titleStyle),
),
ListTile(
onTap: () => {Get.back(), showSetVideoQa()},
@ -421,6 +422,56 @@ class _HeaderControlState extends State<HeaderControl> {
);
}
/// 选择字幕
void showSubtitleDialog() async {
int tempThemeValue = widget.controller!.subTitleCode.value;
int len = widget.videoDetailCtr!.subtitles.length;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('选择字幕'),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 18),
content: StatefulBuilder(builder: (context, StateSetter setState) {
return len == 0
? const SizedBox(
height: 60,
child: Center(
child: Text('没有字幕'),
),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile(
value: -1,
title: const Text('关闭弹幕'),
groupValue: tempThemeValue,
onChanged: (value) {
tempThemeValue = value!;
widget.controller?.toggleSubtitle(value);
Get.back();
},
),
...widget.videoDetailCtr!.subtitles
.map((e) => RadioListTile(
value: e.code,
title: Text(e.title),
groupValue: tempThemeValue,
onChanged: (value) {
tempThemeValue = value!;
widget.controller?.toggleSubtitle(value);
Get.back();
},
))
.toList(),
],
);
}),
);
});
}
/// 选择倍速
void showSetSpeedSheet() {
final double currentSpeed = widget.controller!.playbackSpeed;
@ -680,9 +731,12 @@ class _HeaderControlState extends State<HeaderControl> {
margin: const EdgeInsets.all(12),
child: Column(
children: [
SizedBox(
height: 45,
child: Center(child: Text('选择解码格式', style: titleStyle))),
const SizedBox(
height: 45,
child: Center(
child: Text('选择解码格式', style: titleStyle),
),
),
Expanded(
child: Material(
child: ListView(
@ -1027,9 +1081,12 @@ class _HeaderControlState extends State<HeaderControl> {
margin: const EdgeInsets.all(12),
child: Column(
children: [
SizedBox(
height: 45,
child: Center(child: Text('选择播放顺序', style: titleStyle))),
const SizedBox(
height: 45,
child: Center(
child: Text('选择播放顺序', style: titleStyle),
),
),
Expanded(
child: Material(
child: ListView(
@ -1114,11 +1171,13 @@ class _HeaderControlState extends State<HeaderControl> {
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: Text(
videoIntroController.videoDetail.value.title ?? '',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
child: Obx(
() => Text(
videoIntroController.videoDetail.value.title ?? '',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
),
@ -1158,6 +1217,22 @@ class _HeaderControlState extends State<HeaderControl> {
// ),
// 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,
@ -1229,6 +1304,31 @@ class _HeaderControlState extends State<HeaderControl> {
),
SizedBox(width: buttonSpace),
],
/// 字幕
// SizedBox(
// width: 34,
// height: 34,
// child: IconButton(
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// onPressed: () => showSubtitleDialog(),
// icon: const Icon(
// Icons.closed_caption_off,
// size: 22,
// ),
// ),
// ),
ComBtn(
icon: const Icon(
Icons.closed_caption_off,
size: 22,
color: Colors.white,
),
fuc: () => showSubtitleDialog(),
),
SizedBox(width: buttonSpace),
Obx(
() => SizedBox(
width: 45,

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

@ -21,6 +21,8 @@ import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:status_bar_control/status_bar_control.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../models/video/subTitile/content.dart';
import '../../models/video/subTitile/result.dart';
// import 'package:wakelock_plus/wakelock_plus.dart';
Box videoStorage = GStrorage.video;
@ -73,6 +75,8 @@ class PlPlayerController {
final Rx<bool> _doubleSpeedStatus = false.obs;
final Rx<bool> _controlsLock = false.obs;
final Rx<bool> _isFullScreen = false.obs;
final Rx<bool> _subTitleOpen = false.obs;
final Rx<int> _subTitleCode = (-1).obs;
// 默认投稿视频格式
static Rx<String> _videoType = 'archive'.obs;
@ -97,7 +101,7 @@ class PlPlayerController {
bool _isFirstTime = true;
Timer? _timer;
Timer? _timerForSeek;
late Timer? _timerForSeek;
Timer? _timerForVolume;
Timer? _timerForShowingVolume;
Timer? _timerForGettingVolume;
@ -118,6 +122,7 @@ class PlPlayerController {
PreferredSizeWidget? headerControl;
PreferredSizeWidget? bottomControl;
Widget? danmuWidget;
late RxList subtitles;
/// 数据加载监听
Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream;
@ -147,6 +152,11 @@ class PlPlayerController {
Rx<bool> get mute => _mute;
Stream<bool> get onMuteChanged => _mute.stream;
/// 字幕开启状态
Rx<bool> get subTitleOpen => _subTitleOpen;
Rx<int> get subTitleCode => _subTitleCode;
// Stream<bool> get onSubTitleOpenChanged => _subTitleOpen.stream;
/// [videoPlayerController] instace of Player
Player? get videoPlayerController => _videoPlayerController;
@ -231,6 +241,10 @@ class PlPlayerController {
// 播放顺序相关
PlayRepeat playRepeat = PlayRepeat.pause;
RxList<SubTitileContentModel> subtitleContents =
<SubTitileContentModel>[].obs;
RxString subtitleContent = ''.obs;
void updateSliderPositionSecond() {
int newSecond = _sliderPosition.value.inSeconds;
if (sliderPositionSeconds.value != newSecond) {
@ -350,6 +364,8 @@ class PlPlayerController {
bool enableHeart = true,
// 是否首次加载
bool isFirstTime = true,
// 是否开启字幕
bool enableSubTitle = false,
}) async {
try {
_autoPlay = autoplay;
@ -364,7 +380,9 @@ class PlPlayerController {
_cid = cid;
_enableHeart = enableHeart;
_isFirstTime = isFirstTime;
_subTitleOpen.value = enableSubTitle;
subtitles = [].obs;
subtitleContent.value = '';
if (_videoPlayerController != null &&
_videoPlayerController!.state.playing) {
await pause(notify: false);
@ -575,6 +593,8 @@ class PlPlayerController {
_sliderPosition.value = event;
updateSliderPositionSecond();
}
querySubtitleContent(
videoPlayerController!.state.position.inSeconds.toDouble());
/// 触发回调事件
for (var element in _positionListeners) {
@ -609,6 +629,10 @@ class PlPlayerController {
const Duration(seconds: 1),
() => videoPlayerServiceHandler.onPositionChange(event));
}),
// onSubTitleOpenChanged.listen((bool event) {
// toggleSubtitle(event ? subTitleCode.value : -1);
// })
],
);
}
@ -622,9 +646,6 @@ class PlPlayerController {
/// 跳转至指定位置
Future<void> seekTo(Duration position, {type = 'seek'}) async {
// if (position >= duration.value) {
// position = duration.value - const Duration(milliseconds: 100);
// }
if (position < Duration.zero) {
position = Duration.zero;
}
@ -637,21 +658,13 @@ class PlPlayerController {
await _videoPlayerController?.stream.buffer.first;
}
await _videoPlayerController?.seek(position);
// if (playerStatus.stopped) {
// play();
// }
} else {
print('seek duration else');
_timerForSeek?.cancel();
_timerForSeek =
_timerForSeek ??=
Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {
//_timerForSeek = null;
if (duration.value.inSeconds != 0) {
await _videoPlayerController!.stream.buffer.first;
await _videoPlayerController?.seek(position);
// if (playerStatus.status.value == PlayerStatus.paused) {
// play();
// }
t.cancel();
_timerForSeek = null;
}
@ -1047,12 +1060,61 @@ class PlPlayerController {
}
}
/// 字幕
void toggleSubtitle(int code) {
_subTitleOpen.value = code != -1;
_subTitleCode.value = code;
// if (code == -1) {
// // 关闭字幕
// _subTitleOpen.value = false;
// _subTitleCode.value = code;
// _videoPlayerController?.setSubtitleTrack(SubtitleTrack.no());
// return;
// }
// final SubTitlteItemModel? subtitle = subtitles?.firstWhereOrNull(
// (element) => element.code == code,
// );
// _subTitleOpen.value = true;
// _subTitleCode.value = code;
// _videoPlayerController?.setSubtitleTrack(
// SubtitleTrack.data(
// subtitle!.content!,
// title: subtitle.title,
// language: subtitle.lan,
// ),
// );
}
void querySubtitleContent(double progress) {
if (subTitleCode.value == -1) {
subtitleContent.value = '';
return;
}
if (subtitles.isEmpty) {
return;
}
final SubTitlteItemModel? subtitle = subtitles.firstWhereOrNull(
(element) => element.code == subTitleCode.value,
);
if (subtitle != null && subtitle.body!.isNotEmpty) {
for (var content in subtitle.body!) {
if (progress >= content['from']! && progress <= content['to']!) {
subtitleContent.value = content['content']!;
return;
}
}
}
}
setPlayRepeat(PlayRepeat type) {
playRepeat = type;
videoStorage.put(VideoBoxKey.playRepeat, type.value);
}
Future<void> dispose({String type = 'single'}) async {
print('dispose');
print('dispose: ${playerCount.value}');
// 每次减1最后销毁
if (type == 'single' && playerCount.value > 1) {
_playerCount.value -= 1;
@ -1062,6 +1124,7 @@ class PlPlayerController {
}
_playerCount.value = 0;
try {
print('dispose dispose ---------');
_timer?.cancel();
_timerForVolume?.cancel();
_timerForGettingVolume?.cancel();

View File

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

View File

@ -37,6 +37,7 @@ class PLVideoPlayer extends StatefulWidget {
this.bottomList,
this.customWidget,
this.customWidgets,
this.showEposideCb,
super.key,
});
@ -49,6 +50,7 @@ class PLVideoPlayer extends StatefulWidget {
final Widget? customWidget;
final List<Widget>? customWidgets;
final Function? showEposideCb;
@override
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
@ -214,8 +216,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 上一集
BottomControlType.pre: ComBtn(
icon: const Icon(
Icons.skip_previous_outlined,
size: 15,
Icons.skip_previous_rounded,
size: 21,
color: Colors.white,
),
fuc: () {},
@ -229,8 +231,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 下一集
BottomControlType.next: ComBtn(
icon: const Icon(
Icons.last_page_outlined,
size: 15,
Icons.skip_next_rounded,
size: 21,
color: Colors.white,
),
fuc: () {},
@ -239,6 +241,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 时间进度
BottomControlType.time: Row(
children: [
const SizedBox(width: 8),
Obx(() {
return Text(
_.durationSeconds.value >= 3600
@ -266,6 +269,24 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 空白占位
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(
height: 30,
@ -580,6 +601,45 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
if (widget.danmuWidget != null)
Positioned.fill(top: 4, child: widget.danmuWidget!),
/// 开启且有字幕时展示
Stack(
children: [
Positioned(
left: 0,
right: 0,
bottom: 30,
child: Align(
alignment: Alignment.center,
child: Obx(
() => Visibility(
visible: widget.controller.subTitleCode.value != -1,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: widget.controller.subtitleContent.value != ''
? Colors.black.withOpacity(0.6)
: Colors.transparent,
),
padding: widget.controller.subTitleCode.value != -1
? const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
)
: EdgeInsets.zero,
child: Text(
widget.controller.subtitleContent.value,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
)),
),
),
),
],
),
/// 手势
Positioned.fill(
left: 16,

View File

@ -68,91 +68,8 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
);
},
),
Row(
children: [...buildBottomControl!],
),
// 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),
Row(children: [...buildBottomControl!]),
const SizedBox(height: 10),
],
),
);

View File

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

View File

@ -1,5 +1,7 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -70,9 +72,20 @@ class DownloadUtils {
static Future<bool> downloadImg(String imgUrl,
{String imgType = 'cover'}) async {
try {
if (!await requestPhotoPer()) {
if (!Platform.isAndroid || !await requestPhotoPer()) {
return false;
}
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt <= 32) {
if (!await requestStoragePer()) {
return false;
}
} else {
if (!await requestPhotoPer()) {
return false;
}
}
SmartDialog.showLoading(msg: '保存中');
var response = await Dio()
.get(imgUrl, options: Options(responseType: ResponseType.bytes));

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

@ -19,15 +19,7 @@ class Em {
return regCate(matchStr);
}, onNonMatch: (String str) {
if (str != '') {
str = str
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&#34;', '"')
.replaceAll('&#39;', "'")
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', " ")
.replaceAll('&amp;', "&");
str = decodeHtmlEntities(str);
Map map = {'type': 'text', 'text': str};
res.add(map);
}
@ -35,4 +27,17 @@ class Em {
});
return res;
}
static String decodeHtmlEntities(String title) {
return title
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&#34;', '"')
.replaceAll('&#39;', "'")
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', " ")
.replaceAll('&amp;', "&")
.replaceAll('&#x27;', "'");
}
}

View File

@ -1,10 +1,16 @@
import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart';
import '../models/common/index.dart';
Box setting = GStrorage.setting;
class GlobalData {
int imgQuality = 10;
FullScreenGestureMode fullScreenGestureMode =
FullScreenGestureMode.values.last;
bool enablePlayerControlAnimation = true;
final bool enableMYBar =
setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
// 私有构造函数
GlobalData._();

View File

@ -0,0 +1,34 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import '../pages/home/index.dart';
import '../pages/main/index.dart';
void handleScrollEvent(
ScrollController scrollController,
// StreamController<bool> mainStream,
// StreamController<bool>? searchBarStream,
) {
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
EasyThrottle.throttle(
'stream-throttler',
const Duration(milliseconds: 300),
() {
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);
}
},
);
}

View File

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

32
lib/utils/subtitle.dart Normal file
View File

@ -0,0 +1,32 @@
class SubTitleUtils {
// 格式整理
static String convertToWebVTT(List jsonData) {
String webVTTContent = 'WEBVTT FILE\n\n';
for (int i = 0; i < jsonData.length; i++) {
final item = jsonData[i];
double from = item['from'] as double;
double to = item['to'] as double;
int sid = (item['sid'] ?? 0) as int;
String content = item['content'] as String;
webVTTContent += '$sid\n';
webVTTContent += '${formatTime(from)} --> ${formatTime(to)}\n';
webVTTContent += '$content\n\n';
}
return webVTTContent;
}
static String formatTime(num seconds) {
final String h = (seconds / 3600).floor().toString().padLeft(2, '0');
final String m = (seconds % 3600 / 60).floor().toString().padLeft(2, '0');
final String s = (seconds % 60).floor().toString().padLeft(2, '0');
final String ms =
(seconds * 1000 % 1000).floor().toString().padLeft(3, '0');
if (h == '00') {
return "$m:$s.$ms";
}
return "$h:$m:$s.$ms";
}
}

View File

@ -51,7 +51,7 @@ class Utils {
}
if (time < 3600) {
if (time == 0) {
return time;
return '00:00';
}
final int minute = time ~/ 60;
final double res = time / 60;
@ -208,17 +208,35 @@ class Utils {
static int findClosestNumber(int target, List<int> numbers) {
int minDiff = 127;
late int closestNumber;
int closestNumber = 0; // 初始化为0表示没有找到比目标值小的整数
// 向下查找
try {
for (int number in numbers) {
int diff = (number - target).abs();
if (number < target) {
int diff = target - number; // 计算目标值与当前整数的差值
if (diff < minDiff) {
minDiff = diff;
closestNumber = number;
if (diff < minDiff) {
minDiff = diff;
closestNumber = number;
}
}
}
} catch (_) {}
// 向上查找
if (closestNumber == 0) {
try {
for (int number in numbers) {
int diff = (number - target).abs();
if (diff < minDiff) {
minDiff = diff;
closestNumber = number;
}
}
} catch (_) {}
}
return closestNumber;
}

View File

@ -409,6 +409,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: "direct main"
description:

View File

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