Merge branch 'main' into feature-sendMsg

This commit is contained in:
guozhigq
2024-06-10 16:32:47 +08:00
162 changed files with 4860 additions and 3152 deletions

View File

@ -94,7 +94,7 @@ QQ频道: https://pd.qq.com/s/365esodk3
- [x] 音质选择(视视频而定)
- [x] 解码格式选择(视视频而定)
- [x] 弹幕
- [ ] 字幕
- [x] 字幕
- [x] 记忆播放
- [x] 视频比例:高度/宽度适应、填充、包含等

View File

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

View File

@ -1,6 +1,8 @@
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() {
}

View File

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.9.0'
repositories {
google()
mavenCentral()

BIN
assets/images/coin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

1
assets/loading.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>

View File

@ -10,7 +10,6 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- ReachabilitySwift
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
@ -24,7 +23,7 @@ PODS:
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- gt3_flutter_plugin (0.0.8):
- gt3_flutter_plugin (0.0.9):
- Flutter
- GT3Captcha-iOS
- GT3Captcha-iOS (0.15.8.3)
@ -41,7 +40,6 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- ReachabilitySwift (5.0.0)
- saver_gallery (0.0.1):
- Flutter
- screen_brightness_ios (0.1.0):
@ -101,7 +99,6 @@ SPEC REPOS:
trunk:
- FMDB
- GT3Captcha-iOS
- ReachabilitySwift
- Toast
EXTERNAL SOURCES:
@ -167,22 +164,21 @@ SPEC CHECKSUMS:
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
connectivity_plus: e2dad488011aeb593e219360e804c43cc1af5770
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
@ -190,11 +186,11 @@ SPEC CHECKSUMS:
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
Toast: ec33c32b8688982cecc6348adeae667c1b9938da
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
webview_flutter_wkwebview: 4f3e50f7273d31e5500066ed267e3ae4309c5ae4
webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36
PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be

View File

@ -156,7 +156,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

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

View File

@ -1,5 +1,6 @@
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';
@ -44,27 +45,52 @@ class EpisodeBottomSheet {
title = '${episode.title}${episode.longTitle!}';
break;
}
return ListTile(
onTap: () {
SmartDialog.showToast('切换至「$title');
changeFucCall.call(episode, index);
},
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.gif',
color: primary,
height: 12,
)
: null,
title: Text(
title,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex ? primary : onSurface,
),
),
);
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() {
@ -98,7 +124,7 @@ class EpisodeBottomSheet {
});
return Container(
height: sheetHeight,
color: Theme.of(context).colorScheme.background,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
buildTitle(),

View File

@ -13,8 +13,8 @@ class Skeleton extends StatelessWidget {
var shimmerGradient = LinearGradient(
colors: [
Colors.transparent,
Theme.of(context).colorScheme.background.withAlpha(10),
Theme.of(context).colorScheme.background.withAlpha(10),
Theme.of(context).colorScheme.surface.withAlpha(10),
Theme.of(context).colorScheme.surface.withAlpha(10),
Colors.transparent,
],
stops: const [

View File

@ -34,7 +34,7 @@ class HttpError extends StatelessWidget {
fn!();
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((states) {
backgroundColor: WidgetStateProperty.resolveWith((states) {
return Theme.of(context).colorScheme.primary.withAlpha(20);
}),
),

View File

@ -34,9 +34,11 @@ class NetworkImgLayer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final int defaultImgQuality = GlobalData().imgQuality;
if (src == '' || src == null) {
return placeholder(context);
}
final String imageUrl =
'${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp';
print(imageUrl);
int? memCacheWidth, memCacheHeight;
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

@ -14,7 +14,7 @@ class StatDanMu extends StatelessWidget {
Map<String, Color> colorObject = {
'white': Colors.white,
'gray': Theme.of(context).colorScheme.outline,
'black': Theme.of(context).colorScheme.onBackground.withOpacity(0.8),
'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
};
Color color = colorObject[theme]!;
return Row(

View File

@ -14,7 +14,7 @@ class StatView extends StatelessWidget {
Map<String, Color> colorObject = {
'white': Colors.white,
'gray': Theme.of(context).colorScheme.outline,
'black': Theme.of(context).colorScheme.onBackground.withOpacity(0.8),
'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
};
Color color = colorObject[theme]!;
return Row(

View File

@ -1,6 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.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/user.dart';
import '../../http/video.dart';
@ -16,23 +21,24 @@ class VideoCardH extends StatelessWidget {
const VideoCardH({
super.key,
required this.videoItem,
this.longPress,
this.longPressEnd,
this.onPressedFn,
this.source = 'normal',
this.showOwner = true,
this.showView = true,
this.showDanmaku = true,
this.showPubdate = false,
this.showCharge = false,
});
// ignore: prefer_typing_uninitialized_variables
final videoItem;
final Function()? longPress;
final Function()? longPressEnd;
final Function()? onPressedFn;
// normal 推荐, later 稍后再看, search 搜索
final String source;
final bool showOwner;
final bool showView;
final bool showDanmaku;
final bool showPubdate;
final bool showCharge;
@override
Widget build(BuildContext context) {
@ -43,102 +49,117 @@ class VideoCardH extends StatelessWidget {
type = videoItem.type;
} catch (_) {}
final String heroTag = Utils.makeHeroTag(aid);
return GestureDetector(
onLongPress: () {
if (longPress != null) {
longPress!();
return InkWell(
onTap: () async {
try {
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) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
try {
if (type == 'ketang') {
SmartDialog.showToast('课堂视频暂不支持播放');
return;
}
final int cid =
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
} catch (err) {
SmartDialog.showToast(err.toString());
}
},
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
final double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.textScalerOf(context).scale(1.0)) /
2;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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,
),
onLongPress: () => imageSaveDialog(
context,
videoItem,
SmartDialog.dismiss,
),
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
final double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.textScalerOf(context).scale(1.0)) /
2;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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(
text: Utils.timeFormat(videoItem.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
if (type != 'video')
PBadge(
text: type,
left: 6.0,
bottom: 6.0,
type: 'primary',
),
// if (videoItem.rcmdReason != null &&
// videoItem.rcmdReason.content != '')
// pBadge(videoItem.rcmdReason.content, context,
// 6.0, 6.0, null, null),
],
);
},
),
),
if (videoItem.duration != 0)
PBadge(
text: Utils.timeFormat(videoItem.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
if (type != 'video')
PBadge(
text: type,
left: 6.0,
bottom: 6.0,
type: 'primary',
),
// if (videoItem.rcmdReason != null &&
// videoItem.rcmdReason.content != '')
// pBadge(videoItem.rcmdReason.content, context,
// 6.0, 6.0, null, null),
if (showCharge && videoItem?.isChargingSrc)
const PBadge(
text: '充电专属',
right: 6.0,
top: 6.0,
type: 'primary',
),
],
);
},
),
VideoContent(
videoItem: videoItem,
source: source,
showOwner: showOwner,
showView: showView,
showDanmaku: showDanmaku,
showPubdate: showPubdate,
)
],
),
);
},
),
),
VideoContent(
videoItem: videoItem,
source: source,
showOwner: showOwner,
showView: showView,
showDanmaku: showDanmaku,
showPubdate: showPubdate,
onPressedFn: onPressedFn,
)
],
),
);
},
),
),
);
@ -153,6 +174,7 @@ class VideoContent extends StatelessWidget {
final bool showView;
final bool showDanmaku;
final bool showPubdate;
final Function()? onPressedFn;
const VideoContent({
super.key,
@ -162,6 +184,7 @@ class VideoContent extends StatelessWidget {
this.showView = true,
this.showDanmaku = true,
this.showPubdate = false,
this.onPressedFn,
});
@override
@ -172,7 +195,7 @@ class VideoContent extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (videoItem.title is String) ...[
if (source == 'normal' || source == 'later') ...[
Text(
videoItem.title as String,
textAlign: TextAlign.start,
@ -187,7 +210,7 @@ class VideoContent extends StatelessWidget {
maxLines: 2,
text: TextSpan(
children: [
for (final i in videoItem.title) ...[
for (final i in videoItem.titleList) ...[
TextSpan(
text: i['text'] as String,
style: TextStyle(
@ -254,117 +277,44 @@ class VideoContent extends StatelessWidget {
theme: 'gray',
danmu: videoItem.stat.danmaku as int,
),
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')
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
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,
),
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: WidgetStateProperty.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_smart_dialog/flutter_smart_dialog.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 'stat/danmu.dart';
import 'stat/view.dart';
import '../../http/dynamics.dart';
import '../../http/search.dart';
import '../../http/user.dart';
import '../../http/video.dart';
import '../../models/common/search_type.dart';
import '../../utils/id_utils.dart';
import '../../utils/utils.dart';
import '../constants.dart';
@ -19,15 +20,13 @@ import 'network_img_layer.dart';
class VideoCardV extends StatelessWidget {
final dynamic videoItem;
final int crossAxisCount;
final Function()? longPress;
final Function()? longPressEnd;
final Function? blockUserCb;
const VideoCardV({
Key? key,
required this.videoItem,
required this.crossAxisCount,
this.longPress,
this.longPressEnd,
this.blockUserCb,
}) : super(key: key);
bool isStringNumeric(String str) {
@ -44,23 +43,11 @@ class VideoCardV extends StatelessWidget {
return;
}
int epId = videoItem.param;
SmartDialog.showLoading(msg: '资源获取中');
var result = await SearchHttp.bangumiInfo(seasonId: null, epId: epId);
if (result['status']) {
var bangumiDetail = result['data'];
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,
},
),
);
}
RoutePush.bangumiPush(
null,
epId,
heroTag: heroTag,
);
break;
case 'av':
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);
@ -127,64 +114,57 @@ class VideoCardV extends StatelessWidget {
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: GestureDetector(
onLongPress: () {
if (longPress != null) {
longPress!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async => onPushDetail(heroTag),
child: Column(
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
),
if (videoItem.duration > 0)
if (crossAxisCount == 1) ...[
PBadge(
bottom: 10,
right: 10,
text: Utils.timeFormat(videoItem.duration),
)
] else ...[
PBadge(
bottom: 6,
right: 7,
size: 'small',
type: 'gray',
text: Utils.timeFormat(videoItem.duration),
)
],
return InkWell(
onTap: () async => onPushDetail(heroTag),
onLongPress: () => imageSaveDialog(
context,
videoItem,
SmartDialog.dismiss,
),
borderRadius: BorderRadius.circular(16),
child: Column(
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
),
if (videoItem.duration > 0)
if (crossAxisCount == 1) ...[
PBadge(
bottom: 10,
right: 10,
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,
blockUserCb: blockUserCb,
)
],
),
);
}
@ -193,125 +173,102 @@ class VideoCardV extends StatelessWidget {
class VideoContent extends StatelessWidget {
final dynamic videoItem;
final int crossAxisCount;
const VideoContent(
{Key? key, required this.videoItem, required this.crossAxisCount})
: super(key: key);
final Function? blockUserCb;
const VideoContent({
Key? key,
required this.videoItem,
required this.crossAxisCount,
this.blockUserCb,
}) : 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
Widget build(BuildContext context) {
return Expanded(
flex: crossAxisCount == 1 ? 0 : 1,
child: Padding(
padding: crossAxisCount == 1
? const EdgeInsets.fromLTRB(9, 9, 9, 4)
: const EdgeInsets.fromLTRB(5, 8, 5, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Expanded(
child: Text(
videoItem.title,
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)
]
],
),
return Padding(
padding: crossAxisCount == 1
? const EdgeInsets.fromLTRB(9, 9, 9, 4)
: const EdgeInsets.fromLTRB(5, 8, 5, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
videoItem.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
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')
_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,
blockUserCb: blockUserCb,
);
},
);
},
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
),
)
],
),
],
),
);
}
@ -331,15 +288,9 @@ class VideoStat extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
children: [
StatView(
theme: 'gray',
view: videoItem.stat.view,
),
StatView(theme: 'gray', view: videoItem.stat.view),
const SizedBox(width: 8),
StatDanMu(
theme: 'gray',
danmu: videoItem.stat.danmu,
),
StatDanMu(theme: 'gray', danmu: videoItem.stat.danmu),
if (videoItem is RecVideoItemModel) ...<Widget>[
crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8),
RichText(
@ -358,99 +309,116 @@ class VideoStat extends StatelessWidget {
}
}
class VideoPopupMenu extends StatelessWidget {
final double? size;
final double? iconSize;
class MorePanel extends StatelessWidget {
final dynamic videoItem;
const VideoPopupMenu({
Key? key,
required this.size,
required this.iconSize,
final Function? blockUserCb;
const MorePanel({
super.key,
required this.videoItem,
}) : super(key: key);
this.blockUserCb,
});
Future<dynamic> menuActionHandler(String type) async {
switch (type) {
case 'block':
Get.back();
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();
if (res['status']) {
blockUserCb?.call(videoItem.owner.mid);
}
SmartDialog.showToast(res['msg']);
},
child: const Text('确认'),
)
],
);
},
);
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: iconSize,
),
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))
],
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))),
),
),
),
),
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['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('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

@ -189,7 +189,7 @@ class Api {
'https://s.search.bilibili.com/main/suggest';
// 分类搜索
static const String searchByType = '/x/web-interface/search/type';
static const String searchByType = '/x/web-interface/wbi/search/type';
// 记录视频播放进度
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md
@ -400,12 +400,24 @@ class Api {
'${HttpString.passBaseUrl}/x/passport-login/captcha?source=main_web';
// web端短信验证码
static const String smsCode =
static const String webSmsCode =
'${HttpString.passBaseUrl}/x/passport-login/web/sms/send';
// web端验证码登录
static const String webSmsLogin =
'${HttpString.passBaseUrl}/x/passport-login/web/login/sms';
// web端密码登录
static const String loginInByWebPwd =
'${HttpString.passBaseUrl}/x/passport-login/web/login';
// web端二维码
static const String qrCodeApi =
'${HttpString.passBaseUrl}/x/passport-login/web/qrcode/generate';
// 扫码登录
static const String loginInByQrcode =
'${HttpString.passBaseUrl}/x/passport-login/web/qrcode/poll';
// app端短信验证码
static const String appSmsCode =
@ -517,4 +529,10 @@ class Api {
/// 创建动态
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

@ -41,6 +41,7 @@ class DynamicsHttp {
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
}
}
@ -152,16 +153,28 @@ class DynamicsHttp {
}
static Future dynamicCreate({
required String dynIdStr,
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.toString() + timestamp.toString() + randomNumber.toString();
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(),
@ -174,14 +187,14 @@ class DynamicsHttp {
{'raw_text': rawText ?? '', 'type': 1, 'biz_id': ''}
]
},
'scene': 4,
'scene': scene,
'attach_card': null,
'upload_id': uploadId,
'meta': {
'app_meta': {'from': 'create.dynamic.web', 'mobi_app': 'web'}
}
},
'web_repost_src': {'dyn_id_str': dynIdStr}
'web_repost_src': webRepostSrc
});
if (res.data['code'] == 0) {
return {

View File

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

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:encrypt/encrypt.dart';
import 'package:pilipala/http/constants.dart';
import 'package:uuid/uuid.dart';
import '../models/login/index.dart';
import '../utils/login.dart';
@ -21,32 +22,32 @@ class LoginHttp {
}
}
static Future sendSmsCode({
int? cid,
required int tel,
required String token,
required String challenge,
required String validate,
required String seccode,
}) async {
var res = await Request().post(
Api.appSmsCode,
data: {
'cid': cid,
'tel': tel,
"source": "main_web",
'token': token,
'challenge': challenge,
'validate': validate,
'seccode': seccode,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
// headers: {'user-agent': ApiConstants.userAgent}
),
);
print(res);
}
// static Future sendSmsCode({
// int? cid,
// required int tel,
// required String token,
// required String challenge,
// required String validate,
// required String seccode,
// }) async {
// var res = await Request().post(
// Api.appSmsCode,
// data: {
// 'cid': cid,
// 'tel': tel,
// "source": "main_web",
// 'token': token,
// 'challenge': challenge,
// 'validate': validate,
// 'seccode': seccode,
// },
// options: Options(
// contentType: Headers.formUrlEncodedContentType,
// // headers: {'user-agent': ApiConstants.userAgent}
// ),
// );
// print(res);
// }
// web端验证码
static Future sendWebSmsCode({
@ -60,6 +61,7 @@ class LoginHttp {
Map data = {
'cid': cid,
'tel': tel,
"source": "main_web",
'token': token,
'challenge': challenge,
'validate': validate,
@ -67,17 +69,56 @@ class LoginHttp {
};
FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.smsCode,
Api.webSmsCode,
data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
print(res);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// web端验证码登录
static Future loginInByWebSmsCode() async {}
static Future loginInByWebSmsCode({
int? cid,
required int tel,
required int code,
required String captchaKey,
}) async {
// webSmsLogin
Map data = {
"cid": cid,
"tel": tel,
"code": code,
"source": "main_mini",
"keep": 0,
"captcha_key": captchaKey,
"go_url": HttpString.baseUrl
};
FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.webSmsLogin,
data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// web端密码登录
static Future liginInByWebPwd() async {}
@ -173,4 +214,69 @@ class LoginHttp {
);
print(res);
}
// web端密码登录
static Future loginInByWebPwd({
required int username,
required String password,
required String token,
required String challenge,
required String validate,
required String seccode,
}) async {
Map data = {
'username': username,
'password': password,
'keep': 0,
'token': token,
'challenge': challenge,
'validate': validate,
'seccode': seccode,
'source': 'main-fe-header',
"go_url": HttpString.baseUrl
};
FormData formData = FormData.fromMap({...data});
var res = await Request().post(
Api.loginInByWebPwd,
data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// web端登录二维码
static Future getWebQrcode() async {
var res = await Request().get(Api.qrCodeApi);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// web端二维码轮询登录状态
static Future queryWebQrcodeStatus(String qrcodeKey) async {
var res = await Request()
.get(Api.loginInByQrcode, data: {'qrcode_key': qrcodeKey});
if (res.data['data']['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
}

View File

@ -22,19 +22,14 @@ class ReplyHttp {
return {
'status': true,
'data': ReplyData.fromJson(res.data['data']),
'code': 200,
};
} else {
Map errMap = {
-400: '请求错误',
-404: '无此项',
12002: '当前页面评论功能已关闭',
12009: '评论主体的type不合法',
12061: 'UP主已关闭评论区',
};
return {
'status': false,
'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 '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/common/search_type.dart';
import '../models/search/hot.dart';
@ -73,6 +75,7 @@ class SearchHttp {
required page,
String? order,
int? duration,
int? tids,
}) async {
var reqData = {
'search_type': searchType.type,
@ -82,6 +85,7 @@ class SearchHttp {
'page': page,
if (order != null) 'order': order,
if (duration != null) 'duration': duration,
if (tids != null && tids != -1) 'tids': tids,
};
var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
@ -163,4 +167,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 {
'status': false,
'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']}
};
} 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) {
return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
return {
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
}
}
@ -326,7 +337,12 @@ class UserHttp {
'data': SubFolderModelData.fromJson(res.data['data'])
};
} else {
return {'status': false, 'msg': res.data['message']};
return {
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
}
}
@ -395,4 +411,21 @@ class UserHttp {
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,
data: {
'version': 1,
'feed_version': 'V8',
'feed_version': 'V3',
'homepage_ver': 1,
'ps': ps,
'fresh_idx': freshIdx,
@ -192,22 +192,15 @@ class VideoHttp {
// 视频信息 标题、简介
static Future videoIntro({required String bvid}) async {
var res = await Request().get(Api.videoIntro, data: {'bvid': bvid});
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
if (result.code == 0) {
if (res.data['code'] == 0) {
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
return {'status': true, 'data': result.data!};
} else {
Map errMap = {
-400: '请求错误',
-403: '权限不足',
-404: '视频资源失效',
62002: '稿件不可见',
62004: '稿件审核中',
};
return {
'status': false,
'data': null,
'code': result.code,
'msg': errMap[result.code] ?? '请求异常',
'code': res.data['code'],
'msg': res.data['message'],
};
}
}
@ -394,9 +387,15 @@ class VideoHttp {
'csrf': await Request.getCsrf(),
});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
if (act == 5) {
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
blackMidsList.add(mid);
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
}
return {'status': true, 'data': res.data['data'], 'msg': '成功'};
} else {
return {'status': false, 'data': []};
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
@ -16,13 +17,12 @@ import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart';
import 'package:pilipala/pages/main/view.dart';
import 'package:pilipala/services/disable_battery_opt.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/global_data.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
import 'package:media_kit/media_kit.dart';
import 'package:pilipala/utils/recommend_filter.dart';
import 'package:catcher_2/catcher_2.dart';
import './services/loggeer.dart';
@ -30,46 +30,41 @@ import './services/loggeer.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
.then((_) async {
await GStrorage.init();
await setupServiceLocator();
clearLogs();
Request();
await Request.setCookie();
RecommendFilter();
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
await GStrorage.init();
await setupServiceLocator();
clearLogs();
Request();
await Request.setCookie();
// 异常捕获 logo记录
final Catcher2Options debugConfig = Catcher2Options(
SilentReportMode(),
[
FileHandler(await getLogsPath()),
ConsoleHandler(
enableDeviceParameters: false,
enableApplicationParameters: false,
)
],
);
// 异常捕获 logo记录
final Catcher2Options releaseConfig = Catcher2Options(
SilentReportMode(),
[FileHandler(await getLogsPath())],
);
final Catcher2Options releaseConfig = Catcher2Options(
SilentReportMode(),
[FileHandler(await getLogsPath())],
);
Catcher2(
releaseConfig: releaseConfig,
runAppFunction: () {
runApp(const MyApp());
},
);
Catcher2(
debugConfig: debugConfig,
releaseConfig: releaseConfig,
runAppFunction: () {
runApp(const MyApp());
},
);
// 小白条、导航栏沉浸
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 29) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
statusBarColor: Colors.transparent,
));
}
Data.init();
GlobalData();
PiliSchame.init();
DisableBatteryOpt();
});
PiliSchame.init();
}
class MyApp extends StatelessWidget {
@ -110,6 +105,39 @@ class MyApp extends StatelessWidget {
} catch (_) {}
}
if (Platform.isAndroid) {
return AndroidApp(
brandColor: brandColor,
isDynamicColor: isDynamicColor,
currentThemeValue: currentThemeValue,
textScale: textScale,
);
} else {
return OtherApp(
brandColor: brandColor,
currentThemeValue: currentThemeValue,
textScale: textScale,
);
}
}
}
class AndroidApp extends StatelessWidget {
const AndroidApp({
super.key,
required this.brandColor,
required this.isDynamicColor,
required this.currentThemeValue,
required this.textScale,
});
final Color brandColor;
final bool isDynamicColor;
final ThemeType currentThemeValue;
final double textScale;
@override
Widget build(BuildContext context) {
return DynamicColorBuilder(
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme? lightColorScheme;
@ -129,89 +157,120 @@ class MyApp extends StatelessWidget {
brightness: Brightness.dark,
);
}
ThemeData themeData = ThemeData(
colorScheme: currentThemeValue == ThemeType.dark
? darkColorScheme
: lightColorScheme,
);
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: GlobalData().enableMYBar
? const Color(0x00010000)
: themeData.canvasColor,
systemNavigationBarDividerColor: GlobalData().enableMYBar
? const Color(0x00010000)
: themeData.canvasColor,
systemNavigationBarIconBrightness: currentThemeValue == ThemeType.dark
? Brightness.light
: Brightness.dark,
statusBarColor: Colors.transparent,
));
// 图片缓存
// PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20;
return GetMaterialApp(
title: 'PiliPala',
theme: ThemeData(
colorScheme: currentThemeValue == ThemeType.dark
? darkColorScheme
: lightColorScheme,
snackBarTheme: SnackBarThemeData(
actionTextColor: lightColorScheme.primary,
backgroundColor: lightColorScheme.secondaryContainer,
closeIconColor: lightColorScheme.secondary,
contentTextStyle: TextStyle(color: lightColorScheme.secondary),
elevation: 20,
),
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: ZoomPageTransitionsBuilder(
allowEnterRouteSnapshotting: false,
),
},
),
),
darkTheme: ThemeData(
colorScheme: currentThemeValue == ThemeType.light
? lightColorScheme
: darkColorScheme,
snackBarTheme: SnackBarThemeData(
actionTextColor: darkColorScheme.primary,
backgroundColor: darkColorScheme.secondaryContainer,
closeIconColor: darkColorScheme.secondary,
contentTextStyle: TextStyle(color: darkColorScheme.secondary),
elevation: 20,
),
),
localizationsDelegates: const [
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
locale: const Locale("zh", "CN"),
supportedLocales: const [Locale("zh", "CN"), Locale("en", "US")],
fallbackLocale: const Locale("zh", "CN"),
getPages: Routes.getPages,
home: const MainApp(),
builder: (BuildContext context, Widget? child) {
return FlutterSmartDialog(
toastBuilder: (String msg) => CustomToast(msg: msg),
child: MediaQuery(
data: MediaQuery.of(context)
.copyWith(textScaler: TextScaler.linear(textScale)),
child: child!,
),
);
},
navigatorObservers: [
VideoDetailPage.routeObserver,
SearchPage.routeObserver,
],
return BuildMainApp(
lightColorScheme: lightColorScheme,
darkColorScheme: darkColorScheme,
currentThemeValue: currentThemeValue,
textScale: textScale,
);
}),
);
}
}
class OtherApp extends StatelessWidget {
const OtherApp({
super.key,
required this.brandColor,
required this.currentThemeValue,
required this.textScale,
});
final Color brandColor;
final ThemeType currentThemeValue;
final double textScale;
@override
Widget build(BuildContext context) {
return BuildMainApp(
lightColorScheme: ColorScheme.fromSeed(
seedColor: brandColor,
brightness: Brightness.light,
),
darkColorScheme: ColorScheme.fromSeed(
seedColor: brandColor,
brightness: Brightness.dark,
),
currentThemeValue: currentThemeValue,
textScale: textScale,
);
}
}
class BuildMainApp extends StatelessWidget {
const BuildMainApp({
super.key,
required this.lightColorScheme,
required this.darkColorScheme,
required this.currentThemeValue,
required this.textScale,
});
final ColorScheme lightColorScheme;
final ColorScheme darkColorScheme;
final ThemeType currentThemeValue;
final double textScale;
@override
Widget build(BuildContext context) {
final SnackBarThemeData snackBarTheme = SnackBarThemeData(
actionTextColor: lightColorScheme.primary,
backgroundColor: lightColorScheme.secondaryContainer,
closeIconColor: lightColorScheme.secondary,
contentTextStyle: TextStyle(color: lightColorScheme.secondary),
elevation: 20,
);
return GetMaterialApp(
title: 'PiliPala',
theme: ThemeData(
colorScheme: currentThemeValue == ThemeType.dark
? darkColorScheme
: lightColorScheme,
snackBarTheme: snackBarTheme,
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: ZoomPageTransitionsBuilder(
allowEnterRouteSnapshotting: false,
),
},
),
),
darkTheme: ThemeData(
colorScheme: currentThemeValue == ThemeType.light
? lightColorScheme
: darkColorScheme,
snackBarTheme: snackBarTheme,
),
localizationsDelegates: const [
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
locale: const Locale("zh", "CN"),
supportedLocales: const [Locale("zh", "CN"), Locale("en", "US")],
fallbackLocale: const Locale("zh", "CN"),
getPages: Routes.getPages,
home: const MainApp(),
builder: (BuildContext context, Widget? child) {
return FlutterSmartDialog(
toastBuilder: (String msg) => CustomToast(msg: msg),
child: MediaQuery(
data: MediaQuery.of(context)
.copyWith(textScaler: TextScaler.linear(textScale)),
child: child!,
),
);
},
navigatorObservers: [
VideoDetailPage.routeObserver,
SearchPage.routeObserver,
],
onInit: () {
RecommendFilter();
Data.init();
GlobalData();
},
);
}
}

