Merge branch 'main' into feature-media_kit

This commit is contained in:
guozhigq
2024-05-12 00:54:24 +08:00
137 changed files with 4899 additions and 3301 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

View File

@ -45,7 +45,7 @@
android:fullBackupContent="false" android:fullBackupContent="false"
tools:replace="android:allowBackup"> tools:replace="android:allowBackup">
<activity <activity
android:name="com.ryanheise.audioservice.AudioServiceActivity" android:name="com.guozhigq.pilipala.MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"

View File

@ -1,6 +1,8 @@
package com.guozhigq.pilipala package com.guozhigq.pilipala
import io.flutter.embedding.android.FlutterActivity // import io.flutter.embedding.android.FlutterActivity
import com.ryanheise.audioservice.AudioServiceActivity;
class MainActivity: AudioServiceActivity() {
class MainActivity: FlutterActivity() {
} }

27
change_log/1.0.22.0430.md Normal file
View File

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

14
change_log/1.0.23.0504.md Normal file
View File

@ -0,0 +1,14 @@
## 1.0.23
### 功能
+ 封面下载
### 修复
+ 全屏问题
+ 视频播放器灰屏问题
+ 评论区点击区域问题
更多更新日志可在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

@ -34,9 +34,11 @@ class NetworkImgLayer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final int defaultImgQuality = GlobalData().imgQuality; final int defaultImgQuality = GlobalData().imgQuality;
if (src == '' || src == null) {
return placeholder(context);
}
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,11 @@
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/constants.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/url_utils.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 +21,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 +49,117 @@ 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;
}
if (showCharge && videoItem?.typeid == 33) {
final String redirectUrl = await UrlUtils.parseRedirectUrl(
'${HttpString.baseUrl}/video/$bvid/');
final String lastPathSegment = redirectUrl.split('/').last;
if (lastPathSegment.contains('ss')) {
RoutePush.bangumiPush(
Utils.matchNum(lastPathSegment).first, null);
}
if (lastPathSegment.contains('ep')) {
RoutePush.bangumiPush(
null, Utils.matchNum(lastPathSegment).first);
}
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 +174,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 +184,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 +195,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 +210,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(
@ -254,117 +277,44 @@ class VideoContent extends StatelessWidget {
theme: 'gray', theme: 'gray',
danmu: videoItem.stat.danmaku as int, danmu: videoItem.stat.danmaku as int,
), ),
const Spacer(), const Spacer(),
// SizedBox(
// width: 20,
// height: 20,
// child: IconButton(
// tooltip: '稍后再看',
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// onPressed: () async {
// var res =
// await UserHttp.toViewLater(bvid: videoItem.bvid);
// SmartDialog.showToast(res['msg']);
// },
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// size: 14,
// ),
// ),
// ),
if (source == 'normal') if (source == 'normal')
SizedBox( SizedBox(
width: 24, width: 24,
height: 24, height: 24,
child: PopupMenuButton<String>( child: IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(videoItem: videoItem);
},
);
},
icon: Icon( icon: Icon(
Icons.more_vert_outlined, Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
size: 14, size: 14,
), ),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () async {
var res = 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(),
PopupMenuItem<String>(
onTap: () 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['code'] == 0
? '成功'
: 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))
],
),
),
],
), ),
), ),
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,
),
)
],
], ],
), ),
], ],
@ -373,3 +323,110 @@ class VideoContent extends StatelessWidget {
); );
} }
} }
class MorePanel extends StatelessWidget {
final dynamic videoItem;
const MorePanel({super.key, required this.videoItem});
Future<dynamic> menuActionHandler(String type) async {
switch (type) {
case 'block':
blockUser();
break;
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
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
),
),
),
),
ListTile(
onTap: () async => await menuActionHandler('block'),
minLeadingWidth: 0,
leading: const Icon(Icons.block, size: 19),
title: Text(
'拉黑up主 「${videoItem.owner.name}',
style: Theme.of(context).textTheme.titleSmall,
),
),
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),
),
ListTile(
onTap: () =>
imageSaveDialog(context, videoItem, SmartDialog.dismiss),
minLeadingWidth: 0,
leading: const Icon(Icons.photo_outlined, size: 19),
title:
Text('查看视频封面', style: Theme.of(context).textTheme.titleSmall),
),
const SizedBox(height: 20),
],
),
);
}
}

View File

