Merge branch 'main' into feature-appScheme

This commit is contained in:
guozhigq
2024-05-01 23:19:30 +08:00
126 changed files with 4900 additions and 3024 deletions

View File

@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: branches:
- "main" - "never"
paths-ignore: paths-ignore:
- "**.md" - "**.md"
- "**.txt" - "**.txt"
@ -12,6 +12,7 @@ on:
- ".idea/**" - ".idea/**"
- "!.github/workflows/**" - "!.github/workflows/**"
jobs: jobs:
update_version: update_version:
name: Read and update version name: Read and update version
@ -205,4 +206,4 @@ jobs:
method: sendFile method: sendFile
path: Pilipala-Beta/* path: Pilipala-Beta/*
parse_mode: Markdown parse_mode: Markdown
context: "*Beta版本: v${{ needs.update_version.outputs.new_version }}*\n更新内容: [${{ needs.update_version.outputs.last_commit }}](${{ github.event.head_commit.url }})" context: "*Beta版本: v${{ needs.update_version.outputs.new_version }}*\n更新内容: [${{ needs.update_version.outputs.last_commit }}]"

View File

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

27
change_log/1.0.22.0430.md Normal file
View File

@ -0,0 +1,27 @@
## 1.0.22
### 功能
+ 字幕
+ 全屏时选集
+ 动态转发
+ 评论视频并转发
+ 收藏夹删除
+ 合集显示封面
+ 底部导航栏编辑、排序功能
+ 历史记录进度条展示
+ 直播画质切换
+ 排行榜功能
+ 视频详情页推荐视频开关
+ 显示联合投稿up
### 修复
+ 收藏夹个数错误
+ 封面保存权限问题
+ 合集最后1p未展示
+ up主页关注按钮触发灰屏
### 优化
+ 视频简介查看逻辑
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -49,6 +49,8 @@
<true/> <true/>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>请允许APP保存图片到相册</string> <string>请允许APP保存图片到相册</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>请允许APP保存图片到相册</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>App需要您的同意,才能访问相册</string> <string>App需要您的同意,才能访问相册</string>
<key>NSAppleMusicUsageDescription</key> <key>NSAppleMusicUsageDescription</key>

View File

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:pilipala/common/widgets/network_img_layer.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 isFullScreen || episode?.cover == null || episode?.cover == ''
? 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,
)))
: InkWell(
onTap: () {
SmartDialog.showToast('切换至「$title');
changeFucCall.call(episode, index);
},
child: Padding(
padding:
const EdgeInsets.only(left: 14, right: 14, top: 8, bottom: 8),
child: Row(
children: [
NetworkImgLayer(
width: 130, height: 75, src: episode?.cover ?? ''),
const SizedBox(width: 10),
Expanded(
child: Text(
title,
maxLines: 2,
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

@ -36,7 +36,6 @@ class NetworkImgLayer extends StatelessWidget {
final int defaultImgQuality = GlobalData().imgQuality; final int defaultImgQuality = GlobalData().imgQuality;
final String imageUrl = final String imageUrl =
'${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp'; '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp';
// print(imageUrl);
int? memCacheWidth, memCacheHeight; int? memCacheWidth, memCacheHeight;
double aspectRatio = (width / height).toDouble(); double aspectRatio = (width / height).toDouble();

View File

@ -1,86 +0,0 @@
import 'package:flutter/material.dart';
import '../../utils/download.dart';
import '../constants.dart';
import 'network_img_layer.dart';
class OverlayPop extends StatelessWidget {
const OverlayPop({super.key, this.videoItem, this.closeFn});
final dynamic videoItem;
final Function? closeFn;
@override
Widget build(BuildContext context) {
final double imgWidth = MediaQuery.sizeOf(context).width - 8 * 2;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(10.0),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
NetworkImgLayer(
width: imgWidth,
height: imgWidth / StyleString.aspectRatio,
src: videoItem.pic! as String,
quality: 100,
),
Positioned(
right: 8,
top: 8,
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius:
const BorderRadius.all(Radius.circular(20))),
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => closeFn!(),
icon: const Icon(
Icons.close,
size: 18,
color: Colors.white,
),
),
),
),
],
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
child: Row(
children: [
Expanded(
child: Text(
videoItem.title! as String,
),
),
const SizedBox(width: 4),
IconButton(
tooltip: '保存封面图',
onPressed: () async {
await DownloadUtils.downloadImg(
videoItem.pic != null
? videoItem.pic as String
: videoItem.cover as String,
);
// closeFn!();
},
icon: const Icon(Icons.download, size: 20),
)
],
)),
],
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/utils/image_save.dart';
import '../../http/search.dart'; import '../../http/search.dart';
import '../../http/user.dart'; import '../../http/user.dart';
import '../../http/video.dart'; import '../../http/video.dart';
@ -16,23 +17,24 @@ class VideoCardH extends StatelessWidget {
const VideoCardH({ const VideoCardH({
super.key, super.key,
required this.videoItem, required this.videoItem,
this.longPress, this.onPressedFn,
this.longPressEnd,
this.source = 'normal', this.source = 'normal',
this.showOwner = true, this.showOwner = true,
this.showView = true, this.showView = true,
this.showDanmaku = true, this.showDanmaku = true,
this.showPubdate = false, this.showPubdate = false,
this.showCharge = false,
}); });
// ignore: prefer_typing_uninitialized_variables // ignore: prefer_typing_uninitialized_variables
final videoItem; final videoItem;
final Function()? longPress; final Function()? onPressedFn;
final Function()? longPressEnd; // normal 推荐, later 稍后再看, search 搜索
final String source; final String source;
final bool showOwner; final bool showOwner;
final bool showView; final bool showView;
final bool showDanmaku; final bool showDanmaku;
final bool showPubdate; final bool showPubdate;
final bool showCharge;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -43,102 +45,103 @@ class VideoCardH extends StatelessWidget {
type = videoItem.type; type = videoItem.type;
} catch (_) {} } catch (_) {}
final String heroTag = Utils.makeHeroTag(aid); final String heroTag = Utils.makeHeroTag(aid);
return GestureDetector( return InkWell(
onLongPress: () { onTap: () async {
if (longPress != null) { try {
longPress!(); if (type == 'ketang') {
SmartDialog.showToast('课堂视频暂不支持播放');
return;
}
final int cid =
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
} catch (err) {
SmartDialog.showToast(err.toString());
} }
}, },
// onLongPressEnd: (details) { onLongPress: () => imageSaveDialog(
// if (longPressEnd != null) { context,
// longPressEnd!(); videoItem,
// } SmartDialog.dismiss,
// }, ),
child: InkWell( child: Padding(
onTap: () async { padding: const EdgeInsets.fromLTRB(
try { StyleString.safeSpace, 5, StyleString.safeSpace, 5),
if (type == 'ketang') { child: LayoutBuilder(
SmartDialog.showToast('课堂视频暂不支持播放'); builder: (BuildContext context, BoxConstraints boxConstraints) {
return; final double width = (boxConstraints.maxWidth -
} StyleString.cardSpace *
final int cid = 6 /
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); MediaQuery.textScalerOf(context).scale(1.0)) /
Get.toNamed('/video?bvid=$bvid&cid=$cid', 2;
arguments: {'videoItem': videoItem, 'heroTag': heroTag}); return Container(
} catch (err) { constraints: const BoxConstraints(minHeight: 88),
SmartDialog.showToast(err.toString()); height: width / StyleString.aspectRatio,
} child: Row(
}, mainAxisAlignment: MainAxisAlignment.start,
child: Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.fromLTRB( children: <Widget>[
StyleString.safeSpace, 5, StyleString.safeSpace, 5), AspectRatio(
child: LayoutBuilder( aspectRatio: StyleString.aspectRatio,
builder: (BuildContext context, BoxConstraints boxConstraints) { child: LayoutBuilder(
final double width = (boxConstraints.maxWidth - builder: (BuildContext context,
StyleString.cardSpace * BoxConstraints boxConstraints) {
6 / final double maxWidth = boxConstraints.maxWidth;
MediaQuery.textScalerOf(context).scale(1.0)) / final double maxHeight = boxConstraints.maxHeight;
2; return Stack(
return Container( children: [
constraints: const BoxConstraints(minHeight: 88), Hero(
height: width / StyleString.aspectRatio, tag: heroTag,
child: Row( child: NetworkImgLayer(
mainAxisAlignment: MainAxisAlignment.start, src: videoItem.pic as String,
crossAxisAlignment: CrossAxisAlignment.start, width: maxWidth,
children: <Widget>[ height: maxHeight,
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic as String,
width: maxWidth,
height: maxHeight,
),
), ),
if (videoItem.duration != 0) ),
PBadge( if (videoItem.duration != 0)
text: Utils.timeFormat(videoItem.duration!), PBadge(
right: 6.0, text: Utils.timeFormat(videoItem.duration!),
bottom: 6.0, right: 6.0,
type: 'gray', bottom: 6.0,
), type: 'gray',
if (type != 'video') ),
PBadge( if (type != 'video')
text: type, PBadge(
left: 6.0, text: type,
bottom: 6.0, left: 6.0,
type: 'primary', bottom: 6.0,
), type: 'primary',
// if (videoItem.rcmdReason != null && ),
// videoItem.rcmdReason.content != '') // if (videoItem.rcmdReason != null &&
// pBadge(videoItem.rcmdReason.content, context, // videoItem.rcmdReason.content != '')
// 6.0, 6.0, null, null), // pBadge(videoItem.rcmdReason.content, context,
], // 6.0, 6.0, null, null),
); if (showCharge && videoItem?.isChargingSrc)
}, const PBadge(
), text: '充电专属',
right: 6.0,
top: 6.0,
type: 'primary',
),
],
);
},
), ),
VideoContent( ),
videoItem: videoItem, VideoContent(
source: source, videoItem: videoItem,
showOwner: showOwner, source: source,
showView: showView, showOwner: showOwner,
showDanmaku: showDanmaku, showView: showView,
showPubdate: showPubdate, showDanmaku: showDanmaku,
) showPubdate: showPubdate,
], onPressedFn: onPressedFn,
), )
); ],
}, ),
), );
},
), ),
), ),
); );
@ -153,6 +156,7 @@ class VideoContent extends StatelessWidget {
final bool showView; final bool showView;
final bool showDanmaku; final bool showDanmaku;
final bool showPubdate; final bool showPubdate;
final Function()? onPressedFn;
const VideoContent({ const VideoContent({
super.key, super.key,
@ -162,6 +166,7 @@ class VideoContent extends StatelessWidget {
this.showView = true, this.showView = true,
this.showDanmaku = true, this.showDanmaku = true,
this.showPubdate = false, this.showPubdate = false,
this.onPressedFn,
}); });
@override @override
@ -172,7 +177,7 @@ class VideoContent extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (videoItem.title is String) ...[ if (source == 'normal' || source == 'later') ...[
Text( Text(
videoItem.title as String, videoItem.title as String,
textAlign: TextAlign.start, textAlign: TextAlign.start,
@ -187,7 +192,7 @@ class VideoContent extends StatelessWidget {
maxLines: 2, maxLines: 2,
text: TextSpan( text: TextSpan(
children: [ children: [
for (final i in videoItem.title) ...[ for (final i in videoItem.titleList) ...[
TextSpan( TextSpan(
text: i['text'] as String, text: i['text'] as String,
style: TextStyle( style: TextStyle(
@ -365,6 +370,19 @@ class VideoContent extends StatelessWidget {
], ],
), ),
), ),
if (source == 'later') ...[
IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => onPressedFn?.call(),
icon: Icon(
Icons.clear_outlined,
color: Theme.of(context).colorScheme.outline,
size: 18,
),
)
],
], ],
), ),
], ],

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/image_save.dart';
import '../../models/model_rec_video_item.dart'; import '../../models/model_rec_video_item.dart';
import 'stat/danmu.dart'; import 'stat/danmu.dart';
import 'stat/view.dart'; import 'stat/view.dart';
@ -19,15 +21,11 @@ import 'network_img_layer.dart';
class VideoCardV extends StatelessWidget { class VideoCardV extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
final int crossAxisCount; final int crossAxisCount;
final Function()? longPress;
final Function()? longPressEnd;
const VideoCardV({ const VideoCardV({
Key? key, Key? key,
required this.videoItem, required this.videoItem,
required this.crossAxisCount, required this.crossAxisCount,
this.longPress,
this.longPressEnd,
}) : super(key: key); }) : super(key: key);
bool isStringNumeric(String str) { bool isStringNumeric(String str) {
@ -127,64 +125,53 @@ class VideoCardV extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id); String heroTag = Utils.makeHeroTag(videoItem.id);
return Card( return InkWell(
elevation: 0, onTap: () async => onPushDetail(heroTag),
clipBehavior: Clip.hardEdge, onLongPress: () => imageSaveDialog(
margin: EdgeInsets.zero, context,
child: GestureDetector( videoItem,
onLongPress: () { SmartDialog.dismiss,
if (longPress != null) { ),
longPress!(); borderRadius: BorderRadius.circular(16),
} child: Column(
}, children: [
// onLongPressEnd: (details) { AspectRatio(
// if (longPressEnd != null) { aspectRatio: StyleString.aspectRatio,
// longPressEnd!(); child: LayoutBuilder(builder: (context, boxConstraints) {
// } double maxWidth = boxConstraints.maxWidth;
// }, double maxHeight = boxConstraints.maxHeight;
child: InkWell( return Stack(
onTap: () async => onPushDetail(heroTag), children: [
child: Column( Hero(
children: [ tag: heroTag,
AspectRatio( child: NetworkImgLayer(
aspectRatio: StyleString.aspectRatio, src: videoItem.pic,
child: LayoutBuilder(builder: (context, boxConstraints) { width: maxWidth,
double maxWidth = boxConstraints.maxWidth; height: maxHeight,
double maxHeight = boxConstraints.maxHeight; ),
return Stack( ),
children: [ if (videoItem.duration > 0)
Hero( if (crossAxisCount == 1) ...[
tag: heroTag, PBadge(
child: NetworkImgLayer( bottom: 10,
src: videoItem.pic, right: 10,
width: maxWidth, text: Utils.timeFormat(videoItem.duration),
height: maxHeight, )
), ] else ...[
), PBadge(
if (videoItem.duration > 0) bottom: 6,
if (crossAxisCount == 1) ...[ right: 7,
PBadge( size: 'small',
bottom: 10, type: 'gray',
right: 10, text: Utils.timeFormat(videoItem.duration),
text: Utils.timeFormat(videoItem.duration), )
)
] else ...[
PBadge(
bottom: 6,
right: 7,
size: 'small',
type: 'gray',
text: Utils.timeFormat(videoItem.duration),
)
],
], ],
); ],
}), );
), }),
VideoContent(videoItem: videoItem, crossAxisCount: crossAxisCount)
],
), ),
), VideoContent(videoItem: videoItem, crossAxisCount: crossAxisCount)
],
), ),
); );
} }
@ -196,122 +183,90 @@ class VideoContent extends StatelessWidget {
const VideoContent( const VideoContent(
{Key? key, required this.videoItem, required this.crossAxisCount}) {Key? key, required this.videoItem, required this.crossAxisCount})
: super(key: key); : super(key: key);
Widget _buildBadge(String text, String type, [double fs = 12]) {
return PBadge(
text: text,
stack: 'normal',
size: 'small',
type: type,
fs: fs,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Padding(
flex: crossAxisCount == 1 ? 0 : 1, padding: crossAxisCount == 1
child: Padding( ? const EdgeInsets.fromLTRB(9, 9, 9, 4)
padding: crossAxisCount == 1 : const EdgeInsets.fromLTRB(5, 8, 5, 4),
? const EdgeInsets.fromLTRB(9, 9, 9, 4) child: Column(
: const EdgeInsets.fromLTRB(5, 8, 5, 4), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Text(
// mainAxisAlignment: MainAxisAlignment.spaceBetween, videoItem.title,
children: [ maxLines: 2,
Row( overflow: TextOverflow.ellipsis,
children: [ ),
Expanded( if (crossAxisCount > 1) ...[
child: Text( const SizedBox(height: 2),
videoItem.title, VideoStat(videoItem: videoItem, crossAxisCount: crossAxisCount),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (videoItem.goto == 'av' && crossAxisCount == 1) ...[
const SizedBox(width: 10),
VideoPopupMenu(
size: 32,
iconSize: 18,
videoItem: videoItem,
),
],
],
),
if (crossAxisCount > 1) ...[
const SizedBox(height: 2),
VideoStat(
videoItem: videoItem,
crossAxisCount: crossAxisCount,
),
],
if (crossAxisCount == 1) const SizedBox(height: 4),
Row(
children: [
if (videoItem.goto == 'bangumi') ...[
PBadge(
text: videoItem.bangumiBadge,
stack: 'normal',
size: 'small',
type: 'line',
fs: 9,
)
],
if (videoItem.rcmdReason != null &&
videoItem.rcmdReason.content != '') ...[
PBadge(
text: videoItem.rcmdReason.content,
stack: 'normal',
size: 'small',
type: 'color',
)
],
if (videoItem.goto == 'picture') ...[
const PBadge(
text: '动态',
stack: 'normal',
size: 'small',
type: 'line',
fs: 9,
)
],
if (videoItem.isFollowed == 1) ...[
const PBadge(
text: '已关注',
stack: 'normal',
size: 'small',
type: 'color',
)
],
Expanded(
flex: crossAxisCount == 1 ? 0 : 1,
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
),
if (crossAxisCount == 1) ...[
Text(
'',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
VideoStat(
videoItem: videoItem,
crossAxisCount: crossAxisCount,
),
const Spacer(),
],
if (videoItem.goto == 'av' && crossAxisCount != 1) ...[
VideoPopupMenu(
size: 24,
iconSize: 14,
videoItem: videoItem,
),
] else ...[
const SizedBox(height: 24)
]
],
),
], ],
), if (crossAxisCount == 1) const SizedBox(height: 4),
Row(
children: [
if (videoItem.goto == 'bangumi')
_buildBadge(videoItem.bangumiBadge, 'line', 9),
if (videoItem.rcmdReason?.content != null &&
videoItem.rcmdReason.content != '')
_buildBadge(videoItem.rcmdReason.content, 'color'),
if (videoItem.goto == 'picture') _buildBadge('动态', 'line', 9),
if (videoItem.isFollowed == 1) _buildBadge('已关注', 'color'),
Expanded(
flex: crossAxisCount == 1 ? 0 : 1,
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
),
if (crossAxisCount == 1) ...[
const SizedBox(width: 10),
VideoStat(
videoItem: videoItem,
crossAxisCount: crossAxisCount,
),
const Spacer(),
],
if (videoItem.goto == 'av')
SizedBox(
width: 24,
height: 24,
child: IconButton(
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(videoItem: videoItem);
},
);
},
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
),
)
],
),
],
), ),
); );
} }
@ -331,15 +286,9 @@ class VideoStat extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
StatView( StatView(theme: 'gray', view: videoItem.stat.view),
theme: 'gray',
view: videoItem.stat.view,
),
const SizedBox(width: 8), const SizedBox(width: 8),
StatDanMu( StatDanMu(theme: 'gray', danmu: videoItem.stat.danmu),
theme: 'gray',
danmu: videoItem.stat.danmu,
),
if (videoItem is RecVideoItemModel) ...<Widget>[ if (videoItem is RecVideoItemModel) ...<Widget>[
crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8), crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8),
RichText( RichText(
@ -358,99 +307,98 @@ class VideoStat extends StatelessWidget {
} }
} }
class VideoPopupMenu extends StatelessWidget { class MorePanel extends StatelessWidget {
final double? size;
final double? iconSize;
final dynamic videoItem; final dynamic videoItem;
const MorePanel({super.key, required this.videoItem});
const VideoPopupMenu({ Future<dynamic> menuActionHandler(String type) async {
Key? key, switch (type) {
required this.size, case 'block':
required this.iconSize, blockUser();
required this.videoItem, break;
}) : super(key: key); case 'watchLater':
var res = await UserHttp.toViewLater(bvid: videoItem.bvid as String);
SmartDialog.showToast(res['msg']);
Get.back();
break;
default:
}
}
void blockUser() async {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: Text('确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?'
'\n\n被拉黑的Up可以在隐私设置-黑名单管理中解除'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'点错了',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
var res = await VideoHttp.relationMod(
mid: videoItem.owner.mid,
act: 5,
reSrc: 11,
);
SmartDialog.dismiss();
SmartDialog.showToast(res['msg'] ?? '成功');
},
child: const Text('确认'),
)
],
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return Container(
width: size, padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
height: size, child: Column(
child: PopupMenuButton<String>( mainAxisSize: MainAxisSize.min,
padding: EdgeInsets.zero, children: [
icon: Icon( InkWell(
Icons.more_vert_outlined, onTap: () => Get.back(),
color: Theme.of(context).colorScheme.outline, child: Container(
size: iconSize, height: 35,
), padding: const EdgeInsets.only(bottom: 2),
position: PopupMenuPosition.under, child: Center(
// constraints: const BoxConstraints(maxHeight: 35), child: Container(
onSelected: (String type) {}, width: 32,
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[ height: 3,
PopupMenuItem<String>( decoration: BoxDecoration(
onTap: () async { color: Theme.of(context).colorScheme.outline,
var res = borderRadius: const BorderRadius.all(Radius.circular(3))),
await UserHttp.toViewLater(bvid: videoItem.bvid as String); ),
SmartDialog.showToast(res['msg']); ),
},
value: 'pause',
height: 40,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
), ),
), ),
const PopupMenuDivider(), ListTile(
PopupMenuItem<String>( onTap: () async => await menuActionHandler('block'),
onTap: () async { minLeadingWidth: 0,
SmartDialog.show( leading: const Icon(Icons.block, size: 19),
useSystem: true, title: Text(
animationType: SmartAnimationType.centerFade_otherSlide, '拉黑up主 「${videoItem.owner.name}',
builder: (BuildContext context) { style: Theme.of(context).textTheme.titleSmall,
return AlertDialog(
title: const Text('提示'),
content: Text(
'确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?'
'\n\n被拉黑的Up可以在隐私设置-黑名单管理中解除'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'点错了',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
var res = await VideoHttp.relationMod(
mid: videoItem.owner.mid,
act: 5,
reSrc: 11,
);
SmartDialog.dismiss();
SmartDialog.showToast(res['msg'] ?? '成功');
},
child: const Text('确认'),
)
],
);
},
);
},
value: 'pause',
height: 40,
child: Row(
children: [
const Icon(Icons.block, size: 16),
const SizedBox(width: 6),
Text('拉黑:${videoItem.owner.name}',
style: const TextStyle(fontSize: 13))
],
), ),
), ),
ListTile(
onTap: () async => await menuActionHandler('watchLater'),
minLeadingWidth: 0,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title:
Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall),
),
], ],
), ),
); );

View File

@ -484,11 +484,17 @@ class Api {
/// 激活buvid3 /// 激活buvid3
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi'; 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 userSubFolder = '/x/v3/fav/folder/collected/list';
/// 我的订阅详情 /// 我的订阅详情 type 21
static const userSubFolderDetail = '/x/space/fav/season/list'; static const userSeasonList = '/x/space/fav/season/list';
/// 我的订阅详情 type 11
static const userResourceList = '/x/v3/fav/resource/list';
/// 表情 /// 表情
static const emojiList = '/x/emote/user/panel/web'; static const emojiList = '/x/emote/user/panel/web';
@ -503,4 +509,15 @@ class Api {
/// 排行榜 /// 排行榜
static const String getRankApi = "/x/web-interface/ranking/v2"; static const String getRankApi = "/x/web-interface/ranking/v2";
/// 取消订阅
static const String cancelSub = '/x/v3/fav/season/unfav';
/// 动态转发
static const String dynamicForwardUrl = '/x/dynamic/feed/create/submit_check';
/// 创建动态
static const String dynamicCreate = '/x/dynamic/feed/create/dyn';
/// 删除收藏夹
static const String delFavFolder = '/x/v3/fav/folder/del';
} }

View File

@ -1,3 +1,4 @@
import 'dart:math';
import '../models/dynamics/result.dart'; import '../models/dynamics/result.dart';
import '../models/dynamics/up.dart'; import '../models/dynamics/up.dart';
import 'index.dart'; import 'index.dart';
@ -117,4 +118,94 @@ class DynamicsHttp {
}; };
} }
} }
static Future dynamicForward() async {
var res = await Request().post(
Api.dynamicForwardUrl,
queryParameters: {
'csrf': await Request.getCsrf(),
'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'},
'x-bili-web-req-json': {'spm_id': '333.999'},
},
data: {
'attach_card': null,
'scene': 4,
'content': {
'conetents': [
{'raw_text': "2", 'type': 1, 'biz_id': ""}
]
}
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
static Future dynamicCreate({
required int mid,
required int scene,
int? oid,
String? dynIdStr,
String? rawText,
}) async {
DateTime now = DateTime.now();
int timestamp = now.millisecondsSinceEpoch ~/ 1000;
Random random = Random();
int randomNumber = random.nextInt(9000) + 1000;
String uploadId = '${mid}_${timestamp}_$randomNumber';
Map<String, dynamic> webRepostSrc = {
'dyn_id_str': dynIdStr ?? '',
};
/// 投稿转发
if (scene == 5) {
webRepostSrc = {
'revs_id': {'dyn_type': 8, 'rid': oid}
};
}
var res = await Request().post(Api.dynamicCreate, queryParameters: {
'platform': 'web',
'csrf': await Request.getCsrf(),
'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'},
'x-bili-web-req-json': {'spm_id': '333.999'},
}, data: {
'dyn_req': {
'content': {
'contents': [
{'raw_text': rawText ?? '', 'type': 1, 'biz_id': ''}
]
},
'scene': scene,
'attach_card': null,
'upload_id': uploadId,
'meta': {
'app_meta': {'from': 'create.dynamic.web', 'mobi_app': 'web'}
}
},
'web_repost_src': webRepostSrc
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@ -22,19 +22,14 @@ class ReplyHttp {
return { return {
'status': true, 'status': true,
'data': ReplyData.fromJson(res.data['data']), 'data': ReplyData.fromJson(res.data['data']),
'code': 200,
}; };
} else { } else {
Map errMap = {
-400: '请求错误',
-404: '无此项',
12002: '当前页面评论功能已关闭',
12009: '评论主体的type不合法',
12061: 'UP主已关闭评论区',
};
return { return {
'status': false, 'status': false,
'date': [], 'date': [],
'msg': errMap[res.data['code']] ?? res.data['message'], 'code': res.data['code'],
'msg': res.data['message'],
}; };
} }
} }

View File

@ -163,4 +163,20 @@ class SearchHttp {
}; };
} }
} }
static Future<Map<String, dynamic>> ab2cWithPic(
{int? aid, String? bvid}) async {
Map<String, dynamic> data = {};
if (aid != null) {
data['aid'] = aid;
} else if (bvid != null) {
data['bvid'] = bvid;
}
final dynamic res =
await Request().get(Api.ab2c, data: <String, dynamic>{...data});
return {
'cid': res.data['data'].first['cid'],
'pic': res.data['data'].first['first_frame'],
};
}
} }

View File

@ -330,12 +330,12 @@ class UserHttp {
} }
} }
static Future userSubFolderDetail({ static Future userSeasonList({
required int seasonId, required int seasonId,
required int pn, required int pn,
required int ps, required int ps,
}) async { }) async {
var res = await Request().get(Api.userSubFolderDetail, data: { var res = await Request().get(Api.userSeasonList, data: {
'season_id': seasonId, 'season_id': seasonId,
'ps': ps, 'ps': ps,
'pn': pn, 'pn': pn,
@ -349,4 +349,67 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']}; 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']};
}
}
// 删除文件夹
static Future delFavFolder({required int mediaIds}) async {
var res = await Request().post(
Api.delFavFolder,
queryParameters: {
'media_ids': mediaIds,
'platform': 'web',
'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/user/fav_folder.dart';
import '../models/video/ai.dart'; import '../models/video/ai.dart';
import '../models/video/play/url.dart'; import '../models/video/play/url.dart';
import '../models/video/subTitile/result.dart';
import '../models/video_detail_res.dart'; import '../models/video_detail_res.dart';
import '../utils/recommend_filter.dart'; import '../utils/recommend_filter.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
import '../utils/subtitle.dart';
import '../utils/wbi_sign.dart'; import '../utils/wbi_sign.dart';
import 'api.dart'; import 'api.dart';
import 'init.dart'; import 'init.dart';
@ -33,7 +35,7 @@ class VideoHttp {
Api.recommendListWeb, Api.recommendListWeb,
data: { data: {
'version': 1, 'version': 1,
'feed_version': 'V8', 'feed_version': 'V3',
'homepage_ver': 1, 'homepage_ver': 1,
'ps': ps, 'ps': ps,
'fresh_idx': freshIdx, 'fresh_idx': freshIdx,
@ -190,22 +192,15 @@ class VideoHttp {
// 视频信息 标题、简介 // 视频信息 标题、简介
static Future videoIntro({required String bvid}) async { static Future videoIntro({required String bvid}) async {
var res = await Request().get(Api.videoIntro, data: {'bvid': bvid}); var res = await Request().get(Api.videoIntro, data: {'bvid': bvid});
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); if (res.data['code'] == 0) {
if (result.code == 0) { VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
return {'status': true, 'data': result.data!}; return {'status': true, 'data': result.data!};
} else { } else {
Map errMap = {
-400: '请求错误',
-403: '权限不足',
-404: '视频资源失效',
62002: '稿件不可见',
62004: '稿件审核中',
};
return { return {
'status': false, 'status': false,
'data': null, 'data': null,
'code': result.code, 'code': res.data['code'],
'msg': errMap[result.code] ?? '请求异常', 'msg': res.data['message'],
}; };
} }
} }
@ -476,6 +471,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 { static Future getRankVideoList(int rid) async {
try { try {
@ -498,4 +512,12 @@ class VideoHttp {
return {'status': false, 'data': [], 'msg': err}; 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/services/service_locator.dart';
import 'package:pilipala/utils/app_scheme.dart'; import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart'; import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/global_data.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc. import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
import 'package:pilipala/utils/recommend_filter.dart'; import 'package:pilipala/utils/recommend_filter.dart';
@ -34,6 +35,7 @@ void main() async {
.then((_) async { .then((_) async {
await GStrorage.init(); await GStrorage.init();
await setupServiceLocator(); await setupServiceLocator();
clearLogs();
Request(); Request();
await Request.setCookie(); await Request.setCookie();
RecommendFilter(); RecommendFilter();
@ -64,13 +66,21 @@ void main() async {
); );
// 小白条、导航栏沉浸 // 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); if (Platform.isAndroid) {
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( List<String> versionParts = Platform.version.split('.');
systemNavigationBarColor: Colors.transparent, int androidVersion = int.parse(versionParts[0]);
systemNavigationBarDividerColor: Colors.transparent, if (androidVersion >= 29) {
statusBarColor: Colors.transparent, SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
)); }
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
statusBarColor: Colors.transparent,
));
}
Data.init(); Data.init();
GlobalData();
PiliSchame.init(); PiliSchame.init();
DisableBatteryOpt(); DisableBatteryOpt();
}); });
@ -133,16 +143,43 @@ class MyApp extends StatelessWidget {
brightness: Brightness.dark, brightness: Brightness.dark,
); );
} }
// ThemeData themeData = ThemeData(
// colorScheme: currentThemeValue == ThemeType.dark
// ? darkColorScheme
// : lightColorScheme,
// );
// // 小白条、导航栏沉浸
// if (Platform.isAndroid) {
// List<String> versionParts = Platform.version.split('.');
// int androidVersion = int.parse(versionParts[0]);
// if (androidVersion >= 29) {
// 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; // PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20;
return GetMaterialApp( return GetMaterialApp(
title: 'PiLiPaLa', title: 'PiliPala',
theme: ThemeData( theme: ThemeData(
// fontFamily: 'HarmonyOS',
colorScheme: currentThemeValue == ThemeType.dark colorScheme: currentThemeValue == ThemeType.dark
? darkColorScheme ? darkColorScheme
: lightColorScheme, : lightColorScheme,
useMaterial3: true,
snackBarTheme: SnackBarThemeData( snackBarTheme: SnackBarThemeData(
actionTextColor: lightColorScheme.primary, actionTextColor: lightColorScheme.primary,
backgroundColor: lightColorScheme.secondaryContainer, backgroundColor: lightColorScheme.secondaryContainer,
@ -159,11 +196,9 @@ class MyApp extends StatelessWidget {
), ),
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
// fontFamily: 'HarmonyOS',
colorScheme: currentThemeValue == ThemeType.light colorScheme: currentThemeValue == ThemeType.light
? lightColorScheme ? lightColorScheme
: darkColorScheme, : darkColorScheme,
useMaterial3: true,
snackBarTheme: SnackBarThemeData( snackBarTheme: SnackBarThemeData(
actionTextColor: darkColorScheme.primary, actionTextColor: darkColorScheme.primary,
backgroundColor: darkColorScheme.secondaryContainer, backgroundColor: darkColorScheme.secondaryContainer,

View File

@ -30,6 +30,7 @@ class BangumiListItemModel {
BangumiListItemModel({ BangumiListItemModel({
this.badge, this.badge,
this.badgeType, this.badgeType,
this.pic,
this.cover, this.cover,
// this.firstEp, // this.firstEp,
this.indexShow, this.indexShow,
@ -50,6 +51,7 @@ class BangumiListItemModel {
String? badge; String? badge;
int? badgeType; int? badgeType;
String? pic;
String? cover; String? cover;
String? indexShow; String? indexShow;
int? isFinish; int? isFinish;
@ -70,6 +72,7 @@ class BangumiListItemModel {
BangumiListItemModel.fromJson(Map<String, dynamic> json) { BangumiListItemModel.fromJson(Map<String, dynamic> json) {
badge = json['badge'] == '' ? null : json['badge']; badge = json['badge'] == '' ? null : json['badge'];
badgeType = json['badge_type']; badgeType = json['badge_type'];
pic = json['cover'];
cover = json['cover']; cover = json['cover'];
indexShow = json['index_show']; indexShow = json['index_show'];
isFinish = json['is_finish']; isFinish = json['is_finish'];

View File

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

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/rank/zone/index.dart'; import 'package:pilipala/pages/rank/zone/index.dart';
enum RandType { enum RandType {
@ -74,7 +73,6 @@ List tabsConfig = [
), ),
'label': '全站', 'label': '全站',
'type': RandType.all, 'type': RandType.all,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 0), 'page': const ZonePage(rid: 0),
}, },
{ {
@ -84,7 +82,6 @@ List tabsConfig = [
), ),
'label': '国创相关', 'label': '国创相关',
'type': RandType.creation, 'type': RandType.creation,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 168), 'page': const ZonePage(rid: 168),
}, },
{ {
@ -94,7 +91,6 @@ List tabsConfig = [
), ),
'label': '动画', 'label': '动画',
'type': RandType.animation, 'type': RandType.animation,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 1), 'page': const ZonePage(rid: 1),
}, },
{ {
@ -104,7 +100,6 @@ List tabsConfig = [
), ),
'label': '音乐', 'label': '音乐',
'type': RandType.music, 'type': RandType.music,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 3), 'page': const ZonePage(rid: 3),
}, },
{ {
@ -114,7 +109,6 @@ List tabsConfig = [
), ),
'label': '舞蹈', 'label': '舞蹈',
'type': RandType.dance, 'type': RandType.dance,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 129), 'page': const ZonePage(rid: 129),
}, },
{ {
@ -124,7 +118,6 @@ List tabsConfig = [
), ),
'label': '游戏', 'label': '游戏',
'type': RandType.game, 'type': RandType.game,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 4), 'page': const ZonePage(rid: 4),
}, },
{ {
@ -134,7 +127,6 @@ List tabsConfig = [
), ),
'label': '知识', 'label': '知识',
'type': RandType.knowledge, 'type': RandType.knowledge,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 36), 'page': const ZonePage(rid: 36),
}, },
{ {
@ -144,7 +136,6 @@ List tabsConfig = [
), ),
'label': '科技', 'label': '科技',
'type': RandType.technology, 'type': RandType.technology,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 188), 'page': const ZonePage(rid: 188),
}, },
{ {
@ -154,7 +145,6 @@ List tabsConfig = [
), ),
'label': '运动', 'label': '运动',
'type': RandType.sport, 'type': RandType.sport,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 234), 'page': const ZonePage(rid: 234),
}, },
{ {
@ -164,7 +154,6 @@ List tabsConfig = [
), ),
'label': '汽车', 'label': '汽车',
'type': RandType.car, 'type': RandType.car,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 223), 'page': const ZonePage(rid: 223),
}, },
{ {
@ -174,7 +163,6 @@ List tabsConfig = [
), ),
'label': '生活', 'label': '生活',
'type': RandType.life, 'type': RandType.life,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 160), 'page': const ZonePage(rid: 160),
}, },
{ {
@ -184,7 +172,6 @@ List tabsConfig = [
), ),
'label': '美食', 'label': '美食',
'type': RandType.food, 'type': RandType.food,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 211), 'page': const ZonePage(rid: 211),
}, },
{ {
@ -194,7 +181,6 @@ List tabsConfig = [
), ),
'label': '动物圈', 'label': '动物圈',
'type': RandType.animal, 'type': RandType.animal,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 217), 'page': const ZonePage(rid: 217),
}, },
{ {
@ -204,7 +190,6 @@ List tabsConfig = [
), ),
'label': '鬼畜', 'label': '鬼畜',
'type': RandType.madness, 'type': RandType.madness,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 119), 'page': const ZonePage(rid: 119),
}, },
{ {
@ -214,7 +199,6 @@ List tabsConfig = [
), ),
'label': '时尚', 'label': '时尚',
'type': RandType.fashion, 'type': RandType.fashion,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 155), 'page': const ZonePage(rid: 155),
}, },
{ {
@ -224,7 +208,6 @@ List tabsConfig = [
), ),
'label': '娱乐', 'label': '娱乐',
'type': RandType.entertainment, 'type': RandType.entertainment,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 5), 'page': const ZonePage(rid: 5),
}, },
{ {
@ -234,7 +217,6 @@ List tabsConfig = [
), ),
'label': '影视', 'label': '影视',
'type': RandType.film, 'type': RandType.film,
'ctr': Get.put<ZoneController>,
'page': const ZonePage(rid: 181), 'page': const ZonePage(rid: 181),
} }
]; ];

View File

@ -0,0 +1,63 @@
enum SubtitleType {
// 中文(中国)
zhCN,
// 中文(自动翻译)
aizh,
// 英语(自动生成)
aien,
// 中文(简体)
zhHans,
// 英文(美国)
enUS,
}
extension SubtitleTypeExtension on SubtitleType {
String get description {
switch (this) {
case SubtitleType.zhCN:
return '中文(中国)';
case SubtitleType.aizh:
return '中文(自动翻译)';
case SubtitleType.aien:
return '英语(自动生成)';
case SubtitleType.zhHans:
return '中文(简体)';
case SubtitleType.enUS:
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';
case SubtitleType.zhHans:
return 'zh-Hans';
case SubtitleType.enUS:
return 'en-US';
}
}
}
extension SubtitleCodeExtension on SubtitleType {
int get code {
switch (this) {
case SubtitleType.zhCN:
return 1;
case SubtitleType.aizh:
return 2;
case SubtitleType.aien:
return 3;
case SubtitleType.zhHans:
return 4;
case SubtitleType.enUS:
return 5;
}
}
}

View File

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

View File

@ -414,6 +414,7 @@ class DynamicMajorModel {
this.none, this.none,
this.type, this.type,
this.courses, this.courses,
this.common,
}); });
DynamicArchiveModel? archive; DynamicArchiveModel? archive;
@ -429,6 +430,7 @@ class DynamicMajorModel {
// MAJOR_TYPE_OPUS 图文/文章 // MAJOR_TYPE_OPUS 图文/文章
String? type; String? type;
Map? courses; Map? courses;
Map? common;
DynamicMajorModel.fromJson(Map<String, dynamic> json) { DynamicMajorModel.fromJson(Map<String, dynamic> json) {
archive = json['archive'] != null archive = json['archive'] != null
@ -452,6 +454,7 @@ class DynamicMajorModel {
json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null; json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null;
type = json['type']; type = json['type'];
courses = json['courses'] ?? {}; courses = json['courses'] ?? {};
common = json['common'] ?? {};
} }
} }

View File

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

View File

@ -23,6 +23,7 @@ class HotVideoItemModel {
this.dimension, this.dimension,
this.shortLinkV2, this.shortLinkV2,
this.firstFrame, this.firstFrame,
this.cover,
this.pubLocation, this.pubLocation,
this.seasontype, this.seasontype,
this.isOgv, this.isOgv,
@ -50,6 +51,7 @@ class HotVideoItemModel {
Dimension? dimension; Dimension? dimension;
String? shortLinkV2; String? shortLinkV2;
String? firstFrame; String? firstFrame;
String? cover;
String? pubLocation; String? pubLocation;
int? seasontype; int? seasontype;
bool? isOgv; bool? isOgv;
@ -77,6 +79,7 @@ class HotVideoItemModel {
dimension = Dimension.fromMap(json['dimension']); dimension = Dimension.fromMap(json['dimension']);
shortLinkV2 = json["short_link_v2"]; shortLinkV2 = json["short_link_v2"];
firstFrame = json["first_frame"]; firstFrame = json["first_frame"];
cover = json["first_frame"];
pubLocation = json["pub_location"]; pubLocation = json["pub_location"];
seasontype = json["seasontype"]; seasontype = json["seasontype"];
isOgv = json["isOgv"]; isOgv = json["isOgv"];

View File

@ -25,6 +25,7 @@ class SearchVideoItemModel {
this.aid, this.aid,
this.bvid, this.bvid,
this.title, this.title,
this.titleList,
this.description, this.description,
this.pic, this.pic,
// this.play, // this.play,
@ -54,8 +55,8 @@ class SearchVideoItemModel {
String? arcurl; String? arcurl;
int? aid; int? aid;
String? bvid; String? bvid;
List? title; String? title;
// List? titleList; List? titleList;
String? description; String? description;
String? pic; String? pic;
// String? play; // String? play;
@ -82,8 +83,9 @@ class SearchVideoItemModel {
aid = json['aid']; aid = json['aid'];
bvid = json['bvid']; bvid = json['bvid'];
mid = json['mid']; mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']); // title = Em.regTitle(json['title']);
titleList = Em.regTitle(json['title']);
description = json['description']; description = json['description'];
pic = json['pic'] != null && json['pic'].startsWith('//') pic = json['pic'] != null && json['pic'].startsWith('//')
? 'https:${json['pic']}' ? 'https:${json['pic']}'
@ -232,6 +234,7 @@ class SearchLiveItemModel {
this.userCover, this.userCover,
this.type, this.type,
this.title, this.title,
this.titleList,
this.cover, this.cover,
this.pic, this.pic,
this.online, this.online,
@ -251,7 +254,8 @@ class SearchLiveItemModel {
String? face; String? face;
String? userCover; String? userCover;
String? type; String? type;
List? title; String? title;
List? titleList;
String? cover; String? cover;
String? pic; String? pic;
int? online; int? online;
@ -272,7 +276,8 @@ class SearchLiveItemModel {
face = json['uface']; face = json['uface'];
userCover = json['user_cover']; userCover = json['user_cover'];
type = json['type']; type = json['type'];
title = Em.regTitle(json['title']); title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
titleList = Em.regTitle(json['title']);
cover = json['cover']; cover = json['cover'];
pic = json['cover']; pic = json['cover'];
online = json['online']; online = json['online'];
@ -302,6 +307,7 @@ class SearchMBangumiItemModel {
this.type, this.type,
this.mediaId, this.mediaId,
this.title, this.title,
this.titleList,
this.orgTitle, this.orgTitle,
this.mediaType, this.mediaType,
this.cv, this.cv,
@ -328,7 +334,8 @@ class SearchMBangumiItemModel {
String? type; String? type;
int? mediaId; int? mediaId;
List? title; String? title;
List? titleList;
String? orgTitle; String? orgTitle;
int? mediaType; int? mediaType;
String? cv; String? cv;
@ -355,7 +362,8 @@ class SearchMBangumiItemModel {
SearchMBangumiItemModel.fromJson(Map<String, dynamic> json) { SearchMBangumiItemModel.fromJson(Map<String, dynamic> json) {
type = json['type']; type = json['type'];
mediaId = json['media_id']; mediaId = json['media_id'];
title = Em.regTitle(json['title']); title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
titleList = Em.regTitle(json['title']);
orgTitle = json['org_title']; orgTitle = json['org_title'];
mediaType = json['media_type']; mediaType = json['media_type'];
cv = json['cv']; cv = json['cv'];
@ -437,7 +445,8 @@ class SearchArticleItemModel {
pubTime = json['pub_time']; pubTime = json['pub_time'];
like = json['like']; like = json['like'];
title = Em.regTitle(json['title']); title = Em.regTitle(json['title']);
subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), ''); subTitle =
Em.decodeHtmlEntities(json['title'].replaceAll(RegExp(r'<[^>]*>'), ''));
rankOffset = json['rank_offset']; rankOffset = json['rank_offset'];
mid = json['mid']; mid = json['mid'];
imageUrls = json['image_urls']; imageUrls = json['image_urls'];

View File

@ -39,6 +39,14 @@ extension VideoQualityCode on VideoQuality {
} }
return null; return null;
} }
static int? toCode(VideoQuality quality) {
final index = VideoQuality.values.indexOf(quality);
if (index != -1 && index < _codeList.length) {
return _codeList[index];
}
return null;
}
} }
extension VideoQualityDesc on VideoQuality { extension VideoQualityDesc on VideoQuality {

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

@ -67,6 +67,7 @@ class VideoDetailData {
String? likeIcon; String? likeIcon;
bool? needJumpBv; bool? needJumpBv;
String? epId; String? epId;
List<Staff>? staff;
VideoDetailData({ VideoDetailData({
this.bvid, this.bvid,
@ -103,6 +104,7 @@ class VideoDetailData {
this.likeIcon, this.likeIcon,
this.needJumpBv, this.needJumpBv,
this.epId, this.epId,
this.staff,
}); });
VideoDetailData.fromJson(Map<String, dynamic> json) { VideoDetailData.fromJson(Map<String, dynamic> json) {
@ -155,6 +157,9 @@ class VideoDetailData {
if (json['redirect_url'] != null) { if (json['redirect_url'] != null) {
epId = resolveEpId(json['redirect_url']); epId = resolveEpId(json['redirect_url']);
} }
staff = json["staff"] != null
? List<Staff>.from(json["staff"]!.map((e) => Staff.fromJson(e)))
: null;
} }
String resolveEpId(url) { String resolveEpId(url) {
@ -377,6 +382,7 @@ class Part {
String? weblink; String? weblink;
Dimension? dimension; Dimension? dimension;
String? firstFrame; String? firstFrame;
String? cover;
Part({ Part({
this.cid, this.cid,
@ -388,6 +394,7 @@ class Part {
this.weblink, this.weblink,
this.dimension, this.dimension,
this.firstFrame, this.firstFrame,
this.cover,
}); });
fromRawJson(String str) => Part.fromJson(json.decode(str)); fromRawJson(String str) => Part.fromJson(json.decode(str));
@ -405,7 +412,8 @@ class Part {
dimension = json["dimension"] == null dimension = json["dimension"] == null
? null ? null
: Dimension.fromJson(json["dimension"]); : Dimension.fromJson(json["dimension"]);
firstFrame = json["first_frame"]; firstFrame = json["first_frame"] ?? '';
cover = json["first_frame"] ?? '';
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -629,6 +637,7 @@ class EpisodeItem {
this.attribute, this.attribute,
this.page, this.page,
this.bvid, this.bvid,
this.cover,
}); });
int? seasonId; int? seasonId;
int? sectionId; int? sectionId;
@ -639,6 +648,7 @@ class EpisodeItem {
int? attribute; int? attribute;
Part? page; Part? page;
String? bvid; String? bvid;
String? cover;
EpisodeItem.fromJson(Map<String, dynamic> json) { EpisodeItem.fromJson(Map<String, dynamic> json) {
seasonId = json['season_id']; seasonId = json['season_id'];
@ -650,5 +660,46 @@ class EpisodeItem {
attribute = json['attribute']; attribute = json['attribute'];
page = Part.fromJson(json['page']); page = Part.fromJson(json['page']);
bvid = json['bvid']; bvid = json['bvid'];
cover = json['arc']['pic'];
}
}
class Staff {
Staff({
this.mid,
this.title,
this.name,
this.face,
this.vip,
});
int? mid;
String? title;
String? name;
String? face;
int? status;
Vip? vip;
Staff.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
title = json['title'];
name = json['name'];
face = json['face'];
vip = Vip.fromJson(json['vip']);
}
}
class Vip {
Vip({
this.type,
this.status,
});
int? type;
int? status;
Vip.fromJson(Map<String, dynamic> json) {
type = json['type'];
status = json['status'];
} }
} }

View File

@ -15,6 +15,10 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../common/pages_bottom_sheet.dart';
import '../../../models/common/video_episode_type.dart';
import '../../../utils/drawer.dart';
class BangumiIntroController extends GetxController { class BangumiIntroController extends GetxController {
// 视频bvid // 视频bvid
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
@ -52,6 +56,7 @@ class BangumiIntroController extends GetxController {
RxMap followStatus = {}.obs; RxMap followStatus = {}.obs;
int _tempThemeValue = -1; int _tempThemeValue = -1;
var userInfo; var userInfo;
PersistentBottomSheetController? bottomSheetController;
@override @override
void onInit() { void onInit() {
@ -126,51 +131,37 @@ class BangumiIntroController extends GetxController {
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text('选择投币个数'), title: const Text('选择投币个数'),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
content: StatefulBuilder(builder: (context, StateSetter setState) { content: StatefulBuilder(builder: (context, StateSetter setState) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [1, 2]
RadioListTile( .map(
value: 1, (e) => RadioListTile(
title: const Text('1枚'), value: e,
groupValue: _tempThemeValue, title: Text('$e枚'),
onChanged: (value) { groupValue: _tempThemeValue,
_tempThemeValue = value!; onChanged: (value) async {
Get.appUpdate(); _tempThemeValue = value!;
}, setState(() {});
), var res = await VideoHttp.coinVideo(
RadioListTile( bvid: bvid, multiply: _tempThemeValue);
value: 2, if (res['status']) {
title: const Text('2枚'), SmartDialog.showToast('投币成功 👏');
groupValue: _tempThemeValue, hasCoin.value = true;
onChanged: (value) { bangumiDetail.value.stat!['coins'] =
_tempThemeValue = value!; bangumiDetail.value.stat!['coins'] +
Get.appUpdate(); _tempThemeValue;
}, } else {
), SmartDialog.showToast(res['msg']);
], }
Get.back();
},
),
)
.toList(),
); );
}), }),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('取消')),
TextButton(
onPressed: () async {
var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue);
if (res['status']) {
SmartDialog.showToast('投币成功 👏');
hasCoin.value = true;
bangumiDetail.value.stat!['coins'] =
bangumiDetail.value.stat!['coins'] + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
child: const Text('确定'),
)
],
); );
}); });
} }
@ -224,13 +215,15 @@ class BangumiIntroController extends GetxController {
} }
// 修改分P或番剧分集 // 修改分P或番剧分集
Future changeSeasonOrbangu(bvid, cid, aid) async { Future changeSeasonOrbangu(bvid, cid, aid, cover) async {
// 重新获取视频资源 // 重新获取视频资源
VideoDetailController videoDetailCtr = VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']); Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid; videoDetailCtr.bvid = bvid;
videoDetailCtr.cid.value = cid; videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.oid.value = aid;
videoDetailCtr.cover.value = cover;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();
// 重新请求评论 // 重新请求评论
try { try {
@ -289,6 +282,36 @@ class BangumiIntroController extends GetxController {
int cid = episodes[nextIndex].cid!; int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!; String bvid = episodes[nextIndex].bvid!;
int aid = episodes[nextIndex].aid!; int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid); String cover = episodes[nextIndex].cover!;
changeSeasonOrbangu(bvid, cid, aid, cover);
}
// 播放器底栏 选集 回调
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, item.cover);
SmartDialog.dismiss();
},
).buildShowContent(Get.context!),
);
}
hiddenEpisodeBottomSheet() {
bottomSheetController?.close();
} }
} }

View File

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

View File

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

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

View File

@ -5,7 +5,9 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/bangumi/list.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -14,109 +16,87 @@ class BangumiCardV extends StatelessWidget {
const BangumiCardV({ const BangumiCardV({
super.key, super.key,
required this.bangumiItem, required this.bangumiItem,
this.longPress,
this.longPressEnd,
}); });
final bangumiItem; final BangumiListItemModel bangumiItem;
final Function()? longPress;
final Function()? longPressEnd;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(bangumiItem.mediaId); String heroTag = Utils.makeHeroTag(bangumiItem.mediaId);
return Card( return InkWell(
elevation: 0, onTap: () async {
clipBehavior: Clip.hardEdge, final int seasonId = bangumiItem.seasonId!;
margin: EdgeInsets.zero, SmartDialog.showLoading(msg: '获取中...');
child: GestureDetector( final res = await SearchHttp.bangumiInfo(seasonId: seasonId);
// onLongPress: () { SmartDialog.dismiss().then((value) {
// if (longPress != null) { if (res['status']) {
// longPress!(); if (res['data'].episodes.isEmpty) {
// } SmartDialog.showToast('资源加载失败');
// }, return;
// onLongPressEnd: (details) { }
// if (longPressEnd != null) { EpisodeItem episode = res['data'].episodes.first;
// longPressEnd!(); String bvid = episode.bvid!;
// } int cid = episode.cid!;
// }, String pic = episode.cover!;
child: InkWell( String heroTag = Utils.makeHeroTag(cid);
onTap: () async { Get.toNamed(
final int seasonId = bangumiItem.seasonId; '/video?bvid=$bvid&cid=$cid&seasonId=$seasonId',
SmartDialog.showLoading(msg: '获取中...'); arguments: {
final res = await SearchHttp.bangumiInfo(seasonId: seasonId); 'pic': pic,
SmartDialog.dismiss().then((value) { 'heroTag': heroTag,
if (res['status']) { 'videoType': SearchType.media_bangumi,
if (res['data'].episodes.isEmpty) { 'bangumiItem': res['data'],
SmartDialog.showToast('资源加载失败'); },
return; );
} }
EpisodeItem episode = res['data'].episodes.first; });
String bvid = episode.bvid!; },
int cid = episode.cid!; onLongPress: () =>
String pic = episode.cover!; imageSaveDialog(context, bangumiItem, SmartDialog.dismiss),
String heroTag = Utils.makeHeroTag(cid); child: Column(
Get.toNamed( children: [
'/video?bvid=$bvid&cid=$cid&seasonId=$seasonId', ClipRRect(
arguments: { borderRadius: const BorderRadius.all(
'pic': pic, StyleString.imgRadius,
'heroTag': heroTag, ),
'videoType': SearchType.media_bangumi, child: AspectRatio(
'bangumiItem': res['data'], aspectRatio: 0.65,
}, child: LayoutBuilder(builder: (context, boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: bangumiItem.cover,
width: maxWidth,
height: maxHeight,
),
),
if (bangumiItem.badge != null)
PBadge(
text: bangumiItem.badge,
top: 6,
right: 6,
bottom: null,
left: null),
if (bangumiItem.order != null)
PBadge(
text: bangumiItem.order,
top: null,
right: null,
bottom: 6,
left: 6,
type: 'gray',
),
],
); );
} }),
}); ),
},
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: 0.65,
child: LayoutBuilder(builder: (context, boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: bangumiItem.cover,
width: maxWidth,
height: maxHeight,
),
),
if (bangumiItem.badge != null)
PBadge(
text: bangumiItem.badge,
top: 6,
right: 6,
bottom: null,
left: null),
if (bangumiItem.order != null)
PBadge(
text: bangumiItem.order,
top: null,
right: null,
bottom: 6,
left: 6,
type: 'gray',
),
],
);
}),
),
),
BangumiContent(bangumiItem: bangumiItem)
],
), ),
), BangumiContent(bangumiItem: bangumiItem)
],
), ),
); );
} }

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

@ -25,6 +25,7 @@ class DynamicDetailController extends GetxController {
RxString sortTypeTitle = ReplySortType.time.titles.obs; RxString sortTypeTitle = ReplySortType.time.titles.obs;
RxString sortTypeLabel = ReplySortType.time.labels.obs; RxString sortTypeLabel = ReplySortType.time.labels.obs;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
RxInt replyReqCode = 200.obs;
@override @override
void onInit() { void onInit() {
@ -84,6 +85,7 @@ class DynamicDetailController extends GetxController {
replyList.addAll(replies); replyList.addAll(replies);
} }
} }
replyReqCode.value = res['code'];
isLoadingMore = false; isLoadingMore = false;
return res; return res;
} }

View File

@ -16,6 +16,7 @@ import 'package:pilipala/pages/video/detail/reply_reply/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import '../../../models/video/reply/item.dart';
import '../widgets/dynamic_panel.dart'; import '../widgets/dynamic_panel.dart';
class DynamicDetailPage extends StatefulWidget { class DynamicDetailPage extends StatefulWidget {
@ -182,6 +183,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
scrollController.removeListener(() {}); scrollController.removeListener(() {});
fabAnimationCtr.dispose(); fabAnimationCtr.dispose();
scrollController.dispose(); scrollController.dispose();
titleStreamC.close();
super.dispose(); super.dispose();
} }
@ -194,7 +196,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
centerTitle: false, centerTitle: false,
titleSpacing: 0, titleSpacing: 0,
title: StreamBuilder( title: StreamBuilder(
stream: titleStreamC.stream, stream: titleStreamC.stream.distinct(),
initialData: false, initialData: false,
builder: (context, AsyncSnapshot snapshot) { builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity( return AnimatedOpacity(
@ -210,173 +212,167 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
onRefresh: () async { onRefresh: () async {
await _dynamicDetailController.queryReplyList(); await _dynamicDetailController.queryReplyList();
}, },
child: Stack( child: CustomScrollView(
children: [ controller: scrollController,
CustomScrollView( slivers: [
controller: scrollController, if (action != 'comment')
slivers: [ SliverToBoxAdapter(
if (action != 'comment') child: DynamicPanel(
SliverToBoxAdapter( item: _dynamicDetailController.item,
child: DynamicPanel( source: 'detail',
item: _dynamicDetailController.item, ),
source: 'detail', ),
SliverPersistentHeader(
delegate: _MySliverPersistentHeaderDelegate(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
width: 0.6,
color: Theme.of(context).dividerColor.withOpacity(0.05),
),
), ),
), ),
SliverPersistentHeader( height: 45,
delegate: _MySliverPersistentHeaderDelegate( padding: const EdgeInsets.only(left: 12, right: 6),
child: Container( child: Row(
decoration: BoxDecoration( children: [
color: Theme.of(context).colorScheme.surface, Obx(
border: Border( () => AnimatedSwitcher(
top: BorderSide( duration: const Duration(milliseconds: 400),
width: 0.6, transitionBuilder:
color: Theme.of(context) (Widget child, Animation<double> animation) {
.dividerColor return ScaleTransition(
.withOpacity(0.05), scale: animation, child: child);
},
child: Text(
'${_dynamicDetailController.acount.value}',
key: ValueKey<int>(
_dynamicDetailController.acount.value),
), ),
), ),
), ),
height: 45, const Text('条回复'),
padding: const EdgeInsets.only(left: 12, right: 6), const Spacer(),
child: Row( SizedBox(
children: [ height: 35,
Obx( child: TextButton.icon(
() => AnimatedSwitcher( onPressed: () =>
duration: const Duration(milliseconds: 400), _dynamicDetailController.queryBySort(),
transitionBuilder: icon: const Icon(Icons.sort, size: 16),
(Widget child, Animation<double> animation) { label: Obx(() => Text(
return ScaleTransition( _dynamicDetailController.sortTypeLabel.value,
scale: animation, child: child); style: const TextStyle(fontSize: 13),
}, )),
child: Text( ),
'${_dynamicDetailController.acount.value}', )
key: ValueKey<int>( ],
_dynamicDetailController.acount.value),
),
),
),
const Text('条回复'),
const Spacer(),
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: () =>
_dynamicDetailController.queryBySort(),
icon: const Icon(Icons.sort, size: 16),
label: Obx(() => Text(
_dynamicDetailController
.sortTypeLabel.value,
style: const TextStyle(fontSize: 13),
)),
),
)
],
),
),
), ),
pinned: true,
), ),
FutureBuilder( ),
future: _futureBuilderFuture, pinned: true,
builder: (context, snapshot) { ),
if (snapshot.connectionState == ConnectionState.done) { FutureBuilder(
Map data = snapshot.data as Map; future: _futureBuilderFuture,
if (snapshot.data['status']) { builder: (context, snapshot) {
// 请求成功 if (snapshot.connectionState == ConnectionState.done) {
return Obx( Map data = snapshot.data as Map;
() => _dynamicDetailController.replyList.isEmpty && if (snapshot.data['status']) {
_dynamicDetailController.isLoadingMore RxList<ReplyItemModel> replyList =
? SliverList( _dynamicDetailController.replyList;
delegate: SliverChildBuilderDelegate( // 请求成功
(context, index) { return Obx(
return const VideoReplySkeleton(); () => replyList.isEmpty &&
}, childCount: 8), _dynamicDetailController.isLoadingMore
) ? SliverList(
: SliverList( delegate:
delegate: SliverChildBuilderDelegate( SliverChildBuilderDelegate((context, index) {
(context, index) { return const VideoReplySkeleton();
if (index == }, childCount: 8),
_dynamicDetailController )
.replyList.length) { : SliverList(
return Container( delegate: SliverChildBuilderDelegate(
padding: EdgeInsets.only( (context, index) {
bottom: MediaQuery.of(context) if (index == replyList.length) {
.padding return Container(
.bottom), padding: EdgeInsets.only(
height: MediaQuery.of(context) bottom: MediaQuery.of(context)
.padding .padding
.bottom + .bottom),
100, height: MediaQuery.of(context)
child: Center( .padding
child: Obx( .bottom +
() => Text( 100,
_dynamicDetailController child: Center(
.noMore.value, child: Obx(
style: TextStyle( () => Text(
fontSize: 12, _dynamicDetailController
color: Theme.of(context) .noMore.value,
.colorScheme style: TextStyle(
.outline, fontSize: 12,
), color: Theme.of(context)
), .colorScheme
.outline,
), ),
), ),
); ),
} else { ),
return ReplyItem( );
replyItem: _dynamicDetailController } else {
.replyList[index], return ReplyItem(
showReplyRow: true, replyItem: replyList[index],
replyLevel: '1', showReplyRow: true,
replyReply: (replyItem) => replyLevel: '1',
replyReply(replyItem), replyReply: (replyItem) =>
replyType: replyReply(replyItem),
ReplyType.values[replyType], replyType: ReplyType.values[replyType],
addReply: (replyItem) { addReply: (replyItem) {
_dynamicDetailController replyList[index]
.replyList[index].replies! .replies!
.add(replyItem); .add(replyItem);
}, },
); );
} }
}, },
childCount: _dynamicDetailController childCount: replyList.length + 1,
.replyList.length + ),
1, ),
), );
), } else {
); // 请求错误
} else { return HttpError(
// 请求错误 errMsg: data['msg'],
return HttpError( fn: () => setState(() {}),
errMsg: data['msg'], );
fn: () => setState(() {}), }
); } else {
} // 骨架屏
} else { return SliverList(
// 骨架屏 delegate: SliverChildBuilderDelegate((context, index) {
return SliverList( return const VideoReplySkeleton();
delegate: SliverChildBuilderDelegate((context, index) { }, childCount: 8),
return const VideoReplySkeleton(); );
}, childCount: 8), }
); },
} )
}, ],
) ),
], ),
), floatingActionButton: SlideTransition(
Positioned( position: Tween<Offset>(
bottom: MediaQuery.of(context).padding.bottom + 14, begin: const Offset(0, 2),
right: 14, end: const Offset(0, 0),
child: SlideTransition( ).animate(
position: Tween<Offset>( CurvedAnimation(
begin: const Offset(0, 2), parent: fabAnimationCtr,
end: const Offset(0, 0), curve: Curves.easeInOut,
).animate(CurvedAnimation( ),
parent: fabAnimationCtr, ),
curve: Curves.easeInOut, child: Obx(
)), () => _dynamicDetailController.replyReqCode.value == 12061
child: FloatingActionButton( ? const SizedBox()
: FloatingActionButton(
heroTag: null, heroTag: null,
onPressed: () { onPressed: () {
feedBack(); feedBack();
@ -407,9 +403,6 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
tooltip: '评论动态', tooltip: '评论动态',
child: const Icon(Icons.reply), child: const Icon(Icons.reply),
), ),
),
),
],
), ),
), ),
); );

View File

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

View File

@ -3,38 +3,58 @@ import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/dynamics.dart'; import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:status_bar_control/status_bar_control.dart';
import 'rich_node_panel.dart';
class ActionPanel extends StatefulWidget { class ActionPanel extends StatefulWidget {
const ActionPanel({ const ActionPanel({
super.key, super.key,
this.item, required this.item,
}); });
// ignore: prefer_typing_uninitialized_variables // ignore: prefer_typing_uninitialized_variables
final item; final DynamicItemModel item;
@override @override
State<ActionPanel> createState() => _ActionPanelState(); State<ActionPanel> createState() => _ActionPanelState();
} }
class _ActionPanelState extends State<ActionPanel> { class _ActionPanelState extends State<ActionPanel>
with TickerProviderStateMixin {
final DynamicsController _dynamicsController = Get.put(DynamicsController()); final DynamicsController _dynamicsController = Get.put(DynamicsController());
late ModuleStatModel stat; late ModuleStatModel stat;
bool isProcessing = false; bool isProcessing = false;
double defaultHeight = 260;
RxDouble height = 0.0.obs;
RxBool isExpand = false.obs;
late double statusHeight;
TextEditingController _inputController = TextEditingController();
FocusNode myFocusNode = FocusNode();
String _inputText = '';
void Function()? handleState(Future Function() action) { void Function()? handleState(Future Function() action) {
return isProcessing ? null : () async { return isProcessing
setState(() => isProcessing = true); ? null
await action(); : () async {
setState(() => isProcessing = false); isProcessing = true;
}; await action();
isProcessing = false;
};
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
stat = widget.item!.modules.moduleStat; stat = widget.item.modules!.moduleStat!;
onInit();
}
onInit() async {
statusHeight = await StatusBarControl.getHeight;
} }
// 动态点赞 // 动态点赞
@ -43,7 +63,7 @@ class _ActionPanelState extends State<ActionPanel> {
var item = widget.item!; var item = widget.item!;
String dynamicId = item.idStr!; String dynamicId = item.idStr!;
// 1 已点赞 2 不喜欢 0 未操作 // 1 已点赞 2 不喜欢 0 未操作
Like like = item.modules.moduleStat.like; Like like = item.modules!.moduleStat!.like!;
int count = like.count == '点赞' ? 0 : int.parse(like.count ?? '0'); int count = like.count == '点赞' ? 0 : int.parse(like.count ?? '0');
bool status = like.status!; bool status = like.status!;
int up = status ? 2 : 1; int up = status ? 2 : 1;
@ -51,15 +71,15 @@ class _ActionPanelState extends State<ActionPanel> {
if (res['status']) { if (res['status']) {
SmartDialog.showToast(!status ? '点赞成功' : '取消赞'); SmartDialog.showToast(!status ? '点赞成功' : '取消赞');
if (up == 1) { if (up == 1) {
item.modules.moduleStat.like.count = (count + 1).toString(); item.modules!.moduleStat!.like!.count = (count + 1).toString();
item.modules.moduleStat.like.status = true; item.modules!.moduleStat!.like!.status = true;
} else { } else {
if (count == 1) { if (count == 1) {
item.modules.moduleStat.like.count = '点赞'; item.modules!.moduleStat!.like!.count = '点赞';
} else { } else {
item.modules.moduleStat.like.count = (count - 1).toString(); item.modules!.moduleStat!.like!.count = (count - 1).toString();
} }
item.modules.moduleStat.like.status = false; item.modules!.moduleStat!.like!.status = false;
} }
setState(() {}); setState(() {});
} else { } else {
@ -67,17 +87,307 @@ class _ActionPanelState extends State<ActionPanel> {
} }
} }
// 转发动态预览
Widget dynamicPreview() {
ItemModulesModel? modules = widget.item.modules;
final String type = widget.item.type!;
String? cover = modules?.moduleAuthor?.face;
switch (type) {
/// 图文动态
case 'DYNAMIC_TYPE_DRAW':
cover = modules?.moduleDynamic?.major?.opus?.pics?.first.url;
/// 投稿
case 'DYNAMIC_TYPE_AV':
cover = modules?.moduleDynamic?.major?.archive?.cover;
/// 转发的动态
case 'DYNAMIC_TYPE_FORWARD':
String forwardType = widget.item.orig!.type!;
switch (forwardType) {
/// 图文动态
case 'DYNAMIC_TYPE_DRAW':
cover = modules?.moduleDynamic?.major?.opus?.pics?.first.url;
/// 投稿
case 'DYNAMIC_TYPE_AV':
cover = modules?.moduleDynamic?.major?.archive?.cover;
/// 专栏文章
case 'DYNAMIC_TYPE_ARTICLE':
cover = '';
/// 番剧
case 'DYNAMIC_TYPE_PGC':
cover = '';
/// 纯文字动态
case 'DYNAMIC_TYPE_WORD':
cover = '';
/// 直播
case 'DYNAMIC_TYPE_LIVE_RCMD':
cover = '';
/// 合集查看
case 'DYNAMIC_TYPE_UGC_SEASON':
cover = '';
/// 番剧
case 'DYNAMIC_TYPE_PGC_UNION':
cover = modules?.moduleDynamic?.major?.pgc?.cover;
default:
cover = '';
}
/// 专栏文章
case 'DYNAMIC_TYPE_ARTICLE':
cover = '';
/// 番剧
case 'DYNAMIC_TYPE_PGC':
cover = '';
/// 纯文字动态
case 'DYNAMIC_TYPE_WORD':
cover = '';
/// 直播
case 'DYNAMIC_TYPE_LIVE_RCMD':
cover = '';
/// 合集查看
case 'DYNAMIC_TYPE_UGC_SEASON':
cover = '';
/// 番剧查看
case 'DYNAMIC_TYPE_PGC_UNION':
cover = '';
default:
cover = '';
}
return Container(
width: double.infinity,
height: 95,
margin: const EdgeInsets.fromLTRB(12, 0, 12, 14),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4),
borderRadius: BorderRadius.circular(6),
border: Border(
left: BorderSide(
width: 4,
color: Theme.of(context).colorScheme.primary.withOpacity(0.8)),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'@${widget.item.modules!.moduleAuthor!.name}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 8),
Row(
children: [
NetworkImgLayer(
src: cover ?? '',
width: 34,
height: 34,
type: 'emote',
),
const SizedBox(width: 10),
Expanded(
child: Text.rich(
style: const TextStyle(height: 0),
richNode(widget.item, context),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// Text(data)
],
)
],
),
),
);
}
// 动态转发
void forwardHandler() async {
showModalBottomSheet(
context: context,
enableDrag: false,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return Obx(
() => AnimatedContainer(
duration: Durations.medium1,
onEnd: () async {
if (isExpand.value) {
await Future.delayed(const Duration(milliseconds: 80));
myFocusNode.requestFocus();
}
},
height: height.value + MediaQuery.of(context).padding.bottom,
child: Column(
children: [
AnimatedContainer(
duration: Durations.medium1,
height: isExpand.value ? statusHeight : 0,
),
Padding(
padding: EdgeInsets.fromLTRB(
isExpand.value ? 10 : 16,
10,
isExpand.value ? 14 : 12,
0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (isExpand.value) ...[
IconButton(
onPressed: () => togglePanelState(false),
icon: const Icon(Icons.close),
),
Text(
'转发动态',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
)
] else ...[
const Text(
'转发动态',
style: TextStyle(fontWeight: FontWeight.bold),
)
],
isExpand.value
? FilledButton(
onPressed: () => dynamicForward('forward'),
child: const Text('转发'),
)
: TextButton(
onPressed: () {},
child: const Text('立即转发'),
)
],
),
),
if (!isExpand.value) ...[
GestureDetector(
onTap: () => togglePanelState(true),
behavior: HitTestBehavior.translucent,
child: Container(
width: double.infinity,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.fromLTRB(16, 0, 10, 14),
child: Text(
'说点什么吧',
textAlign: TextAlign.start,
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
),
),
] else ...[
Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 0),
child: TextField(
maxLines: 5,
focusNode: myFocusNode,
controller: _inputController,
onChanged: (value) {
setState(() {
_inputText = value;
});
},
decoration: const InputDecoration(
border: InputBorder.none,
hintText: '说点什么吧',
),
),
),
],
dynamicPreview(),
if (!isExpand.value) ...[
const Divider(thickness: 0.1, height: 1),
ListTile(
onTap: () => Get.back(),
minLeadingWidth: 0,
dense: true,
title: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
textAlign: TextAlign.center,
),
),
]
],
),
),
);
},
);
}
togglePanelState(status) {
if (!status) {
Get.back();
height.value = defaultHeight;
_inputText = '';
_inputController.clear();
} else {
height.value = Get.size.height;
}
isExpand.value = !(isExpand.value);
}
dynamicForward(String type) async {
String dynamicId = widget.item.idStr!;
var res = await DynamicsHttp.dynamicCreate(
dynIdStr: dynamicId,
mid: _dynamicsController.userInfo.mid,
rawText: _inputText,
scene: 4,
);
if (res['status']) {
SmartDialog.showToast(type == 'forward' ? '转发成功' : '发布成功');
togglePanelState(false);
}
}
@override
void dispose() {
myFocusNode.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var color = Theme.of(context).colorScheme.outline; var color = Theme.of(context).colorScheme.outline;
var primary = Theme.of(context).colorScheme.primary; var primary = Theme.of(context).colorScheme.primary;
height.value = defaultHeight;
print('height.value: ${height.value}');
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
Expanded( Expanded(
flex: 1, flex: 1,
child: TextButton.icon( child: TextButton.icon(
onPressed: () {}, onPressed: forwardHandler,
icon: const Icon( icon: const Icon(
FontAwesomeIcons.shareFromSquare, FontAwesomeIcons.shareFromSquare,
size: 16, size: 16,

View File

@ -1,15 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import '../../../models/dynamics/result.dart';
import 'action_panel.dart'; import 'action_panel.dart';
import 'author_panel.dart'; import 'author_panel.dart';
import 'content_panel.dart'; import 'content_panel.dart';
import 'forward_panel.dart'; import 'forward_panel.dart';
class DynamicPanel extends StatelessWidget { class DynamicPanel extends StatelessWidget {
final dynamic item; final DynamicItemModel item;
final String? source; final String? source;
DynamicPanel({this.item, this.source, Key? key}) : super(key: key); DynamicPanel({required this.item, this.source, Key? key}) : super(key: key);
final DynamicsController _dynamicsController = Get.put(DynamicsController()); final DynamicsController _dynamicsController = Get.put(DynamicsController());
@override @override
@ -41,8 +42,8 @@ class DynamicPanel extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: AuthorPanel(item: item), child: AuthorPanel(item: item),
), ),
if (item!.modules!.moduleDynamic!.desc != null || if (item.modules!.moduleDynamic!.desc != null ||
item!.modules!.moduleDynamic!.major != null) item.modules!.moduleDynamic!.major != null)
Content(item: item, source: source), Content(item: item, source: source),
forWard(item, context, _dynamicsController, source), forWard(item, context, _dynamicsController, source),
const SizedBox(height: 2), const SizedBox(height: 2),

View File

@ -2,6 +2,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'additional_panel.dart'; import 'additional_panel.dart';
@ -182,6 +183,61 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
) )
], ],
); );
// 活动
case 'DYNAMIC_TYPE_COMMON_SQUARE':
return Padding(
padding: const EdgeInsets.only(top: 8),
child: InkWell(
onTap: () {
Get.toNamed('/webview', parameters: {
'url': item.modules.moduleDynamic.major.common['jump_url'],
'type': 'url',
'pageTitle': item.modules.moduleDynamic.major.common['title']
});
},
child: Container(
width: double.infinity,
padding:
const EdgeInsets.only(left: 12, top: 10, right: 12, bottom: 10),
color: Theme.of(context).dividerColor.withOpacity(0.08),
child: Row(
children: [
NetworkImgLayer(
width: 45,
height: 45,
src: item.modules.moduleDynamic.major.common['cover'],
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.modules.moduleDynamic.major.common['title'],
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
item.modules.moduleDynamic.major.common['desc'],
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
)
],
),
// TextButton(onPressed: () {}, child: Text('123'))
),
),
);
default: default:
return const SizedBox( return const SizedBox(
width: double.infinity, width: double.infinity,

View File

@ -10,10 +10,11 @@ import 'package:pilipala/utils/storage.dart';
class FavController extends GetxController { class FavController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Rx<FavFolderData> favFolderData = FavFolderData().obs; Rx<FavFolderData> favFolderData = FavFolderData().obs;
RxList<FavFolderItemData> favFolderList = <FavFolderItemData>[].obs;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo; UserInfoData? userInfo;
int currentPage = 1; int currentPage = 1;
int pageSize = 10; int pageSize = 60;
RxBool hasMore = true.obs; RxBool hasMore = true.obs;
Future<dynamic> queryFavFolder({type = 'init'}) async { Future<dynamic> queryFavFolder({type = 'init'}) async {
@ -32,9 +33,10 @@ class FavController extends GetxController {
if (res['status']) { if (res['status']) {
if (type == 'init') { if (type == 'init') {
favFolderData.value = res['data']; favFolderData.value = res['data'];
favFolderList.value = res['data'].list;
} else { } else {
if (res['data'].list.isNotEmpty) { if (res['data'].list.isNotEmpty) {
favFolderData.value.list!.addAll(res['data'].list); favFolderList.addAll(res['data'].list);
favFolderData.update((val) {}); favFolderData.update((val) {});
} }
} }
@ -49,4 +51,13 @@ class FavController extends GetxController {
Future onLoad() async { Future onLoad() async {
queryFavFolder(type: 'onload'); queryFavFolder(type: 'onload');
} }
removeFavFolder({required int mediaIds}) async {
for (var i in favFolderList) {
if (i.id == mediaIds) {
favFolderList.remove(i);
break;
}
}
}
} }

View File

@ -62,11 +62,10 @@ class _FavPageState extends State<FavPage> {
return Obx( return Obx(
() => ListView.builder( () => ListView.builder(
controller: scrollController, controller: scrollController,
itemCount: _favController.favFolderData.value.list!.length, itemCount: _favController.favFolderList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return FavItem( return FavItem(
favFolderItem: favFolderItem: _favController.favFolderList[index]);
_favController.favFolderData.value.list![index]);
}, },
), ),
); );

View File

@ -13,14 +13,16 @@ class FavItem extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(favFolderItem.fid); String heroTag = Utils.makeHeroTag(favFolderItem.fid);
return InkWell( return InkWell(
onTap: () => Get.toNamed( onTap: () async {
'/favDetail', Get.toNamed(
arguments: favFolderItem, '/favDetail',
parameters: { arguments: favFolderItem,
'heroTag': heroTag, parameters: {
'mediaId': favFolderItem.id.toString(), 'heroTag': heroTag,
}, 'mediaId': favFolderItem.id.toString(),
), },
);
},
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(12, 7, 12, 7), padding: const EdgeInsets.fromLTRB(12, 7, 12, 7),
child: LayoutBuilder( child: LayoutBuilder(

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/user/fav_detail.dart'; import 'package:pilipala/models/user/fav_detail.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/fav/index.dart';
class FavDetailController extends GetxController { class FavDetailController extends GetxController {
FavFolderItemData? item; FavFolderItemData? item;
@ -74,4 +76,41 @@ class FavDetailController extends GetxController {
onLoad() { onLoad() {
queryUserFavFolderDetail(type: 'onLoad'); queryUserFavFolderDetail(type: 'onLoad');
} }
onDelFavFolder() async {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('确定删除这个收藏夹吗?'),
actions: [
TextButton(
onPressed: () async {
SmartDialog.dismiss();
},
child: Text(
'点错了',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
var res = await UserHttp.delFavFolder(mediaIds: mediaId!);
SmartDialog.dismiss();
SmartDialog.showToast(res['status'] ? '操作成功' : res['msg']);
if (res['status']) {
FavController favController = Get.find<FavController>();
await favController.removeFavFolder(mediaIds: mediaId!);
Get.back();
}
},
child: const Text('确认'),
)
],
);
},
);
}
} }

View File

@ -53,6 +53,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
titleStreamC.close();
super.dispose(); super.dispose();
} }
@ -67,7 +68,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
pinned: true, pinned: true,
titleSpacing: 0, titleSpacing: 0,
title: StreamBuilder( title: StreamBuilder(
stream: titleStreamC.stream, stream: titleStreamC.stream.distinct(),
initialData: false, initialData: false,
builder: (context, AsyncSnapshot snapshot) { builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity( return AnimatedOpacity(
@ -100,11 +101,19 @@ class _FavDetailPageState extends State<FavDetailPage> {
Get.toNamed('/favSearch?searchType=0&mediaId=$mediaId'), Get.toNamed('/favSearch?searchType=0&mediaId=$mediaId'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
), ),
// IconButton( PopupMenuButton<String>(
// onPressed: () {}, icon: const Icon(Icons.more_vert_outlined),
// icon: const Icon(Icons.more_vert), position: PopupMenuPosition.under,
// ), onSelected: (String type) {},
const SizedBox(width: 6), itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () => _favDetailController.onDelFavFolder(),
value: 'pause',
child: const Text('删除收藏夹'),
),
],
),
const SizedBox(width: 14),
], ],
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: Container( background: Container(

View File

@ -1,3 +1,4 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
@ -7,6 +8,7 @@ import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../../../common/widgets/badge.dart'; import '../../../common/widgets/badge.dart';
@ -61,6 +63,11 @@ class FavVideoCardH extends StatelessWidget {
epId != null ? SearchType.media_bangumi : SearchType.video, epId != null ? SearchType.media_bangumi : SearchType.video,
}); });
}, },
onLongPress: () => imageSaveDialog(
context,
videoItem,
SmartDialog.dismiss,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(

View File

@ -185,7 +185,7 @@ class HistoryItem extends StatelessWidget {
? '已看完' ? '已看完'
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}', : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
right: 6.0, right: 6.0,
bottom: 6.0, bottom: 8.0,
type: 'gray', 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) VideoContent(videoItem: videoItem, ctr: ctr)

View File

@ -35,7 +35,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
userLogin.value = userInfo != null; userLogin.value = userInfo != null;
userFace.value = userInfo != null ? userInfo.face : ''; userFace.value = userInfo != null ? userInfo.face : '';
hideSearchBar = hideSearchBar =
setting.get(SettingBoxKey.hideSearchBar, defaultValue: true); setting.get(SettingBoxKey.hideSearchBar, defaultValue: false);
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) { if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
searchDefault(); searchDefault();
} }
@ -114,4 +114,10 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
defaultSearch.value = res.data['data']['name']; defaultSearch.value = res.data['data']['name'];
} }
} }
@override
void onClose() {
searchBarStream.close();
super.onClose();
}
} }

View File

@ -171,7 +171,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder( return StreamBuilder(
stream: stream, stream: stream!.distinct(),
initialData: true, initialData: true,
builder: (BuildContext context, AsyncSnapshot snapshot) { builder: (BuildContext context, AsyncSnapshot snapshot) {
final RxBool isUserLoggedIn = ctr!.userLogin; final RxBool isUserLoggedIn = ctr!.userLogin;
@ -214,6 +214,34 @@ class UserInfoWidget extends StatelessWidget {
final VoidCallback? callback; final VoidCallback? callback;
final HomeController? ctr; final HomeController? ctr;
Widget buildLoggedInWidget(context) {
return Stack(
children: [
NetworkImgLayer(
type: 'avatar',
width: 34,
height: 34,
src: userFace,
),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => callback?.call(),
splashColor: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.3),
borderRadius: const BorderRadius.all(
Radius.circular(50),
),
),
),
)
],
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
@ -231,31 +259,7 @@ class UserInfoWidget extends StatelessWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
Obx( Obx(
() => userLogin.value () => userLogin.value
? Stack( ? buildLoggedInWidget(context)
children: [
NetworkImgLayer(
type: 'avatar',
width: 34,
height: 34,
src: userFace,
),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => callback?.call(),
splashColor: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.3),
borderRadius: const BorderRadius.all(
Radius.circular(50),
),
),
),
)
],
)
: DefaultUser(callback: () => callback!()), : DefaultUser(callback: () => callback!()),
), ),
], ],
@ -402,30 +406,27 @@ class SearchBar extends StatelessWidget {
color: colorScheme.onSecondaryContainer.withOpacity(0.05), color: colorScheme.onSecondaryContainer.withOpacity(0.05),
child: InkWell( child: InkWell(
splashColor: colorScheme.primaryContainer.withOpacity(0.3), splashColor: colorScheme.primaryContainer.withOpacity(0.3),
onTap: () => Get.toNamed( onTap: () => Get.toNamed('/search',
'/search', parameters: {'hintText': ctr!.defaultSearch.value}),
parameters: {'hintText': ctr!.defaultSearch.value}, child: Padding(
), padding: const EdgeInsets.symmetric(horizontal: 14),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 14), Icon(
Icon( Icons.search_outlined,
Icons.search_outlined, color: colorScheme.onSecondaryContainer,
color: colorScheme.onSecondaryContainer, ),
), const SizedBox(width: 10),
const SizedBox(width: 10), Obx(
Obx( () => Text(
() => Expanded(
child: Text(
ctr!.defaultSearch.value, ctr!.defaultSearch.value,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(color: colorScheme.outline), style: TextStyle(color: colorScheme.outline),
), ),
), ),
), ],
const SizedBox(width: 15), ),
],
), ),
), ),
), ),

View File

@ -1,17 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_h.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/hot/controller.dart';
import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/utils/main_stream.dart';
class HotPage extends StatefulWidget { class HotPage extends StatefulWidget {
const HotPage({Key? key}) : super(key: key); const HotPage({Key? key}) : super(key: key);
@ -34,10 +30,6 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
super.initState(); super.initState();
_futureBuilderFuture = _hotController.queryHotFeed('init'); _futureBuilderFuture = _hotController.queryHotFeed('init');
scrollController = _hotController.scrollController; scrollController = _hotController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener( scrollController.addListener(
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
@ -47,16 +39,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
_hotController.onLoad(); _hotController.onLoad();
} }
} }
handleScrollEvent(scrollController);
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);
}
}, },
); );
} }
@ -93,15 +76,6 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
return VideoCardH( return VideoCardH(
videoItem: _hotController.videoList[index], videoItem: _hotController.videoList[index],
showPubdate: true, showPubdate: true,
longPress: () {
_hotController.popupDialog = _createPopupDialog(
_hotController.videoList[index]);
Overlay.of(context)
.insert(_hotController.popupDialog!);
},
longPressEnd: () {
_hotController.popupDialog?.remove();
},
); );
}, childCount: _hotController.videoList.length), }, childCount: _hotController.videoList.length),
), ),
@ -137,14 +111,4 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
), ),
); );
} }
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
closeFn: _hotController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _hotController.popupDialog?.remove),
),
);
}
} }

View File

@ -84,7 +84,7 @@ class _LaterPageState extends State<LaterPage> {
return VideoCardH( return VideoCardH(
videoItem: videoItem, videoItem: videoItem,
source: 'later', source: 'later',
longPress: () => _laterController.toViewDel( onPressedFn: () => _laterController.toViewDel(
aid: videoItem.aid)); aid: videoItem.aid));
}, childCount: _laterController.laterList.length), }, childCount: _laterController.laterList.length),
) )

View File

@ -2,15 +2,11 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.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/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/utils/main_stream.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/live_item.dart'; import 'widgets/live_item.dart';
@ -36,10 +32,6 @@ class _LivePageState extends State<LivePage>
super.initState(); super.initState();
_futureBuilderFuture = _liveController.queryLiveList('init'); _futureBuilderFuture = _liveController.queryLiveList('init');
scrollController = _liveController.scrollController; scrollController = _liveController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener( scrollController.addListener(
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
@ -49,16 +41,7 @@ class _LivePageState extends State<LivePage>
_liveController.onLoad(); _liveController.onLoad();
}); });
} }
handleScrollEvent(scrollController);
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);
}
}, },
); );
} }
@ -127,16 +110,6 @@ class _LivePageState extends State<LivePage>
); );
} }
OverlayEntry _createPopupDialog(liveItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
closeFn: _liveController.popupDialog?.remove,
child: OverlayPop(
videoItem: liveItem, closeFn: _liveController.popupDialog?.remove),
),
);
}
Widget contentGrid(ctr, liveList) { Widget contentGrid(ctr, liveList) {
// double maxWidth = Get.size.width; // double maxWidth = Get.size.width;
// int baseWidth = 500; // int baseWidth = 500;
@ -167,14 +140,6 @@ class _LivePageState extends State<LivePage>
? LiveCardV( ? LiveCardV(
liveItem: liveList[index], liveItem: liveList[index],
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,
longPress: () {
_liveController.popupDialog =
_createPopupDialog(liveList[index]);
Overlay.of(context).insert(_liveController.popupDialog!);
},
longPressEnd: () {
_liveController.popupDialog?.remove();
},
) )
: const VideoCardVSkeleton(); : const VideoCardVSkeleton();
}, },

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -9,81 +11,66 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
class LiveCardV extends StatelessWidget { class LiveCardV extends StatelessWidget {
final LiveItemModel liveItem; final LiveItemModel liveItem;
final int crossAxisCount; final int crossAxisCount;
final Function()? longPress;
final Function()? longPressEnd;
const LiveCardV({ const LiveCardV({
Key? key, Key? key,
required this.liveItem, required this.liveItem,
required this.crossAxisCount, required this.crossAxisCount,
this.longPress,
this.longPressEnd,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomId); String heroTag = Utils.makeHeroTag(liveItem.roomId);
return Card( return InkWell(
elevation: 0, onLongPress: () => imageSaveDialog(
clipBehavior: Clip.hardEdge, context,
margin: EdgeInsets.zero, liveItem,
child: GestureDetector( SmartDialog.dismiss,
onLongPress: () { ),
if (longPress != null) { borderRadius: BorderRadius.circular(16),
longPress!(); onTap: () async {
} Get.toNamed('/liveRoom?roomid=${liveItem.roomId}',
}, arguments: {'liveItem': liveItem, 'heroTag': heroTag});
// onLongPressEnd: (details) { },
// if (longPressEnd != null) { child: Column(
// longPressEnd!(); children: [
// } ClipRRect(
// }, borderRadius: const BorderRadius.all(StyleString.imgRadius),
child: InkWell( child: AspectRatio(
onTap: () async { aspectRatio: StyleString.aspectRatio,
Get.toNamed('/liveRoom?roomid=${liveItem.roomId}', child: LayoutBuilder(builder: (context, boxConstraints) {
arguments: {'liveItem': liveItem, 'heroTag': heroTag}); double maxWidth = boxConstraints.maxWidth;
}, double maxHeight = boxConstraints.maxHeight;
child: Column( return Stack(
children: [ children: [
ClipRRect( Hero(
borderRadius: const BorderRadius.all(StyleString.imgRadius), tag: heroTag,
child: AspectRatio( child: NetworkImgLayer(
aspectRatio: StyleString.aspectRatio, src: liveItem.cover!,
child: LayoutBuilder(builder: (context, boxConstraints) { width: maxWidth,
double maxWidth = boxConstraints.maxWidth; height: maxHeight,
double maxHeight = boxConstraints.maxHeight; ),
return Stack( ),
children: [ if (crossAxisCount != 1)
Hero( Positioned(
tag: heroTag, left: 0,
child: NetworkImgLayer( right: 0,
src: liveItem.cover!, bottom: 0,
width: maxWidth, child: AnimatedOpacity(
height: maxHeight, opacity: 1,
duration: const Duration(milliseconds: 200),
child: VideoStat(
liveItem: liveItem,
), ),
), ),
if (crossAxisCount != 1) ),
Positioned( ],
left: 0, );
right: 0, }),
bottom: 0, ),
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 200),
child: VideoStat(
liveItem: liveItem,
),
),
),
],
);
}),
),
),
LiveContent(liveItem: liveItem, crossAxisCount: crossAxisCount)
],
), ),
), LiveContent(liveItem: liveItem, crossAxisCount: crossAxisCount)
],
), ),
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.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/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.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/pages/media/index.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class MediaPage extends StatefulWidget { class MediaPage extends StatefulWidget {
@ -30,26 +28,11 @@ class _MediaPageState extends State<MediaPage>
super.initState(); super.initState();
mediaController = Get.put(MediaController()); mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder(); _futureBuilderFuture = mediaController.queryFavFolder();
ScrollController scrollController = mediaController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
mediaController.userLogin.listen((status) { mediaController.userLogin.listen((status) {
setState(() { setState(() {
_futureBuilderFuture = mediaController.queryFavFolder(); _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);
}
},
);
} }
@override @override

View File

@ -54,6 +54,7 @@ class _MemberPageState extends State<MemberPage>
@override @override
void dispose() { void dispose() {
_extendNestCtr.removeListener(() {}); _extendNestCtr.removeListener(() {});
appbarStream.close();
super.dispose(); super.dispose();
} }
@ -65,7 +66,7 @@ class _MemberPageState extends State<MemberPage>
children: [ children: [
AppBar( AppBar(
title: StreamBuilder( title: StreamBuilder(
stream: appbarStream.stream, stream: appbarStream.stream.distinct(),
initialData: false, initialData: false,
builder: (BuildContext context, AsyncSnapshot snapshot) { builder: (BuildContext context, AsyncSnapshot snapshot) {
return AnimatedOpacity( return AnimatedOpacity(
@ -281,8 +282,8 @@ class _MemberPageState extends State<MemberPage>
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data!; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
return Obx( return Obx(
() => Stack( () => Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
@ -302,7 +303,14 @@ class _MemberPageState extends State<MemberPage>
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleMedium! .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), const SizedBox(width: 2),
if (_memberController.memberInfo.value.sex == '') if (_memberController.memberInfo.value.sex == '')

View File

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

View File

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

View File

@ -79,6 +79,7 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
videoItem: list[index], videoItem: list[index],
showOwner: false, showOwner: false,
showPubdate: true, showPubdate: true,
showCharge: true,
); );
}, },
childCount: list.length, childCount: list.length,

View File

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

View File

@ -5,6 +5,7 @@ import 'package:pilipala/pages/member_dynamics/index.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../../common/widgets/http_error.dart'; import '../../common/widgets/http_error.dart';
import '../../models/dynamics/result.dart';
import '../dynamics/widgets/dynamic_panel.dart'; import '../dynamics/widgets/dynamic_panel.dart';
class MemberDynamicsPage extends StatefulWidget { class MemberDynamicsPage extends StatefulWidget {
@ -66,7 +67,8 @@ class _MemberDynamicsPageState extends State<MemberDynamicsPage> {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data != null) { if (snapshot.data != null) {
Map data = snapshot.data as Map; Map data = snapshot.data as Map;
List list = _memberDynamicController.dynamicsList; RxList<DynamicItemModel> list =
_memberDynamicController.dynamicsList;
if (data['status']) { if (data['status']) {
return Obx( return Obx(
() => list.isNotEmpty () => list.isNotEmpty

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class MemberSeasonsItem extends StatelessWidget { class MemberSeasonsItem extends StatelessWidget {
@ -25,10 +27,15 @@ class MemberSeasonsItem extends StatelessWidget {
child: InkWell( child: InkWell(
onTap: () async { onTap: () async {
int cid = int cid =
await SearchHttp.ab2c(aid: seasonItem.aid, bvid: seasonItem.bvid); await SearchHttp.ab2c(aid: seasonItem.aid, bvid: seasonItem.bvid);
Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid', Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid',
arguments: {'videoItem': seasonItem, 'heroTag': heroTag}); arguments: {'videoItem': seasonItem, 'heroTag': heroTag});
}, },
onLongPress: () => imageSaveDialog(
context,
seasonItem,
SmartDialog.dismiss,
),
child: Column( child: Column(
children: [ children: [
AspectRatio( AspectRatio(
@ -51,8 +58,7 @@ class MemberSeasonsItem extends StatelessWidget {
bottom: 6, bottom: 6,
right: 6, right: 6,
type: 'gray', type: 'gray',
text: Utils.CustomStamp_str( text: Utils.timeFormat(seasonItem.duration),
timestamp: seasonItem.pubdate, date: 'YY-MM-DD'),
) )
], ],
); );

View File

@ -4,12 +4,13 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/common/rank_type.dart'; import 'package:pilipala/models/common/rank_type.dart';
import 'package:pilipala/pages/rank/zone/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class RankController extends GetxController with GetTickerProviderStateMixin { class RankController extends GetxController with GetTickerProviderStateMixin {
bool flag = false; bool flag = false;
late RxList tabs = [].obs; late RxList tabs = [].obs;
RxInt initialIndex = 1.obs; RxInt initialIndex = 0.obs;
late TabController tabController; late TabController tabController;
late List tabsCtrList; late List tabsCtrList;
late List<Widget> tabsPageList; late List<Widget> tabsPageList;
@ -29,20 +30,22 @@ class RankController extends GetxController with GetTickerProviderStateMixin {
void onRefresh() { void onRefresh() {
int index = tabController.index; int index = tabController.index;
var ctr = tabsCtrList[index]; final ZoneController ctr = tabsCtrList[index];
ctr().onRefresh(); ctr.onRefresh();
} }
void animateToTop() { void animateToTop() {
int index = tabController.index; int index = tabController.index;
var ctr = tabsCtrList[index]; final ZoneController ctr = tabsCtrList[index];
ctr().animateToTop(); ctr.animateToTop();
} }
void setTabConfig() async { void setTabConfig() async {
tabs.value = tabsConfig; tabs.value = tabsConfig;
initialIndex.value = 0; initialIndex.value = 0;
tabsCtrList = tabs.map((e) => e['ctr']).toList(); tabsCtrList = tabs
.map((e) => Get.put(ZoneController(), tag: e['rid'].toString()))
.toList();
tabsPageList = tabs.map<Widget>((e) => e['page']).toList(); tabsPageList = tabs.map<Widget>((e) => e['page']).toList();
tabController = TabController( tabController = TabController(
@ -50,21 +53,11 @@ class RankController extends GetxController with GetTickerProviderStateMixin {
length: tabs.length, length: tabs.length,
vsync: this, vsync: this,
); );
// 监听 tabController 切换 }
if (enableGradientBg) {
tabController.animation!.addListener(() { @override
if (tabController.indexIsChanging) { void onClose() {
if (initialIndex.value != tabController.index) { searchBarStream.close();
initialIndex.value = tabController.index; super.onClose();
}
} else {
final int temp = tabController.animation!.value.round();
if (initialIndex.value != temp) {
initialIndex.value = temp;
tabController.index = initialIndex.value;
}
}
});
}
} }
} }

View File

@ -102,7 +102,7 @@ class _RankPageState extends State<RankPage>
onTap: (value) { onTap: (value) {
feedBack(); feedBack();
if (_rankController.initialIndex.value == value) { if (_rankController.initialIndex.value == value) {
_rankController.tabsCtrList[value]().animateToTop(); _rankController.tabsCtrList[value].animateToTop();
} }
_rankController.initialIndex.value = value; _rankController.initialIndex.value = value;
}, },

View File

@ -42,12 +42,15 @@ class ZoneController extends GetxController {
// 返回顶部并刷新 // 返回顶部并刷新
void animateToTop() async { void animateToTop() async {
if (scrollController.offset >= if (scrollController.hasClients) {
MediaQuery.of(Get.context!).size.height * 5) { if (scrollController.offset >=
scrollController.jumpTo(0); MediaQuery.of(Get.context!).size.height * 5) {
} else { scrollController.jumpTo(0);
await scrollController.animateTo(0, } else {
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); await scrollController.animateTo(0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut);
}
} }
} }
} }

View File

@ -1,17 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_h.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/pages/rank/zone/index.dart';
import 'package:pilipala/utils/main_stream.dart';
class ZonePage extends StatefulWidget { class ZonePage extends StatefulWidget {
const ZonePage({Key? key, required this.rid}) : super(key: key); const ZonePage({Key? key, required this.rid}) : super(key: key);
@ -22,21 +18,22 @@ class ZonePage extends StatefulWidget {
State<ZonePage> createState() => _ZonePageState(); State<ZonePage> createState() => _ZonePageState();
} }
class _ZonePageState extends State<ZonePage> { class _ZonePageState extends State<ZonePage>
final ZoneController _zoneController = Get.put(ZoneController()); with AutomaticKeepAliveClientMixin {
late ZoneController _zoneController;
List videoList = []; List videoList = [];
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
late ScrollController scrollController; late ScrollController scrollController;
@override
bool get wantKeepAlive => true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_zoneController = Get.put(ZoneController(), tag: widget.rid.toString());
_futureBuilderFuture = _zoneController.queryRankFeed('init', widget.rid); _futureBuilderFuture = _zoneController.queryRankFeed('init', widget.rid);
scrollController = _zoneController.scrollController; scrollController = _zoneController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener( scrollController.addListener(
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
@ -46,16 +43,7 @@ class _ZonePageState extends State<ZonePage> {
_zoneController.onLoad(); _zoneController.onLoad();
} }
} }
handleScrollEvent(scrollController);
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);
}
}, },
); );
} }
@ -68,6 +56,7 @@ class _ZonePageState extends State<ZonePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
return await _zoneController.onRefresh(); return await _zoneController.onRefresh();
@ -91,15 +80,6 @@ class _ZonePageState extends State<ZonePage> {
return VideoCardH( return VideoCardH(
videoItem: _zoneController.videoList[index], videoItem: _zoneController.videoList[index],
showPubdate: true, showPubdate: true,
longPress: () {
_zoneController.popupDialog = _createPopupDialog(
_zoneController.videoList[index]);
Overlay.of(context)
.insert(_zoneController.popupDialog!);
},
longPressEnd: () {
_zoneController.popupDialog?.remove();
},
); );
}, childCount: _zoneController.videoList.length), }, childCount: _zoneController.videoList.length),
), ),
@ -135,14 +115,4 @@ class _ZonePageState extends State<ZonePage> {
), ),
); );
} }
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
closeFn: _zoneController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _zoneController.popupDialog?.remove),
),
);
}
} }

View File

@ -2,16 +2,12 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.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/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_v.dart'; import 'package:pilipala/common/widgets/video_card_v.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/utils/main_stream.dart';
import 'package:pilipala/pages/main/index.dart';
import 'controller.dart'; import 'controller.dart';
@ -35,10 +31,6 @@ class _RcmdPageState extends State<RcmdPage>
super.initState(); super.initState();
_futureBuilderFuture = _rcmdController.queryRcmdFeed('init'); _futureBuilderFuture = _rcmdController.queryRcmdFeed('init');
ScrollController scrollController = _rcmdController.scrollController; ScrollController scrollController = _rcmdController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener( scrollController.addListener(
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
@ -49,15 +41,7 @@ class _RcmdPageState extends State<RcmdPage>
_rcmdController.onLoad(); _rcmdController.onLoad();
}); });
} }
final ScrollDirection direction = handleScrollEvent(scrollController);
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
searchBarStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
searchBarStream.add(false);
}
}, },
); );
} }
@ -132,16 +116,6 @@ class _RcmdPageState extends State<RcmdPage>
); );
} }
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
closeFn: _rcmdController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _rcmdController.popupDialog?.remove),
),
);
}
Widget contentGrid(ctr, videoList) { Widget contentGrid(ctr, videoList) {
// double maxWidth = Get.size.width; // double maxWidth = Get.size.width;
// int baseWidth = 500; // int baseWidth = 500;
@ -172,14 +146,6 @@ class _RcmdPageState extends State<RcmdPage>
? VideoCardV( ? VideoCardV(
videoItem: videoList[index], videoItem: videoList[index],
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,
longPress: () {
_rcmdController.popupDialog =
_createPopupDialog(videoList[index]);
Overlay.of(context).insert(_rcmdController.popupDialog!);
},
longPressEnd: () {
_rcmdController.popupDialog?.remove();
},
) )
: const VideoCardVSkeleton(); : const VideoCardVSkeleton();
}, },

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
Widget searchLivePanel(BuildContext context, ctr, list) { Widget searchLivePanel(BuildContext context, ctr, list) {
@ -42,15 +44,15 @@ class LiveItem extends StatelessWidget {
Get.toNamed('/liveRoom?roomid=${liveItem.roomid}', Get.toNamed('/liveRoom?roomid=${liveItem.roomid}',
arguments: {'liveItem': liveItem, 'heroTag': heroTag}); arguments: {'liveItem': liveItem, 'heroTag': heroTag});
}, },
onLongPress: () => imageSaveDialog(
context,
liveItem,
SmartDialog.dismiss,
),
child: Column( child: Column(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.all(StyleString.imgRadius),
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio( child: AspectRatio(
aspectRatio: StyleString.aspectRatio, aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) { child: LayoutBuilder(builder: (context, boxConstraints) {
@ -108,7 +110,7 @@ class LiveContent extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
for (var i in liveItem.title) ...[ for (var i in liveItem.titleList) ...[
TextSpan( TextSpan(
text: i['text'], text: i['text'],
style: TextStyle( style: TextStyle(

View File

@ -63,7 +63,7 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onSurface), color: Theme.of(context).colorScheme.onSurface),
children: [ children: [
for (var i in i.title) ...[ for (var i in i.titleList) ...[
TextSpan( TextSpan(
text: i['text'], text: i['text'],
style: TextStyle( style: TextStyle(

View File

@ -35,7 +35,11 @@ class SearchVideoPanel extends StatelessWidget {
padding: index == 0 padding: index == 0
? const EdgeInsets.only(top: 2) ? const EdgeInsets.only(top: 2)
: EdgeInsets.zero, : EdgeInsets.zero,
child: VideoCardH(videoItem: i, showPubdate: true), child: VideoCardH(
videoItem: i,
showPubdate: true,
source: 'search',
),
); );
}, },
), ),

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

@ -131,7 +131,7 @@ class _PlaySettingState extends State<PlaySetting> {
title: '开启硬解', title: '开启硬解',
subTitle: '以较低功耗播放视频', subTitle: '以较低功耗播放视频',
setKey: SettingBoxKey.enableHA, setKey: SettingBoxKey.enableHA,
defaultVal: true, defaultVal: false,
), ),
const SetSwitchItem( const SetSwitchItem(
title: '观看人数', title: '观看人数',

View File

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

View File

@ -29,7 +29,7 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
return AlertDialog( return AlertDialog(
title: Text(widget.title), title: Text(widget.title),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
content: StatefulBuilder(builder: (context, StateSetter setState) { content: StatefulBuilder(builder: (context, StateSetter setState) {
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
@ -44,6 +44,7 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
setState(() { setState(() {
_tempValue = value as T; _tempValue = value as T;
}); });
Navigator.pop(context, _tempValue);
}, },
), ),
] ]
@ -51,19 +52,6 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
), ),
); );
}), }),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () => Navigator.pop(context, _tempValue),
child: const Text('确定'),
)
],
); );
} }
} }

View File

@ -46,4 +46,40 @@ class SubController extends GetxController {
Future onLoad() async { Future onLoad() async {
querySubFolder(type: 'onload'); 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) { itemBuilder: (context, index) {
return SubItem( return SubItem(
subFolderItem: 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 { class SubItem extends StatelessWidget {
final SubFolderItemData subFolderItem; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -20,6 +25,7 @@ class SubItem extends StatelessWidget {
parameters: { parameters: {
'heroTag': heroTag, 'heroTag': heroTag,
'seasonId': subFolderItem.id.toString(), 'seasonId': subFolderItem.id.toString(),
'type': subFolderItem.type.toString(),
}, },
), ),
child: Padding( child: Padding(
@ -51,7 +57,10 @@ class SubItem extends StatelessWidget {
}, },
), ),
), ),
VideoContent(subFolderItem: subFolderItem) VideoContent(
subFolderItem: subFolderItem,
cancelSub: cancelSub,
)
], ],
), ),
); );
@ -64,7 +73,8 @@ class SubItem extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final SubFolderItemData subFolderItem; final SubFolderItemData subFolderItem;
const VideoContent({super.key, required this.subFolderItem}); final Function(SubFolderItemData)? cancelSub;
const VideoContent({super.key, required this.subFolderItem, this.cancelSub});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -100,6 +110,24 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, 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 { class SubDetailController extends GetxController {
late SubFolderItemData item; late SubFolderItemData item;
late int seasonId; late int seasonId;
late String heroTag; late String heroTag;
int currentPage = 1; int currentPage = 1;
@ -15,28 +14,37 @@ class SubDetailController extends GetxController {
RxList<SubDetailMediaItem> subList = <SubDetailMediaItem>[].obs; RxList<SubDetailMediaItem> subList = <SubDetailMediaItem>[].obs;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
int mediaCount = 0; int mediaCount = 0;
late int channelType;
@override @override
void onInit() { void onInit() {
item = Get.arguments; item = Get.arguments;
if (Get.parameters.keys.isNotEmpty) { final parameters = Get.parameters;
seasonId = int.parse(Get.parameters['seasonId']!); if (parameters.isNotEmpty) {
heroTag = Get.parameters['heroTag']!; seasonId = int.tryParse(parameters['seasonId'] ?? '') ?? 0;
heroTag = parameters['heroTag'] ?? '';
channelType = int.tryParse(parameters['type'] ?? '') ?? 0;
} }
super.onInit(); super.onInit();
} }
Future<dynamic> queryUserSubFolderDetail({type = 'init'}) async { Future<dynamic> queryUserSeasonList({type = 'init'}) async {
if (type == 'onLoad' && subList.length >= mediaCount) { if (type == 'onLoad' && subList.length >= mediaCount) {
loadingText.value = '没有更多了'; loadingText.value = '没有更多了';
return; return;
} }
isLoadingMore = true; isLoadingMore = true;
var res = await UserHttp.userSubFolderDetail( var res = channelType == 21
seasonId: seasonId, ? await UserHttp.userSeasonList(
ps: 20, seasonId: seasonId,
pn: currentPage, ps: 20,
); pn: currentPage,
)
: await UserHttp.userResourceList(
seasonId: seasonId,
ps: 20,
pn: currentPage,
);
if (res['status']) { if (res['status']) {
subInfo.value = res['data'].info; subInfo.value = res['data'].info;
if (currentPage == 1 && type == 'init') { if (currentPage == 1 && type == 'init') {
@ -55,6 +63,6 @@ class SubDetailController extends GetxController {
} }
onLoad() { onLoad() {
queryUserSubFolderDetail(type: 'onLoad'); queryUserSeasonList(type: 'onLoad');
} }
} }

View File

@ -26,13 +26,11 @@ class _SubDetailPageState extends State<SubDetailPage> {
Get.put(SubDetailController()); Get.put(SubDetailController());
late StreamController<bool> titleStreamC; // a late StreamController<bool> titleStreamC; // a
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
late String seasonId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
seasonId = Get.parameters['seasonId']!; _futureBuilderFuture = _subDetailController.queryUserSeasonList();
_futureBuilderFuture = _subDetailController.queryUserSubFolderDetail();
titleStreamC = StreamController<bool>(); titleStreamC = StreamController<bool>();
_controller.addListener( _controller.addListener(
() { () {
@ -55,6 +53,7 @@ class _SubDetailPageState extends State<SubDetailPage> {
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
titleStreamC.close();
super.dispose(); super.dispose();
} }
@ -69,7 +68,7 @@ class _SubDetailPageState extends State<SubDetailPage> {
pinned: true, pinned: true,
titleSpacing: 0, titleSpacing: 0,
title: StreamBuilder( title: StreamBuilder(
stream: titleStreamC.stream, stream: titleStreamC.stream.distinct(),
initialData: false, initialData: false,
builder: (context, AsyncSnapshot snapshot) { builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity( return AnimatedOpacity(
@ -161,15 +160,18 @@ class _SubDetailPageState extends State<SubDetailPage> {
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Obx(
'${Utils.numFormat(_subDetailController.item.viewCount)}次播放', () => Text(
style: TextStyle( '${Utils.numFormat(_subDetailController.subInfo.value.cntInfo?['play'])}次播放',
fontSize: Theme.of(context) style: TextStyle(
.textTheme fontSize: Theme.of(context)
.labelSmall! .textTheme
.fontSize, .labelSmall!
color: Theme.of(context).colorScheme.outline), .fontSize,
), color:
Theme.of(context).colorScheme.outline),
),
)
], ],
), ),
), ),
@ -182,14 +184,12 @@ class _SubDetailPageState extends State<SubDetailPage> {
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx( child: Text(
() => Text( '${_subDetailController.item.mediaCount}条视频',
'${_subDetailController.subList.length}条视频', style: TextStyle(
style: TextStyle( fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
fontSize: color: Theme.of(context).colorScheme.outline,
Theme.of(context).textTheme.labelMedium!.fontSize, letterSpacing: 1,
color: Theme.of(context).colorScheme.outline,
letterSpacing: 1),
), ),
), ),
), ),
@ -198,8 +198,8 @@ class _SubDetailPageState extends State<SubDetailPage> {
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
if (_subDetailController.item.mediaCount == 0) { if (_subDetailController.item.mediaCount == 0) {
return const NoData(); return const NoData();
} else { } else {
@ -219,7 +219,7 @@ class _SubDetailPageState extends State<SubDetailPage> {
} }
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() {}), fn: () => setState(() {}),
); );
} }

View File

@ -1,3 +1,4 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
@ -5,6 +6,7 @@ import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../../../common/widgets/badge.dart'; import '../../../common/widgets/badge.dart';
@ -40,6 +42,11 @@ class SubVideoCardH extends StatelessWidget {
'videoType': SearchType.video, 'videoType': SearchType.video,
}); });
}, },
onLongPress: () => imageSaveDialog(
context,
videoItem,
SmartDialog.dismiss,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(

View File

@ -20,7 +20,9 @@ import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/video_utils.dart'; import 'package:pilipala/utils/video_utils.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
import '../../../models/video/subTitile/content.dart';
import '../../../http/danmaku.dart'; import '../../../http/danmaku.dart';
import '../../../plugin/pl_player/models/bottom_control_type.dart';
import '../../../utils/id_utils.dart'; import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart'; import 'widgets/header_control.dart';
@ -49,7 +51,7 @@ class VideoDetailController extends GetxController
/// 播放器配置 画质 音质 解码格式 /// 播放器配置 画质 音质 解码格式
late VideoQuality currentVideoQa; late VideoQuality currentVideoQa;
AudioQuality? currentAudioQa; AudioQuality? currentAudioQa;
late VideoDecodeFormats currentDecodeFormats; VideoDecodeFormats? currentDecodeFormats;
// 是否开始自动播放 存在多p的情况下第二p需要为true // 是否开始自动播放 存在多p的情况下第二p需要为true
RxBool autoPlay = true.obs; RxBool autoPlay = true.obs;
// 视频资源是否有效 // 视频资源是否有效
@ -57,7 +59,7 @@ class VideoDetailController extends GetxController
// 封面图的展示 // 封面图的展示
RxBool isShowCover = true.obs; RxBool isShowCover = true.obs;
// 硬解 // 硬解
RxBool enableHA = true.obs; RxBool enableHA = false.obs;
/// 本地存储 /// 本地存储
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
@ -71,6 +73,7 @@ class VideoDetailController extends GetxController
ReplyItemModel? firstFloor; ReplyItemModel? firstFloor;
final scaffoldKey = GlobalKey<ScaffoldState>(); final scaffoldKey = GlobalKey<ScaffoldState>();
RxString bgCover = ''.obs; RxString bgCover = ''.obs;
RxString cover = ''.obs;
PlPlayerController plPlayerController = PlPlayerController.getInstance(); PlPlayerController plPlayerController = PlPlayerController.getInstance();
late VideoItem firstVideo; late VideoItem firstVideo;
@ -90,32 +93,41 @@ class VideoDetailController extends GetxController
late bool enableCDN; late bool enableCDN;
late int? cacheVideoQa; late int? cacheVideoQa;
late String cacheDecode; late String cacheDecode;
late int cacheAudioQa; late int defaultAudioQa;
PersistentBottomSheetController? replyReplyBottomSheetCtr; PersistentBottomSheetController? replyReplyBottomSheetCtr;
RxList<SubTitileContentModel> subtitleContents =
<SubTitileContentModel>[].obs;
late bool enableRelatedVideo; late bool enableRelatedVideo;
List subtitles = [];
RxList<BottomControlType> bottomList = [
BottomControlType.playOrPause,
BottomControlType.time,
BottomControlType.space,
BottomControlType.fit,
BottomControlType.fullscreen,
].obs;
RxDouble sheetHeight = 0.0.obs;
RxString archiveSourceType = 'dash'.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
final Map argMap = Get.arguments; final Map argMap = Get.arguments;
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
var keys = argMap.keys.toList(); if (argMap.containsKey('videoItem')) {
if (keys.isNotEmpty) { var args = argMap['videoItem'];
if (keys.contains('videoItem')) { updateCover(args.pic);
var args = argMap['videoItem'];
if (args.pic != null && args.pic != '') {
videoItem['pic'] = args.pic;
}
}
if (keys.contains('pic')) {
videoItem['pic'] = argMap['pic'];
}
} }
if (argMap.containsKey('pic')) {
updateCover(argMap['pic']);
}
tabCtr = TabController(length: 2, vsync: this); tabCtr = TabController(length: 2, vsync: this);
autoPlay.value = autoPlay.value =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true); setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true); enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: false);
enableRelatedVideo = enableRelatedVideo =
setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true); setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true);
if (userInfo == null || if (userInfo == null ||
@ -142,16 +154,17 @@ class VideoDetailController extends GetxController
// 预设的解码格式 // 预设的解码格式
cacheDecode = setting.get(SettingBoxKey.defaultDecode, cacheDecode = setting.get(SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code); defaultValue: VideoDecodeFormats.values.last.code);
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa, defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code); defaultValue: AudioQuality.hiRes.code);
oid.value = IdUtils.bv2av(Get.parameters['bvid']!); oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
getSubtitle();
} }
showReplyReplyPanel() { showReplyReplyPanel(oid, fRpid, firstFloor) {
replyReplyBottomSheetCtr = replyReplyBottomSheetCtr =
scaffoldKey.currentState?.showBottomSheet((BuildContext context) { scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
return VideoReplyReplyPanel( return VideoReplyReplyPanel(
oid: oid.value, oid: oid,
rpid: fRpid, rpid: fRpid,
closePanel: () => { closePanel: () => {
fRpid = 0, fRpid = 0,
@ -159,6 +172,7 @@ class VideoDetailController extends GetxController
firstFloor: firstFloor, firstFloor: firstFloor,
replyType: ReplyType.video, replyType: ReplyType.video,
source: 'videoDetail', source: 'videoDetail',
sheetHeight: sheetHeight.value,
); );
}); });
replyReplyBottomSheetCtr?.closed.then((value) { replyReplyBottomSheetCtr?.closed.then((value) {
@ -174,37 +188,43 @@ class VideoDetailController extends GetxController
plPlayerController.isBuffering.value = false; plPlayerController.isBuffering.value = false;
plPlayerController.buffered.value = Duration.zero; plPlayerController.buffered.value = Duration.zero;
/// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl if (archiveSourceType.value == 'dash') {
List<VideoItem> videoList = /// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList(); List<VideoItem> videoList =
try { data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList();
firstVideo = videoList try {
.firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code)); firstVideo = videoList.firstWhere(
} catch (_) { (i) => i.codecs!.startsWith(currentDecodeFormats?.code));
if (currentVideoQa == VideoQuality.dolbyVision) { } catch (_) {
firstVideo = videoList.first; if (currentVideoQa == VideoQuality.dolbyVision) {
currentDecodeFormats = firstVideo = videoList.first;
VideoDecodeFormatsCode.fromString(videoList.first.codecs!)!; currentDecodeFormats =
} else { VideoDecodeFormatsCode.fromString(videoList.first.codecs!)!;
// 当前格式不可用 } else {
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get( // 当前格式不可用
SettingBoxKey.defaultDecode, currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
defaultValue: VideoDecodeFormats.values.last.code))!; SettingBoxKey.defaultDecode,
firstVideo = videoList defaultValue: VideoDecodeFormats.values.last.code))!;
.firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code)); firstVideo = videoList.firstWhere(
(i) => i.codecs!.startsWith(currentDecodeFormats?.code));
}
}
videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl
if (currentAudioQa != null) {
final AudioItem firstAudio = data.dash!.audio!.firstWhere(
(AudioItem i) => i.id == currentAudioQa!.code,
orElse: () => data.dash!.audio!.first,
);
audioUrl = firstAudio.baseUrl ?? '';
} }
} }
videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl if (archiveSourceType.value == 'durl') {
if (currentAudioQa != null) { cacheVideoQa = VideoQualityCode.toCode(currentVideoQa);
final AudioItem firstAudio = data.dash!.audio!.firstWhere( queryVideoUrl();
(AudioItem i) => i.id == currentAudioQa!.code,
orElse: () => data.dash!.audio!.first,
);
audioUrl = firstAudio.baseUrl ?? '';
} }
playerInit(); playerInit();
} }
@ -251,11 +271,14 @@ class VideoDetailController extends GetxController
/// 开启自动全屏时在player初始化完成后立即传入headerControl /// 开启自动全屏时在player初始化完成后立即传入headerControl
plPlayerController.headerControl = headerControl; plPlayerController.headerControl = headerControl;
plPlayerController.subtitles.value = subtitles;
} }
// 视频链接 // 视频链接
Future queryVideoUrl() async { Future queryVideoUrl() async {
var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid); var result =
await VideoHttp.videoUrl(cid: cid.value, bvid: bvid, qn: cacheVideoQa);
if (result['status']) { if (result['status']) {
data = result['data']; data = result['data'];
if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) { if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) {
@ -273,8 +296,22 @@ class VideoDetailController extends GetxController
} }
return result; return result;
} }
if (data.durl != null) {
archiveSourceType.value = 'durl';
videoUrl = data.durl!.first.url!;
audioUrl = '';
defaultST = Duration.zero;
firstVideo = VideoItem();
currentVideoQa = VideoQualityCode.fromCode(data.quality!)!;
if (autoPlay.value) {
await playerInit();
isShowCover.value = false;
}
return result;
}
final List<VideoItem> allVideosList = data.dash!.video!; final List<VideoItem> allVideosList = data.dash!.video!;
try { try {
archiveSourceType.value = 'dash';
// 当前可播放的最高质量视频 // 当前可播放的最高质量视频
int currentHighVideoQa = allVideosList.first.quality!.code; int currentHighVideoQa = allVideosList.first.quality!.code;
// 预设的画质为null则当前可用的最高质量 // 预设的画质为null则当前可用的最高质量
@ -304,7 +341,7 @@ class VideoDetailController extends GetxController
// 当前视频没有对应格式返回第一个 // 当前视频没有对应格式返回第一个
bool flag = false; bool flag = false;
for (var i in supportDecodeFormats) { for (var i in supportDecodeFormats) {
if (i.startsWith(currentDecodeFormats.code)) { if (i.startsWith(currentDecodeFormats?.code)) {
flag = true; flag = true;
} }
} }
@ -318,7 +355,7 @@ class VideoDetailController extends GetxController
/// 取出符合当前解码格式的videoItem /// 取出符合当前解码格式的videoItem
try { try {
firstVideo = videosList.firstWhere( firstVideo = videosList.firstWhere(
(e) => e.codecs!.startsWith(currentDecodeFormats.code)); (e) => e.codecs!.startsWith(currentDecodeFormats?.code));
} catch (_) { } catch (_) {
firstVideo = videosList.first; firstVideo = videosList.first;
} }
@ -346,9 +383,9 @@ class VideoDetailController extends GetxController
if (audiosList.isNotEmpty) { if (audiosList.isNotEmpty) {
final List<int> numbers = audiosList.map((map) => map.id!).toList(); final List<int> numbers = audiosList.map((map) => map.id!).toList();
int closestNumber = Utils.findClosestNumber(cacheAudioQa, numbers); int closestNumber = Utils.findClosestNumber(defaultAudioQa, numbers);
if (!numbers.contains(cacheAudioQa) && if (!numbers.contains(defaultAudioQa) &&
numbers.any((e) => e > cacheAudioQa)) { numbers.any((e) => e > defaultAudioQa)) {
closestNumber = 30280; closestNumber = 30280;
} }
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber); firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);
@ -388,6 +425,45 @@ class VideoDetailController extends GetxController
: print('replyReplyBottomSheetCtr is null'); : 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() { void showShootDanmakuSheet() {
final TextEditingController textController = TextEditingController(); final TextEditingController textController = TextEditingController();
@ -469,4 +545,10 @@ class VideoDetailController extends GetxController
}, },
); );
} }
void updateCover(String? pic) {
if (pic != null) {
cover.value = videoItem['pic'] = pic;
}
}
} }

View File

@ -18,6 +18,9 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../../common/pages_bottom_sheet.dart';
import '../../../../models/common/video_episode_type.dart';
import '../../../../utils/drawer.dart';
import '../related/index.dart'; import '../related/index.dart';
import 'widgets/group_panel.dart'; import 'widgets/group_panel.dart';
@ -25,15 +28,10 @@ class VideoIntroController extends GetxController {
VideoIntroController({required this.bvid}); VideoIntroController({required this.bvid});
// 视频bvid // 视频bvid
String bvid; String bvid;
// 请求状态
RxBool isLoading = false.obs;
// 视频详情 请求返回 // 视频详情 请求返回
Rx<VideoDetailData> videoDetail = VideoDetailData().obs; Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
// up主粉丝数 // up主粉丝数
Map userStat = {'follower': '-'}; Map userStat = {'follower': '-'};
// 是否点赞 // 是否点赞
RxBool hasLike = false.obs; RxBool hasLike = false.obs;
// 是否投币 // 是否投币
@ -59,6 +57,8 @@ class VideoIntroController extends GetxController {
bool isPaused = false; bool isPaused = false;
String heroTag = ''; String heroTag = '';
late ModelResult modelResult; late ModelResult modelResult;
PersistentBottomSheetController? bottomSheetController;
late bool enableRelatedVideo;
@override @override
void onInit() { void onInit() {
@ -75,6 +75,8 @@ class VideoIntroController extends GetxController {
queryOnlineTotal(); queryOnlineTotal();
startTimer(); // 在页面加载时启动定时器 startTimer(); // 在页面加载时启动定时器
} }
enableRelatedVideo =
setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true);
} }
// 获取视频简介&分p // 获取视频简介&分p
@ -88,6 +90,7 @@ class VideoIntroController extends GetxController {
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
videoDetailCtr.tabs.value = ['简介', '评论 ${result['data']?.stat?.reply}']; videoDetailCtr.tabs.value = ['简介', '评论 ${result['data']?.stat?.reply}'];
videoDetailCtr.cover.value = result['data'].pic ?? '';
// 获取到粉丝数再返回 // 获取到粉丝数再返回
await queryUserStat(); await queryUserStat();
} }
@ -217,50 +220,36 @@ class VideoIntroController extends GetxController {
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text('选择投币个数'), title: const Text('选择投币个数'),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
content: StatefulBuilder(builder: (context, StateSetter setState) { content: StatefulBuilder(builder: (context, StateSetter setState) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [1, 2]
RadioListTile( .map(
value: 1, (e) => RadioListTile(
title: const Text('1枚'), value: e,
groupValue: _tempThemeValue, title: Text('$e枚'),
onChanged: (value) { groupValue: _tempThemeValue,
_tempThemeValue = value!; onChanged: (value) async {
Get.appUpdate(); _tempThemeValue = value!;
}, setState(() {});
), var res = await VideoHttp.coinVideo(
RadioListTile( bvid: bvid, multiply: _tempThemeValue);
value: 2, if (res['status']) {
title: const Text('2枚'), SmartDialog.showToast('投币成功 👏');
groupValue: _tempThemeValue, hasCoin.value = true;
onChanged: (value) { videoDetail.value.stat!.coin =
_tempThemeValue = value!; videoDetail.value.stat!.coin! + _tempThemeValue;
Get.appUpdate(); } else {
}, SmartDialog.showToast(res['msg']);
), }
], Get.back();
},
),
)
.toList(),
); );
}), }),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('取消')),
TextButton(
onPressed: () async {
var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue);
if (res['status']) {
SmartDialog.showToast('投币成功 👏');
hasCoin.value = true;
videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
child: const Text('确定'))
],
); );
}); });
} }
@ -444,19 +433,23 @@ class VideoIntroController extends GetxController {
} }
// 修改分P或番剧分集 // 修改分P或番剧分集
Future changeSeasonOrbangu(bvid, cid, aid) async { Future changeSeasonOrbangu(bvid, cid, aid, cover) async {
// 重新获取视频资源 // 重新获取视频资源
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
final ReleatedController releatedCtr = if (enableRelatedVideo) {
Get.find<ReleatedController>(tag: heroTag); final ReleatedController releatedCtr =
Get.find<ReleatedController>(tag: heroTag);
releatedCtr.bvid = bvid;
releatedCtr.queryRelatedVideo();
}
videoDetailCtr.bvid = bvid; videoDetailCtr.bvid = bvid;
videoDetailCtr.oid.value = aid ?? IdUtils.bv2av(bvid); videoDetailCtr.oid.value = aid ?? IdUtils.bv2av(bvid);
videoDetailCtr.cid.value = cid; videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.cover.value = cover;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();
releatedCtr.bvid = bvid;
releatedCtr.queryRelatedVideo();
// 重新请求评论 // 重新请求评论
try { try {
/// 未渲染回复组件时可能异常 /// 未渲染回复组件时可能异常
@ -503,6 +496,7 @@ class VideoIntroController extends GetxController {
void nextPlay() { void nextPlay() {
final List episodes = []; final List episodes = [];
bool isPages = false; bool isPages = false;
late String cover;
if (videoDetail.value.ugcSeason != null) { if (videoDetail.value.ugcSeason != null) {
final UgcSeason ugcSeason = videoDetail.value.ugcSeason!; final UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
final List<SectionItem> sections = ugcSeason.sections!; final List<SectionItem> sections = ugcSeason.sections!;
@ -519,6 +513,7 @@ class VideoIntroController extends GetxController {
final int currentIndex = final int currentIndex =
episodes.indexWhere((e) => e.cid == lastPlayCid.value); episodes.indexWhere((e) => e.cid == lastPlayCid.value);
int nextIndex = currentIndex + 1; int nextIndex = currentIndex + 1;
cover = episodes[nextIndex].cover;
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
final PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat; final PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
@ -535,7 +530,7 @@ class VideoIntroController extends GetxController {
final int cid = episodes[nextIndex].cid!; final int cid = episodes[nextIndex].cid!;
final String rBvid = isPages ? bvid : episodes[nextIndex].bvid; final String rBvid = isPages ? bvid : episodes[nextIndex].bvid;
final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!; final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;
changeSeasonOrbangu(rBvid, cid, rAid); changeSeasonOrbangu(rBvid, cid, rAid, cover);
} }
// 设置关注分组 // 设置关注分组
@ -562,4 +557,53 @@ class VideoIntroController extends GetxController {
} }
return res; return res;
} }
hiddenEpisodeBottomSheet() {
bottomSheetController?.close();
}
// 播放器底栏 选集 回调
void showEposideHandler() {
late List episodes;
VideoEpidoesType dataType = VideoEpidoesType.videoEpisode;
if (videoDetail.value.ugcSeason != null) {
dataType = VideoEpidoesType.videoEpisode;
final List<SectionItem> sections = videoDetail.value.ugcSeason!.sections!;
for (int i = 0; i < sections.length; i++) {
final List<EpisodeItem> episodesList = sections[i].episodes!;
for (int j = 0; j < episodesList.length; j++) {
if (episodesList[j].cid == lastPlayCid.value) {
episodes = episodesList;
continue;
}
}
}
}
if (videoDetail.value.pages != null &&
videoDetail.value.pages!.length > 1) {
dataType = VideoEpidoesType.videoPart;
episodes = videoDetail.value.pages!;
}
DrawerUtils.showRightDialog(
child: EpisodeBottomSheet(
episodes: episodes,
currentCid: lastPlayCid.value,
dataType: dataType,
context: Get.context!,
sheetHeight: Get.size.height,
isFullScreen: true,
changeFucCall: (item, index) {
if (dataType == VideoEpidoesType.videoEpisode) {
changeSeasonOrbangu(
IdUtils.av2bv(item.aid), item.cid, item.aid, item.cover);
}
if (dataType == VideoEpidoesType.videoPart) {
changeSeasonOrbangu(bvid, item.cid, null, item.cover);
}
SmartDialog.dismiss();
},
).buildShowContent(Get.context!),
);
}
} }

View File

@ -1,3 +1,5 @@
import 'package:expandable/expandable.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -15,11 +17,13 @@ import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../../../../http/user.dart';
import 'widgets/action_item.dart'; import 'widgets/action_item.dart';
import 'widgets/fav_panel.dart'; import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart'; import 'widgets/intro_detail.dart';
import 'widgets/page.dart'; import 'widgets/page_panel.dart';
import 'widgets/season.dart'; import 'widgets/season_panel.dart';
import 'widgets/staff_up_item.dart';
class VideoIntroPanel extends StatefulWidget { class VideoIntroPanel extends StatefulWidget {
final String bvid; final String bvid;
@ -132,11 +136,13 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
late double sheetHeight; late double sheetHeight;
late final dynamic owner; late final dynamic owner;
late final dynamic follower; late final dynamic follower;
late final dynamic followStatus;
late int mid; late int mid;
late String memberHeroTag; late String memberHeroTag;
late bool enableAi; late bool enableAi;
bool isProcessing = false; bool isProcessing = false;
RxBool isExpand = false.obs;
late ExpandableController _expandableCtr;
void Function()? handleState(Future Function() action) { void Function()? handleState(Future Function() action) {
return isProcessing return isProcessing
? null ? null
@ -158,8 +164,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
owner = widget.videoDetail!.owner; owner = widget.videoDetail!.owner;
follower = Utils.numFormat(videoIntroController.userStat['follower']); follower = Utils.numFormat(videoIntroController.userStat['follower']);
followStatus = videoIntroController.followStatus;
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true); enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
_expandableCtr = ExpandableController(initialExpanded: false);
} }
// 收藏 // 收藏
@ -212,13 +218,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// 视频介绍 // 视频介绍
showIntroDetail() { showIntroDetail() {
feedBack(); feedBack();
showBottomSheet( isExpand.value = !(isExpand.value);
context: context, _expandableCtr.toggle();
enableDrag: true,
builder: (BuildContext context) {
return IntroDetail(videoDetail: widget.videoDetail!);
},
);
} }
// 用户主页 // 用户主页
@ -242,6 +243,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
); );
} }
@override
void dispose() {
_expandableCtr.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData t = Theme.of(context); final ThemeData t = Theme.of(context);
@ -259,14 +266,40 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
GestureDetector( GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(), onTap: () => showIntroDetail(),
child: Text( onLongPress: () async {
widget.videoDetail!.title!, feedBack();
style: const TextStyle( await Clipboard.setData(
fontSize: 18, ClipboardData(text: widget.videoDetail!.title!));
fontWeight: FontWeight.bold, SmartDialog.showToast('标题已复制');
},
child: ExpandablePanel(
controller: _expandableCtr,
collapsed: Text(
widget.videoDetail!.title!,
softWrap: true,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
expanded: Text(
widget.videoDetail!.title!,
softWrap: true,
maxLines: 4,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
theme: const ExpandableThemeData(
animationDuration: Duration(milliseconds: 300),
scrollAnimationDuration: Duration(milliseconds: 300),
crossFadePoint: 0,
fadeCurve: Curves.ease,
sizeCurve: Curves.linear,
), ),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
), ),
Stack( Stack(
@ -330,9 +363,23 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
], ],
), ),
/// 视频简介
ExpandablePanel(
controller: _expandableCtr,
collapsed: const SizedBox(height: 0),
expanded: IntroDetail(videoDetail: widget.videoDetail!),
theme: const ExpandableThemeData(
animationDuration: Duration(milliseconds: 300),
scrollAnimationDuration: Duration(milliseconds: 300),
crossFadePoint: 0,
fadeCurve: Curves.ease,
sizeCurve: Curves.linear,
),
),
/// 点赞收藏转发 /// 点赞收藏转发
actionGrid(context, videoIntroController), Material(child: actionGrid(context, videoIntroController)),
// 合集 // 合集 videoPart 简洁
if (widget.videoDetail!.ugcSeason != null) ...[ if (widget.videoDetail!.ugcSeason != null) ...[
Obx( Obx(
() => SeasonPanel( () => SeasonPanel(
@ -340,92 +387,148 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
cid: videoIntroController.lastPlayCid.value != 0 cid: videoIntroController.lastPlayCid.value != 0
? videoIntroController.lastPlayCid.value ? videoIntroController.lastPlayCid.value
: widget.videoDetail!.pages!.first.cid, : widget.videoDetail!.pages!.first.cid,
sheetHeight: sheetHeight, sheetHeight: videoDetailCtr.sheetHeight.value,
changeFuc: (bvid, cid, aid) => changeFuc: (bvid, cid, aid, cover) =>
videoIntroController.changeSeasonOrbangu(bvid, cid, aid), videoIntroController.changeSeasonOrbangu(
bvid,
cid,
aid,
cover,
),
videoIntroCtr: videoIntroController,
), ),
) )
], ],
// 合集 videoEpisode
if (widget.videoDetail!.pages != null && if (widget.videoDetail!.pages != null &&
widget.videoDetail!.pages!.length > 1) ...[ widget.videoDetail!.pages!.length > 1) ...[
Obx(() => PagesPanel( Obx(
pages: widget.videoDetail!.pages!, () => PagesPanel(
cid: videoIntroController.lastPlayCid.value, pages: widget.videoDetail!.pages!,
sheetHeight: sheetHeight, cid: videoIntroController.lastPlayCid.value,
changeFuc: (cid) => videoIntroController.changeSeasonOrbangu( sheetHeight: videoDetailCtr.sheetHeight.value,
videoIntroController.bvid, cid, null), changeFuc: (cid, cover) =>
)) videoIntroController.changeSeasonOrbangu(
videoIntroController.bvid,
cid,
null,
cover,
),
videoIntroCtr: videoIntroController,
),
)
], ],
GestureDetector( if (widget.videoDetail!.staff == null)
onTap: onPushMember, GestureDetector(
child: Container( onTap: onPushMember,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), child: Container(
child: Row( padding:
children: [ const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
NetworkImgLayer( child: Row(
type: 'avatar', children: [
src: widget.videoDetail!.owner!.face, NetworkImgLayer(
width: 34, type: 'avatar',
height: 34, src: widget.videoDetail!.owner!.face,
fadeInDuration: Duration.zero, width: 34,
fadeOutDuration: Duration.zero, height: 34,
), fadeInDuration: Duration.zero,
const SizedBox(width: 10), fadeOutDuration: Duration.zero,
Text(owner.name, style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
follower,
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
), ),
), const SizedBox(width: 10),
const Spacer(), Text(owner.name, style: const TextStyle(fontSize: 13)),
Obx(() => AnimatedOpacity( const SizedBox(width: 6),
opacity: Text(
videoIntroController.followStatus.isEmpty ? 0 : 1, follower,
duration: const Duration(milliseconds: 50), style: TextStyle(
child: SizedBox( fontSize: t.textTheme.labelSmall!.fontSize,
height: 32, color: outline,
child: Obx( ),
() => videoIntroController.followStatus.isNotEmpty ),
? TextButton( const Spacer(),
onPressed: Obx(
videoIntroController.actionRelationMod, () {
style: TextButton.styleFrom( final bool isFollowed =
padding: const EdgeInsets.only( videoIntroController.followStatus['attribute'] != 0;
left: 8, right: 8), return videoIntroController.followStatus.isEmpty
foregroundColor: ? const SizedBox()
followStatus['attribute'] != 0 : SizedBox(
? outline height: 32,
: t.colorScheme.onPrimary, child: TextButton(
backgroundColor: onPressed:
followStatus['attribute'] != 0 videoIntroController.actionRelationMod,
? t.colorScheme.onInverseSurface style: TextButton.styleFrom(
: t.colorScheme padding: const EdgeInsets.only(
.primary, // 设置按钮背景色 left: 8,
right: 8,
), ),
child: Text( foregroundColor: isFollowed
followStatus['attribute'] != 0 ? outline
? '已关注' : t.colorScheme.onPrimary,
: '关注', backgroundColor: isFollowed
style: TextStyle( ? t.colorScheme.onInverseSurface
fontSize: t : t.colorScheme.primary, // 设置按钮背景色
.textTheme.labelMedium!.fontSize),
),
)
: ElevatedButton(
onPressed:
videoIntroController.actionRelationMod,
child: const Text('关注'),
), ),
), child: Text(
), isFollowed ? '已关注' : '关注',
)), style: TextStyle(
], fontSize:
t.textTheme.labelMedium!.fontSize,
),
),
),
);
},
)
],
),
), ),
), ),
), if (widget.videoDetail!.staff != null) ...[
const SizedBox(height: 15),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
),
children: [
TextSpan(
text: '创作团队',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(fontWeight: FontWeight.bold),
),
const WidgetSpan(child: SizedBox(width: 6)),
TextSpan(
text: '${widget.videoDetail!.staff!.length}',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
)
],
),
),
SizedBox(
height: 120,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
for (int i = 0;
i < widget.videoDetail!.staff!.length;
i++) ...[
StaffUpItem(item: widget.videoDetail!.staff![i])
],
],
),
),
],
),
]
], ],
)), )),
); );
@ -438,6 +541,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
margin: const EdgeInsets.only(top: 6, bottom: 4), margin: const EdgeInsets.only(top: 6, bottom: 4),
height: constraints.maxWidth / 5 * 0.8, height: constraints.maxWidth / 5 * 0.8,
child: GridView.count( child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
primary: false, primary: false,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
crossAxisCount: 5, crossAxisCount: 5,
@ -451,12 +555,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
selectStatus: videoIntroController.hasLike.value, selectStatus: videoIntroController.hasLike.value,
text: widget.videoDetail!.stat!.like!.toString()), text: widget.videoDetail!.stat!.like!.toString()),
), ),
// ActionItem(
// icon: const Icon(FontAwesomeIcons.clock),
// onTap: () => videoIntroController.actionShareVideo(),
// selectStatus: false,
// loadingStatus: loadingStatus,
// text: '稍后再看'),
Obx( Obx(
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.b), icon: const Icon(FontAwesomeIcons.b),
@ -477,10 +575,14 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
), ),
), ),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.comment), icon: const Icon(FontAwesomeIcons.clock),
onTap: () => videoDetailCtr.tabCtr.animateTo(1), onTap: () async {
final res =
await UserHttp.toViewLater(bvid: widget.videoDetail!.bvid);
SmartDialog.showToast(res['msg']);
},
selectStatus: false, selectStatus: false,
text: widget.videoDetail!.stat!.reply!.toString(), text: '稍后看',
), ),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare), icon: const Icon(FontAwesomeIcons.shareFromSquare),
@ -493,4 +595,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
); );
}); });
} }
// Widget StaffPanel(BuildContext context, videoIntroController) {
// return
// }
} }

View File

@ -1,16 +1,11 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
Box localCache = GStrorage.localCache;
late double sheetHeight;
class IntroDetail extends StatelessWidget { class IntroDetail extends StatelessWidget {
const IntroDetail({ const IntroDetail({
super.key, super.key,
@ -20,105 +15,60 @@ class IntroDetail extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
sheetHeight = localCache.get('sheetHeight'); return SizedBox(
return Container( width: double.infinity,
color: Theme.of(context).colorScheme.background, child: Column(
padding: EdgeInsets.only( crossAxisAlignment: CrossAxisAlignment.start,
left: 14, children: <Widget>[
right: 14, const SizedBox(height: 4),
bottom: MediaQuery.of(context).padding.bottom + 20), Row(
height: sheetHeight, children: [
child: Column( GestureDetector(
children: [ onTap: () {
InkWell( feedBack();
onTap: () => Get.back(), Clipboard.setData(ClipboardData(text: videoDetail!.bvid!));
child: Container( SmartDialog.showToast('已复制');
height: 35, },
padding: const EdgeInsets.only(bottom: 2), child: Text(
child: Center( videoDetail!.bvid!,
child: Container( style: TextStyle(
width: 32, fontSize: 13,
height: 3, color: Theme.of(context).colorScheme.primary),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius:
const BorderRadius.all(Radius.circular(3))),
),
), ),
), ),
const SizedBox(width: 10),
GestureDetector(
onTap: () {
feedBack();
Clipboard.setData(
ClipboardData(text: videoDetail!.aid!.toString()));
SmartDialog.showToast('已复制');
},
child: Text(
videoDetail!.aid!.toString(),
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.primary),
),
)
],
),
const SizedBox(height: 4),
SelectableRegion(
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Text.rich(
style: const TextStyle(height: 1.4),
TextSpan(
children: [
buildContent(context, videoDetail!),
],
),
), ),
Expanded( ),
child: SingleChildScrollView( ],
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, );
children: [
Text(
videoDetail!.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Row(
children: [
StatView(
theme: 'gray',
view: videoDetail!.stat!.view,
size: 'medium',
),
const SizedBox(width: 10),
StatDanMu(
theme: 'gray',
danmu: videoDetail!.stat!.danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text(
Utils.dateFormat(videoDetail!.pubdate,
formatType: 'detail'),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
],
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: SelectableRegion(
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
videoDetail!.bvid!,
style: const TextStyle(fontSize: 13),
),
const SizedBox(height: 4),
Text.rich(
style: const TextStyle(
height: 1.4,
// fontSize: 13,
),
TextSpan(
children: [
buildContent(context, videoDetail!),
],
),
),
],
),
),
),
],
),
),
)
],
));
} }
InlineSpan buildContent(BuildContext context, content) { InlineSpan buildContent(BuildContext context, content) {

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,186 @@
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 '../../../../../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();
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 {
widget.changeFuc?.call(item.cid, item.cover);
currentIndex.value = i;
_bottomSheetController?.close();
scrollToIndex();
}
void scrollToIndex() {
WidgetsBinding.instance.addPostFrameCallback((_) {
// 在回调函数中获取更新后的状态
final double offset = min((currentIndex * 150) - 75,
listViewScrollCtr.position.maxScrollExtent);
if (currentIndex.value == 0) {
listViewScrollCtr.jumpTo(0);
} else {
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: 55,
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: 2,
style: TextStyle(
fontSize: 13,
color: isCurrentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
overflow: TextOverflow.ellipsis,
))
],
),
),
),
),
);
},
),
)
],
);
}
}

View File

@ -1,226 +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';
import 'package:pilipala/utils/id_utils.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class SeasonPanel extends StatefulWidget {
const SeasonPanel({
super.key,
required this.ugcSeason,
this.cid,
this.sheetHeight,
this.changeFuc,
});
final UgcSeason ugcSeason;
final int? cid;
final double? sheetHeight;
final Function? changeFuc;
@override
State<SeasonPanel> createState() => _SeasonPanelState();
}
class _SeasonPanelState extends State<SeasonPanel> {
late List<EpisodeItem> episodes;
late int cid;
late int currentIndex;
final String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
final ScrollController _scrollController = ScrollController();
final ItemScrollController itemScrollController = ItemScrollController();
@override
void initState() {
super.initState();
cid = widget.cid!;
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
/// 根据 cid 找到对应集,找到对应 episodes
/// 有多个episodes时只显示其中一个
/// TODO 同时显示多个合集
final List<SectionItem> sections = widget.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 == cid) {
episodes = episodesList;
continue;
}
}
}
/// 取对应 season_id 的 episodes
// episodes = widget.ugcSeason.sections!
// .firstWhere((e) => e.seasonId == widget.ugcSeason.id)
// .episodes!;
currentIndex = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
_videoDetailController.cid.listen((int p0) {
cid = p0;
setState(() {});
currentIndex = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
});
}
void changeFucCall(item, int i) async {
await widget.changeFuc!(
IdUtils.av2bv(item.aid),
item.cid,
item.aid,
);
currentIndex = i;
Get.back();
setState(() {});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Widget buildEpisodeListItem(
EpisodeItem episode,
int index,
bool isCurrentIndex,
) {
Color primary = Theme.of(context).colorScheme.primary;
return ListTile(
onTap: () => changeFucCall(episode, index),
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.gif',
color: primary,
height: 12,
)
: null,
title: Text(
episode.title!,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex
? primary
: Theme.of(context).colorScheme.onSurface,
),
),
);
}
@override
Widget build(BuildContext context) {
return Builder(builder: (BuildContext context) {
return Container(
margin: const EdgeInsets.only(
top: 8,
left: 2,
right: 2,
bottom: 2,
),
child: Material(
color: Theme.of(context).colorScheme.onInverseSurface,
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,
),
),
),
],
),
);
});
},
),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
child: Row(
children: <Widget>[
Expanded(
child: Text(
'合集:${widget.ugcSeason.title!}',
style: Theme.of(context).textTheme.labelMedium,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 15),
Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
height: 12,
),
const SizedBox(width: 10),
Text(
'${currentIndex + 1}/${episodes.length}',
style: Theme.of(context).textTheme.labelMedium,
),
const SizedBox(width: 6),
const Icon(
Icons.arrow_forward_ios_outlined,
size: 13,
)
],
),
),
),
),
);
});
}
}

View File

@ -0,0 +1,165 @@
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 '../../../../../models/common/video_episode_type.dart';
import '../controller.dart';
class SeasonPanel extends StatefulWidget {
const SeasonPanel({
super.key,
required this.ugcSeason,
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();
}
class _SeasonPanelState extends State<SeasonPanel> {
late List<EpisodeItem> episodes;
late int cid;
late RxInt currentIndex = (-1).obs;
final String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
late PersistentBottomSheetController? _bottomSheetController;
@override
void initState() {
super.initState();
cid = widget.cid!;
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
/// 根据 cid 找到对应集,找到对应 episodes
/// 有多个episodes时只显示其中一个
/// TODO 同时显示多个合集
final List<SectionItem> sections = widget.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 == cid) {
episodes = episodesList;
continue;
}
}
}
/// 取对应 season_id 的 episodes
currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
_videoDetailController.cid.listen((int p0) {
cid = p0;
currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
});
}
void changeFucCall(item, int i) async {
widget.changeFuc?.call(
IdUtils.av2bv(item.aid),
item.cid,
item.aid,
item.cover,
);
currentIndex.value = i;
_bottomSheetController?.close();
}
Widget buildEpisodeListItem(
EpisodeItem episode,
int index,
bool isCurrentIndex,
) {
Color primary = Theme.of(context).colorScheme.primary;
return ListTile(
onTap: () => changeFucCall(episode, index),
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.gif',
color: primary,
height: 12,
)
: null,
title: Text(
episode.title!,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex
? primary
: Theme.of(context).colorScheme.onSurface,
),
),
);
}
@override
Widget build(BuildContext context) {
return Builder(builder: (BuildContext context) {
return Container(
margin: const EdgeInsets.only(
top: 8,
left: 2,
right: 2,
bottom: 2,
),
child: Material(
color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge,
child: InkWell(
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(
children: <Widget>[
Expanded(
child: Text(
'合集:${widget.ugcSeason.title!}',
style: Theme.of(context).textTheme.labelMedium,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 15),
Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
height: 12,
),
const SizedBox(width: 10),
Obx(() => Text(
'${currentIndex.value + 1}/${episodes.length}',
style: Theme.of(context).textTheme.labelMedium,
)),
const SizedBox(width: 6),
const Icon(
Icons.arrow_forward_ios_outlined,
size: 13,
)
],
),
),
),
),
);
});
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/utils/utils.dart';
class StaffUpItem extends StatelessWidget {
final Staff item;
const StaffUpItem({
super.key,
required this.item,
});
@override
Widget build(BuildContext context) {
final String heroTag = Utils.makeHeroTag(item.mid);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 15),
GestureDetector(
onTap: () => Get.toNamed(
'/member?mid=${item.mid}',
arguments: {'face': item.face, 'heroTag': heroTag},
),
child: Hero(
tag: heroTag,
child: NetworkImgLayer(
width: 45,
height: 45,
src: item.face,
type: 'avatar',
),
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: SizedBox(
width: 85,
child: Text(
item.name!,
overflow: TextOverflow.ellipsis,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(
color: item.vip!.status == 1
? const Color.fromARGB(255, 251, 100, 163)
: null,
),
),
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: SizedBox(
width: 85,
child: Text(
item.title!,
overflow: TextOverflow.ellipsis,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: 12,
),
),
),
),
],
);
}
}

View File

@ -1,9 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
import './controller.dart'; import './controller.dart';
@ -54,20 +52,6 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel>
child: VideoCardH( child: VideoCardH(
videoItem: relatedVideoList[index], videoItem: relatedVideoList[index],
showPubdate: true, showPubdate: true,
longPress: () {
try {
_releatedController.popupDialog =
_createPopupDialog(_releatedController
.relatedVideoList[index]);
Overlay.of(context)
.insert(_releatedController.popupDialog!);
} catch (err) {
return {};
}
},
longPressEnd: () {
_releatedController.popupDialog?.remove();
},
), ),
); );
} }
@ -89,15 +73,4 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel>
}, },
); );
} }
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (BuildContext context) => AnimatedDialog(
closeFn: _releatedController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem,
closeFn: _releatedController.popupDialog?.remove),
),
);
}
} }

View File

@ -37,6 +37,7 @@ class VideoReplyController extends GetxController {
RxString sortTypeLabel = ReplySortType.time.labels.obs; RxString sortTypeLabel = ReplySortType.time.labels.obs;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
RxInt replyReqCode = 200.obs;
@override @override
void onInit() { void onInit() {
@ -106,6 +107,7 @@ class VideoReplyController extends GetxController {
replyList.addAll(replies); replyList.addAll(replies);
} }
} }
replyReqCode.value = res['code'];
isLoadingMore = false; isLoadingMore = false;
return res; return res;
} }

View File

@ -117,7 +117,8 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
videoDetailCtr.oid.value = replyItem.oid; videoDetailCtr.oid.value = replyItem.oid;
videoDetailCtr.fRpid = replyItem.rpid!; videoDetailCtr.fRpid = replyItem.rpid!;
videoDetailCtr.firstFloor = replyItem; videoDetailCtr.firstFloor = replyItem;
videoDetailCtr.showReplyReplyPanel(); videoDetailCtr.showReplyReplyPanel(
replyItem.oid, replyItem.rpid!, replyItem);
} }
} }
@ -149,13 +150,16 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
delegate: _MySliverPersistentHeaderDelegate( delegate: _MySliverPersistentHeaderDelegate(
child: Container( child: Container(
height: 40, height: 40,
padding: const EdgeInsets.fromLTRB(12, 6, 6, 0), padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
color: Theme.of(context).colorScheme.surface,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Obx(
'${_videoReplyController.sortTypeLabel.value}评论', () => Text(
style: const TextStyle(fontSize: 13), '${_videoReplyController.sortTypeLabel.value}评论',
style: const TextStyle(fontSize: 13),
),
), ),
SizedBox( SizedBox(
height: 35, height: 35,
@ -273,32 +277,39 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
parent: fabAnimationCtr, parent: fabAnimationCtr,
curve: Curves.easeInOut, curve: Curves.easeInOut,
)), )),
child: FloatingActionButton( child: Obx(
heroTag: null, () => _videoReplyController.replyReqCode.value == 12061
onPressed: () { ? const SizedBox()
feedBack(); : FloatingActionButton(
showModalBottomSheet( heroTag: null,
context: context, onPressed: () {
isScrollControlled: true, feedBack();
builder: (BuildContext context) { showModalBottomSheet(
return VideoReplyNewDialog( context: context,
oid: _videoReplyController.aid ?? isScrollControlled: true,
IdUtils.bv2av(Get.parameters['bvid']!), builder: (BuildContext context) {
root: 0, return VideoReplyNewDialog(
parent: 0, oid: _videoReplyController.aid ??
replyType: ReplyType.video, IdUtils.bv2av(Get.parameters['bvid']!),
); root: 0,
}, parent: 0,
).then( replyType: ReplyType.video,
(value) => { );
// 完成评论,数据添加 },
if (value != null && value['data'] != null) ).then(
{_videoReplyController.replyList.add(value['data'])} (value) => {
}, // 完成评论,数据添加
); if (value != null && value['data'] != null)
}, {
tooltip: '发表评论', _videoReplyController.replyList
child: const Icon(Icons.reply), .add(value['data'])
}
},
);
},
tooltip: '发表评论',
child: const Icon(Icons.reply),
),
), ),
), ),
), ),

View File

@ -12,7 +12,6 @@ import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply_new/index.dart'; import 'package:pilipala/pages/video/detail/reply_new/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/url_utils.dart'; import 'package:pilipala/utils/url_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -59,28 +58,23 @@ class ReplyItem extends StatelessWidget {
}, },
); );
}, },
child: Column( child: Container(
children: [ padding: const EdgeInsets.fromLTRB(12, 14, 8, 5),
Padding( decoration: BoxDecoration(
padding: const EdgeInsets.fromLTRB(12, 14, 8, 5), border: Border(
child: content(context), bottom: BorderSide(
), width: 1,
Divider( color:
indent: 55, Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.5),
endIndent: 15, ))),
height: 0.3, child: content(context),
color: Theme.of(context)
.colorScheme
.onInverseSurface
.withOpacity(0.5),
)
],
), ),
), ),
); );
} }
Widget lfAvtar(BuildContext context, String heroTag) { Widget lfAvtar(BuildContext context, String heroTag) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
return Stack( return Stack(
children: [ children: [
Hero( Hero(
@ -100,11 +94,11 @@ class ReplyItem extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7), borderRadius: BorderRadius.circular(7),
color: Theme.of(context).colorScheme.background, color: colorScheme.background,
), ),
child: Icon( child: Icon(
Icons.offline_bolt, Icons.offline_bolt,
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
size: 16, size: 16,
), ),
), ),
@ -117,7 +111,7 @@ class ReplyItem extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7), borderRadius: BorderRadius.circular(7),
color: Theme.of(context).colorScheme.background, color: colorScheme.background,
), ),
child: Image.asset( child: Image.asset(
'assets/images/big-vip.png', 'assets/images/big-vip.png',
@ -131,6 +125,8 @@ class ReplyItem extends StatelessWidget {
Widget content(BuildContext context) { Widget content(BuildContext context) {
final String heroTag = Utils.makeHeroTag(replyItem!.mid); final String heroTag = Utils.makeHeroTag(replyItem!.mid);
ColorScheme colorScheme = Theme.of(context).colorScheme;
TextTheme textTheme = Theme.of(context).textTheme;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@ -160,16 +156,17 @@ class ReplyItem extends StatelessWidget {
style: TextStyle( style: TextStyle(
color: replyItem!.member!.vip!['vipStatus'] > 0 color: replyItem!.member!.vip!['vipStatus'] > 0
? const Color.fromARGB(255, 251, 100, 163) ? const Color.fromARGB(255, 251, 100, 163)
: Theme.of(context).colorScheme.outline, : colorScheme.outline,
fontSize: 13, fontSize: 13,
), ),
), ),
const SizedBox(width: 6), Padding(
Image.asset( padding: const EdgeInsets.only(left: 6, right: 6),
'assets/images/lv/lv${replyItem!.member!.level}.png', child: Image.asset(
height: 11, 'assets/images/lv/lv${replyItem!.member!.level}.png',
height: 11,
),
), ),
const SizedBox(width: 6),
if (replyItem!.isUp!) if (replyItem!.isUp!)
const PBadge( const PBadge(
text: 'UP', text: 'UP',
@ -184,9 +181,8 @@ class ReplyItem extends StatelessWidget {
Text( Text(
Utils.dateFormat(replyItem!.ctime), Utils.dateFormat(replyItem!.ctime),
style: TextStyle( style: TextStyle(
fontSize: fontSize: textTheme.labelSmall!.fontSize,
Theme.of(context).textTheme.labelSmall!.fontSize, color: colorScheme.outline,
color: Theme.of(context).colorScheme.outline,
), ),
), ),
if (replyItem!.replyControl != null && if (replyItem!.replyControl != null &&
@ -194,11 +190,8 @@ class ReplyItem extends StatelessWidget {
Text( Text(
'${replyItem!.replyControl!.location!}', '${replyItem!.replyControl!.location!}',
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context) fontSize: textTheme.labelSmall!.fontSize,
.textTheme color: colorScheme.outline),
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.outline),
), ),
], ],
) )
@ -256,6 +249,8 @@ class ReplyItem extends StatelessWidget {
// 感谢、回复、复制 // 感谢、回复、复制
Widget bottonAction(BuildContext context, replyControl) { Widget bottonAction(BuildContext context, replyControl) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
TextTheme textTheme = Theme.of(context).textTheme;
return Row( return Row(
children: <Widget>[ children: <Widget>[
const SizedBox(width: 32), const SizedBox(width: 32),
@ -287,15 +282,13 @@ class ReplyItem extends StatelessWidget {
}, },
child: Row(children: [ child: Row(children: [
Icon(Icons.reply, Icon(Icons.reply,
size: 18, size: 18, color: colorScheme.outline.withOpacity(0.8)),
color:
Theme.of(context).colorScheme.outline.withOpacity(0.8)),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
'回复', '回复',
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, fontSize: textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline, color: colorScheme.outline,
), ),
), ),
]), ]),
@ -306,8 +299,8 @@ class ReplyItem extends StatelessWidget {
Text( Text(
'up主觉得很赞', 'up主觉得很赞',
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize), fontSize: textTheme.labelMedium!.fontSize),
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
], ],
@ -316,8 +309,8 @@ class ReplyItem extends StatelessWidget {
Text( Text(
'热评', '热评',
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize), fontSize: textTheme.labelMedium!.fontSize),
), ),
const Spacer(), const Spacer(),
ZanButton(replyItem: replyItem, replyType: replyType), ZanButton(replyItem: replyItem, replyType: replyType),
@ -347,10 +340,13 @@ class ReplyItemRow extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isShow = replyControl!.isShow!; final bool isShow = replyControl!.isShow!;
final int extraRow = replyControl != null && isShow ? 1 : 0; final int extraRow = replyControl != null && isShow ? 1 : 0;
ColorScheme colorScheme = Theme.of(context).colorScheme;
TextTheme textTheme = Theme.of(context).textTheme;
return Container( return Container(
margin: const EdgeInsets.only(left: 42, right: 4, top: 0), margin: const EdgeInsets.only(left: 42, right: 4, top: 0),
child: Material( child: Material(
color: Theme.of(context).colorScheme.onInverseSurface, color: colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
animationDuration: Duration.zero, animationDuration: Duration.zero,
@ -361,7 +357,9 @@ class ReplyItemRow extends StatelessWidget {
for (int i = 0; i < replies!.length; i++) ...[ for (int i = 0; i < replies!.length; i++) ...[
InkWell( InkWell(
// 一楼点击评论展开评论详情 // 一楼点击评论展开评论详情
onTap: () => replyReply!(replyItem), // onTap: () {
// replyReply?.call(replyItem);
// },
onLongPress: () { onLongPress: () {
feedBack(); feedBack();
showModalBottomSheet( showModalBottomSheet(
@ -379,7 +377,7 @@ class ReplyItemRow extends StatelessWidget {
8, 8,
i == 0 && (extraRow == 1 || replies!.length > 1) ? 8 : 5, i == 0 && (extraRow == 1 || replies!.length > 1) ? 8 : 5,
8, 8,
i == 0 && (extraRow == 1 || replies!.length > 1) ? 5 : 6, 6,
), ),
child: Text.rich( child: Text.rich(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -393,7 +391,7 @@ class ReplyItemRow extends StatelessWidget {
.textTheme .textTheme
.titleSmall! .titleSmall!
.fontSize, .fontSize,
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () {
@ -436,8 +434,7 @@ class ReplyItemRow extends StatelessWidget {
child: Text.rich( child: Text.rich(
TextSpan( TextSpan(
style: TextStyle( style: TextStyle(
fontSize: fontSize: textTheme.labelMedium!.fontSize,
Theme.of(context).textTheme.labelMedium!.fontSize,
), ),
children: [ children: [
if (replyControl!.upReply!) if (replyControl!.upReply!)
@ -445,7 +442,7 @@ class ReplyItemRow extends StatelessWidget {
TextSpan( TextSpan(
text: replyControl!.entryText!, text: replyControl!.entryText!,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
), ),
) )
], ],
@ -464,6 +461,7 @@ InlineSpan buildContent(
BuildContext context, replyItem, replyReply, fReplyItem) { BuildContext context, replyItem, replyReply, fReplyItem) {
final String routePath = Get.currentRoute; final String routePath = Get.currentRoute;
bool isVideoPage = routePath.startsWith('/video'); bool isVideoPage = routePath.startsWith('/video');
ColorScheme colorScheme = Theme.of(context).colorScheme;
// replyItem 当前回复内容 // replyItem 当前回复内容
// replyReply 查看二楼回复(回复详情)回调 // replyReply 查看二楼回复(回复详情)回调
@ -525,14 +523,18 @@ InlineSpan buildContent(
if (jumpUrlKeysList.isNotEmpty) { if (jumpUrlKeysList.isNotEmpty) {
patternStr += '|${jumpUrlKeysList.join('|')}'; patternStr += '|${jumpUrlKeysList.join('|')}';
} }
RegExp bv23Regex = RegExp(r'https://b23\.tv/[a-zA-Z0-9]{7}');
final RegExp pattern = RegExp(patternStr); final RegExp pattern = RegExp(patternStr);
List<String> matchedStrs = []; List<String> matchedStrs = [];
void addPlainTextSpan(str) { void addPlainTextSpan(str) {
spanChilds.add(TextSpan( spanChilds.add(
TextSpan(
text: str, text: str,
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => ..onTap = () =>
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem))); replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem),
),
);
} }
// 分割文本并处理每个部分 // 分割文本并处理每个部分
@ -560,7 +562,7 @@ InlineSpan buildContent(
TextSpan( TextSpan(
text: matchStr, text: matchStr,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () {
@ -580,7 +582,7 @@ InlineSpan buildContent(
text: ' $matchStr ', text: ' $matchStr ',
style: isVideoPage style: isVideoPage
? TextStyle( ? TextStyle(
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
) )
: null, : null,
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
@ -620,14 +622,14 @@ InlineSpan buildContent(
child: Image.network( child: Image.network(
content.jumpUrl[matchStr]['prefix_icon'], content.jumpUrl[matchStr]['prefix_icon'],
height: 19, height: 19,
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
), ),
) )
], ],
TextSpan( TextSpan(
text: content.jumpUrl[matchStr]['title'], text: content.jumpUrl[matchStr]['title'],
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () async { ..onTap = () async {
@ -717,7 +719,7 @@ InlineSpan buildContent(
TextSpan( TextSpan(
text: matchStr, text: matchStr,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () {
@ -734,8 +736,36 @@ InlineSpan buildContent(
return ''; return '';
}, },
onNonMatch: (String nonMatchStr) { onNonMatch: (String nonMatchStr) {
addPlainTextSpan(nonMatchStr); return nonMatchStr.splitMapJoin(
return nonMatchStr; bv23Regex,
onMatch: (Match match) {
String matchStr = match[0]!;
spanChilds.add(
TextSpan(
text: ' $matchStr ',
style: isVideoPage
? TextStyle(
color: colorScheme.primary,
)
: null,
recognizer: TapGestureRecognizer()
..onTap = () => Get.toNamed(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': matchStr
},
),
),
);
return '';
},
onNonMatch: (String nonMatchOtherStr) {
addPlainTextSpan(nonMatchOtherStr);
return nonMatchOtherStr;
},
);
}, },
); );
@ -754,14 +784,14 @@ InlineSpan buildContent(
child: Image.network( child: Image.network(
content.jumpUrl[patternStr]['prefix_icon'], content.jumpUrl[patternStr]['prefix_icon'],
height: 19, height: 19,
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
), ),
) )
], ],
TextSpan( TextSpan(
text: content.jumpUrl[patternStr]['title'], text: content.jumpUrl[patternStr]['title'],
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: colorScheme.primary,
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () {
@ -965,7 +995,8 @@ class MorePanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color errorColor = Theme.of(context).colorScheme.error; ColorScheme colorScheme = Theme.of(context).colorScheme;
TextTheme textTheme = Theme.of(context).textTheme;
return Container( return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column( child: Column(
@ -981,7 +1012,7 @@ class MorePanel extends StatelessWidget {
width: 32, width: 32,
height: 3, height: 3,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline, color: colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))), borderRadius: const BorderRadius.all(Radius.circular(3))),
), ),
), ),
@ -991,13 +1022,13 @@ class MorePanel extends StatelessWidget {
onTap: () async => await menuActionHandler('copyAll'), onTap: () async => await menuActionHandler('copyAll'),
minLeadingWidth: 0, minLeadingWidth: 0,
leading: const Icon(Icons.copy_all_outlined, size: 19), leading: const Icon(Icons.copy_all_outlined, size: 19),
title: Text('复制全部', style: Theme.of(context).textTheme.titleSmall), title: Text('复制全部', style: textTheme.titleSmall),
), ),
ListTile( ListTile(
onTap: () async => await menuActionHandler('copyFreedom'), onTap: () async => await menuActionHandler('copyFreedom'),
minLeadingWidth: 0, minLeadingWidth: 0,
leading: const Icon(Icons.copy_outlined, size: 19), leading: const Icon(Icons.copy_outlined, size: 19),
title: Text('自由复制', style: Theme.of(context).textTheme.titleSmall), title: Text('自由复制', style: textTheme.titleSmall),
), ),
// ListTile( // ListTile(
// onTap: () async => await menuActionHandler('block'), // onTap: () async => await menuActionHandler('block'),

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/emote.dart'; import 'package:pilipala/models/video/reply/emote.dart';
@ -40,6 +41,9 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
double keyboardHeight = 0.0; // 键盘高度 double keyboardHeight = 0.0; // 键盘高度
final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间 final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间
String toolbarType = 'input'; String toolbarType = 'input';
RxBool isForward = false.obs;
RxBool showForward = false.obs;
RxString message = ''.obs;
@override @override
void initState() { void initState() {
@ -52,6 +56,10 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
_autoFocus(); _autoFocus();
// 监听聚焦状态 // 监听聚焦状态
_focuslistener(); _focuslistener();
final String routePath = Get.currentRoute;
if (routePath.startsWith('/video')) {
showForward.value = true;
}
} }
_autoFocus() async { _autoFocus() async {
@ -73,21 +81,31 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
Future submitReplyAdd() async { Future submitReplyAdd() async {
feedBack(); feedBack();
String message = _replyContentController.text; // String message = _replyContentController.text;
var result = await VideoHttp.replyAdd( var result = await VideoHttp.replyAdd(
type: widget.replyType ?? ReplyType.video, type: widget.replyType ?? ReplyType.video,
oid: widget.oid!, oid: widget.oid!,
root: widget.root!, root: widget.root!,
parent: widget.parent!, parent: widget.parent!,
message: widget.replyItem != null && widget.replyItem!.root != 0 message: widget.replyItem != null && widget.replyItem!.root != 0
? ' 回复 @${widget.replyItem!.member!.uname!} : $message' ? ' 回复 @${widget.replyItem!.member!.uname!} : ${message.value}'
: message, : message.value,
); );
if (result['status']) { if (result['status']) {
SmartDialog.showToast(result['data']['success_toast']); SmartDialog.showToast(result['data']['success_toast']);
Get.back(result: { Get.back(result: {
'data': ReplyItemModel.fromJson(result['data']['reply'], ''), 'data': ReplyItemModel.fromJson(result['data']['reply'], ''),
}); });
/// 投稿、番剧页面
if (isForward.value) {
await DynamicsHttp.dynamicCreate(
mid: 0,
rawText: message.value,
oid: widget.oid!,
scene: 5,
);
}
} else { } else {
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
@ -142,7 +160,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double keyboardHeight = EdgeInsets.fromViewPadding( double _keyboardHeight = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio) View.of(context).viewInsets, View.of(context).devicePixelRatio)
.bottom; .bottom;
return Container( return Container(
@ -171,7 +189,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
child: TextField( child: TextField(
controller: _replyContentController, controller: _replyContentController,
minLines: 1, minLines: 3,
maxLines: null, maxLines: null,
autofocus: false, autofocus: false,
focusNode: replyContentFocusNode, focusNode: replyContentFocusNode,
@ -182,6 +200,9 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
fontSize: 14, fontSize: 14,
)), )),
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
onChanged: (text) {
message.value = text;
},
), ),
), ),
), ),
@ -224,9 +245,39 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
toolbarType: toolbarType, toolbarType: toolbarType,
selected: toolbarType == 'emote', selected: toolbarType == 'emote',
), ),
const SizedBox(width: 6),
Obx(
() => showForward.value
? TextButton.icon(
onPressed: () {
isForward.value = !isForward.value;
},
icon: Icon(
isForward.value
? Icons.check_box
: Icons.check_box_outline_blank,
size: 22),
label: const Text('转发到动态'),
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(
isForward.value
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
),
)
: const SizedBox(),
),
const Spacer(), const Spacer(),
TextButton( SizedBox(
onPressed: () => submitReplyAdd(), child: const Text('发送')) height: 36,
child: Obx(
() => FilledButton(
onPressed: message.isNotEmpty ? submitReplyAdd : null,
child: const Text('发送'),
),
),
),
], ],
), ),
), ),
@ -235,7 +286,11 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
height: toolbarType == 'input' ? keyboardHeight : emoteHeight, height: toolbarType == 'input'
? (_keyboardHeight > keyboardHeight
? _keyboardHeight
: keyboardHeight)
: emoteHeight,
child: EmotePanel( child: EmotePanel(
onChoose: (package, emote) => onChooseEmote(package, emote), onChoose: (package, emote) => onChooseEmote(package, emote),
), ),

View File

@ -19,6 +19,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
this.firstFloor, this.firstFloor,
this.source, this.source,
this.replyType, this.replyType,
this.sheetHeight,
super.key, super.key,
}); });
final int? oid; final int? oid;
@ -27,6 +28,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
final ReplyItemModel? firstFloor; final ReplyItemModel? firstFloor;
final String? source; final String? source;
final ReplyType? replyType; final ReplyType? replyType;
final double? sheetHeight;
@override @override
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState(); State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
@ -36,7 +38,6 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
late VideoReplyReplyController _videoReplyReplyController; late VideoReplyReplyController _videoReplyReplyController;
late AnimationController replyAnimationCtl; late AnimationController replyAnimationCtl;
final Box<dynamic> localCache = GStrorage.localCache; final Box<dynamic> localCache = GStrorage.localCache;
late double sheetHeight;
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
late ScrollController scrollController; late ScrollController scrollController;
@ -62,7 +63,6 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
}, },
); );
sheetHeight = localCache.get('sheetHeight');
_futureBuilderFuture = _videoReplyReplyController.queryReplyList(); _futureBuilderFuture = _videoReplyReplyController.queryReplyList();
} }
@ -77,33 +77,31 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: widget.source == 'videoDetail' ? sheetHeight : null, height: widget.source == 'videoDetail' ? widget.sheetHeight : null,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: Column( child: Column(
children: [ children: [
if (widget.source == 'videoDetail') if (widget.source == 'videoDetail')
Container( AppBar(
height: 45, toolbarHeight: 45,
padding: const EdgeInsets.only(left: 12, right: 2), automaticallyImplyLeading: false,
child: Row( centerTitle: false,
mainAxisAlignment: MainAxisAlignment.spaceBetween, title: Text(
children: <Widget>[ '评论详情',
const Text('评论详情'), style: Theme.of(context).textTheme.titleSmall,
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () {
_videoReplyReplyController.currentPage = 0;
widget.closePanel?.call;
Navigator.pop(context);
},
),
],
), ),
actions: [
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () {
_videoReplyReplyController.currentPage = 0;
widget.closePanel?.call;
Navigator.pop(context);
},
),
const SizedBox(width: 14),
],
), ),
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
Expanded( Expanded(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
@ -140,8 +138,8 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (BuildContext context, snapshot) { builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
final Map data = snapshot.data as Map; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
// 请求成功 // 请求成功
return Obx( return Obx(
() => SliverList( () => SliverList(
@ -199,7 +197,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
} else { } else {
// 请求错误 // 请求错误
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求错误',
fn: () => setState(() {}), fn: () => setState(() {}),
); );
} }

View File

@ -23,7 +23,9 @@ import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/services/service_locator.dart'; import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:status_bar_control/status_bar_control.dart';
import '../../../plugin/pl_player/models/bottom_control_type.dart';
import '../../../services/shutdown_timer_service.dart'; import '../../../services/shutdown_timer_service.dart';
import 'widgets/app_bar.dart'; import 'widgets/app_bar.dart';
@ -37,7 +39,7 @@ class VideoDetailPage extends StatefulWidget {
} }
class _VideoDetailPageState extends State<VideoDetailPage> class _VideoDetailPageState extends State<VideoDetailPage>
with TickerProviderStateMixin, RouteAware { with TickerProviderStateMixin, RouteAware, WidgetsBindingObserver {
late VideoDetailController vdCtr; late VideoDetailController vdCtr;
PlPlayerController? plPlayerController; PlPlayerController? plPlayerController;
final ScrollController _extendNestCtr = ScrollController(); final ScrollController _extendNestCtr = ScrollController();
@ -46,7 +48,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late BangumiIntroController bangumiIntroController; late BangumiIntroController bangumiIntroController;
late String heroTag; late String heroTag;
PlayerStatus playerStatus = PlayerStatus.playing; Rx<PlayerStatus> playerStatus = PlayerStatus.playing.obs;
double doubleOffset = 0; double doubleOffset = 0;
final Box<dynamic> localCache = GStrorage.localCache; final Box<dynamic> localCache = GStrorage.localCache;
@ -60,12 +62,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late bool autoPiP; late bool autoPiP;
late Floating floating; late Floating floating;
bool isShowing = true; bool isShowing = true;
// 生命周期监听
late final AppLifecycleListener _lifecycleListener;
late double statusHeight;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
getStatusHeight();
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
vdCtr = Get.put(VideoDetailController(), tag: heroTag); vdCtr = Get.put(VideoDetailController(), tag: heroTag);
vdCtr.sheetHeight.value = localCache.get('sheetHeight');
videoIntroController = Get.put( videoIntroController = Get.put(
VideoIntroController(bvid: Get.parameters['bvid']!), VideoIntroController(bvid: Get.parameters['bvid']!),
tag: heroTag); tag: heroTag);
@ -94,6 +101,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
floating = vdCtr.floating!; floating = vdCtr.floating!;
autoEnterPip(); autoEnterPip();
} }
WidgetsBinding.instance.addObserver(this);
lifecycleListener();
} }
// 获取视频资源,初始化播放器 // 获取视频资源,初始化播放器
@ -107,10 +116,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// 流 // 流
appbarStreamListen() { appbarStreamListen() {
appbarStream = StreamController<double>(); appbarStream = StreamController<double>.broadcast();
_extendNestCtr.addListener( _extendNestCtr.addListener(
() { () {
final double offset = _extendNestCtr.position.pixels; final double offset = _extendNestCtr.position.pixels;
vdCtr.sheetHeight.value =
Get.size.height - videoHeight - statusBarHeight + offset;
appbarStream.add(offset); appbarStream.add(offset);
}, },
); );
@ -118,7 +129,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// 播放器状态监听 // 播放器状态监听
void playerListener(PlayerStatus? status) async { void playerListener(PlayerStatus? status) async {
playerStatus = status!; playerStatus.value = status!;
if (status == PlayerStatus.completed) { if (status == PlayerStatus.completed) {
// 结束播放退出全屏 // 结束播放退出全屏
if (autoExitFullcreen) { if (autoExitFullcreen) {
@ -176,10 +187,33 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController?.isFullScreen.listen((bool isFullScreen) { plPlayerController?.isFullScreen.listen((bool isFullScreen) {
if (isFullScreen) { if (isFullScreen) {
vdCtr.hiddenReplyReplyPanel(); vdCtr.hiddenReplyReplyPanel();
if (vdCtr.videoType == SearchType.video) {
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);
}
}
if (vdCtr.videoType == SearchType.media_bangumi) {
bangumiIntroController.hiddenEpisodeBottomSheet();
if (bangumiIntroController.bangumiDetail.value.episodes != null &&
bangumiIntroController.bangumiDetail.value.episodes!.length > 1) {
vdCtr.bottomList.insert(3, BottomControlType.episode);
}
}
} else {
if (vdCtr.bottomList.contains(BottomControlType.episode)) {
vdCtr.bottomList.removeAt(3);
}
} }
}); });
} }
getStatusHeight() async {
statusHeight = await StatusBarControl.getHeight;
}
@override @override
void dispose() { void dispose() {
shutdownTimerService.handleWaitingFinished(); shutdownTimerService.handleWaitingFinished();
@ -196,6 +230,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
floating.dispose(); floating.dispose();
} }
appbarStream.close(); appbarStream.close();
WidgetsBinding.instance.removeObserver(this);
_lifecycleListener.dispose();
super.dispose(); super.dispose();
} }
@ -212,6 +248,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoIntroController.isPaused = true; videoIntroController.isPaused = true;
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause(); plPlayerController!.pause();
vdCtr.clearSubtitleContent();
} }
setState(() => isShowing = false); setState(() => isShowing = false);
super.didPushNext(); super.didPushNext();
@ -222,7 +259,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
void didPopNext() async { void didPopNext() async {
if (plPlayerController != null && if (plPlayerController != null &&
plPlayerController!.videoPlayerController != null) { plPlayerController!.videoPlayerController != null) {
setState(() => isShowing = true); setState(() {
vdCtr.setSubtitleContent();
isShowing = true;
});
} }
vdCtr.isFirstTime = false; vdCtr.isFirstTime = false;
final bool autoplay = autoPlayEnable; final bool autoplay = autoPlayEnable;
@ -254,9 +294,168 @@ class _VideoDetailPageState extends State<VideoDetailPage>
} }
} }
// 生命周期监听
void lifecycleListener() {
_lifecycleListener = AppLifecycleListener(
// onResume: () => _handleTransition('resume'),
// 后台
// onInactive: () => _handleTransition('inactive'),
// 在Android和iOS端不生效
// onHide: () => _handleTransition('hide'),
onShow: () => _handleTransition('show'),
onPause: () => _handleTransition('pause'),
onRestart: () => _handleTransition('restart'),
onDetach: () => _handleTransition('detach'),
);
}
void _handleTransition(String name) {
switch (name) {
case 'show' || 'restart':
plPlayerController?.danmakuController?.clear();
break;
}
}
/// 手动播放
Widget handlePlayPanel() {
return Stack(
children: [
GestureDetector(
onTap: handlePlay,
child: Image.network(
vdCtr.videoItem['pic'],
width: Get.width,
height: videoHeight,
fit: BoxFit.cover, // 适应方式根据需要调整
),
),
buildCustomAppBar(),
Positioned(
right: 12,
bottom: 10,
child: GestureDetector(
onTap: handlePlay,
child: Image.asset(
'assets/images/play.png',
width: 60,
height: 60,
),
),
),
],
);
}
/// tabbar
Widget tabbarBuild() {
return Container(
width: double.infinity,
height: 45,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
),
),
child: Material(
child: Row(
children: [
Expanded(
child: Obx(
() => TabBar(
padding: EdgeInsets.zero,
controller: vdCtr.tabCtr,
labelStyle: const TextStyle(fontSize: 13),
labelPadding: const EdgeInsets.symmetric(horizontal: 10.0),
dividerColor: Colors.transparent,
tabs:
vdCtr.tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
Flexible(
flex: 1,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Obx(() => AnimatedOpacity(
opacity: playerStatus.value != PlayerStatus.playing
? 1
: 0,
duration: const Duration(milliseconds: 100),
child: const Icon(
Icons.drag_handle_rounded,
size: 20,
color: Colors.grey,
),
)),
const SizedBox(width: 8),
SizedBox(
height: 32,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => vdCtr.showShootDanmakuSheet(),
child:
const Text('发弹幕', style: TextStyle(fontSize: 12)),
),
),
SizedBox(
width: 38,
height: 38,
child: Obx(
() => !vdCtr.isShowCover.value
? IconButton(
onPressed: () {
plPlayerController?.isOpenDanmu.value =
!(plPlayerController?.isOpenDanmu.value ??
false);
},
icon: !(plPlayerController?.isOpenDanmu.value ??
false)
? SvgPicture.asset(
'assets/images/video/danmu_close.svg',
// ignore: deprecated_member_use
color: Theme.of(context)
.colorScheme
.outline,
)
: SvgPicture.asset(
'assets/images/video/danmu_open.svg',
// ignore: deprecated_member_use
color: Theme.of(context)
.colorScheme
.primary,
),
)
: IconButton(
icon: SvgPicture.asset(
'assets/images/video/danmu_close.svg',
// ignore: deprecated_member_use
color: Theme.of(context).colorScheme.outline,
),
onPressed: () {},
),
),
),
const SizedBox(width: 18),
],
),
),
),
],
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16;
final sizeContext = MediaQuery.sizeOf(context); final sizeContext = MediaQuery.sizeOf(context);
final _context = MediaQuery.of(context); final _context = MediaQuery.of(context);
late double defaultVideoHeight = sizeContext.width * 9 / 16; late double defaultVideoHeight = sizeContext.width * 9 / 16;
@ -288,15 +487,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
() { () {
return !vdCtr.autoPlay.value return !vdCtr.autoPlay.value
? const SizedBox() ? const SizedBox()
: PLVideoPlayer( : Obx(
controller: plPlayerController!, () => PLVideoPlayer(
headerControl: vdCtr.headerControl, controller: plPlayerController!,
danmuWidget: Obx( headerControl: vdCtr.headerControl,
() => PlDanmaku( danmuWidget: PlDanmaku(
key: Key(vdCtr.danmakuCid.value.toString()), key: Key(vdCtr.danmakuCid.value.toString()),
cid: vdCtr.danmakuCid.value, cid: vdCtr.danmakuCid.value,
playerController: plPlayerController!, playerController: plPlayerController!,
), ),
bottomList: vdCtr.bottomList,
showEposideCb: () => vdCtr.videoType == SearchType.video
? videoIntroController.showEposideHandler()
: bangumiIntroController.showEposideHandler(),
), ),
); );
}, },
@ -308,133 +511,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}, },
); );
/// tabbar
Widget tabbarBuild = Container(
width: double.infinity,
height: 45,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
),
),
child: Material(
child: Row(
children: [
Flexible(
flex: 1,
child: Obx(
() => TabBar(
padding: EdgeInsets.zero,
controller: vdCtr.tabCtr,
labelStyle: const TextStyle(fontSize: 13),
labelPadding:
const EdgeInsets.symmetric(horizontal: 10.0), // 设置每个标签的宽度
dividerColor: Colors.transparent,
tabs: vdCtr.tabs
.map(
(String name) => Tab(text: name),
)
.toList(),
),
),
),
Flexible(
flex: 1,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: 32,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => vdCtr.showShootDanmakuSheet(),
child:
const Text('发弹幕', style: TextStyle(fontSize: 12)),
),
),
SizedBox(
width: 38,
height: 38,
child: Obx(
() => IconButton(
onPressed: () {
plPlayerController?.isOpenDanmu.value =
!(plPlayerController?.isOpenDanmu.value ??
false);
},
icon: !(plPlayerController?.isOpenDanmu.value ??
false)
? SvgPicture.asset(
'assets/images/video/danmu_close.svg',
)
: SvgPicture.asset(
'assets/images/video/danmu_open.svg',
// ignore: deprecated_member_use
color:
Theme.of(context).colorScheme.primary,
),
),
),
),
const SizedBox(width: 14),
],
),
)),
],
),
),
);
/// 手动播放
Widget handlePlayPanel() {
return Stack(
children: [
GestureDetector(
onTap: () {
handlePlay();
},
child: NetworkImgLayer(
type: 'emote',
src: vdCtr.videoItem['pic'],
width: Get.width,
height: videoHeight.value,
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: buildCustomAppBar(),
),
Positioned(
right: 12,
bottom: 10,
child: IconButton(
tooltip: '播放',
onPressed: () => handlePlay(),
icon: Image.asset(
'assets/images/play.png',
width: 60,
height: 60,
)),
),
],
);
}
Widget childWhenDisabled = SafeArea( Widget childWhenDisabled = SafeArea(
top: MediaQuery.of(context).orientation == Orientation.portrait && top: MediaQuery.of(context).orientation == Orientation.portrait &&
plPlayerController?.isFullScreen.value == true, plPlayerController?.isFullScreen.value == true,
bottom: MediaQuery.of(context).orientation == Orientation.portrait && bottom: MediaQuery.of(context).orientation == Orientation.portrait &&
plPlayerController?.isFullScreen.value == true, plPlayerController?.isFullScreen.value == true,
left: false, //plPlayerController?.isFullScreen.value != true, left: false,
right: false, //plPlayerController?.isFullScreen.value != true, right: false,
child: Stack( child: Stack(
children: [ children: [
Scaffold( Scaffold(
@ -455,9 +538,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return <Widget>[ return <Widget>[
Obx( Obx(
() { () {
if (MediaQuery.of(context).orientation == final Orientation orientation =
Orientation.landscape || MediaQuery.of(context).orientation;
plPlayerController?.isFullScreen.value == true) { final bool isFullScreen =
plPlayerController?.isFullScreen.value == true;
final double expandedHeight =
orientation == Orientation.landscape || isFullScreen
? (MediaQuery.sizeOf(context).height -
(orientation == Orientation.landscape
? 0
: MediaQuery.of(context).padding.top))
: videoHeight.value;
if (orientation == Orientation.landscape ||
isFullScreen) {
enterFullScreen(); enterFullScreen();
} else { } else {
exitFullScreen(); exitFullScreen();
@ -469,15 +562,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
forceElevated: innerBoxIsScrolled, forceElevated: innerBoxIsScrolled,
expandedHeight: MediaQuery.of(context).orientation == expandedHeight: expandedHeight,
Orientation.landscape ||
plPlayerController?.isFullScreen.value == true
? (MediaQuery.sizeOf(context).height -
(MediaQuery.of(context).orientation ==
Orientation.landscape
? 0
: MediaQuery.of(context).padding.top))
: videoHeight.value,
backgroundColor: Colors.black, backgroundColor: Colors.black,
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: PopScope( background: PopScope(
@ -497,13 +582,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
child: LayoutBuilder( child: LayoutBuilder(
builder: (BuildContext context, builder: (BuildContext context,
BoxConstraints boxConstraints) { BoxConstraints boxConstraints) {
// final double maxWidth =
// boxConstraints.maxWidth;
// final double maxHeight =
// boxConstraints.maxHeight;
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
if (isShowing) videoPlayerPanel, if (isShowing)
Padding(
padding: EdgeInsets.only(top: 0),
child: videoPlayerPanel,
),
/// 关闭自动播放时 手动播放 /// 关闭自动播放时 手动播放
Obx( Obx(
@ -535,7 +620,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
Orientation.landscape || Orientation.landscape ||
plPlayerController?.isFullScreen.value == true plPlayerController?.isFullScreen.value == true
? MediaQuery.sizeOf(context).height ? MediaQuery.sizeOf(context).height
: playerStatus != PlayerStatus.playing : playerStatus.value != PlayerStatus.playing
? kToolbarHeight ? kToolbarHeight
: pinnedHeaderHeight; : pinnedHeaderHeight;
}, },
@ -545,7 +630,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: Column( child: Column(
children: [ children: [
tabbarBuild, tabbarBuild(),
Expanded( Expanded(
child: TabBarView( child: TabBarView(
controller: vdCtr.tabCtr, controller: vdCtr.tabCtr,
@ -596,13 +681,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 重新进入会刷新 /// 重新进入会刷新
// 播放完成/暂停播放 // 播放完成/暂停播放
StreamBuilder( StreamBuilder(
stream: appbarStream.stream, stream: appbarStream.stream.distinct(),
initialData: 0, initialData: 0,
builder: ((context, snapshot) { builder: ((context, snapshot) {
return ScrollAppBar( return ScrollAppBar(
snapshot.data!.toDouble(), snapshot.data!.toDouble(),
() => continuePlay(), () => continuePlay(),
playerStatus, playerStatus.value,
null, null,
); );
}), }),

Some files were not shown because too many files have changed in this diff Show More