View File

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

View File

@ -0,0 +1,93 @@
// 操作类型的枚举值:点赞 不喜欢 收藏 投币 稍后再看 下载封面 后台播放 听视频 分享 下载视频
import 'package:flutter/material.dart';
import 'package:get/get.dart';
enum ActionType {
like,
coin,
collect,
watchLater,
share,
dislike,
downloadCover,
copyLink,
// backgroundPlay,
// listenVideo,
// downloadVideo,
}
extension ActionTypeExtension on ActionType {
String get value => [
'like',
'coin',
'collect',
'watchLater',
'share',
'dislike',
'downloadCover',
'copyLink',
// 'backgroundPlay',
// 'listenVideo',
// 'downloadVideo',
][index];
String get label => [
'点赞视频',
'投币',
'收藏视频',
'稍后再看',
'视频分享',
'不喜欢',
'下载封面',
'复制链接',
// '后台播放',
// '听视频',
// '下载视频',
][index];
}
List<Map> actionMenuConfig = [
{
'icon': const Icon(Icons.thumb_up_alt_outlined),
'label': '点赞视频',
'value': ActionType.like,
},
{
'icon': Image.asset(
'assets/images/coin.png',
width: 26,
color: IconTheme.of(Get.context!).color!.withOpacity(0.65),
),
'label': '投币',
'value': ActionType.coin,
},
{
'icon': const Icon(Icons.star_border),
'label': '收藏视频',
'value': ActionType.collect,
},
{
'icon': const Icon(Icons.watch_later_outlined),
'label': '稍后再看',
'value': ActionType.watchLater,
},
{
'icon': const Icon(Icons.share),
'label': '视频分享',
'value': ActionType.share,
},
{
'icon': const Icon(Icons.thumb_down_alt_outlined),
'label': '不喜欢',
'value': ActionType.dislike,
},
{
'icon': const Icon(Icons.image_outlined),
'label': '下载封面',
'value': ActionType.downloadCover,
},
{
'icon': const Icon(Icons.link_outlined),
'label': '复制链接',
'value': ActionType.copyLink,
},
];