@ -1,14 +1,15 @@
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 'package:pilipala/utils/route_push.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';
import '../../http/dynamics.dart'; import '../../http/dynamics.dart';
import '../../http/search.dart';
import '../../http/user.dart'; import '../../http/user.dart';
import '../../http/video.dart'; import '../../http/video.dart';
import '../../models/common/search_type.dart';
import '../../utils/id_utils.dart'; import '../../utils/id_utils.dart';
import '../../utils/utils.dart'; import '../../utils/utils.dart';
import '../constants.dart'; import '../constants.dart';
@ -19,15 +20,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) {
@ -44,23 +41,11 @@ class VideoCardV extends StatelessWidget {
return; return;
} }
int epId = videoItem.param; int epId = videoItem.param;
SmartDialog.showLoading(msg: '资源获取中'); RoutePush.bangumiPush(
var result = await SearchHttp.bangumiInfo(seasonId: null, epId: epId); null,
if (result['status']) { epId,
var bangumiDetail = result['data']; heroTag: heroTag,
int cid = bangumiDetail.episodes!.first.cid; );
String bvid = IdUtils.av2bv(bangumiDetail.episodes!.first.aid);
SmartDialog.dismiss().then(
(value) => Get.toNamed(
'/video?bvid=$bvid&cid=$cid&epId=$epId',
arguments: {
'pic': videoItem.pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
},
),
);
}
break; break;
case 'av': case 'av':
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid); String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);
@ -127,64 +112,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 +170,91 @@ 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(
padding: EdgeInsets.zero,
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 +274,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 +295,107 @@ 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),
),
ListTile(
onTap: () =>
imageSaveDialog(context, videoItem, SmartDialog.dismiss),
minLeadingWidth: 0,
leading: const Icon(Icons.photo_outlined, size: 19),
title:
Text('查看视频封面', style: Theme.of(context).textTheme.titleSmall),
),
const SizedBox(height: 20),
], ],
), ),
); );

View File

@ -490,8 +490,11 @@ class Api {
/// 我的订阅 /// 我的订阅
static const userSubFolder = '/x/v3/fav/folder/collected/list'; static const userSubFolder = '/x/v3/fav/folder/collected/list';
/// 我的订阅详情 /// 我的订阅详情 type 21
static const userSubFolderDetail = '/x/space/fav/season/list'; static const userSeasonList = '/x/space/fav/season/list';
/// 我的订阅详情 type 11
static const userResourceList = '/x/v3/fav/resource/list';
/// 表情 /// 表情
static const emojiList = '/x/emote/user/panel/web'; static const emojiList = '/x/emote/user/panel/web';
@ -506,4 +509,18 @@ 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';
/// 搜索结果计数
static const String searchCount = '/x/web-interface/wbi/search/all/v2';
} }

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';
@ -40,6 +41,7 @@ class DynamicsHttp {
'status': false, 'status': false,
'data': [], 'data': [],
'msg': res.data['message'], 'msg': res.data['message'],
'code': res.data['code'],
}; };
} }
} }
@ -117,4 +119,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

@ -46,7 +46,7 @@ class ApiInterceptor extends Interceptor {
// 处理网络请求错误 // 处理网络请求错误
// handler.next(err); // handler.next(err);
String url = err.requestOptions.uri.toString(); String url = err.requestOptions.uri.toString();
if (!url.contains('heartBeat')) { if (!url.contains('heartbeat')) {
SmartDialog.showToast( SmartDialog.showToast(
await dioError(err), await dioError(err),
displayType: SmartToastType.onlyRefresh, displayType: SmartToastType.onlyRefresh,

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

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/search/all.dart';
import 'package:pilipala/utils/wbi_sign.dart';
import '../models/bangumi/info.dart'; import '../models/bangumi/info.dart';
import '../models/common/search_type.dart'; import '../models/common/search_type.dart';
import '../models/search/hot.dart'; import '../models/search/hot.dart';
@ -163,4 +165,42 @@ 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'],
};
}
static Future<Map<String, dynamic>> searchCount(
{required String keyword}) async {
Map<String, dynamic> data = {
'keyword': keyword,
'web_location': 333.999,
};
Map params = await WbiSign().makSign(data);
final dynamic res = await Request().get(Api.searchCount, data: params);
if (res.data['code'] == 0) {
return {
'status': true,
'data': SearchAllModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
}
} }

View File

@ -62,7 +62,8 @@ class UserHttp {
return { return {
'status': false, 'status': false,
'data': [], 'data': [],
'msg': res.data['message'] ?? '账号未登录' 'msg': res.data['message'],
'code': res.data['code'],
}; };
} }
} }
@ -111,7 +112,12 @@ class UserHttp {
'data': {'list': list, 'count': res.data['data']['count']} 'data': {'list': list, 'count': res.data['data']['count']}
}; };
} else { } else {
return {'status': false, 'data': [], 'msg': res.data['message']}; return {
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
} }
} }
@ -126,7 +132,12 @@ class UserHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': HistoryData.fromJson(res.data['data'])}; return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
} else { } else {
return {'status': false, 'data': [], 'msg': res.data['message']}; return {
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
} }
} }
@ -326,16 +337,21 @@ class UserHttp {
'data': SubFolderModelData.fromJson(res.data['data']) 'data': SubFolderModelData.fromJson(res.data['data'])
}; };
} else { } else {
return {'status': false, 'msg': res.data['message']}; return {
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
} }
} }
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 +365,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