View File

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

View File

@ -69,9 +69,10 @@ class RecVideoItemAppModel {
: null;
// 由于app端api并不会直接返回与owner的关注状态
// 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态从而与web端接口等效
RegExp regex = RegExp(r'已关注|新关注');
isFollowed = rcmdReason != null &&
rcmdReason!.content != null &&
rcmdReason!.content!.contains('关注')
regex.hasMatch(rcmdReason!.content!)
? 1
: 0;
// 如果是就无需再显示推荐原因交由view统一处理即可

View File

@ -23,6 +23,7 @@ class HotVideoItemModel {
this.dimension,
this.shortLinkV2,
this.firstFrame,
this.cover,
this.pubLocation,
this.seasontype,
this.isOgv,
@ -50,6 +51,7 @@ class HotVideoItemModel {
Dimension? dimension;
String? shortLinkV2;
String? firstFrame;
String? cover;
String? pubLocation;
int? seasontype;
bool? isOgv;
@ -77,6 +79,7 @@ class HotVideoItemModel {
dimension = Dimension.fromMap(json['dimension']);
shortLinkV2 = json["short_link_v2"];
firstFrame = json["first_frame"];
cover = json["first_frame"];
pubLocation = json["pub_location"];
seasontype = json["seasontype"];
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.bvid,
this.title,
this.titleList,
this.description,
this.pic,
// this.play,
@ -54,8 +55,8 @@ class SearchVideoItemModel {
String? arcurl;
int? aid;
String? bvid;
List? title;
// List? titleList;
String? title;
List? titleList;
String? description;
String? pic;
// String? play;
@ -82,8 +83,9 @@ class SearchVideoItemModel {
aid = json['aid'];
bvid = json['bvid'];
mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']);
title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
// title = Em.regTitle(json['title']);
titleList = Em.regTitle(json['title']);
description = json['description'];
pic = json['pic'] != null && json['pic'].startsWith('//')
? 'https:${json['pic']}'
@ -232,6 +234,7 @@ class SearchLiveItemModel {
this.userCover,
this.type,
this.title,
this.titleList,
this.cover,
this.pic,
this.online,
@ -251,7 +254,8 @@ class SearchLiveItemModel {
String? face;
String? userCover;
String? type;
List? title;
String? title;
List? titleList;
String? cover;
String? pic;
int? online;
@ -272,7 +276,8 @@ class SearchLiveItemModel {
face = json['uface'];
userCover = json['user_cover'];
type = json['type'];
title = Em.regTitle(json['title']);
title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
titleList = Em.regTitle(json['title']);
cover = json['cover'];
pic = json['cover'];
online = json['online'];
@ -302,6 +307,7 @@ class SearchMBangumiItemModel {
this.type,
this.mediaId,
this.title,
this.titleList,
this.orgTitle,
this.mediaType,
this.cv,
@ -328,7 +334,8 @@ class SearchMBangumiItemModel {
String? type;
int? mediaId;
List? title;
String? title;
List? titleList;
String? orgTitle;
int? mediaType;
String? cv;
@ -355,7 +362,8 @@ class SearchMBangumiItemModel {
SearchMBangumiItemModel.fromJson(Map<String, dynamic> json) {
type = json['type'];
mediaId = json['media_id'];
title = Em.regTitle(json['title']);
title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
titleList = Em.regTitle(json['title']);
orgTitle = json['org_title'];
mediaType = json['media_type'];
cv = json['cv'];

View File

@ -39,6 +39,14 @@ extension VideoQualityCode on VideoQuality {
}
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 {

View File

@ -67,6 +67,7 @@ class VideoDetailData {
String? likeIcon;
bool? needJumpBv;
String? epId;
List<Staff>? staff;
VideoDetailData({
this.bvid,
@ -103,6 +104,7 @@ class VideoDetailData {
this.likeIcon,
this.needJumpBv,
this.epId,
this.staff,
});
VideoDetailData.fromJson(Map<String, dynamic> json) {
@ -155,6 +157,9 @@ class VideoDetailData {
if (json['redirect_url'] != null) {
epId = resolveEpId(json['redirect_url']);
}
staff = json["staff"] != null
? List<Staff>.from(json["staff"]!.map((e) => Staff.fromJson(e)))
: null;
}
String resolveEpId(url) {
@ -377,6 +382,7 @@ class Part {
String? weblink;
Dimension? dimension;
String? firstFrame;
String? cover;
Part({
this.cid,
@ -388,6 +394,7 @@ class Part {
this.weblink,
this.dimension,
this.firstFrame,
this.cover,
});
fromRawJson(String str) => Part.fromJson(json.decode(str));
@ -405,7 +412,8 @@ class Part {
dimension = json["dimension"] == null
? null
: Dimension.fromJson(json["dimension"]);
firstFrame = json["first_frame"];
firstFrame = json["first_frame"] ?? '';
cover = json["first_frame"] ?? '';
}
Map<String, dynamic> toJson() {
@ -629,6 +637,7 @@ class EpisodeItem {
this.attribute,
this.page,
this.bvid,
this.cover,
});
int? seasonId;
int? sectionId;
@ -639,6 +648,7 @@ class EpisodeItem {
int? attribute;
Part? page;
String? bvid;
String? cover;
EpisodeItem.fromJson(Map<String, dynamic> json) {
seasonId = json['season_id'];
@ -650,5 +660,46 @@ class EpisodeItem {
attribute = json['attribute'];
page = Part.fromJson(json['page']);
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 remoteVersion = ''.obs;
late LatestDataModel remoteAppInfo;
RxBool isUpdate = true.obs;
RxBool isUpdate = false.obs;
RxBool isLoading = true.obs;
late LatestDataModel data;

View File

@ -131,51 +131,37 @@ class BangumiIntroController extends GetxController {
builder: (context) {
return AlertDialog(
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) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile(
value: 1,
title: const Text('1枚'),
groupValue: _tempThemeValue,
onChanged: (value) {
_tempThemeValue = value!;
Get.appUpdate();
},
),
RadioListTile(
value: 2,
title: const Text('2枚'),
groupValue: _tempThemeValue,
onChanged: (value) {
_tempThemeValue = value!;
Get.appUpdate();
},
),
],
children: [1, 2]
.map(
(e) => RadioListTile(
value: e,
title: Text('$e枚'),
groupValue: _tempThemeValue,
onChanged: (value) async {
_tempThemeValue = value!;
setState(() {});
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();
},
),
)
.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('确定'),
)
],
);
});
}
@ -229,13 +215,15 @@ class BangumiIntroController extends GetxController {
}
// 修改分P或番剧分集
Future changeSeasonOrbangu(bvid, cid, aid) async {
Future changeSeasonOrbangu(bvid, cid, aid, cover) async {
// 重新获取视频资源
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid;
videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.oid.value = aid;
videoDetailCtr.cover.value = cover;
videoDetailCtr.queryVideoUrl();
// 重新请求评论
try {
@ -294,7 +282,8 @@ class BangumiIntroController extends GetxController {
int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!;
int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid);
String cover = episodes[nextIndex].cover!;
changeSeasonOrbangu(bvid, cid, aid, cover);
}
// 播放器底栏 选集 回调
@ -315,7 +304,7 @@ class BangumiIntroController extends GetxController {
sheetHeight: Get.size.height,
isFullScreen: true,
changeFucCall: (item, index) {
changeSeasonOrbangu(item.bvid, item.cid, item.aid);
changeSeasonOrbangu(item.bvid, item.cid, item.aid, item.cover);
SmartDialog.dismiss();
},
).buildShowContent(Get.context!),

View File

@ -194,7 +194,8 @@ class _BangumiInfoState extends State<BangumiInfo> {
src: widget.bangumiDetail!.cover!,
),
PBadge(
text: '评分 ${widget.bangumiDetail!.rating!['score']!}',
text:
'评分 ${widget.bangumiDetail?.rating?['score']! ?? '暂无'}',
top: null,
right: 6,
bottom: 6,
@ -231,11 +232,11 @@ class _BangumiInfoState extends State<BangumiInfo> {
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
padding:
WidgetStateProperty.all(EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith(
(Set<MaterialState> states) {
WidgetStateProperty.resolveWith(
(Set<WidgetState> states) {
return t.colorScheme.primaryContainer
.withOpacity(0.7);
}),
@ -322,8 +323,8 @@ class _BangumiInfoState extends State<BangumiInfo> {
pages: widget.bangumiDetail!.episodes!,
cid: cid! ?? widget.bangumiDetail!.episodes!.first.cid!,
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) =>
bangumiIntroController.changeSeasonOrbangu(bvid, cid, aid),
changeFuc: (bvid, cid, aid, cover) => bangumiIntroController
.changeSeasonOrbangu(bvid, cid, aid, cover),
bangumiDetail: bangumiIntroController.bangumiDetail.value,
bangumiIntroController: bangumiIntroController,
)

View File

@ -20,10 +20,10 @@ class IntroDetail extends StatelessWidget {
sheetHeight = localCache.get('sheetHeight');
TextStyle smallTitle = TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onBackground,
color: Theme.of(context).colorScheme.onSurface,
);
return Container(
color: Theme.of(context).colorScheme.background,
color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.only(left: 14, right: 14),
height: sheetHeight,
child: Column(

View File

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:nil/nil.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/utils/main_stream.dart';
@ -142,10 +141,10 @@ class _BangumiPageState extends State<BangumiPage>
),
);
} else {
return nil;
return const SizedBox();
}
} else {
return nil;
return const SizedBox();
}
},
),
@ -216,7 +215,7 @@ class _BangumiPageState extends State<BangumiPage>
(BuildContext context, int index) {
return bangumiList!.isNotEmpty
? BangumiCardV(bangumiItem: bangumiList[index])
: nil;
: const SizedBox();
},
childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10,
),

View File

@ -84,9 +84,12 @@ class _BangumiPanelState extends State<BangumiPanel> {
item.bvid,
item.cid,
item.aid,
item.cover,
);
_bottomSheetController?.close();
currentIndex = i;
if (_bottomSheetController != null) {
_bottomSheetController?.close();
}
currentIndex.value = i;
scrollToIndex();
}
@ -136,7 +139,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
padding: WidgetStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
widget.bangumiIntroController?.bottomSheetController =

View File

@ -1,11 +1,10 @@
import 'package:flutter/material.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/widgets/badge.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/bangumi/list.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -14,109 +13,67 @@ class BangumiCardV extends StatelessWidget {
const BangumiCardV({
super.key,
required this.bangumiItem,
this.longPress,
this.longPressEnd,
});
final bangumiItem;
final Function()? longPress;
final Function()? longPressEnd;
final BangumiListItemModel bangumiItem;
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(bangumiItem.mediaId);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: GestureDetector(
// onLongPress: () {
// if (longPress != null) {
// longPress!();
// }
// },
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
final int seasonId = bangumiItem.seasonId;
SmartDialog.showLoading(msg: '获取中...');
final res = await SearchHttp.bangumiInfo(seasonId: seasonId);
SmartDialog.dismiss().then((value) {
if (res['status']) {
if (res['data'].episodes.isEmpty) {
SmartDialog.showToast('资源加载失败');
return;
}
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=$seasonId',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
return InkWell(
onTap: () {
RoutePush.bangumiPush(
bangumiItem.seasonId,
null,
heroTag: heroTag,
);
},
onLongPress: () =>
imageSaveDialog(context, bangumiItem, SmartDialog.dismiss),
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(
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',
),
],
);
}
});
},
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)
],
),
);
}

View File

@ -14,6 +14,7 @@ import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/feed_back.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/utils.dart';
@ -70,7 +71,7 @@ class DynamicsController extends GetxController {
Future queryFollowDynamic({type = 'init'}) async {
if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'};
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
if (type == 'init') {
dynamicsList.clear();
@ -220,25 +221,7 @@ class DynamicsController extends GetxController {
print('DYNAMIC_TYPE_PGC_UNION 番剧');
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
if (pgc.epid != null) {
SmartDialog.showLoading(msg: '获取中...');
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'],
},
);
}
RoutePush.bangumiPush(null, pgc.epid);
}
break;
}
@ -246,7 +229,7 @@ class DynamicsController extends GetxController {
Future queryFollowUp({type = 'init'}) async {
if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'};
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
if (type == 'init') {
upData.value.upList = <UpItem>[];

View File

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

View File

@ -196,7 +196,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
centerTitle: false,
titleSpacing: 0,
title: StreamBuilder(
stream: titleStreamC.stream.distinct(),
stream: titleStreamC.stream,
initialData: false,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
@ -369,35 +369,40 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
curve: Curves.easeInOut,
),
),
child: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return VideoReplyNewDialog(
oid: _dynamicDetailController.oid ??
IdUtils.bv2av(Get.parameters['bvid']!),
root: 0,
parent: 0,
replyType: ReplyType.values[replyType],
);
},
).then(
(value) => {
// 完成评论,数据添加
if (value != null && value['data'] != null)
{
_dynamicDetailController.replyList.add(value['data']),
_dynamicDetailController.acount.value++
}
},
);
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
child: Obx(
() => _dynamicDetailController.replyReqCode.value == 12061
? const SizedBox()
: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return VideoReplyNewDialog(
oid: _dynamicDetailController.oid ??
IdUtils.bv2av(Get.parameters['bvid']!),
root: 0,
parent: 0,
replyType: ReplyType.values[replyType],
);
},
).then(
(value) => {
// 完成评论,数据添加
if (value != null && value['data'] != null)
{
_dynamicDetailController.replyList
.add(value['data']),
_dynamicDetailController.acount.value++
}
},
);
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
),
),
),
);

View File

@ -11,6 +11,7 @@ import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.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 '../mine/controller.dart';
@ -161,13 +162,12 @@ class _DynamicsPageState extends State<DynamicsPage>
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.surfaceContainerHighest
.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
),
thumbDecoration: BoxDecoration(
color:
Theme.of(context).colorScheme.background,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
),
duration: const Duration(milliseconds: 300),
@ -224,8 +224,8 @@ class _DynamicsPageState extends State<DynamicsPage>
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data;
if (data['status']) {
Map? data = snapshot.data;
if (data != null && data['status']) {
List<DynamicItemModel> list =
_dynamicsController.dynamicsList;
return Obx(
@ -248,24 +248,21 @@ class _DynamicsPageState extends State<DynamicsPage>
}
},
);
} else if (data['msg'] == "账号未登录") {
return HttpError(
errMsg: data['msg'],
btnText: "去登录",
fn: () {
mineController.onLogin();
},
);
} else {
return HttpError(
errMsg: data['msg'],
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
setState(() {
_futureBuilderFuture =
_dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp =
_dynamicsController.queryFollowUp();
});
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp =
_dynamicsController.queryFollowUp();
});
}
},
);
}

View File

@ -14,10 +14,10 @@ import 'rich_node_panel.dart';
class ActionPanel extends StatefulWidget {
const ActionPanel({
super.key,
this.item,
required this.item,
});
// ignore: prefer_typing_uninitialized_variables
final item;
final DynamicItemModel item;
@override
State<ActionPanel> createState() => _ActionPanelState();
@ -28,7 +28,8 @@ class _ActionPanelState extends State<ActionPanel>
final DynamicsController _dynamicsController = Get.put(DynamicsController());
late ModuleStatModel stat;
bool isProcessing = false;
RxDouble height = (310.0).obs;
double defaultHeight = 260;
RxDouble height = 0.0.obs;
RxBool isExpand = false.obs;
late double statusHeight;
TextEditingController _inputController = TextEditingController();
@ -48,7 +49,7 @@ class _ActionPanelState extends State<ActionPanel>
@override
void initState() {
super.initState();
stat = widget.item!.modules.moduleStat;
stat = widget.item.modules!.moduleStat!;
onInit();
}
@ -62,7 +63,7 @@ class _ActionPanelState extends State<ActionPanel>
var item = widget.item!;
String dynamicId = item.idStr!;
// 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');
bool status = like.status!;
int up = status ? 2 : 1;
@ -70,15 +71,15 @@ class _ActionPanelState extends State<ActionPanel>
if (res['status']) {
SmartDialog.showToast(!status ? '点赞成功' : '取消赞');
if (up == 1) {
item.modules.moduleStat.like.count = (count + 1).toString();
item.modules.moduleStat.like.status = true;
item.modules!.moduleStat!.like!.count = (count + 1).toString();
item.modules!.moduleStat!.like!.status = true;
} else {
if (count == 1) {
item.modules.moduleStat.like.count = '点赞';
item.modules!.moduleStat!.like!.count = '点赞';
} 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(() {});
} else {
@ -88,50 +89,134 @@ class _ActionPanelState extends State<ActionPanel>
// 转发动态预览
Widget dynamicPreview() {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 14, 12),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4),
borderRadius: BorderRadius.circular(12),
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: 12, horizontal: 14),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
),
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: [
Text(
'@${widget.item.modules.moduleAuthor.name}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
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,
),
),
const SizedBox(height: 6),
Row(
children: [
NetworkImgLayer(
src: widget.item.modules.moduleAuthor.face,
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)
],
)
// Text(data)
],
)),
)
],
),
),
);
}
@ -149,56 +234,54 @@ class _ActionPanelState extends State<ActionPanel>
duration: Durations.medium1,
onEnd: () async {
if (isExpand.value) {
await Future.delayed(const Duration(milliseconds: 120));
await Future.delayed(const Duration(milliseconds: 80));
myFocusNode.requestFocus();
}
},
height: height.value,
height: height.value + MediaQuery.of(context).padding.bottom,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: Durations.medium1,
height: isExpand.value ? statusHeight : 0,
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 12, 0),
padding: EdgeInsets.fromLTRB(
isExpand.value ? 10 : 16,
10,
isExpand.value ? 14 : 12,
0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AnimatedSwitcher(
duration: Durations.medium1,
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: isExpand.value
? IconButton(
onPressed: () => togglePanelState(false),
icon: const Icon(Icons.close))
: const Text(
'转发动态',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
AnimatedSwitcher(
duration: Durations.medium1,
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: isExpand.value
? FilledButton(
onPressed: () => dynamicForward('forward'),
child: const Text('转发'),
)
: TextButton(
onPressed: () {},
child: const Text('立即转发'),
),
),
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('立即转发'),
)
],
),
),
@ -209,7 +292,7 @@ class _ActionPanelState extends State<ActionPanel>
child: Container(
width: double.infinity,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.fromLTRB(16, 0, 10, 15),
padding: const EdgeInsets.fromLTRB(16, 0, 10, 14),
child: Text(
'说点什么吧',
textAlign: TextAlign.start,
@ -251,9 +334,6 @@ class _ActionPanelState extends State<ActionPanel>
textAlign: TextAlign.center,
),
),
SizedBox(
height: MediaQuery.of(context).padding.bottom + 20,
)
]
],
),
@ -266,7 +346,7 @@ class _ActionPanelState extends State<ActionPanel>
togglePanelState(status) {
if (!status) {
Get.back();
height.value = 310;
height.value = defaultHeight;
_inputText = '';
_inputController.clear();
} else {
@ -281,6 +361,7 @@ class _ActionPanelState extends State<ActionPanel>
dynIdStr: dynamicId,
mid: _dynamicsController.userInfo.mid,
rawText: _inputText,
scene: 4,
);
if (res['status']) {
SmartDialog.showToast(type == 'forward' ? '转发成功' : '发布成功');
@ -298,6 +379,8 @@ class _ActionPanelState extends State<ActionPanel>
Widget build(BuildContext context) {
var color = Theme.of(context).colorScheme.outline;
var primary = Theme.of(context).colorScheme.primary;
height.value = defaultHeight;
print('height.value: ${height.value}');
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [

View File

@ -19,7 +19,7 @@ Widget addWidget(item, context, type, {floor = 1}) {
};
Color bgColor = floor == 1
? Theme.of(context).dividerColor.withOpacity(0.08)
: Theme.of(context).colorScheme.background;
: Theme.of(context).colorScheme.surface;
switch (type) {
case 'ADDITIONAL_TYPE_UGC':
// 转发的投稿

View File

@ -52,7 +52,7 @@ class AuthorPanel extends StatelessWidget {
color: item.modules.moduleAuthor!.vip != null &&
item.modules.moduleAuthor!.vip['status'] > 0
? const Color.fromARGB(255, 251, 100, 163)
: Theme.of(context).colorScheme.onBackground,
: Theme.of(context).colorScheme.onSurface,
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
),
),
@ -82,7 +82,7 @@ class AuthorPanel extends StatelessWidget {
height: 32,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
padding: WidgetStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showModalBottomSheet(

View File

@ -9,7 +9,7 @@ import 'forward_panel.dart';
class DynamicPanel extends StatelessWidget {
final dynamic item;
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());
@override
@ -41,8 +41,8 @@ class DynamicPanel extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: AuthorPanel(item: item),
),
if (item!.modules!.moduleDynamic!.desc != null ||
item!.modules!.moduleDynamic!.major != null)
if (item.modules!.moduleDynamic!.desc != null ||
item.modules!.moduleDynamic!.major != null)
Content(item: item, source: source),
forWard(item, context, _dynamicsController, source),
const SizedBox(height: 2),

View File

@ -2,6 +2,7 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart';
import 'additional_panel.dart';
@ -182,6 +183,116 @@ 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'))
),
),
);
case 'DYNAMIC_TYPE_MUSIC':
final Map music = item.modules.moduleDynamic.major.music;
return Padding(
padding: const EdgeInsets.only(top: 8),
child: InkWell(
onTap: () {
Get.toNamed('/webview', parameters: {
'url': "https:${music['jump_url']}",
'type': 'url',
'pageTitle': music['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: music['cover'],
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
music['title'],
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
music['label'],
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:
return const SizedBox(
width: double.infinity,

View File

@ -1,3 +1,4 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -30,6 +31,31 @@ class _UpPanelState extends State<UpPanel> {
liveList = widget.upData.liveList!;
}
void onClickUp(data, i) {
currentMid = data.mid;
Get.find<DynamicsController>().mid.value = data.mid;
Get.find<DynamicsController>().upInfo.value = data;
Get.find<DynamicsController>().onSelectUp(data.mid);
int liveLen = liveList.length;
int upLen = upList.length;
double itemWidth = contentWidth + itemPadding.horizontal;
double screenWidth = MediaQuery.sizeOf(context).width;
double moveDistance = 0.0;
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
} else {
moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
}
data.hasUpdate = false;
scrollController.animateTo(
moveDistance,
duration: const Duration(milliseconds: 200),
curve: Curves.linear,
);
setState(() {});
}
@override
Widget build(BuildContext context) {
listFormat();
@ -43,7 +69,7 @@ class _UpPanelState extends State<UpPanel> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
color: Theme.of(context).colorScheme.background,
color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -69,7 +95,7 @@ class _UpPanelState extends State<UpPanel> {
),
Container(
height: 90,
color: Theme.of(context).colorScheme.background,
color: Theme.of(context).colorScheme.surface,
child: Row(
children: [
Flexible(
@ -120,30 +146,10 @@ class _UpPanelState extends State<UpPanel> {
onTap: () {
feedBack();
if (data.type == 'up') {
currentMid = data.mid;
Get.find<DynamicsController>().mid.value = data.mid;
Get.find<DynamicsController>().upInfo.value = data;
Get.find<DynamicsController>().onSelectUp(data.mid);
int liveLen = liveList.length;
int upLen = upList.length;
double itemWidth = contentWidth + itemPadding.horizontal;
double screenWidth = MediaQuery.sizeOf(context).width;
double moveDistance = 0.0;
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
moveDistance =
(i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
} else {
moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
}
data.hasUpdate = false;
scrollController.animateTo(
moveDistance,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
setState(() {});
EasyThrottle.throttle('follow', const Duration(milliseconds: 300),
() {
onClickUp(data, i);
});
} else if (data.type == 'live') {
LiveItemModel liveItem = LiveItemModel.fromJson({
'title': data.title,

View File

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

View File

@ -10,16 +10,22 @@ import 'package:pilipala/utils/storage.dart';
class FavController extends GetxController {
final ScrollController scrollController = ScrollController();
Rx<FavFolderData> favFolderData = FavFolderData().obs;
RxList<FavFolderItemData> favFolderList = <FavFolderItemData>[].obs;
Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo;
int currentPage = 1;
int pageSize = 60;
RxBool hasMore = true.obs;
Future<dynamic> queryFavFolder({type = 'init'}) async {
@override
void onInit() {
userInfo = userInfoCache.get('userInfoCache');
super.onInit();
}
Future<dynamic> queryFavFolder({type = 'init'}) async {
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录'};
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
if (!hasMore.value) {
return;
@ -32,9 +38,10 @@ class FavController extends GetxController {
if (res['status']) {
if (type == 'init') {
favFolderData.value = res['data'];
favFolderList.value = res['data'].list;
} else {
if (res['data'].list.isNotEmpty) {
favFolderData.value.list!.addAll(res['data'].list);
favFolderList.addAll(res['data'].list);
favFolderData.update((val) {});
}
}
@ -49,4 +56,13 @@ class FavController extends GetxController {
Future onLoad() async {
queryFavFolder(type: 'onload');
}
removeFavFolder({required int mediaIds}) async {
for (var i in favFolderList) {
if (i.id == mediaIds) {
favFolderList.remove(i);
break;
}
}
}
}

View File

@ -1,9 +1,11 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/fav/index.dart';
import 'package:pilipala/pages/fav/widgets/item.dart';
import 'package:pilipala/utils/route_push.dart';
class FavPage extends StatefulWidget {
const FavPage({super.key});
@ -57,16 +59,15 @@ class _FavPageState extends State<FavPage> {
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
Map? data = snapshot.data;
if (data != null && data['status']) {
return Obx(
() => ListView.builder(
controller: scrollController,
itemCount: _favController.favFolderData.value.list!.length,
itemCount: _favController.favFolderList.length,
itemBuilder: (context, index) {
return FavItem(
favFolderItem:
_favController.favFolderData.value.list![index]);
favFolderItem: _favController.favFolderList[index]);
},
),
);
@ -75,15 +76,30 @@ class _FavPageState extends State<FavPage> {
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_favController.queryFavFolder();
});
}
},
),
],
);
}
} else {
// 骨架屏
return const Text('请求中');
return ListView.builder(
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
itemCount: 10,
);
}
},
),

View File

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

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/user/fav_detail.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/fav/index.dart';
class FavDetailController extends GetxController {
FavFolderItemData? item;
@ -74,4 +76,41 @@ class FavDetailController extends GetxController {
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

@ -101,11 +101,19 @@ class _FavDetailPageState extends State<FavDetailPage> {
Get.toNamed('/favSearch?searchType=0&mediaId=$mediaId'),
icon: const Icon(Icons.search_outlined),
),
// IconButton(
// onPressed: () {},
// icon: const Icon(Icons.more_vert),
// ),
const SizedBox(width: 6),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert_outlined),
position: PopupMenuPosition.under,
onSelected: (String type) {},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () => _favDetailController.onDelFavFolder(),
value: 'pause',
child: const Text('删除收藏夹'),
),
],
),
const SizedBox(width: 14),
],
flexibleSpace: FlexibleSpaceBar(
background: Container(

View File

@ -1,3 +1,4 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.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/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../../../common/widgets/badge.dart';
@ -61,6 +63,11 @@ class FavVideoCardH extends StatelessWidget {
epId != null ? SearchType.media_bangumi : SearchType.video,
});
},
onLongPress: () => imageSaveDialog(
context,
videoItem,
SmartDialog.dismiss,
),
child: Column(
children: [
Padding(
@ -210,7 +217,7 @@ class VideoContent extends StatelessWidget {
bottom: -4,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
padding: WidgetStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showDialog(

View File

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/history.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController {
@ -15,14 +16,20 @@ class HistoryController extends GetxController {
RxBool isLoading = false.obs;
RxBool enableMultiple = false.obs;
RxInt checkedCount = 0.obs;
Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo;
@override
void onInit() {
super.onInit();
historyStatus();
userInfo = userInfoCache.get('userInfoCache');
}
Future queryHistoryList({type = 'init'}) async {
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
int max = 0;
int viewAt = 0;
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/no_data.dart';
import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/utils/route_push.dart';
import 'widgets/item.dart';
@ -183,8 +184,8 @@ class _HistoryPageState extends State<HistoryPage> {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data;
if (data['status']) {
Map? data = snapshot.data;
if (data != null && data['status']) {
return Obx(
() => _historyController.historyList.isNotEmpty
? SliverList(
@ -209,8 +210,18 @@ class _HistoryPageState extends State<HistoryPage> {
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_historyController.queryHistoryList();
});
}
},
);
}
} 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/user.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/search_type.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/history_search/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/utils.dart';
class HistoryItem extends StatelessWidget {
@ -101,26 +101,11 @@ class HistoryItem extends StatelessWidget {
}
} else {
if (videoItem.history.epid != '') {
SmartDialog.showLoading(msg: '获取中...');
var res =
await SearchHttp.bangumiInfo(epId: videoItem.history.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'],
},
);
}
RoutePush.bangumiPush(
null,
videoItem.history.epid,
heroTag: heroTag,
);
}
}
} else {
@ -213,7 +198,8 @@ class HistoryItem extends StatelessWidget {
duration: const Duration(milliseconds: 200),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(
StyleString.imgRadius.x),
color: Colors.black.withOpacity(
ctr!.enableMultiple.value &&
videoItem.checked
@ -231,11 +217,10 @@ class HistoryItem extends StatelessWidget {
curve: Curves.easeInOut,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
padding: WidgetStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty
.resolveWith(
WidgetStateProperty.resolveWith(
(states) {
return Colors.white
.withOpacity(0.8);
@ -258,7 +243,7 @@ class HistoryItem extends StatelessWidget {
),
),
),
videoItem.progress != 0
videoItem.progress != 0 && videoItem.duration != 0
? Positioned(
left: 3,
right: 3,

View File

@ -10,9 +10,8 @@ class HistorySearchController extends GetxController {
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
String hintText = '搜索';
RxString loadingStatus = 'init'.obs;
RxBool loadingStatus = false.obs;
RxString loadingText = '加载中...'.obs;
bool hasRequest = false;
late int mid;
RxString uname = ''.obs;
int pn = 1;
@ -36,8 +35,7 @@ class HistorySearchController extends GetxController {
// 提交搜索内容
void submit() {
loadingStatus.value = 'loading';
if (hasRequest) {
if (!loadingStatus.value) {
pn = 1;
searchHistories();
}
@ -48,6 +46,7 @@ class HistorySearchController extends GetxController {
if (type == 'onLoad' && loadingText.value == '没有更多了') {
return;
}
loadingStatus.value = true;
var res = await UserHttp.searchHistory(
pn: pn,
keyword: controller.value.text,
@ -63,9 +62,8 @@ class HistorySearchController extends GetxController {
loadingText.value = '没有更多了';
}
pn += 1;
hasRequest = true;
}
loadingStatus.value = 'finish';
loadingStatus.value = false;
return res;
}
@ -86,6 +84,6 @@ class HistorySearchController extends GetxController {
historyList.removeWhere((e) => e.kid == kid);
SmartDialog.showToast(res['msg']);
}
loadingStatus.value = 'finish';
// loadingStatus.value = fasle;
}
}

View File

@ -2,7 +2,6 @@ import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/pages/history/widgets/item.dart';
@ -16,20 +15,19 @@ class HistorySearchPage extends StatefulWidget {
}
class _HistorySearchPageState extends State<HistorySearchPage> {
final HistorySearchController _historySearchCtr =
Get.put(HistorySearchController());
final HistorySearchController _hisCtr = Get.put(HistorySearchController());
late ScrollController scrollController;
@override
void initState() {
super.initState();
scrollController = _historySearchCtr.scrollController;
scrollController = _hisCtr.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_historySearchCtr.onLoad();
_hisCtr.onLoad();
});
}
},
@ -50,19 +48,19 @@ class _HistorySearchPageState extends State<HistorySearchPage> {
titleSpacing: 0,
actions: [
IconButton(
onPressed: () => _historySearchCtr.submit(),
onPressed: () => _hisCtr.submit(),
icon: const Icon(Icons.search_outlined, size: 22)),
const SizedBox(width: 10)
],
title: Obx(
() => TextField(
autofocus: true,
focusNode: _historySearchCtr.searchFocusNode,
controller: _historySearchCtr.controller.value,
focusNode: _hisCtr.searchFocusNode,
controller: _hisCtr.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _historySearchCtr.onChange(value),
onChanged: (value) => _hisCtr.onChange(value),
decoration: InputDecoration(
hintText: _historySearchCtr.hintText,
hintText: _hisCtr.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
@ -70,103 +68,61 @@ class _HistorySearchPageState extends State<HistorySearchPage> {
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _historySearchCtr.onClear(),
onPressed: () => _hisCtr.onClear(),
),
),
onSubmitted: (String value) => _historySearchCtr.submit(),
onSubmitted: (String value) => _hisCtr.submit(),
),
),
),
body: Obx(
() => Column(
children: _historySearchCtr.loadingStatus.value == 'init'
? [const SizedBox()]
: [
Expanded(
child: FutureBuilder(
future: _historySearchCtr.searchHistories(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => _historySearchCtr.historyList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount:
_historySearchCtr.historyList.length +
1,
itemBuilder: (context, index) {
if (index ==
_historySearchCtr
.historyList.length) {
return Container(
height: MediaQuery.of(context)
.padding
.bottom +
60,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
child: Center(
child: Obx(
() => Text(
_historySearchCtr
.loadingText.value,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
fontSize: 13),
),
),
),
);
} else {
return HistoryItem(
videoItem: _historySearchCtr
.historyList[index],
ctr: _historySearchCtr,
onChoose: null,
onUpdateMultiple: () => null,
);
}
},
)
: _historySearchCtr.loadingStatus.value ==
'loading'
? const SizedBox(child: Text('加载中...'))
: const CustomScrollView(
slivers: <Widget>[
NoData(),
],
),
);
} else {
return CustomScrollView(
slivers: <Widget>[
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
);
}
() {
return _hisCtr.loadingStatus.value && _hisCtr.historyList.isEmpty
? ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
)
: _hisCtr.historyList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount: _hisCtr.historyList.length + 1,
itemBuilder: (context, index) {
if (index == _hisCtr.historyList.length) {
return Container(
height: MediaQuery.of(context).padding.bottom + 60,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom),
child: Center(
child: Obx(
() => Text(
_hisCtr.loadingText.value,
style: TextStyle(
color:
Theme.of(context).colorScheme.outline,
fontSize: 13,
),
),
),
),
);
} else {
// 骨架屏
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
return HistoryItem(
videoItem: _hisCtr.historyList[index],
ctr: _hisCtr,
onChoose: null,
onUpdateMultiple: () => null,
);
}
},
),
),
],
),
)
: const CustomScrollView(
slivers: <Widget>[
NoData(),
],
);
},
),
);
}

View File

@ -214,6 +214,34 @@ class UserInfoWidget extends StatelessWidget {
final VoidCallback? callback;
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
Widget build(BuildContext context) {
return Row(
@ -231,31 +259,7 @@ class UserInfoWidget extends StatelessWidget {
const SizedBox(width: 8),
Obx(
() => userLogin.value
? 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),
),
),
),
)
],
)
? buildLoggedInWidget(context)
: DefaultUser(callback: () => callback!()),
),
],
@ -274,8 +278,8 @@ class DefaultUser extends StatelessWidget {
height: 38,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: MaterialStateProperty.resolveWith((states) {
padding: WidgetStateProperty.all(EdgeInsets.zero),
backgroundColor: WidgetStateProperty.resolveWith((states) {
return Theme.of(context).colorScheme.onInverseSurface;
}),
),
@ -367,8 +371,8 @@ class CustomChip extends StatelessWidget {
),
backgroundColor: secondaryContainer,
selectedColor: secondaryContainer,
color: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) => secondaryContainer.withAlpha(200)),
color: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) => secondaryContainer.withAlpha(200)),
padding: const EdgeInsets.fromLTRB(7, 1, 7, 1),
label: Text(label, style: chipTextStyle),
onPressed: () => onTap(),
@ -402,30 +406,27 @@ class SearchBar extends StatelessWidget {
color: colorScheme.onSecondaryContainer.withOpacity(0.05),
child: InkWell(
splashColor: colorScheme.primaryContainer.withOpacity(0.3),
onTap: () => Get.toNamed(
'/search',
parameters: {'hintText': ctr!.defaultSearch.value},
),
child: Row(
children: [
const SizedBox(width: 14),
Icon(
Icons.search_outlined,
color: colorScheme.onSecondaryContainer,
),
const SizedBox(width: 10),
Obx(
() => Expanded(
child: Text(
onTap: () => Get.toNamed('/search',
parameters: {'hintText': ctr!.defaultSearch.value}),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Row(
children: [
Icon(
Icons.search_outlined,
color: colorScheme.onSecondaryContainer,
),
const SizedBox(width: 10),
Obx(
() => Text(
ctr!.defaultSearch.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: colorScheme.outline),
),
),
),
const SizedBox(width: 15),
],
],
),
),
),
),

View File

@ -3,8 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
@ -78,15 +76,6 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
return VideoCardH(
videoItem: _hotController.videoList[index],
showPubdate: true,
longPress: () {
_hotController.popupDialog = _createPopupDialog(
_hotController.videoList[index]);
Overlay.of(context)
.insert(_hotController.popupDialog!);
},
longPressEnd: () {
_hotController.popupDialog?.remove();
},
);
}, childCount: _hotController.videoList.length),
),
@ -122,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_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/user.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 {
final ScrollController scrollController = ScrollController();
RxList<HotVideoItemModel> laterList = <HotVideoItemModel>[].obs;
int count = 0;
RxBool isLoading = false.obs;
Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo;
@override
void onInit() {
super.onInit();
userInfo = userInfoCache.get('userInfoCache');
}
Future queryLaterList() async {
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
isLoading.value = true;
var res = await UserHttp.seeYouLater();
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/video_card_h.dart';
import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/utils/route_push.dart';
class LaterPage extends StatefulWidget {
const LaterPage({super.key});
@ -72,8 +73,8 @@ class _LaterPageState extends State<LaterPage> {
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
Map? data = snapshot.data;
if (data != null && data['status']) {
return Obx(
() => _laterController.laterList.isNotEmpty &&
!_laterController.isLoading.value
@ -84,7 +85,7 @@ class _LaterPageState extends State<LaterPage> {
return VideoCardH(
videoItem: videoItem,
source: 'later',
longPress: () => _laterController.toViewDel(
onPressedFn: () => _laterController.toViewDel(
aid: videoItem.aid));
}, childCount: _laterController.laterList.length),
)
@ -96,10 +97,18 @@ class _LaterPageState extends State<LaterPage> {
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {
_futureBuilderFuture = _laterController.queryLaterList();
}),
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_laterController.queryLaterList();
});
}
},
);
}
} else {

View File

@ -5,9 +5,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'controller.dart';
@ -112,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) {
// double maxWidth = Get.size.width;
// int baseWidth = 500;
@ -152,14 +140,6 @@ class _LivePageState extends State<LivePage>
? LiveCardV(
liveItem: liveList[index],
crossAxisCount: crossAxisCount,
longPress: () {
_liveController.popupDialog =
_createPopupDialog(liveList[index]);
Overlay.of(context).insert(_liveController.popupDialog!);
},
longPressEnd: () {
_liveController.popupDialog?.remove();
},
)
: const VideoCardVSkeleton();
},

View File

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

@ -17,8 +17,7 @@ class LiveRoomController extends GetxController {
double volume = 0.0;
// 静音状态
RxBool volumeOff = false.obs;
PlPlayerController plPlayerController =
PlPlayerController.getInstance(videoType: 'live');
PlPlayerController plPlayerController = PlPlayerController(videoType: 'live');
Rx<RoomInfoH5Model> roomInfoH5 = RoomInfoH5Model().obs;
late bool enableCDN;
late int currentQn;

View File

@ -124,7 +124,7 @@ class _BottomControlState extends State<BottomControl> {
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
padding: WidgetStateProperty.all(EdgeInsets.zero),
),
onPressed: () async {
bool canUsePiP = false;
@ -153,7 +153,8 @@ class _BottomControlState extends State<BottomControl> {
size: 20,
color: Colors.white,
),
fuc: () => widget.controller!.triggerFullScreen(),
fuc: () => widget.controller!.triggerFullScreen(
status: !(widget.controller!.isFullScreen.value)),
),
],
),

View File

@ -1,11 +1,14 @@
import 'dart:async';
import 'dart:io';
import 'package:encrypt/encrypt.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/login.dart';
import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';
import 'package:pilipala/models/login/index.dart';
import 'package:pilipala/utils/login.dart';
class LoginPageController extends GetxController {
final GlobalKey mobFormKey = GlobalKey<FormState>();
@ -26,9 +29,23 @@ class LoginPageController extends GetxController {
final Gt3FlutterPlugin captcha = Gt3FlutterPlugin();
// 倒计时60s
RxInt seconds = 60.obs;
Timer? timer;
RxBool smsCodeSendStatus = false.obs;
// 默认密码登录
RxInt loginType = 0.obs;
late String captchaKey;
late int tel;
late int webSmsCode;
RxInt validSeconds = 180.obs;
Timer? validTimer;
late String qrcodeKey;
// 监听pageView切换
void onPageChange(int index) {
currentIndex.value = index;
@ -43,6 +60,7 @@ class LoginPageController extends GetxController {
curve: Curves.easeInOut,
);
passwordTextFieldNode.requestFocus();
(mobFormKey.currentState as FormState).save();
}
}
@ -86,18 +104,64 @@ class LoginPageController extends GetxController {
}
}
// 验证码登录
void loginInByCode() {
if ((msgCodeFormKey.currentState as FormState).validate()) {}
// web端密码登录
void loginInByWebPassword() async {
if ((passwordFormKey.currentState as FormState).validate()) {
getCaptcha((data) async {
CaptchaDataModel captchaData = data;
var webKeyRes = await LoginHttp.getWebKey();
if (webKeyRes['status']) {
String rhash = webKeyRes['data']['hash'];
String key = webKeyRes['data']['key'];
dynamic publicKey = RSAKeyParser().parse(key);
String passwordEncryptyed = Encrypter(RSA(publicKey: publicKey))
.encrypt(rhash + passwordTextController.text)
.base64;
var res = await LoginHttp.loginInByWebPwd(
username: tel,
password: passwordEncryptyed,
token: captchaData.token!,
challenge: captchaData.geetest!.challenge!,
validate: captchaData.validate!,
seccode: captchaData.seccode!,
);
if (res['status']) {
await LoginUtils.confirmLogin('', null);
} else {
SmartDialog.showToast(res['msg']);
}
} else {
SmartDialog.showToast(webKeyRes['msg']);
}
});
}
}
// app端验证码
void getMsgCode() async {
// web端验证码登录
void loginInByCode() async {
if ((msgCodeFormKey.currentState as FormState).validate()) {
(msgCodeFormKey.currentState as FormState).save();
var res = await LoginHttp.loginInByWebSmsCode(
cid: 86,
tel: tel,
code: webSmsCode,
captchaKey: captchaKey,
);
if (res['status']) {
await LoginUtils.confirmLogin('', null);
} else {
SmartDialog.showToast(res['msg']);
}
}
}
// 获取app端验证码
void getAppMsgCode() async {
getCaptcha((data) async {
CaptchaDataModel captchaData = data;
var res = await LoginHttp.sendAppSmsCode(
cid: 86,
tel: 13734077064,
tel: tel,
token: captchaData.token!,
challenge: captchaData.geetest!.challenge!,
validate: captchaData.validate!,
@ -121,7 +185,7 @@ class LoginPageController extends GetxController {
captcha.addEventHandler(onShow: (Map<String, dynamic> message) async {
SmartDialog.dismiss();
}, onClose: (Map<String, dynamic> message) async {
SmartDialog.showToast('关闭验证');
SmartDialog.showToast('取消验证');
}, onResult: (Map<String, dynamic> message) async {
debugPrint("Captcha result: $message");
String code = message["code"];
@ -201,4 +265,72 @@ class LoginPageController extends GetxController {
captcha.startCaptcha(registerData);
} else {}
}
// 获取web端验证码
void getWebMsgCode() async {
getCaptcha((data) async {
CaptchaDataModel captchaData = data;
var res = await LoginHttp.sendWebSmsCode(
cid: 86,
tel: tel,
token: captchaData.token!,
challenge: captchaData.geetest!.challenge!,
validate: captchaData.validate!,
seccode: captchaData.seccode!,
);
if (res['status']) {
captchaKey = res['data']['captcha_key'];
SmartDialog.showToast('验证码已发送');
// 倒计时60s
smsCodeSendStatus.value = true;
startTimer();
} else {
SmartDialog.showToast(res['msg']);
}
});
}
// 验证码倒计时
void startTimer() {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (seconds.value > 0) {
seconds.value--;
} else {
seconds.value = 60;
smsCodeSendStatus.value = false;
timer.cancel();
}
});
}
// 获取登录二维码
Future getWebQrcode() async {
var res = await LoginHttp.getWebQrcode();
validSeconds.value = 180;
if (res['status']) {
qrcodeKey = res['data']['qrcode_key'];
validTimer = Timer.periodic(const Duration(seconds: 1), (validTimer) {
if (validSeconds.value > 0) {
validSeconds.value--;
queryWebQrcodeStatus();
} else {
getWebQrcode();
validTimer.cancel();
}
});
return res;
} else {
SmartDialog.showToast(res['msg']);
}
}
// 轮询二维码登录状态
Future queryWebQrcodeStatus() async {
var res = await LoginHttp.queryWebQrcodeStatus(qrcodeKey);
if (res['status']) {
await LoginUtils.confirmLogin('', null);
validTimer?.cancel();
Get.back();
}
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'controller.dart';
@ -14,8 +15,10 @@ class _LoginPageState extends State<LoginPage> {
final LoginPageController _loginPageCtr = Get.put(LoginPageController());
@override
void initState() {
super.initState();
void dispose() {
_loginPageCtr.validTimer?.cancel();
_loginPageCtr.timer?.cancel();
super.dispose();
}
@override
@ -37,6 +40,107 @@ class _LoginPageState extends State<LoginPage> {
icon: const Icon(Icons.arrow_back),
),
),
actions: [
IconButton(
tooltip: '浏览器打开',
onPressed: () {
Get.offNamed(
'/webview',
parameters: {
'url': 'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
},
icon: const Icon(Icons.language, size: 20),
),
IconButton(
tooltip: '二维码登录',
onPressed: () {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setState) {
return AlertDialog(
title: Row(
children: [
const Text('扫码登录'),
IconButton(
onPressed: () {
setState(() {});
},
icon: const Icon(Icons.refresh),
),
],
),
contentPadding: const EdgeInsets.fromLTRB(0, 0, 0, 4),
content: AspectRatio(
aspectRatio: 1,
child: Container(
width: 200,
padding: const EdgeInsets.all(12),
child: FutureBuilder(
future: _loginPageCtr.getWebQrcode(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
return QrImageView(
data: data['data']['url'],
backgroundColor: Colors.white,
);
} else {
return const Center(
child: SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(),
),
);
}
},
),
),
),
actions: [
TextButton(
onPressed: () {},
child: Obx(() {
return Text(
'有效期: ${_loginPageCtr.validSeconds.value}s',
style: Theme.of(context).textTheme.titleMedium,
);
}),
),
TextButton(
onPressed: () {},
child: Text(
'检查登录状态',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleMedium!
.fontSize,
),
),
)
],
);
});
},
).then((value) {
_loginPageCtr.validTimer!.cancel();
});
},
icon: const Icon(Icons.qr_code, size: 20),
),
const SizedBox(width: 22),
],
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
@ -64,17 +168,9 @@ class _LoginPageState extends State<LoginPage> {
fontSize: 34,
fontWeight: FontWeight.w500),
),
Row(
children: [
Text(
'请使用您的 BiliBili 账号登录。',
style: Theme.of(context).textTheme.titleSmall!,
),
GestureDetector(
onTap: () {},
child: const Icon(Icons.info_outline, size: 16),
)
],
Text(
'请使用您的 BiliBili 账号登录。',
style: Theme.of(context).textTheme.titleSmall!,
),
Container(
margin: const EdgeInsets.only(top: 38, bottom: 15),
@ -93,35 +189,12 @@ class _LoginPageState extends State<LoginPage> {
validator: (v) {
return v!.trim().isNotEmpty ? null : "手机号码不能为空";
},
onSaved: (val) {
print(val);
},
onSaved: (val) => _loginPageCtr.tel = int.parse(val!),
onEditingComplete: () {
_loginPageCtr.nextStep();
},
),
),
GestureDetector(
onTap: () {
Get.offNamed(
'/webview',
parameters: {
'url':
'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
},
child: Padding(
padding: const EdgeInsets.only(left: 2),
child: Text(
'使用网页端登录',
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
),
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -176,8 +249,7 @@ class _LoginPageState extends State<LoginPage> {
IconButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
WidgetStateProperty.resolveWith((states) {
return Theme.of(context)
.colorScheme
.primary
@ -236,7 +308,7 @@ class _LoginPageState extends State<LoginPage> {
.primary, // 设置按钮背景色
),
onPressed: () =>
_loginPageCtr.loginInByAppPassword(),
_loginPageCtr.loginInByWebPassword(),
child: const Text('确认登录'),
)
],
@ -267,8 +339,7 @@ class _LoginPageState extends State<LoginPage> {
IconButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
WidgetStateProperty.resolveWith((states) {
return Theme.of(context)
.colorScheme
.primary
@ -308,21 +379,28 @@ class _LoginPageState extends State<LoginPage> {
? null
: "验证码不能为空";
},
onSaved: (val) {
print(val);
},
onSaved: (val) => _loginPageCtr.webSmsCode =
int.parse(val!),
),
Positioned(
right: 8,
top: 4,
child: Center(
child: TextButton(
onPressed: () =>
_loginPageCtr.getMsgCode(),
child: const Text('获取验证码'),
Obx(() {
return Positioned(
right: 8,
top: 0,
child: Center(
child: TextButton(
onPressed: _loginPageCtr
.smsCodeSendStatus.value
? null
: () =>
_loginPageCtr.getWebMsgCode(),
child: _loginPageCtr
.smsCodeSendStatus.value
? Text(
'重新获取(${_loginPageCtr.seconds.value}s)')
: const Text('获取验证码')),
),
),
),
);
})
],
),
),

View File

@ -40,10 +40,10 @@ class MainController extends GetxController {
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)];
setNavBarConfig();
if (dynamicBadgeType.value != DynamicBadgeMode.hidden) {
getUnreadDynamic();
}
setNavBarConfig();
}
void onBackPressed(BuildContext context) {

View File

@ -138,14 +138,14 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
duration: const Duration(milliseconds: 500),
offset: Offset(0, snapshot.data ? 0 : 1),
child: GlobalData().enableMYBar
? NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: _mainController.selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: Obx(
() => Badge(
? Obx(
() => NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: _mainController.selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: Badge(
label: _mainController
.dynamicBadgeType.value ==
DynamicBadgeMode.number
@ -159,25 +159,25 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
e['count'] > 0,
child: e['icon'],
),
),
selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
)
: BottomNavigationBar(
currentIndex: _mainController.selectedIndex,
type: BottomNavigationBarType.fixed,
onTap: (value) => setIndex(value),
iconSize: 16,
selectedFontSize: 12,
unselectedFontSize: 12,
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
icon: Obx(
() => Badge(
: Obx(
() => BottomNavigationBar(
currentIndex: _mainController.selectedIndex,
type: BottomNavigationBarType.fixed,
onTap: (value) => setIndex(value),
iconSize: 16,
selectedFontSize: 12,
unselectedFontSize: 12,
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
icon: Badge(
label: _mainController
.dynamicBadgeType.value ==
DynamicBadgeMode.number
@ -191,12 +191,12 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
e['count'] > 0,
child: e['icon'],
),
),
activeIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
activeIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
),
);
},

View File

@ -105,7 +105,7 @@ class _MediaPageState extends State<MediaPage>
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
ListTile(
onTap: () {},
onTap: () => Get.toNamed('/fav'),
leading: null,
dense: true,
title: Padding(
@ -178,10 +178,10 @@ class _MediaPageState extends State<MediaPage>
child: Center(
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
padding: WidgetStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith(
WidgetStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme

View File

@ -90,9 +90,8 @@ class _MemberPageState extends State<MemberPage>
() => Text(
_memberController.memberInfo.value.name ?? '',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground,
color:
Theme.of(context).colorScheme.onSurface,
fontSize: 14),
),
),

View File

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

View File

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

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.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/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart';
class MemberSeasonsItem extends StatelessWidget {
@ -29,6 +31,11 @@ class MemberSeasonsItem extends StatelessWidget {
Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid',
arguments: {'videoItem': seasonItem, 'heroTag': heroTag});
},
onLongPress: () => imageSaveDialog(
context,
seasonItem,
SmartDialog.dismiss,
),
child: Column(
children: [
AspectRatio(

View File

@ -33,15 +33,7 @@ class MineController extends GetxController {
onLogin() async {
if (!userLogin.value) {
Get.toNamed(
'/webview',
parameters: {
'url': 'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
// Get.toNamed('/loginPage');
Get.toNamed('/loginPage', preventDuplicates: false);
} else {
int mid = userInfo.value.mid!;
String face = userInfo.value.face!;

View File

@ -3,8 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
@ -82,15 +80,6 @@ class _ZonePageState extends State<ZonePage>
return VideoCardH(
videoItem: _zoneController.videoList[index],
showPubdate: true,
longPress: () {
_zoneController.popupDialog = _createPopupDialog(
_zoneController.videoList[index]);
Overlay.of(context)
.insert(_zoneController.popupDialog!);
},
longPressEnd: () {
_zoneController.popupDialog?.remove();
},
);
}, childCount: _zoneController.videoList.length),
),
@ -126,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

@ -1,4 +1,5 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/video.dart';
@ -106,4 +107,10 @@ class RcmdController extends GetxController {
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
void blockUserCb(mid) {
videoList.removeWhere((e) => e.owner.mid == mid);
videoList.refresh();
SmartDialog.showToast('已移除相关视频');
}
}

View File

@ -5,9 +5,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_v.dart';
import 'package:pilipala/utils/main_stream.dart';
@ -118,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) {
// double maxWidth = Get.size.width;
// int baseWidth = 500;
@ -158,14 +146,7 @@ class _RcmdPageState extends State<RcmdPage>
? VideoCardV(
videoItem: videoList[index],
crossAxisCount: crossAxisCount,
longPress: () {
_rcmdController.popupDialog =
_createPopupDialog(videoList[index]);
Overlay.of(context).insert(_rcmdController.popupDialog!);
},
longPressEnd: () {
_rcmdController.popupDialog?.remove();
},
blockUserCb: (mid) => ctr.blockUserCb(mid),
)
: const VideoCardVSkeleton();
},

View File

@ -5,18 +5,25 @@ class SearchText extends StatelessWidget {
final Function? onSelect;
final int? searchTextIdx;
final Function? onLongSelect;
final bool isSelect;
const SearchText({
super.key,
this.searchText,
this.onSelect,
this.searchTextIdx,
this.onLongSelect,
this.isSelect = false,
});
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
color: isSelect
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
child: Padding(
padding: EdgeInsets.zero,
@ -34,7 +41,10 @@ class SearchText extends StatelessWidget {
child: Text(
searchText!,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant),
color: isSelect
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),

View File

@ -16,14 +16,18 @@ class SearchPanelController extends GetxController {
RxString order = ''.obs;
// 视频时长筛选 仅用于搜索视频
RxInt duration = 0.obs;
// 视频分区筛选 仅用于搜索视频 -1时不传
RxInt tids = (-1).obs;
Future onSearch({type = 'init'}) async {
var result = await SearchHttp.searchByType(
searchType: searchType!,
keyword: keyword!,
page: page.value,
order: searchType!.type != 'video' ? null : order.value,
duration: searchType!.type != 'video' ? null : duration.value);
searchType: searchType!,
keyword: keyword!,
page: page.value,
order: searchType!.type != 'video' ? null : order.value,
duration: searchType!.type != 'video' ? null : duration.value,
tids: searchType!.type != 'video' ? null : tids.value,
);
if (result['status']) {
if (type == 'onRefresh') {
resultList.value = result['data'].list;

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.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/widgets/network_img_layer.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart';
Widget searchLivePanel(BuildContext context, ctr, list) {
@ -42,15 +44,15 @@ class LiveItem extends StatelessWidget {
Get.toNamed('/liveRoom?roomid=${liveItem.roomid}',
arguments: {'liveItem': liveItem, 'heroTag': heroTag});
},
onLongPress: () => imageSaveDialog(
context,
liveItem,
SmartDialog.dismiss,
),
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
borderRadius: const BorderRadius.all(StyleString.imgRadius),
child: AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
@ -108,7 +110,7 @@ class LiveContent extends StatelessWidget {
RichText(
text: TextSpan(
children: [
for (var i in liveItem.title) ...[
for (var i in liveItem.titleList) ...[
TextSpan(
text: i['text'],
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/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/utils.dart';
Widget searchMbangumiPanel(BuildContext context, ctr, list) {
@ -63,7 +64,7 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface),
children: [
for (var i in i.title) ...[
for (var i in i.titleList) ...[
TextSpan(
text: i['text'],
style: TextStyle(
@ -108,28 +109,8 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
SizedBox(
height: 32,
child: ElevatedButton(
onPressed: () async {
SmartDialog.showLoading(msg: '获取中...');
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'],
},
);
}
});
onPressed: () {
RoutePush.bangumiPush(i.seasonId, null);
},
child: const Text('观看'),
),

View File

@ -3,6 +3,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/search/widgets/search_text.dart';
import 'package:pilipala/pages/search_panel/index.dart';
class SearchVideoPanel extends StatelessWidget {
@ -35,7 +36,11 @@ class SearchVideoPanel extends StatelessWidget {
padding: index == 0
? const EdgeInsets.only(top: 2)
: EdgeInsets.zero,
child: VideoCardH(videoItem: i, showPubdate: true),
child: VideoCardH(
videoItem: i,
showPubdate: true,
source: 'search',
),
);
},
),
@ -88,9 +93,9 @@ class SearchVideoPanel extends StatelessWidget {
height: 32,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
padding: WidgetStateProperty.all(EdgeInsets.zero),
),
onPressed: () => controller.onShowFilterDialog(ctr),
onPressed: () => controller.onShowFilterSheet(ctr),
icon: Icon(
Icons.filter_list_outlined,
size: 18,
@ -161,7 +166,33 @@ class VideoPanelController extends GetxController {
{'label': '30-60分钟', 'value': 3},
{'label': '60分钟+', 'value': 4},
];
List<Map<String, dynamic>> partFiltersList = [
{'label': '全部', 'value': -1},
{'label': '动画', 'value': 1},
{'label': '番剧', 'value': 13},
{'label': '国创', 'value': 167},
{'label': '音乐', 'value': 3},
{'label': '舞蹈', 'value': 129},
{'label': '游戏', 'value': 4},
{'label': '知识', 'value': 36},
{'label': '科技', 'value': 188},
{'label': '运动', 'value': 234},
{'label': '汽车', 'value': 223},
{'label': '生活', 'value': 160},
{'label': '美食', 'value': 211},
{'label': '动物', 'value': 217},
{'label': '鬼畜', 'value': 119},
{'label': '时尚', 'value': 155},
{'label': '资讯', 'value': 202},
{'label': '娱乐', 'value': 5},
{'label': '影视', 'value': 181},
{'label': '记录', 'value': 177},
{'label': '电影', 'value': 23},
{'label': '电视', 'value': 11},
];
RxInt currentTimeFilterval = 0.obs;
RxInt currentPartFilterval = (-1).obs;
@override
void onInit() {
@ -215,4 +246,100 @@ class VideoPanelController extends GetxController {
},
);
}
onShowFilterSheet(searchPanelCtr) {
showModalBottomSheet(
context: Get.context!,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setState) {
return Container(
color: Theme.of(Get.context!).colorScheme.surface,
padding: const EdgeInsets.only(top: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTile(
title: Text('内容时长'),
),
Padding(
padding: const EdgeInsets.only(
left: 14,
right: 14,
bottom: 14,
),
child: Wrap(
spacing: 10,
runSpacing: 10,
direction: Axis.horizontal,
textDirection: TextDirection.ltr,
children: [
for (var i in timeFiltersList)
Obx(
() => SearchText(
searchText: i['label'],
searchTextIdx: i['value'],
isSelect:
currentTimeFilterval.value == i['value'],
onSelect: (value) async {
currentTimeFilterval.value = i['value'];
setState(() {});
SmartDialog.showToast("${i['label']}」的筛选结果");
SearchPanelController ctr =
Get.find<SearchPanelController>(
tag: 'video${searchPanelCtr.keyword!}');
ctr.duration.value = i['value'];
Get.back();
SmartDialog.showLoading(msg: '获取中');
await ctr.onRefresh();
SmartDialog.dismiss();
},
onLongSelect: (value) => {},
),
)
],
),
),
const ListTile(
title: Text('内容分区'),
),
Padding(
padding: const EdgeInsets.only(left: 14, right: 14),
child: Wrap(
spacing: 10,
runSpacing: 10,
direction: Axis.horizontal,
textDirection: TextDirection.ltr,
children: [
for (var i in partFiltersList)
SearchText(
searchText: i['label'],
searchTextIdx: i['value'],
isSelect: currentPartFilterval.value == i['value'],
onSelect: (value) async {
currentPartFilterval.value = i['value'];
setState(() {});
SmartDialog.showToast("${i['label']}」的筛选结果");
SearchPanelController ctr =
Get.find<SearchPanelController>(
tag: 'video${searchPanelCtr.keyword!}');
ctr.tids.value = i['value'];
Get.back();
SmartDialog.showLoading(msg: '获取中');
await ctr.onRefresh();
SmartDialog.dismiss();
},
onLongSelect: (value) => {},
)
],
),
)
],
),
);
},
);
},
);
}
}

View File

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

View File

@ -240,10 +240,10 @@ class _ExtraSettingState extends State<ExtraSetting> {
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.isNotEmpty &&
states.first == MaterialState.selected) {
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null; // All other states will use the default thumbIcon.

View File

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/action_type.dart';
import 'package:pilipala/utils/global_data.dart';
import '../../../utils/storage.dart';
class ActionMenuSetPage extends StatefulWidget {
const ActionMenuSetPage({super.key});
@override
State<ActionMenuSetPage> createState() => _ActionMenuSetPageState();
}
class _ActionMenuSetPageState extends State<ActionMenuSetPage> {
Box setting = GStrorage.setting;
late List<String> actionTypeSort;
late List<Map> allLabels;
@override
void initState() {
super.initState();
actionTypeSort = setting.get(SettingBoxKey.actionTypeSort,
defaultValue: ['like', 'coin', 'collect', 'watchLater', 'share']);
allLabels = actionMenuConfig;
allLabels.sort((a, b) {
int indexA = actionTypeSort.indexOf((a['value'] as ActionType).value);
int indexB = actionTypeSort.indexOf((b['value'] as ActionType).value);
if (indexA == -1) indexA = actionTypeSort.length;
if (indexB == -1) indexB = actionTypeSort.length;
return indexA.compareTo(indexB);
});
}
void saveEdit() {
List<String> sortedTabbar = allLabels
.where((i) => actionTypeSort.contains((i['value'] as ActionType).value))
.map<String>((i) => (i['value'] as ActionType).value)
.toList();
setting.put(SettingBoxKey.actionTypeSort, sortedTabbar);
GlobalData().actionTypeSort = sortedTabbar;
SmartDialog.showToast('操作成功');
}
void onReorder(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final tabsItem = allLabels.removeAt(oldIndex);
allLabels.insert(newIndex, tabsItem);
});
}
@override
Widget build(BuildContext context) {
final listTiles = [
for (int i = 0; i < allLabels.length; i++) ...[
CheckboxListTile(
key: Key((allLabels[i]['value'] as ActionType).value),
value: actionTypeSort
.contains((allLabels[i]['value'] as ActionType).value),
onChanged: (bool? newValue) {
String actionTypeId = (allLabels[i]['value'] as ActionType).value;
if (!newValue!) {
actionTypeSort.remove(actionTypeId);
} else {
actionTypeSort.add(actionTypeId);
}
setState(() {});
},
title: Row(
children: [
allLabels[i]['icon'],
const SizedBox(width: 8),
Text(allLabels[i]['label']),
],
),
secondary: const Icon(Icons.drag_indicator_rounded),
)
]
];
return Scaffold(
appBar: AppBar(
title: const Text('视频操作菜单'),
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,
),
);
}
}

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