@ -35,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,
@ -192,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'],
}; };
} }
} }

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -20,6 +21,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 +36,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 +67,20 @@ void main() async {
); );
// 小白条、导航栏沉浸 // 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); if (Platform.isAndroid) {
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( final androidInfo = await DeviceInfoPlugin().androidInfo;
systemNavigationBarColor: Colors.transparent, if (androidInfo.version.sdkInt >= 29) {
systemNavigationBarDividerColor: Colors.transparent, SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
statusBarColor: Colors.transparent, }
)); 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

@ -5,6 +5,10 @@ enum SubtitleType {
aizh, aizh,
// 英语(自动生成) // 英语(自动生成)
aien, aien,
// 中文(简体)
zhHans,
// 英文(美国)
enUS,
} }
extension SubtitleTypeExtension on SubtitleType { extension SubtitleTypeExtension on SubtitleType {
@ -16,6 +20,10 @@ extension SubtitleTypeExtension on SubtitleType {
return '中文(自动翻译)'; return '中文(自动翻译)';
case SubtitleType.aien: case SubtitleType.aien:
return '英语(自动生成)'; return '英语(自动生成)';
case SubtitleType.zhHans:
return '中文(简体)';
case SubtitleType.enUS:
return '英文(美国)';
} }
} }
} }
@ -29,6 +37,10 @@ extension SubtitleIdExtension on SubtitleType {
return 'ai-zh'; return 'ai-zh';
case SubtitleType.aien: case SubtitleType.aien:
return 'ai-en'; return 'ai-en';
case SubtitleType.zhHans:
return 'zh-Hans';
case SubtitleType.enUS:
return 'en-US';
} }
} }
} }
@ -42,6 +54,10 @@ extension SubtitleCodeExtension on SubtitleType {
return 2; return 2;
case SubtitleType.aien: case SubtitleType.aien:
return 3; 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

@ -0,0 +1,9 @@
class SearchAllModel {
SearchAllModel({this.topTList});
Map? topTList;
SearchAllModel.fromJson(Map<String, dynamic> json) {
topTList = json['top_tlist'];
}
}

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'];

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

@ -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

@ -218,7 +218,7 @@ class AboutController extends GetxController {
RxString currentVersion = ''.obs; RxString currentVersion = ''.obs;
RxString remoteVersion = ''.obs; RxString remoteVersion = ''.obs;
late LatestDataModel remoteAppInfo; late LatestDataModel remoteAppInfo;
RxBool isUpdate = true.obs; RxBool isUpdate = false.obs;
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
late LatestDataModel data; late LatestDataModel data;

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

@ -1,11 +1,10 @@
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: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/http/search.dart'; import 'package:pilipala/models/bangumi/list.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/utils/route_push.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 +13,67 @@ 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: () {
clipBehavior: Clip.hardEdge, RoutePush.bangumiPush(
margin: EdgeInsets.zero, bangumiItem.seasonId,
child: GestureDetector( null,
// onLongPress: () { heroTag: heroTag,
// if (longPress != null) { );
// longPress!(); },
// } onLongPress: () =>
// }, imageSaveDialog(context, bangumiItem, SmartDialog.dismiss),
// onLongPressEnd: (details) { child: Column(
// if (longPressEnd != null) { children: [
// longPressEnd!(); ClipRRect(
// } borderRadius: const BorderRadius.all(
// }, StyleString.imgRadius,
child: InkWell( ),
onTap: () async { child: AspectRatio(
final int seasonId = bangumiItem.seasonId; aspectRatio: 0.65,
SmartDialog.showLoading(msg: '获取中...'); child: LayoutBuilder(builder: (context, boxConstraints) {
final res = await SearchHttp.bangumiInfo(seasonId: seasonId); final double maxWidth = boxConstraints.maxWidth;
SmartDialog.dismiss().then((value) { final double maxHeight = boxConstraints.maxHeight;
if (res['status']) { return Stack(
if (res['data'].episodes.isEmpty) { children: [
SmartDialog.showToast('资源加载失败'); Hero(
return; tag: heroTag,
} child: NetworkImgLayer(
EpisodeItem episode = res['data'].episodes.first; src: bangumiItem.cover,
String bvid = episode.bvid!; width: maxWidth,
int cid = episode.cid!; height: maxHeight,
String pic = episode.cover!; ),
String heroTag = Utils.makeHeroTag(cid); ),
Get.toNamed( if (bangumiItem.badge != null)
'/video?bvid=$bvid&cid=$cid&seasonId=$seasonId', PBadge(
arguments: { text: bangumiItem.badge,
'pic': pic, top: 6,
'heroTag': heroTag, right: 6,
'videoType': SearchType.media_bangumi, bottom: null,
'bangumiItem': res['data'], 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

@ -14,6 +14,7 @@ import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.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 'package:pilipala/utils/route_push.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';
@ -70,7 +71,7 @@ class DynamicsController extends GetxController {
Future queryFollowDynamic({type = 'init'}) async { Future queryFollowDynamic({type = 'init'}) async {
if (!userLogin.value) { if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'}; return {'status': false, 'msg': '账号未登录', 'code': -101};
} }
if (type == 'init') { if (type == 'init') {
dynamicsList.clear(); dynamicsList.clear();
@ -220,25 +221,7 @@ class DynamicsController extends GetxController {
print('DYNAMIC_TYPE_PGC_UNION 番剧'); print('DYNAMIC_TYPE_PGC_UNION 番剧');
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc; DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
if (pgc.epid != null) { if (pgc.epid != null) {
SmartDialog.showLoading(msg: '获取中...'); RoutePush.bangumiPush(null, pgc.epid);
var res = await SearchHttp.bangumiInfo(epId: pgc.epid);
SmartDialog.dismiss();
if (res['status']) {
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=${res['data'].seasonId}',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
} }
break; break;
} }
@ -246,7 +229,7 @@ class DynamicsController extends GetxController {
Future queryFollowUp({type = 'init'}) async { Future queryFollowUp({type = 'init'}) async {
if (!userLogin.value) { if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'}; return {'status': false, 'msg': '账号未登录', 'code': -101};
} }
if (type == 'init') { if (type == 'init') {
upData.value.upList = <UpItem>[]; upData.value.upList = <UpItem>[];

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();
} }
@ -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,15 @@ 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/route_push.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../mine/controller.dart'; import '../mine/controller.dart';
@ -44,8 +44,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 +53,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);
}
}, },
); );
@ -234,8 +225,8 @@ class _DynamicsPageState extends State<DynamicsPage>
if (snapshot.data == null) { if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox()); return const SliverToBoxAdapter(child: SizedBox());
} }
Map data = snapshot.data; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
List<DynamicItemModel> list = List<DynamicItemModel> list =
_dynamicsController.dynamicsList; _dynamicsController.dynamicsList;
return Obx( return Obx(
@ -258,24 +249,21 @@ class _DynamicsPageState extends State<DynamicsPage>
} }
}, },
); );
} else if (data['msg'] == "账号未登录") {
return HttpError(
errMsg: data['msg'],
btnText: "去登录",
fn: () {
mineController.onLogin();
},
);
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () { fn: () {
setState(() { if (data?['code'] == -101) {
_futureBuilderFuture = RoutePush.loginRedirectPush();
_dynamicsController.queryFollowDynamic(); } else {
_futureBuilderFutureUp = setState(() {
_dynamicsController.queryFollowUp(); _futureBuilderFuture =
}); _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp =
_dynamicsController.queryFollowUp();
});
}
}, },
); );
} }

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

@ -9,7 +9,7 @@ import 'forward_panel.dart';
class DynamicPanel extends StatelessWidget { class DynamicPanel extends StatelessWidget {
final dynamic item; final dynamic 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 +41,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

@ -80,14 +80,11 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
double width = box.maxWidth; double width = box.maxWidth;
return Stack( return Stack(
children: [ children: [
Hero( NetworkImgLayer(
tag: content.bvid, type: floor == 1 ? 'emote' : null,
child: NetworkImgLayer( width: width,
type: floor == 1 ? 'emote' : null, height: width / StyleString.aspectRatio,
width: width, src: content.cover,
height: width / StyleString.aspectRatio,
src: content.cover,
),
), ),
if (content.badge != null && type == 'pgc') if (content.badge != null && type == 'pgc')
PBadge( PBadge(

View File

@ -10,16 +10,22 @@ 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 { @override
void onInit() {
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
super.onInit();
}
Future<dynamic> queryFavFolder({type = 'init'}) async {
if (userInfo == null) { if (userInfo == null) {
return {'status': false, 'msg': '账号未登录'}; return {'status': false, 'msg': '账号未登录', 'code': -101};
} }
if (!hasMore.value) { if (!hasMore.value) {
return; return;
@ -32,9 +38,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 +56,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

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/fav/index.dart'; import 'package:pilipala/pages/fav/index.dart';
import 'package:pilipala/pages/fav/widgets/item.dart'; import 'package:pilipala/pages/fav/widgets/item.dart';
import 'package:pilipala/utils/route_push.dart';
class FavPage extends StatefulWidget { class FavPage extends StatefulWidget {
const FavPage({super.key}); const FavPage({super.key});
@ -57,16 +58,15 @@ class _FavPageState extends State<FavPage> {
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
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]);
}, },
), ),
); );
@ -75,8 +75,18 @@ class _FavPageState extends State<FavPage> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
slivers: [ slivers: [
HttpError( HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() {}), btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_favController.queryFavFolder();
});
}
},
), ),
], ],
); );

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

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/history.dart'; import 'package:pilipala/models/user/history.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController { class HistoryController extends GetxController {
@ -15,14 +16,20 @@ class HistoryController extends GetxController {
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
RxBool enableMultiple = false.obs; RxBool enableMultiple = false.obs;
RxInt checkedCount = 0.obs; RxInt checkedCount = 0.obs;
Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
historyStatus(); historyStatus();
userInfo = userInfoCache.get('userInfoCache');
} }
Future queryHistoryList({type = 'init'}) async { Future queryHistoryList({type = 'init'}) async {
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
int max = 0; int max = 0;
int viewAt = 0; int viewAt = 0;
if (type == 'onload') { if (type == 'onload') {

View File

@ -5,6 +5,7 @@ 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/no_data.dart'; import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/pages/history/index.dart'; import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/utils/route_push.dart';
import 'widgets/item.dart'; import 'widgets/item.dart';
@ -183,8 +184,8 @@ class _HistoryPageState extends State<HistoryPage> {
if (snapshot.data == null) { if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox()); return const SliverToBoxAdapter(child: SizedBox());
} }
Map data = snapshot.data; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
return Obx( return Obx(
() => _historyController.historyList.isNotEmpty () => _historyController.historyList.isNotEmpty
? SliverList( ? SliverList(
@ -209,8 +210,18 @@ class _HistoryPageState extends State<HistoryPage> {
); );
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() {}), btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_historyController.queryHistoryList();
});
}
},
); );
} }
} else { } else {

View File

@ -7,13 +7,13 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.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/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart'; import 'package:pilipala/models/common/business_type.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/history_search/index.dart'; import 'package:pilipala/pages/history_search/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 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class HistoryItem extends StatelessWidget { class HistoryItem extends StatelessWidget {
@ -101,26 +101,11 @@ class HistoryItem extends StatelessWidget {
} }
} else { } else {
if (videoItem.history.epid != '') { if (videoItem.history.epid != '') {
SmartDialog.showLoading(msg: '获取中...'); RoutePush.bangumiPush(
var res = null,
await SearchHttp.bangumiInfo(epId: videoItem.history.epid); videoItem.history.epid,
SmartDialog.dismiss(); heroTag: heroTag,
if (res['status']) { );
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=${res['data'].seasonId}',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
} }
} }
} else { } else {
@ -258,24 +243,27 @@ class HistoryItem extends StatelessWidget {
), ),
), ),
), ),
Positioned( videoItem.progress != 0
left: 3, ? Positioned(
right: 3, left: 3,
bottom: 0, right: 3,
child: ClipRRect( bottom: 0,
borderRadius: BorderRadius.only( child: ClipRRect(
bottomLeft: borderRadius: BorderRadius.only(
Radius.circular(StyleString.imgRadius.x), bottomLeft: Radius.circular(
bottomRight: StyleString.imgRadius.x),
Radius.circular(StyleString.imgRadius.x), bottomRight: Radius.circular(
), StyleString.imgRadius.x),
child: LinearProgressIndicator( ),
value: videoItem.progress == -1 child: LinearProgressIndicator(
? 100 value: videoItem.progress == -1
: videoItem.progress / videoItem.duration, ? 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

@ -1,16 +1,30 @@
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:hive/hive.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/model_hot_video_item.dart'; import 'package:pilipala/models/model_hot_video_item.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart';
class LaterController extends GetxController { class LaterController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
RxList<HotVideoItemModel> laterList = <HotVideoItemModel>[].obs; RxList<HotVideoItemModel> laterList = <HotVideoItemModel>[].obs;
int count = 0; int count = 0;
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo;
@override
void onInit() {
super.onInit();
userInfo = userInfoCache.get('userInfoCache');
}
Future queryLaterList() async { Future queryLaterList() async {
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
isLoading.value = true; isLoading.value = true;
var res = await UserHttp.seeYouLater(); var res = await UserHttp.seeYouLater();
if (res['status']) { if (res['status']) {

View File

@ -5,6 +5,7 @@ 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/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/pages/later/index.dart'; import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/utils/route_push.dart';
class LaterPage extends StatefulWidget { class LaterPage extends StatefulWidget {
const LaterPage({super.key}); const LaterPage({super.key});
@ -72,8 +73,8 @@ class _LaterPageState extends State<LaterPage> {
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
return Obx( return Obx(
() => _laterController.laterList.isNotEmpty && () => _laterController.laterList.isNotEmpty &&
!_laterController.isLoading.value !_laterController.isLoading.value
@ -84,7 +85,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),
) )
@ -96,10 +97,18 @@ class _LaterPageState extends State<LaterPage> {
); );
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() { btnText: data?['code'] == -101 ? '去登录' : null,
_futureBuilderFuture = _laterController.queryLaterList(); fn: () {
}), if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_laterController.queryLaterList();
});
}
},
); );
} }
} else { } else {

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,16 +33,14 @@ 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(
SettingBoxKey.dynamicBadgeMode, SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)]; defaultValue: DynamicBadgeMode.number.code)];
setNavBarConfig();
if (dynamicBadgeType.value != DynamicBadgeMode.hidden) { if (dynamicBadgeType.value != DynamicBadgeMode.hidden) {
getUnreadDynamic(); getUnreadDynamic();
} }
@ -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( ? Obx(
onDestinationSelected: (value) => setIndex(value), () => NavigationBar(
selectedIndex: _mainController.selectedIndex, onDestinationSelected: (value) => setIndex(value),
destinations: <Widget>[ selectedIndex: _mainController.selectedIndex,
..._mainController.navigationBars.map((e) { destinations: <Widget>[
return NavigationDestination( ..._mainController.navigationBars.map((e) {
icon: Obx( return NavigationDestination(
() => Badge( icon: 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( : Obx(
currentIndex: _mainController.selectedIndex, () => BottomNavigationBar(
onTap: (value) => setIndex(value), currentIndex: _mainController.selectedIndex,
iconSize: 16, type: BottomNavigationBarType.fixed,
selectedFontSize: 12, onTap: (value) => setIndex(value),
unselectedFontSize: 12, iconSize: 16,
items: [ selectedFontSize: 12,
..._mainController.navigationBars.map((e) { unselectedFontSize: 12,
return BottomNavigationBarItem( items: [
icon: Obx( ..._mainController.navigationBars.map((e) {
() => Badge( return BottomNavigationBarItem(
label: icon: 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

@ -6,6 +6,7 @@ import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/theme_type.dart'; import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart'; import 'package:pilipala/models/user/stat.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class MineController extends GetxController { class MineController extends GetxController {
@ -33,14 +34,7 @@ class MineController extends GetxController {
onLogin() async { onLogin() async {
if (!userLogin.value) { if (!userLogin.value) {
Get.toNamed( RoutePush.loginPush();
'/webview',
parameters: {
'url': 'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
// Get.toNamed('/loginPage'); // Get.toNamed('/loginPage');
} else { } else {
int mid = userInfo.value.mid!; int mid = userInfo.value.mid!;

View File

@ -4,6 +4,7 @@ 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 {
@ -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(
@ -51,4 +54,10 @@ class RankController extends GetxController with GetTickerProviderStateMixin {
vsync: this, vsync: this,
); );
} }
@override
void onClose() {
searchBarStream.close();
super.onClose();
}
} }

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);
@ -38,10 +34,6 @@ class _ZonePageState extends State<ZonePage>
_zoneController = Get.put(ZoneController(), tag: widget.rid.toString()); _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 >=
@ -51,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);
}
}, },
); );
} }
@ -97,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),
), ),
@ -141,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

@ -7,6 +7,7 @@ import 'package:pilipala/common/widgets/network_img_layer.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/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
Widget searchMbangumiPanel(BuildContext context, ctr, list) { Widget searchMbangumiPanel(BuildContext context, ctr, list) {
@ -63,7 +64,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(
@ -108,28 +109,8 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
SizedBox( SizedBox(
height: 32, height: 32,
child: ElevatedButton( child: ElevatedButton(
onPressed: () async { onPressed: () {
SmartDialog.showLoading(msg: '获取中...'); RoutePush.bangumiPush(i.seasonId, null);
var res = await SearchHttp.bangumiInfo(
seasonId: i.seasonId);
SmartDialog.dismiss().then((value) {
if (res['status']) {
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=${i.seasonId}',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
});
}, },
child: const Text('观看'), child: const Text('观看'),
), ),

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

@ -1,8 +1,11 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/search_type.dart';
class SearchResultController extends GetxController { class SearchResultController extends GetxController {
String? keyword; String? keyword;
int tabIndex = 0; int tabIndex = 0;
RxList searchTabs = [].obs;
@override @override
void onInit() { void onInit() {
@ -10,5 +13,21 @@ class SearchResultController extends GetxController {
if (Get.parameters.keys.isNotEmpty) { if (Get.parameters.keys.isNotEmpty) {
keyword = Get.parameters['keyword']; keyword = Get.parameters['keyword'];
} }
searchTabs.value = SearchType.values
.map((type) => {'label': type.label, 'id': type.type})
.toList();
querySearchCount();
}
Future querySearchCount() async {
var result = await SearchHttp.searchCount(keyword: keyword!);
if (result['status']) {
for (var i in searchTabs) {
final count = result['data'].topTList[i['id']];
i['count'] = count > 99 ? '99+' : count.toString();
}
searchTabs.refresh();
}
return result;
} }
} }

View File

@ -13,7 +13,7 @@ class SearchResultPage extends StatefulWidget {
class _SearchResultPageState extends State<SearchResultPage> class _SearchResultPageState extends State<SearchResultPage>
with TickerProviderStateMixin { with TickerProviderStateMixin {
late SearchResultController? _searchResultController; late SearchResultController _searchResultController;
late TabController? _tabController; late TabController? _tabController;
@override @override
@ -25,7 +25,7 @@ class _SearchResultPageState extends State<SearchResultPage>
_tabController = TabController( _tabController = TabController(
vsync: this, vsync: this,
length: SearchType.values.length, length: SearchType.values.length,
initialIndex: _searchResultController!.tabIndex, initialIndex: _searchResultController.tabIndex,
); );
} }
@ -46,7 +46,7 @@ class _SearchResultPageState extends State<SearchResultPage>
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: Text( child: Text(
'${_searchResultController!.keyword}', '${_searchResultController.keyword}',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@ -64,35 +64,39 @@ class _SearchResultPageState extends State<SearchResultPage>
splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明 splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明 highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
), ),
child: TabBar( child: Obx(
controller: _tabController, () => (TabBar(
tabs: [ controller: _tabController,
for (var i in SearchType.values) Tab(text: i.label), tabs: [
], for (var i in _searchResultController.searchTabs)
isScrollable: true, Tab(text: "${i['label']} ${i['count'] ?? ''}")
indicatorWeight: 0, ],
indicatorPadding: isScrollable: true,
const EdgeInsets.symmetric(horizontal: 3, vertical: 8), indicatorWeight: 0,
indicator: BoxDecoration( indicatorPadding:
color: Theme.of(context).colorScheme.secondaryContainer, const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
borderRadius: const BorderRadius.all(Radius.circular(20)), indicator: BoxDecoration(
), color: Theme.of(context).colorScheme.secondaryContainer,
indicatorSize: TabBarIndicatorSize.tab, borderRadius: const BorderRadius.all(Radius.circular(20)),
labelColor: Theme.of(context).colorScheme.onSecondaryContainer, ),
labelStyle: const TextStyle(fontSize: 13), indicatorSize: TabBarIndicatorSize.tab,
dividerColor: Colors.transparent, labelColor:
unselectedLabelColor: Theme.of(context).colorScheme.outline, Theme.of(context).colorScheme.onSecondaryContainer,
tabAlignment: TabAlignment.start, labelStyle: const TextStyle(fontSize: 13),
onTap: (index) { dividerColor: Colors.transparent,
if (index == _searchResultController!.tabIndex) { unselectedLabelColor: Theme.of(context).colorScheme.outline,
Get.find<SearchPanelController>( tabAlignment: TabAlignment.start,
tag: SearchType.values[index].type + onTap: (index) {
_searchResultController!.keyword!) if (index == _searchResultController.tabIndex) {
.animateToTop(); Get.find<SearchPanelController>(
} tag: SearchType.values[index].type +
_searchResultController.keyword!)
.animateToTop();
}
_searchResultController!.tabIndex = index; _searchResultController.tabIndex = index;
}, },
)),
), ),
), ),
), ),
@ -102,7 +106,7 @@ class _SearchResultPageState extends State<SearchResultPage>
children: [ children: [
for (var i in SearchType.values) ...{ for (var i in SearchType.values) ...{
SearchPanel( SearchPanel(
keyword: _searchResultController!.keyword, keyword: _searchResultController.keyword,
searchType: i, searchType: i,
tag: DateTime.now().millisecondsSinceEpoch.toString(), tag: DateTime.now().millisecondsSinceEpoch.toString(),
) )

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

@ -17,10 +17,15 @@ class SubController extends GetxController {
int pageSize = 20; int pageSize = 20;
RxBool hasMore = true.obs; RxBool hasMore = true.obs;
Future<dynamic> querySubFolder({type = 'init'}) async { @override
void onInit() {
super.onInit();
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
}
Future<dynamic> querySubFolder({type = 'init'}) async {
if (userInfo == null) { if (userInfo == null) {
return {'status': false, 'msg': '账号未登录'}; return {'status': false, 'msg': '账号未登录', 'code': -101};
} }
var res = await UserHttp.userSubFolder( var res = await UserHttp.userSubFolder(
pn: currentPage, pn: currentPage,
@ -46,4 +51,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

@ -2,6 +2,7 @@ import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/utils/route_push.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/item.dart'; import 'widgets/item.dart';
@ -58,7 +59,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);
}, },
), ),
); );
@ -67,8 +69,18 @@ class _SubPageState extends State<SubPage> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
slivers: [ slivers: [
HttpError( HttpError(
errMsg: data?['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() {}), btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_subController.querySubFolder();
});
}
},
), ),
], ],
); );

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

@ -22,6 +22,7 @@ import 'package:screen_brightness/screen_brightness.dart';
import '../../../models/video/subTitile/content.dart'; import '../../../models/video/subTitile/content.dart';
import '../../../http/danmaku.dart'; import '../../../http/danmaku.dart';
import '../../../plugin/pl_player/models/bottom_control_type.dart';
import '../../../utils/id_utils.dart'; import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart'; import 'widgets/header_control.dart';
@ -50,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;
// 视频资源是否有效 // 视频资源是否有效
@ -58,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;
@ -72,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;
@ -91,35 +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 = RxList<SubTitileContentModel> subtitleContents =
<SubTitileContentModel>[].obs; <SubTitileContentModel>[].obs;
late bool enableRelatedVideo; late bool enableRelatedVideo;
List subtitles = []; List subtitles = [];
RxList<BottomControlType> bottomList = [
BottomControlType.playOrPause,
BottomControlType.time,
BottomControlType.space,
BottomControlType.fit,
BottomControlType.fullscreen,
].obs;
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 ||
@ -146,17 +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(); 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,
@ -164,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) {
@ -179,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();
} }
@ -262,7 +277,8 @@ class VideoDetailController extends GetxController
// 视频链接 // 视频链接
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('试看')) {
@ -280,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则当前可用的最高质量
@ -311,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;
} }
} }
@ -325,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;
} }
@ -353,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);
@ -515,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,24 @@ 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; videoDetailCtr.getSubtitle();
releatedCtr.queryRelatedVideo();
// 重新请求评论 // 重新请求评论
try { try {
/// 未渲染回复组件时可能异常 /// 未渲染回复组件时可能异常
@ -503,6 +497,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 +514,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 +531,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 +558,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,4 +1,5 @@
import 'package:expandable/expandable.dart'; 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';
@ -20,8 +21,9 @@ 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;
@ -134,7 +136,6 @@ 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;
@ -163,7 +164,6 @@ 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); _expandableCtr = ExpandableController(initialExpanded: false);
} }
@ -266,6 +266,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
GestureDetector( GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(), onTap: () => showIntroDetail(),
onLongPress: () async {
feedBack();
await Clipboard.setData(
ClipboardData(text: widget.videoDetail!.title!));
SmartDialog.showToast('标题已复制');
},
child: ExpandablePanel( child: ExpandablePanel(
controller: _expandableCtr, controller: _expandableCtr,
collapsed: Text( collapsed: Text(
@ -372,8 +378,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
), ),
/// 点赞收藏转发 /// 点赞收藏转发
actionGrid(context, videoIntroController), Material(child: actionGrid(context, videoIntroController)),
// 合集 // 合集 videoPart 简洁
if (widget.videoDetail!.ugcSeason != null) ...[ if (widget.videoDetail!.ugcSeason != null) ...[
Obx( Obx(
() => SeasonPanel( () => SeasonPanel(
@ -381,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])
],
],
),
),
],
),
]
], ],
)), )),
); );
@ -533,4 +595,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
); );
}); });
} }
// Widget StaffPanel(BuildContext context, videoIntroController) {
// return
// }
} }

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.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:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class IntroDetail extends StatelessWidget { class IntroDetail extends StatelessWidget {
@ -16,26 +17,47 @@ class IntroDetail extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: SelectableRegion( child: Column(
focusNode: FocusNode(), crossAxisAlignment: CrossAxisAlignment.start,
selectionControls: MaterialTextSelectionControls(), children: <Widget>[
child: Column( const SizedBox(height: 4),
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: <Widget>[ children: [
const SizedBox(height: 4), GestureDetector(
GestureDetector( onTap: () {
onTap: () { feedBack();
Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); Clipboard.setData(ClipboardData(text: videoDetail!.bvid!));
SmartDialog.showToast('已复制'); SmartDialog.showToast('已复制');
}, },
child: Text( child: Text(
videoDetail!.bvid!, videoDetail!.bvid!,
style: TextStyle( style: TextStyle(
fontSize: 13, color: Theme.of(context).colorScheme.primary), fontSize: 13,
color: Theme.of(context).colorScheme.primary),
),
), ),
), const SizedBox(width: 10),
const SizedBox(height: 4), GestureDetector(
Text.rich( 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), style: const TextStyle(height: 1.4),
TextSpan( TextSpan(
children: [ children: [
@ -43,8 +65,8 @@ class IntroDetail extends StatelessWidget {
], ],
), ),
), ),
], ),
), ],
), ),
); );
} }

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