mod: merge main

This commit is contained in:
guozhigq
2023-05-15 09:52:09 +08:00
141 changed files with 7011 additions and 363 deletions

View File

@ -47,10 +47,11 @@ android {
applicationId "com.example.pilipala"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
// minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
minSdkVersion 19
}
buildTypes {

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#ffffff</color>
</resources>

Binary file not shown.

BIN
assets/fonts/fansCard.ttf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/images/lv/lv0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

BIN
assets/images/lv/lv1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

BIN
assets/images/lv/lv2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

BIN
assets/images/lv/lv3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

BIN
assets/images/lv/lv4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

BIN
assets/images/lv/lv5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

BIN
assets/images/lv/lv6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

View File

@ -2,23 +2,44 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- image_gallery_saver (1.5.0):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.0.4):
- Flutter
- ReachabilitySwift (5.0.0)
- share_plus (0.0.1):
- Flutter
- sqflite (0.0.2):
- Flutter
- FMDB (>= 2.7.5)
- url_launcher_ios (0.0.1):
- Flutter
- webview_cookie_manager (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_cookie_manager (from `.symlinks/plugins/webview_cookie_manager/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
SPEC REPOS:
trunk:
@ -28,21 +49,42 @@ SPEC REPOS:
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
image_gallery_saver:
:path: ".symlinks/plugins/image_gallery_saver/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_cookie_manager:
:path: ".symlinks/plugins/webview_cookie_manager/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
COCOAPODS: 1.11.3
COCOAPODS: 1.12.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,119 @@
import 'package:pilipala/common/constants.dart';
import 'package:flutter/material.dart';
import 'skeleton.dart';
class VideoCardHSkeleton extends StatefulWidget {
const VideoCardHSkeleton({super.key});
@override
State<VideoCardHSkeleton> createState() => _VideoCardHSkeletonState();
}
class _VideoCardHSkeletonState extends State<VideoCardHSkeleton> {
@override
Widget build(BuildContext context) {
return Skeleton(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.cardSpace, 7, StyleString.cardSpace, 7),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return SizedBox(
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
double PR = MediaQuery.of(context).devicePixelRatio;
return Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
borderRadius: BorderRadius.circular(
StyleString.imgRadius.x),
),
);
},
),
),
// VideoContent(videoItem: videoItem)
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
width: 200,
height: 11,
margin: const EdgeInsets.only(bottom: 5),
),
Container(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
width: 150,
height: 13,
),
const Spacer(),
Container(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
width: 100,
height: 13,
margin: const EdgeInsets.only(bottom: 5),
),
Row(
children: [
Container(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
width: 40,
height: 13,
margin: const EdgeInsets.only(right: 8),
),
Container(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
width: 40,
height: 13,
),
],
)
],
),
)),
],
),
);
},
),
),
Divider(
height: 1,
indent: 8,
endIndent: 12,
color: Theme.of(context).dividerColor.withOpacity(0.08),
)
],
),
);
}
}

View File

@ -23,14 +23,7 @@ class VideoCardVSkeleton extends StatelessWidget {
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: Theme.of(context)
.colorScheme
.outline
.withOpacity(0.1),
),
),
);
},
@ -49,15 +42,15 @@ class VideoCardVSkeleton extends StatelessWidget {
Container(
width: 200,
height: 13,
margin: const EdgeInsets.only(bottom: 5),
color: Theme.of(context).colorScheme.background,
),
const SizedBox(height: 5),
Container(
width: 150,
height: 13,
margin: const EdgeInsets.only(bottom: 12),
color: Theme.of(context).colorScheme.background,
),
const SizedBox(height: 12),
Container(
width: 80,
height: 13,

View File

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'skeleton.dart';
class VideoReplySkeleton extends StatelessWidget {
const VideoReplySkeleton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
return Skeleton(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 8, 2),
child: Row(
children: [
ClipOval(
child: Container(
width: 34,
height: 34,
color: bgColor,
),
),
const SizedBox(width: 12),
Container(
width: 80,
height: 13,
color: bgColor,
)
],
),
),
Container(
width: double.infinity,
margin:
const EdgeInsets.only(top: 4, left: 57, right: 6, bottom: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
width: 300,
height: 14,
margin: const EdgeInsets.only(bottom: 4),
color: bgColor,
),
Container(
width: 180,
height: 14,
margin: const EdgeInsets.only(bottom: 10),
color: bgColor,
),
Row(
children: [
Container(
width: 40,
height: 14,
margin: const EdgeInsets.only(bottom: 4),
color: bgColor,
),
const Spacer(),
Container(
width: 40,
height: 14,
margin: const EdgeInsets.only(bottom: 4),
color: bgColor,
),
const SizedBox(width: 8)
],
)
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
const AppBarWidget({
required this.child,
required this.controller,
required this.visible,
Key? key,
}) : super(key: key);
final PreferredSizeWidget child;
final AnimationController controller;
final bool visible;
@override
// TODO: implement preferredSize
Size get preferredSize => child.preferredSize;
@override
Widget build(BuildContext context) {
visible ? controller.reverse() : controller.forward();
return SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(0, -1),
).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOutBack,
)),
child: child,
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class HttpError extends StatelessWidget {
HttpError({required this.errMsg, required this.fn, super.key});
String errMsg = '';
final Function()? fn;
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: SizedBox(
height: 150,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
errMsg,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
fn!();
},
child: const Text('点击重试'))
],
),
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:pilipala/common/constants.dart';
class NetworkImgLayer extends StatelessWidget {
final String? src;
@ -29,11 +30,16 @@ class NetworkImgLayer extends StatelessWidget {
// double pr = 2;
return src != ''
? ClipRRect(
borderRadius: BorderRadius.circular(type == 'avatar' ? 50 : 4),
borderRadius: BorderRadius.circular(type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x),
child: CachedNetworkImage(
imageUrl: src!,
width: width ?? double.infinity,
height: height ?? double.infinity,
alignment: Alignment.center,
maxWidthDiskCache: ((cacheW ?? width!) * pr).toInt(),
// maxHeightDiskCache: (cacheH ?? height!).toInt(),
memCacheWidth: ((cacheW ?? width!) * pr).toInt(),

View File

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/utils/utils.dart';
@ -11,21 +12,21 @@ class StatDanMu extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color color =
theme == 'white' ? Colors.white : Theme.of(context).colorScheme.outline;
return Row(
children: [
Image.asset(
'assets/images/dm_$theme.png',
width: size == 'medium' ? 16 : 14,
height: size == 'medium' ? 16 : 14,
Icon(
CupertinoIcons.ellipses_bubble,
size: 14,
color: color,
),
const SizedBox(width: 2),
const SizedBox(width: 3),
Text(
Utils.numFormat(danmu!),
style: TextStyle(
fontSize: size == 'medium' ? 12 : 11,
color: theme == 'white'
? Colors.white
: Theme.of(context).colorScheme.outline,
color: color,
),
)
],

View File

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
class UpTag extends StatelessWidget {
const UpTag({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 14,
height: 10,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
border: Border.all(color: Theme.of(context).colorScheme.outline)),
margin: const EdgeInsets.only(right: 4),
child: Center(
child: Text(
'UP',
style: TextStyle(
fontSize: 6, color: Theme.of(context).colorScheme.outline),
),
),
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/utils/utils.dart';
@ -6,26 +7,26 @@ class StatView extends StatelessWidget {
final int? view;
final String? size;
const StatView({Key? key, this.theme, this.view, this.size}) : super(key: key);
const StatView({Key? key, this.theme, this.view, this.size})
: super(key: key);
@override
Widget build(BuildContext context) {
Color color =
theme == 'white' ? Colors.white : Theme.of(context).colorScheme.outline;
return Row(
children: [
Image.asset(
'assets/images/view_$theme.png',
width: size == 'medium' ? 16 : 14,
height: size == 'medium' ? 16 : 14,
Icon(
CupertinoIcons.play_rectangle,
size: 13,
color: color,
),
const SizedBox(width: 2),
const SizedBox(width: 3),
Text(
Utils.numFormat(view!),
// videoItem['stat']['view'].toString(),
style: TextStyle(
fontSize: size == 'medium' ? 12 : 11,
color: theme == 'white'
? Colors.white
: Theme.of(context).colorScheme.outline,
color: color,
),
),
],

View File

@ -7,6 +7,7 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 水平布局
class VideoCardH extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
var videoItem;
Function()? longPress;
Function()? longPressEnd;
@ -20,9 +21,9 @@ class VideoCardH extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
child: Ink(
child: GestureDetector(
int aid = videoItem.aid;
String heroTag = Utils.makeHeroTag(aid);
return GestureDetector(
onLongPress: () {
longPress!();
},
@ -32,14 +33,16 @@ class VideoCardH extends StatelessWidget {
child: InkWell(
onTap: () async {
await Future.delayed(const Duration(milliseconds: 200));
int aid = videoItem.aid ?? videoItem.id;
Get.toNamed('/video?aid=$aid',
arguments: {'videoItem': videoItem});
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
},
child: Container(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.cardSpace, 5, StyleString.cardSpace, 5),
child: LayoutBuilder(builder: (context, boxConstraints) {
StyleString.cardSpace, 7, StyleString.cardSpace, 7),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return SizedBox(
@ -50,22 +53,24 @@ class VideoCardH extends StatelessWidget {
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
// child: ClipRRect(
// borderRadius: StyleString.mdRadius,
child: LayoutBuilder(
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
double PR = MediaQuery.of(context).devicePixelRatio;
double PR =
MediaQuery.of(context).devicePixelRatio;
return Stack(
children: [
NetworkImgLayer(
Hero(
tag: heroTag,
child: NetworkImgLayer(
// src: videoItem['pic'] +
// '@${(maxWidth * 2).toInt()}w',
src: videoItem.pic + '@.webp',
width: maxWidth,
height: maxHeight,
),
),
// Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,),
Positioned(
right: 4,
@ -74,30 +79,36 @@ class VideoCardH extends StatelessWidget {
padding: const EdgeInsets.symmetric(
vertical: 1, horizontal: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.black54.withOpacity(0.4)),
borderRadius:
BorderRadius.circular(4),
color:
Colors.black54.withOpacity(0.4)),
child: Text(
Utils.timeFormat(videoItem.duration!),
style: const TextStyle(
fontSize: 11, color: Colors.white),
),
),
// Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,),
)
],
);
},
),
// ),
),
VideoContent(videoItem: videoItem)
],
),
);
}),
// height: 124,
},
),
),
Divider(
height: 1,
indent: 8,
endIndent: 12,
color: Theme.of(context).dividerColor.withOpacity(0.08),
)
],
),
),
);
@ -126,7 +137,7 @@ class VideoContent extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
const Spacer(),
if (videoItem.rcmdReason != '' &&
if (videoItem.rcmdReason != null &&
videoItem.rcmdReason.content != '')
Container(
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5),
@ -145,12 +156,6 @@ class VideoContent extends StatelessWidget {
const SizedBox(height: 4),
Row(
children: [
Image.asset(
'assets/images/up_gray.png',
width: 14,
height: 12,
),
const SizedBox(width: 2),
Text(
videoItem.owner.name,
style: TextStyle(

View File

@ -22,6 +22,7 @@ class VideoCardV extends StatelessWidget {
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id);
return Card(
elevation: 0.8,
clipBehavior: Clip.hardEdge,
@ -40,7 +41,7 @@ class VideoCardV extends StatelessWidget {
onTap: () async {
await Future.delayed(const Duration(milliseconds: 200));
Get.toNamed('/video?aid=${videoItem.id}',
arguments: {'videoItem': videoItem});
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
},
child: Column(
children: [
@ -57,13 +58,16 @@ class VideoCardV extends StatelessWidget {
double PR = MediaQuery.of(context).devicePixelRatio;
return Stack(
children: [
NetworkImgLayer(
Hero(
tag: heroTag,
child: NetworkImgLayer(
// 指定图片尺寸
// src: videoItem.pic + '@${(maxWidth * 2).toInt()}w',
src: videoItem.pic + '@.webp',
width: maxWidth,
height: maxHeight,
),
),
Positioned(
left: 0,
right: 0,
@ -77,7 +81,7 @@ class VideoCardV extends StatelessWidget {
duration: videoItem.duration,
),
),
)
),
],
);
}),
@ -141,6 +145,25 @@ class VideoContent extends StatelessWidget {
),
),
const SizedBox(width: 4)
] else if (videoItem.isFollowed == 1) ...[
Container(
padding: const EdgeInsets.fromLTRB(3, 1, 3, 1),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.6),
borderRadius: BorderRadius.circular(3)),
child: Text(
'已关注',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelSmall!.fontSize,
color: Theme.of(context).colorScheme.primary,
),
),
),
const SizedBox(width: 4)
],
Expanded(
child: LayoutBuilder(builder:

View File

@ -1,9 +1,124 @@
class Api {
// 推荐视频
static const String recommendList = '/x/web-interface/index/top/feed/rcmd';
static const String recommendList = '/x/web-interface/index/top/rcmd';
// 热门视频
static const String hotList = '/x/web-interface/popular';
// 视频详情
// 竖屏 https://api.bilibili.com/x/web-interface/view?aid=527403921
static const String videoDetail = '/x/web-interface/view';
// https://api.bilibili.com/x/web-interface/view/detail 获取视频超详细信息(web端)
static const String videoIntro = '/x/web-interface/view';
// 视频详情 超详细
// https://api.bilibili.com/x/web-interface/view/detail?aid=527403921
/// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/action.md
// 点赞 Post
/// aid num 稿件avid 必要(可选) avid与bvid任选一个
/// bvid str 稿件bvid 必要(可选) avid与bvid任选一个
/// like num 操作方式 必要 1点赞 2取消赞
// csrf str CSRF Token位于cookie 必要
// https://api.bilibili.com/x/web-interface/archive/like
static const String likeVideo = '/x/web-interface/archive/like';
//判断视频是否被点赞双端Get
// access_key str APP登录Token APP方式必要
/// aid num 稿件avid 必要(可选) avid与bvid任选一个
/// bvid str 稿件bvid 必要(可选) avid与bvid任选一个
// https://api.bilibili.com/x/web-interface/archive/has/like
static const String hasLikeVideo = '/x/web-interface/archive/has/like';
// 视频点踩 web端不支持
// 投币视频web端POST
/// aid num 稿件avid 必要(可选) avid与bvid任选一个
/// bvid str 稿件bvid 必要(可选) avid与bvid任选一个
/// multiply num 投币数量 必要 上限为2
/// select_like num 是否附加点赞 非必要 0不点赞 1同时点赞 默认为0
// csrf str CSRF Token位于cookie 必要
// https://api.bilibili.com/x/web-interface/coin/add
static const String coinVideo = '/x/web-interface/coin/add';
// 判断视频是否被投币双端GET
// access_key str APP登录Token APP方式必要
/// aid num 稿件avid 必要(可选) avid与bvid任选一个
/// bvid str 稿件bvid 必要(可选) avid与bvid任选一个
/// https://api.bilibili.com/x/web-interface/archive/coins
static const String hasCoinVideo = '/x/web-interface/archive/coins';
// 收藏视频双端POST
// access_key str APP登录Token APP方式必要
/// rid num 稿件avid 必要
/// type num 必须为2 必要
/// add_media_ids nums 需要加入的收藏夹mlid 非必要 同时添加多个,用,%2C分隔
/// del_media_ids nums 需要取消的收藏夹mlid 非必要 同时取消多个,用,%2C分隔
// csrf str CSRF Token位于cookie Cookie方式必要
// https://api.bilibili.com/medialist/gateway/coll/resource/deal
// https://api.bilibili.com/x/v3/fav/resource/deal
static const String favVideo = '/x/v3/fav/resource/deal';
// 判断视频是否被收藏双端GET
/// aid
// https://api.bilibili.com/x/v2/fav/video/favoured
static const String hasFavVideo = '/x/v2/fav/video/favoured';
// 分享视频 Web端 POST
// https://api.bilibili.com/x/web-interface/share/add
// aid num 稿件avid 必要(可选) avid与bvid任选一个
// bvid str 稿件bvid 必要(可选) avid与bvid任选一个
// csrf str CSRF Token位于cookie 必要
// 一键三连
// https://api.bilibili.com/x/web-interface/archive/like/triple
// aid num 稿件avid 必要(可选) avid与bvid任选一个
// bvid str 稿件bvid 必要(可选) avid与bvid任选一个
// csrf str CSRF Token位于cookie 必要
static const String oneThree = '/x/web-interface/archive/like/triple';
// 获取指定用户创建的所有收藏夹信息
// 该接口也能查询目标内容id存在于那些收藏夹中
// up_mid num 目标用户mid 必要
// type num 目标内容属性 非必要 默认为全部 0全部 2视频稿件
// rid num 目标 视频稿件avid
static const String videoInFolder = '/x/v3/fav/folder/created/list-all';
// 视频详情页 相关视频
static const String relatedList = '/x/web-interface/archive/related';
// 评论列表
static const String replyList = '/x/v2/reply';
// 楼中楼
static const String replyReplyList = '/x/v2/reply/reply';
// 发表评论
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/comment/action.md
static const String replyAdd = '/x/v2/reply/add';
// 用户(被)关注数、投稿数
// https://api.bilibili.com/x/relation/stat?vmid=697166795
static const String userStat = '/x/relation/stat';
// 获取用户信息
static const String userInfo = '/x/web-interface/nav';
// 获取当前用户状态
static const String userStatOwner = '/x/web-interface/nav/stat';
// 收藏夹
// https://api.bilibili.com/x/v3/fav/folder/created/list?pn=1&ps=10&up_mid=17340771
static const String userFavFolder = '/x/v3/fav/folder/created/list';
/// 收藏夹 详情
/// media_id int 收藏夹id
/// pn int 当前页
/// ps int pageSize
/// keyword String 搜索词
/// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿
/// tid int 分区id
// https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0
static const String userFavFolderDetail = '/x/v3/fav/resource/list';
// 正在直播的up & 关注的up
// https://api.bilibili.com/x/polymer/web-dynamic/v1/portal
}

View File

@ -12,6 +12,7 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart';
class Request {
static final Request _instance = Request._internal();
static late CookieManager cookieManager;
factory Request() => _instance;
@ -31,11 +32,9 @@ class Request {
ignoreExpires: true,
storage: FileStorage(cookiePath),
);
dio.interceptors.add(CookieManager(cookieJar));
var cookie = await CookieManager(cookieJar)
.cookieJar
cookieManager = CookieManager(cookieJar);
dio.interceptors.add(cookieManager);
var cookie = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
if (cookie.isEmpty) {
try {
@ -46,6 +45,27 @@ class Request {
}
}
// 移除cookie
static removeCookie() async {
await cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseUrl), []);
await cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseApiUrl), []);
cookieManager.cookieJar.deleteAll();
dio.interceptors.add(cookieManager);
}
// 从cookie中获取 csrf token
static Future<String> getCsrf() async {
var cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseApiUrl));
// for (var i in cookies) {
// print(i);
// }
var token = cookies.firstWhere((e) => e.name == 'bili_jct').value;
return token;
}
/*
* config it and create
*/
@ -111,20 +131,21 @@ class Request {
return response;
} on DioError catch (e) {
print('get error: $e');
return Future.error(ApiInterceptor.dioError(e));
return Future.error(await ApiInterceptor.dioError(e));
}
}
/*
* post请求
*/
post(url, {data, options, cancelToken, extra}) async {
post(url, {data, queryParameters, options, cancelToken, extra}) async {
print('post-data: $data');
Response response;
try {
response = await dio.post(
url,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
@ -132,7 +153,7 @@ class Request {
return response;
} on DioError catch (e) {
print('post error: $e');
return Future.error(ApiInterceptor.dioError(e));
return Future.error(await ApiInterceptor.dioError(e));
}
}

View File

@ -1,10 +1,12 @@
import 'package:dio/dio.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart' hide Response;
class ApiInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// print("请求之前");
print("请求之前");
// 在请求之前添加头部或认证信息
// options.headers['Authorization'] = 'Bearer token';
// options.headers['Content-Type'] = 'application/json';
@ -13,15 +15,14 @@ class ApiInterceptor extends Interceptor {
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// print("响应之前");
handler.next(response);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
void onError(DioError err, ErrorInterceptorHandler handler) async {
// 处理网络请求错误
handler.next(err);
// handler.next(err);
SmartDialog.showToast(await dioError(err));
super.onError(err, handler);
}
@ -43,7 +44,7 @@ class ApiInterceptor extends Interceptor {
return "发送请求超时,请检查网络设置";
case DioErrorType.unknown:
var res = await checkConect();
return "$res 网络异常,请稍后重试!";
return res + " \n 网络异常,请稍后重试!";
default:
return "Dio异常";
}

70
lib/http/reply.dart Normal file
View File

@ -0,0 +1,70 @@
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
class ReplyHttp {
static Future replyList({
required String oid,
required int pageNum,
required int type,
int sort = 1,
}) async {
var res = await Request().get(Api.replyList, data: {
'oid': oid,
'pn': pageNum,
'type': type,
'sort': 1,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
Map errMap = {
-400: '请求错误',
-404: '无此项',
12002: '评论区已关闭',
12009: '评论主体的type不合法',
};
return {
'status': false,
'date': [],
'msg': errMap[res.data['code']] ?? '请求异常',
};
}
}
static Future replyReplyList({
required String oid,
required String root,
required int pageNum,
required int type,
int sort = 1,
}) async {
var res = await Request().get(Api.replyReplyList, data: {
'oid': oid,
'root': root,
'pn': pageNum,
'type': type,
'sort': 1,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
Map errMap = {
-400: '请求错误',
-404: '无此项',
12002: '评论区已关闭',
12009: '评论主体的type不合法',
};
return {
'status': false,
'date': [],
'msg': errMap[res.data['code']] ?? '请求异常',
};
}
}
}

79
lib/http/user.dart Normal file
View File

@ -0,0 +1,79 @@
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/user/fav_detail.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart';
class UserHttp {
static Future<dynamic> userStat({required int mid}) async {
var res = await Request().get(Api.userStat, data: {'vmid': mid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false};
}
}
static Future<dynamic> userInfo() async {
var res = await Request().get(Api.userInfo);
if (res.data['code'] == 0) {
UserInfoData data = UserInfoData.fromJson(res.data['data']);
return {'status': true, 'data': data};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
static Future<dynamic> userStatOwner() async {
var res = await Request().get(Api.userStatOwner);
if (res.data['code'] == 0) {
UserStat data = UserStat.fromJson(res.data['data']);
return {'status': true, 'data': data};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// 收藏夹
static Future<dynamic> userfavFolder({
required int pn,
required int ps,
required int mid,
}) async {
var res = await Request().get(Api.userFavFolder, data: {
'pn': pn,
'ps': ps,
'up_mid': mid,
});
if (res.data['code'] == 0) {
FavFolderData data = FavFolderData.fromJson(res.data['data']);
return {'status': true, 'data': data};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
static Future<dynamic> userFavFolderDetail(
{required int mediaId,
required int pn,
required int ps,
String keyword = '',
String order = 'mtime'}) async {
var res = await Request().get(Api.userFavFolderDetail, data: {
'media_id': mediaId,
'pn': pn,
'ps': ps,
'keyword': keyword,
'order': order,
'type': 0,
'tid': 0
});
if (res.data['code'] == 0) {
FavDetailData data = FavDetailData.fromJson(res.data['data']);
return {'status': true, 'data': data};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
}

227
lib/http/video.dart Normal file
View File

@ -0,0 +1,227 @@
import 'dart:developer';
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/model_hot_video_item.dart';
import 'package:pilipala/models/model_rec_video_item.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video_detail_res.dart';
/// res.data['code'] == 0 请求正常返回结果
/// res.data['data'] 为结果
/// 返回{'status': bool, 'data': List}
/// view层根据 status 判断渲染逻辑
class VideoHttp {
// 首页推荐视频
static Future rcmdVideoList({required int ps, required int freshIdx}) async {
try {
var res = await Request().get(
Api.recommendList,
data: {
'feed_version': 'V4',
'ps': ps,
'fresh_idx': freshIdx,
},
);
if (res.data['code'] == 0) {
List<RecVideoItemModel> list = [];
for (var i in res.data['data']['item']) {
list.add(RecVideoItemModel.fromJson(i));
}
return {'status': true, 'data': list};
} else {
return {'status': false, 'data': [], 'msg': ''};
}
} catch (err) {
return {'status': false, 'data': [], 'msg': err.toString()};
}
}
// 最热视频
static Future hotVideoList({required int pn, required int ps}) async {
try {
var res = await Request().get(
Api.hotList,
data: {'pn': pn, 'ps': ps},
);
if (res.data['code'] == 0) {
List<HotVideoItemModel> list = [];
for (var i in res.data['data']['list']) {
list.add(HotVideoItemModel.fromJson(i));
}
return {'status': true, 'data': list};
} else {
return {'status': false, 'data': []};
}
} catch (err) {
return {'status': false, 'data': [], 'msg': err};
}
}
// 视频信息 标题、简介
static Future videoIntro({required String aid}) async {
var res = await Request().get(Api.videoIntro, data: {'aid': aid});
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
if (result.code == 0) {
return {'status': true, 'data': result.data!};
} else {
Map errMap = {
-400: '请求错误',
-403: '权限不足',
-404: '无视频',
62002: '稿件不可见',
62004: '稿件审核中',
};
return {
'status': false,
'data': null,
'msg': errMap[result.code] ?? '请求异常',
};
}
}
// 相关视频
static Future relatedVideoList({required String aid}) async {
var res = await Request().get(Api.relatedList, data: {'aid': aid});
if (res.data['code'] == 0) {
List<HotVideoItemModel> list = [];
for (var i in res.data['data']) {
list.add(HotVideoItemModel.fromJson(i));
}
return {'status': true, 'data': list};
} else {
return {'status': false, 'data': []};
}
}
// 获取点赞状态
static Future hasLikeVideo({required String aid}) async {
var res = await Request().get(Api.hasLikeVideo, data: {'aid': aid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': []};
}
}
// 获取投币状态
static Future hasCoinVideo({required String aid}) async {
var res = await Request().get(Api.hasCoinVideo, data: {'aid': aid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': true, 'data': []};
}
}
// 获取收藏状态
static Future hasFavVideo({required String aid}) async {
var res = await Request().get(Api.hasFavVideo, data: {'aid': aid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': []};
}
}
// 一键三连
static Future oneThree({required String aid}) async {
var res = await Request().post(
Api.oneThree,
queryParameters: {
'aid': aid,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// (取消)点赞
static Future likeVideo({required String aid, required bool type}) async {
var res = await Request().post(
Api.likeVideo,
queryParameters: {
'aid': aid,
'like': type ? 1 : 2,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// (取消)收藏
static Future favVideo(
{required String aid, String? addIds, String? delIds}) async {
var res = await Request().post(Api.favVideo, queryParameters: {
'rid': aid,
'type': 2,
'add_media_ids': addIds ?? '',
'del_media_ids': delIds ?? '',
'csrf': await Request.getCsrf(),
});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': []};
}
}
// 查看视频被收藏在哪个文件夹
static Future videoInFolder({required int mid, required String rid}) async {
var res = await Request()
.get(Api.videoInFolder, data: {'up_mid': mid, 'rid': rid});
if (res.data['code'] == 0) {
FavFolderData data = FavFolderData.fromJson(res.data['data']);
return {'status': true, 'data': data};
} else {
return {'status': false, 'data': []};
}
}
// 发表评论 replyAdd
// type num 评论区类型代码 必要 类型代码见表
// oid num 目标评论区id 必要
// root num 根评论rpid 非必要 二级评论以上使用
// parent num 父评论rpid 非必要 二级评论同根评论id 大于二级评论为要回复的评论id
// message str 发送评论内容 必要 最大1000字符
// plat num 发送平台标识 非必要 1web端 2安卓客户端 3ios客户端 4wp客户端
static Future replyAdd({
required ReplyType type,
required int oid,
required String message,
int? root,
int? parent,
}) async {
if(message == ''){
return {'status': false, 'data': [], 'msg': '请输入评论内容'};
}
print('root:$root');
print('parent: $parent');
var res = await Request()
.post(Api.replyAdd, queryParameters: {
'type': type.index,
'oid': oid,
'root': root ?? '',
'parent': parent == null || parent == 0 ? '' : parent,
'message': message,
'csrf': await Request.getCsrf(),
});
log(res.toString());
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': []};
}
}
}

View File

@ -1,12 +1,15 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/router/app_pages.dart';
import 'package:pilipala/pages/main/view.dart';
import 'package:pilipala/utils/storage.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await GStrorage.init();
await Request.setCookie();
runApp(const MyApp());
}
@ -25,12 +28,22 @@ class MyApp extends StatelessWidget {
theme: ThemeData(
colorScheme: lightDynamic ??
ColorScheme.fromSeed(
seedColor: Colors.green, brightness: Brightness.light),
useMaterial3: true),
darkTheme: ThemeData(colorScheme: darkDynamic, useMaterial3: true),
seedColor: Colors.green,
brightness: Brightness.light,
),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: darkDynamic ??
ColorScheme.fromSeed(
seedColor: Colors.green,
brightness: Brightness.dark,
),
useMaterial3: true,
),
getPages: Routes.getPages,
home: const MainApp(),
// home: const Scaffold(),
builder: FlutterSmartDialog.init(),
);
}),
);

View File

@ -0,0 +1,46 @@
enum ReplyType {
unset,
// 视频
video,
// 话题
topic,
// 活动
activity,
// 小视频
videoS,
// 小黑屋封禁信息
blockMsg,
// 公告信息
publicMsg,
// 直播活动
liveActivity,
// 活动稿件
activityFile,
// 直播公告
livePublic,
// 相簿
album,
// 专栏
column,
// 票务
ticket,
// 音频
audio,
// 点评
comment,
// 动态
dynamics,
// 播单
playList,
// 音乐播单
musicPlayList,
// 漫画
comics1,
// 漫画
comics2,
// 漫画
comics3,
// 课程
course,
}

View File

@ -80,7 +80,9 @@ class HotVideoItemModel {
pubLocation = json["pub_location"];
seasontype = json["seasontype"];
isOgv = json["isOgv"];
rcmdReason = RcmdReason.fromJson(json['rcmd_reason']);
rcmdReason = json['rcmd_reason'] != ''
? RcmdReason.fromJson(json['rcmd_reason'])
: null;
}
}

View File

@ -13,6 +13,7 @@ class RecVideoItemModel {
this.pubdate,
this.owner,
this.stat,
this.isFollowed,
this.rcmdReason,
});
@ -27,6 +28,7 @@ class RecVideoItemModel {
int? pubdate = -1;
Owner? owner;
Stat? stat;
int? isFollowed;
RcmdReason? rcmdReason;
RecVideoItemModel.fromJson(Map<String, dynamic> json) {
@ -41,6 +43,7 @@ class RecVideoItemModel {
pubdate = json["pubdate"];
owner = Owner.fromJson(json["owner"]);
stat = Stat.fromJson(json["stat"]);
isFollowed = json["is_followed"] ?? 0;
rcmdReason = json["rcmd_reason"] != null
? RcmdReason.fromJson(json["rcmd_reason"])
: RcmdReason(content: '');

View File

@ -0,0 +1,100 @@
import 'package:pilipala/models/model_owner.dart';
class FavDetailData {
FavDetailData({
this.info,
this.medias,
this.hasMore,
});
Map? info;
List<FavDetailItemData>? medias;
bool? hasMore;
FavDetailData.fromJson(Map<String, dynamic> json) {
info = json['info'];
medias = json['medias'] != null
? json['medias']
.map<FavDetailItemData>((e) => FavDetailItemData.fromJson(e))
.toList()
: [FavDetailItemData()];
hasMore = json['has_more'];
}
}
class FavDetailItemData {
FavDetailItemData({
this.id,
this.type,
this.title,
this.pic,
this.intro,
this.page,
this.duration,
this.owner,
this.attr,
this.cntInfo,
this.link,
this.ctime,
this.pubdate,
this.favTime,
this.bvId,
this.bvid,
// this.season,
// this.ogv,
this.stat,
});
int? id;
int? type;
String? title;
String? pic;
String? intro;
int? page;
int? duration;
Owner? owner;
int? attr;
Map? cntInfo;
String? link;
int? ctime;
int? pubdate;
int? favTime;
String? bvId;
String? bvid;
Stat? stat;
FavDetailItemData.fromJson(Map<String, dynamic> json) {
id = json['id'];
type = json['type'];
title = json['title'];
pic = json['cover'];
intro = json['intro'];
page = json['page'];
duration = json['duration'];
owner = Owner.fromJson(json['upper']);
attr = json['attr'];
cntInfo = json['cnt_info'];
link = json['link'];
ctime = json['ctime'];
pubdate = json['pubtime'];
favTime = json['fav_time'];
bvId = json['bv_id'];
bvid = json['bvid'];
stat = Stat.fromJson(json['cnt_info']);
}
}
class Stat {
Stat({
this.view,
this.danmaku,
});
int? view;
int? danmaku;
Stat.fromJson(Map<String, dynamic> json) {
view = json['play'];
danmaku = json['danmaku'];
}
}

View File

@ -0,0 +1,108 @@
class FavFolderData {
FavFolderData({
this.count,
this.list,
this.hasMore,
});
int? count;
List<FavFolderItemData>? list;
bool? hasMore;
FavFolderData.fromJson(Map<String, dynamic> json) {
count = json['count'];
list = json['list'] != null
? json['list']
.map<FavFolderItemData>((e) => FavFolderItemData.fromJson(e))
.toList()
: [FavFolderItemData()];
hasMore = json['has_more'];
}
}
class FavFolderItemData {
FavFolderItemData({
this.id,
this.fid,
this.mid,
this.attr,
this.title,
this.cover,
this.upper,
this.coverType,
this.intro,
this.ctime,
this.mtime,
this.state,
this.favState,
this.mediaCount,
this.viewCount,
this.vt,
this.playSwitch,
this.type,
this.link,
this.bvid,
});
int? id;
int? fid;
int? mid;
int? attr;
String? title;
String? cover;
Upper? upper;
int? coverType;
String? intro;
int? ctime;
int? mtime;
int? state;
int? favState;
int? mediaCount;
int? viewCount;
int? vt;
int? playSwitch;
int? type;
String? link;
String? bvid;
FavFolderItemData.fromJson(Map<String, dynamic> json) {
id = json['id'];
fid = json['fid'];
mid = json['mid'];
attr = json['attr'];
title = json['title'];
cover = json['cover'];
upper = json['upper'] != null ? Upper.fromJson(json['upper']) : Upper();
coverType = json['cover_type'];
intro = json['intro'];
ctime = json['ctime'];
mtime = json['mtime'];
state = json['state'];
favState = json['fav_state'];
mediaCount = json['media_count'];
viewCount = json['view_count'];
vt = json['vt'];
playSwitch = json['play_switch'];
type = json['type'];
link = json['link'];
bvid = json['bvid'];
}
}
class Upper {
Upper({
this.mid,
this.name,
this.face,
});
int? mid;
String? name;
String? face;
Upper.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
name = json['name'];
face = json['face'];
}
}

103
lib/models/user/info.dart Normal file
View File

@ -0,0 +1,103 @@
class UserInfoData {
UserInfoData({
this.isLogin,
this.emailVerified,
this.face,
this.levelInfo,
this.mid,
this.mobileVerified,
this.money,
this.moral,
this.official,
this.officialVerify,
this.pendant,
this.scores,
this.uname,
this.vipDueDate,
this.vipStatus,
this.vipType,
this.vipPayType,
this.vipThemeType,
this.vipLabel,
this.vipAvatarSub,
this.vipNicknameColor,
this.wallet,
this.hasShop,
this.shopUrl,
});
bool? isLogin;
int? emailVerified;
String? face;
LevelInfo? levelInfo;
int? mid;
int? mobileVerified;
int? money;
int? moral;
Map? official;
Map? officialVerify;
Map? pendant;
int? scores;
String? uname;
int? vipDueDate;
int? vipStatus;
int? vipType;
int? vipPayType;
int? vipThemeType;
Map? vipLabel;
int? vipAvatarSub;
String? vipNicknameColor;
Map? wallet;
bool? hasShop;
String? shopUrl;
UserInfoData.fromJson(Map<String, dynamic> json) {
isLogin = json['isLogin'] ?? false;
emailVerified = json['email_verified'];
face = json['face'];
levelInfo = json['level_info'] != null
? LevelInfo.fromJson(json['level_info'])
: LevelInfo();
mid = json['mid'];
mobileVerified = json['mobile_verified'];
money = json['money'];
moral = json['moral'];
official = json['official'];
officialVerify = json['officialVerify'];
pendant = json['pendant'];
scores = json['scores'];
uname = json['uname'];
vipDueDate = json['vipDueDate'];
vipStatus = json['vipStatus'];
vipType = json['vipType'];
vipPayType = json['vip_pay_type'];
vipThemeType = json['vip_theme_type'];
vipLabel = json['vip_label'];
vipAvatarSub = json['vip_avatar_subscript'];
vipNicknameColor = json['vip_nickname_color'];
wallet = json['wallet'];
hasShop = json['has_shop'];
shopUrl = json['shop_url'];
}
}
class LevelInfo {
LevelInfo({
this.currentLevel,
this.currentMin,
this.currentExp,
this.nextExp,
});
int? currentLevel;
int? currentMin;
int? currentExp;
int? nextExp;
LevelInfo.fromJson(Map<String, dynamic> json) {
currentLevel = json['current_level'];
currentMin = json['current_min'];
currentExp = json['current_exp'];
nextExp = json['next_exp'];
}
}

17
lib/models/user/stat.dart Normal file
View File

@ -0,0 +1,17 @@
class UserStat {
UserStat({
this.following,
this.follower,
this.dynamicCount,
});
int? following;
int? follower;
int? dynamicCount;
UserStat.fromJson(Map<String, dynamic> json) {
following = json['following'];
follower = json['follower'];
dynamicCount = json['dynamic_count'];
}
}

View File

@ -0,0 +1,17 @@
class ReplyConfig {
ReplyConfig({
this.showtopic,
this.showUpFlag,
this.readOnly,
});
int? showtopic;
bool? showUpFlag;
bool? readOnly;
ReplyConfig.fromJson(Map<String, dynamic> json) {
showtopic = json['showtopic'];
showUpFlag = json['show_up_flag'];
readOnly = json['read_only'];
}
}

View File

@ -0,0 +1,29 @@
class ReplyContent {
ReplyContent({
this.message,
this.atNameToMid, // @的用户的mid null
this.memebers, // 被@的用户List 如果有的话 []
this.emote, // 表情包 如果有的话 null
this.jumpUrl, // {}
this.pictures, // {}
this.vote,
});
String? message;
Map? atNameToMid;
List? memebers;
Map? emote;
Map? jumpUrl;
List? pictures;
Map? vote;
ReplyContent.fromJson(Map<String, dynamic> json) {
message = json['message'];
atNameToMid = json['at_name_to_mid'] ?? {};
memebers = json['memebers'] ?? [];
emote = json['emote'] ?? {};
jumpUrl = json['jump_url'] ?? {};
pictures = json['pictures'] ?? [];
vote = json['vote'] ?? {};
}
}

View File

@ -0,0 +1,40 @@
import 'package:pilipala/models/video/reply/item.dart';
import 'config.dart';
import 'page.dart';
import 'upper.dart';
class ReplyData {
ReplyData({
this.page,
this.config,
this.replies,
this.topReplies,
this.upper,
});
ReplyPage? page;
ReplyConfig? config;
late List<ReplyItemModel>? replies;
late List<ReplyItemModel>? topReplies;
ReplyUpper? upper;
ReplyData.fromJson(Map<String, dynamic> json) {
page = ReplyPage.fromJson(json['page']);
config = ReplyConfig.fromJson(json['config']);
replies = json['replies'] != null
? json['replies']
.map<ReplyItemModel>(
(item) => ReplyItemModel.fromJson(item, json['upper']['mid']))
.toList()
: [];
topReplies = json['top_replies'] != null
? json['top_replies']
.map<ReplyItemModel>((item) => ReplyItemModel.fromJson(
item, json['upper']['mid'],
isTopStatus: true))
.toList()
: [];
upper = ReplyUpper.fromJson(json['upper']);
}
}

View File

@ -0,0 +1,159 @@
import 'content.dart';
import 'member.dart';
class ReplyItemModel {
ReplyItemModel({
this.rpid,
this.oid,
this.type,
this.mid,
this.root,
this.parent,
this.dialog,
this.count,
this.floor,
this.state,
this.fansgrade,
this.attr,
this.ctime,
this.rpidStr,
this.rootStr,
this.parentStr,
this.like,
this.action,
this.member,
this.content,
this.replies,
this.assist,
this.upAction,
this.invisible,
this.replyControl,
this.isUp,
this.isTop,
this.cardLabel,
});
int? rpid;
int? oid;
int? type;
int? mid;
int? root;
int? parent;
int? dialog;
int? count;
int? floor;
int? state;
int? fansgrade;
int? attr;
int? ctime;
String? rpidStr;
String? rootStr;
String? parentStr;
int? like;
int? action;
ReplyMember? member;
ReplyContent? content;
List? replies;
int? assist;
UpAction? upAction;
bool? invisible;
ReplyControl? replyControl;
bool? isUp;
bool? isTop = false;
List? cardLabel;
ReplyItemModel.fromJson(Map<String, dynamic> json, upperMid,
{isTopStatus = false}) {
rpid = json['rpid'];
oid = json['oid'];
type = json['type'];
mid = json['mid'];
root = json['root'];
parent = json['parent'];
dialog = json['dialog'];
count = json['count'];
floor = json['floor'];
state = json['state'];
fansgrade = json['fansgrade'];
attr = json['attr'];
ctime = json['ctime'];
rpidStr = json['rpid_str'];
rootStr = json['root_str'];
parentStr = json['parent_str'];
like = json['like'];
action = json['action'];
member = ReplyMember.fromJson(json['member']);
content = ReplyContent.fromJson(json['content']);
replies = json['replies'] != null
? json['replies']
.map((item) => ReplyItemModel.fromJson(item, upperMid))
.toList()
: [];
assist = json['assist'];
upAction = UpAction.fromJson(json['up_action']);
invisible = json['invisible'];
replyControl = json['reply_control'] == null
? null
: ReplyControl.fromJson(json['reply_control']);
isUp = upperMid.toString() == json['member']['mid'];
isTop = isTopStatus;
cardLabel = json['card_label'] != null
? json['card_label'].map((e) => e['text_content']).toList()
: [];
}
}
class UpAction {
UpAction({this.like, this.reply});
bool? like;
bool? reply;
UpAction.fromJson(Map<String, dynamic> json) {
like = json['like'];
reply = json['reply'];
}
}
class ReplyControl {
ReplyControl({
this.upReply,
this.isUpTop,
this.upLike,
this.isShow,
this.entryText,
this.titleText,
this.time,
this.location,
});
bool? upReply;
bool? isUpTop;
bool? upLike;
bool? isShow;
String? entryText;
String? titleText;
String? time;
String? location;
ReplyControl.fromJson(Map<String, dynamic> json) {
upReply = json['up_reply'] ?? false;
isUpTop = json['is_up_top'] ?? false;
upLike = json['up_like'] ?? false;
if (json['sub_reply_entry_text'] != null) {
final RegExp regex = RegExp(r"\d+");
final RegExpMatch match = regex.firstMatch(
json['sub_reply_entry_text'] == null
? ''
: json['sub_reply_entry_text']!)!;
isShow = int.parse(match.group(0)!) >= 3;
} else {
isShow = false;
}
entryText = json['sub_reply_entry_text'];
titleText = json['sub_reply_title_text'];
time = json['time_desc'];
location = json['location'] != null ? json['location'].split('')[1] : '';
}
}

View File

@ -0,0 +1,71 @@
import 'package:get/get.dart';
class ReplyMember {
ReplyMember({
this.mid,
this.uname,
this.sign,
this.avatar,
this.level,
this.pendant,
this.officialVerify,
this.vip,
this.fansDetail,
});
String? mid;
String? uname;
String? sign;
String? avatar;
int? level;
Pendant? pendant;
Map? officialVerify;
Map? vip;
Map? fansDetail;
UserSailing? userSailing;
ReplyMember.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
uname = json['uname'];
sign = json['sign'];
avatar = json['avatar'];
level = json['level_info']['current_level'];
pendant = Pendant.fromJson(json['pendant']);
officialVerify = json['officia_verify'];
vip = json['vip'];
fansDetail = json['fans_detail'];
userSailing = json['user_sailing'] != null
? UserSailing.fromJson(json['user_sailing'])
: UserSailing();
}
}
class Pendant {
Pendant({
this.pid,
this.name,
this.image,
});
int? pid;
String? name;
String? image;
Pendant.fromJson(Map<String, dynamic> json) {
pid = json['pid'];
name = json['name'];
image = json['image'];
}
}
class UserSailing {
UserSailing({this.pendant, this.cardbg});
Map? pendant;
Map? cardbg;
UserSailing.fromJson(Map<String, dynamic> json) {
pendant = json['pendant'];
cardbg = json['cardbg'];
}
}

View File

@ -0,0 +1,20 @@
class ReplyPage {
ReplyPage({
this.num,
this.size,
this.count,
this.acount,
});
int? num;
int? size;
int? count;
int? acount;
ReplyPage.fromJson(Map<String, dynamic> json) {
num = json['num'];
size = json['size'];
count = json['count'];
acount = json['acount'];
}
}

View File

@ -0,0 +1 @@
class ReplyTop {}

View File

@ -0,0 +1,18 @@
import 'item.dart';
class ReplyUpper {
ReplyUpper({
this.mid,
this.top,
});
int? mid;
ReplyItemModel? top;
ReplyUpper.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
top = json['top'] != null
? ReplyItemModel.fromJson(json['top'], json['mid'])
: null;
}
}

View File

@ -0,0 +1,524 @@
import 'dart:convert';
class VideoDetailResponse {
int? code;
String? message;
int? ttl;
VideoDetailData? data;
VideoDetailResponse({
this.code,
this.message,
this.ttl,
this.data,
});
VideoDetailResponse.fromJson(Map<String, dynamic> json) {
code = json["code"];
message = json["message"];
ttl = json["ttl"];
data = json["data"] == null ? null : VideoDetailData.fromJson(json["data"]);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["code"] = code;
data["message"] = message;
data["ttl"] = ttl;
data["data"] = data;
return data;
}
}
class VideoDetailData {
String? bvid;
int? aid;
int? videos;
int? tid;
String? tname;
int? copyright;
String? pic;
String? title;
int? pubdate;
int? ctime;
String? desc;
List<DescV2>? descV2;
int? state;
int? duration;
Map<String, int>? rights;
Owner? owner;
Stat? stat;
String? videoDynamic;
int? cid;
Dimension? dimension;
dynamic premiere;
int? teenageMode;
bool? isChargeableSeason;
bool? isStory;
bool? noCache;
List<Page>? pages;
Subtitle? subtitle;
// Label? label;
bool? isSeasonDisplay;
UserGarb? userGarb;
HonorReply? honorReply;
String? likeIcon;
bool? needJumpBv;
VideoDetailData({
this.bvid,
this.aid,
this.videos,
this.tid,
this.tname,
this.copyright,
this.pic,
this.title,
this.pubdate,
this.ctime,
this.desc,
this.descV2,
this.state,
this.duration,
this.rights,
this.owner,
this.stat,
this.videoDynamic,
this.cid,
this.dimension,
this.premiere,
this.teenageMode,
this.isChargeableSeason,
this.isStory,
this.noCache,
this.pages,
this.subtitle,
this.isSeasonDisplay,
this.userGarb,
this.honorReply,
this.likeIcon,
this.needJumpBv,
});
VideoDetailData.fromJson(Map<String, dynamic> json) {
bvid = json["bvid"];
aid = json["aid"];
videos = json["videos"];
tid = json["tid"];
tname = json["tname"];
copyright = json["copyright"];
pic = json["pic"];
title = json["title"];
pubdate = json["pubdate"];
ctime = json["ctime"];
desc = json["desc"];
descV2 = json["desc_v2"] == null
? []
: List<DescV2>.from(json["desc_v2"]!.map((e) => DescV2.fromJson(e)));
state = json["state"];
duration = json["duration"];
rights =
Map.from(json["rights"]!).map((k, v) => MapEntry<String, int>(k, v));
owner = json["owner"] == null ? null : Owner.fromJson(json["owner"]);
stat = json["stat"] == null ? null : Stat.fromJson(json["stat"]);
videoDynamic = json["dynamic"];
cid = json["cid"];
dimension = json["dimension"] == null
? null
: Dimension.fromJson(json["dimension"]);
premiere = json["premiere"];
teenageMode = json["teenage_mode"];
isChargeableSeason = json["is_chargeable_season"];
isStory = json["is_story"];
noCache = json["no_cache"];
pages = json["pages"] == null
? []
: List<Page>.from(json["pages"]!.map((e) => Page.fromJson(e)));
subtitle =
json["subtitle"] == null ? null : Subtitle.fromJson(json["subtitle"]);
isSeasonDisplay = json["is_season_display"];
userGarb =
json["user_garb"] == null ? null : UserGarb.fromJson(json["user_garb"]);
honorReply = json["honor_reply"] == null
? null
: HonorReply.fromJson(json["honor_reply"]);
likeIcon = json["like_icon"];
needJumpBv = json["need_jump_bv"];
}
Map<String, dynamic> toJson() => {
"bvid": bvid,
"aid": aid,
"videos": videos,
"tid": tid,
"tname": tname,
"copyright": copyright,
"pic": pic,
"title": title,
"pubdate": pubdate,
"ctime": ctime,
"desc": desc,
"desc_v2": descV2 == null
? []
: List<dynamic>.from(descV2!.map((e) => e.toJson())),
"state": state,
"duration": duration,
"rights":
Map.from(rights!).map((k, v) => MapEntry<String, dynamic>(k, v)),
"owner": owner?.toJson(),
"stat": stat?.toJson(),
"dynamic": videoDynamic,
"cid": cid,
"dimension": dimension?.toJson(),
"premiere": premiere,
"teenage_mode": teenageMode,
"is_chargeable_season": isChargeableSeason,
"is_story": isStory,
"no_cache": noCache,
"pages": pages == null
? []
: List<dynamic>.from(pages!.map((e) => e.toJson())),
"subtitle": subtitle?.toJson(),
"is_season_display": isSeasonDisplay,
"user_garb": userGarb?.toJson(),
"honor_reply": honorReply?.toJson(),
"like_icon": likeIcon,
"need_jump_bv": needJumpBv,
};
}
class DescV2 {
String? rawText;
int? type;
int? bizId;
DescV2({
this.rawText,
this.type,
this.bizId,
});
fromRawJson(String str) {
return DescV2.fromJson(json.decode(str));
}
String toRawJson() => json.encode(toJson());
DescV2.fromJson(Map<String, dynamic> json) {
rawText = json["raw_text"];
type = json["type"];
bizId = json["biz_id"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["raw_text"] = rawText;
data["type"] = type;
data["biz_id"] = bizId;
return data;
}
}
class Dimension {
int? width;
int? height;
int? rotate;
Dimension({
this.width,
this.height,
this.rotate,
});
fromRawJson(String str) => Dimension.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Dimension.fromJson(Map<String, dynamic> json) {
width = json["width"];
height = json["height"];
rotate = json["rotate"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["width"] = width;
data["height"] = height;
data["rotate"] = rotate;
data["data"] = data;
return data;
}
}
class HonorReply {
List<Honor>? honor;
HonorReply({
this.honor,
});
fromRawJson(String str) => HonorReply.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
HonorReply.fromJson(Map<String, dynamic> json) {
honor = json["honor"] == null
? []
: List<Honor>.from(json["honor"]!.map((x) => Honor.fromJson(x)));
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["honor"] =
honor == null ? [] : List<dynamic>.from(honor!.map((x) => x.toJson()));
return data;
}
}
class Honor {
int? aid;
int? type;
String? desc;
int? weeklyRecommendNum;
Honor({
this.aid,
this.type,
this.desc,
this.weeklyRecommendNum,
});
fromRawJson(String str) => Honor.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Honor.fromJson(Map<String, dynamic> json) {
aid = json["aid"];
type = json["type"];
desc = json["desc"];
weeklyRecommendNum = json["weekly_recommend_num"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["aid"] = aid;
data["type"] = type;
data["desc"] = desc;
data["weekly_recommend_num"] = weeklyRecommendNum;
return data;
}
}
class Owner {
int? mid;
String? name;
String? face;
Owner({
this.mid,
this.name,
this.face,
});
fromRawJson(String str) => Owner.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Owner.fromJson(Map<String, dynamic> json) {
mid = json["mid"];
name = json["name"];
face = json["face"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["mid"] = mid;
data["name"] = name;
data["face"] = face;
return data;
}
}
class Page {
int? cid;
int? page;
String? from;
String? pagePart;
int? duration;
String? vid;
String? weblink;
Dimension? dimension;
String? firstFrame;
Page({
this.cid,
this.page,
this.from,
this.pagePart,
this.duration,
this.vid,
this.weblink,
this.dimension,
this.firstFrame,
});
fromRawJson(String str) => Page.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Page.fromJson(Map<String, dynamic> json) {
cid = json["cid"];
page = json["page"];
from = json["from"];
pagePart = json["part"];
duration = json["duration"];
vid = json["vid"];
weblink = json["weblink"];
dimension = json["dimension"] == null
? null
: Dimension.fromJson(json["dimension"]);
firstFrame = json["first_frame"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["cid"] = cid;
data["page"] = page;
data["from"] = from;
data["part"] = pagePart;
data["duration"] = duration;
data["vid"] = vid;
data["weblink"] = weblink;
data["dimension"] = dimension?.toJson();
data["first_frame"] = firstFrame;
return data;
}
}
class Stat {
int? aid;
int? view;
int? danmaku;
int? reply;
int? favorite;
int? coin;
int? share;
int? nowRank;
int? hisRank;
int? like;
int? dislike;
String? evaluation;
String? argueMsg;
Stat({
this.aid,
this.view,
this.danmaku,
this.reply,
this.favorite,
this.coin,
this.share,
this.nowRank,
this.hisRank,
this.like,
this.dislike,
this.evaluation,
this.argueMsg,
});
fromRawJson(String str) => Stat.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Stat.fromJson(Map<String, dynamic> json) {
aid = json["aid"];
view = json["view"];
danmaku = json["danmaku"];
reply = json["reply"];
favorite = json["favorite"];
coin = json["coin"];
share = json["share"];
nowRank = json["now_rank"];
hisRank = json["his_rank"];
like = json["like"];
dislike = json["dislike"];
evaluation = json["evaluation"];
argueMsg = json["argue_msg"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["aid"] = aid;
data["view"] = view;
data["danmaku"] = danmaku;
data["reply"] = reply;
data["favorite"] = favorite;
data["coin"] = coin;
data["share"] = share;
data["now_rank"] = nowRank;
data["his_rank"] = hisRank;
data["like"] = like;
data["dislike"] = dislike;
data["evaluation"] = evaluation;
data["argue_msg"] = argueMsg;
return data;
}
}
class Subtitle {
bool? allowSubmit;
List<dynamic>? list;
Subtitle({
this.allowSubmit,
this.list,
});
fromRawJson(String str) => Subtitle.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Subtitle.fromJson(Map<String, dynamic> json) {
allowSubmit = json["allow_submit"];
list = json["list"] == null
? []
: List<dynamic>.from(json["list"]!.map((x) => x));
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["allow_submit"] = allowSubmit;
data["list"] = list == null ? [] : List<dynamic>.from(list!.map((x) => x));
return data;
}
}
class UserGarb {
String? urlImageAniCut;
UserGarb({
this.urlImageAniCut,
});
fromRawJson(String str) => UserGarb.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
UserGarb.fromJson(Map<String, dynamic> json) {
urlImageAniCut = json["url_image_ani_cut"];
}
Map<String, dynamic> toJson() => {"url_image_ani_cut": urlImageAniCut};
}
class Label {}

View File

@ -0,0 +1,18 @@
import 'package:get/get.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/utils/storage.dart';
class FavController extends GetxController {
Rx<FavFolderData> favFolderData = FavFolderData().obs;
Future<dynamic> queryFavFolder() async {
var res = await await UserHttp.userfavFolder(
pn: 1,
ps: 10,
mid: GStrorage.user.get(UserBoxKey.userMid),
);
favFolderData.value = res['data'];
return res;
}
}

4
lib/pages/fav/index.dart Normal file
View File

@ -0,0 +1,4 @@
library fav;
export './controller.dart';
export './view.dart';

75
lib/pages/fav/view.dart Normal file
View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/fav/index.dart';
class FavPage extends StatefulWidget {
const FavPage({super.key});
@override
State<FavPage> createState() => _FavPageState();
}
class _FavPageState extends State<FavPage> {
final FavController _favController = Get.put(FavController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('我的收藏'),
),
body: FutureBuilder(
future: _favController.queryFavFolder(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => ListView.builder(
itemCount: _favController.favFolderData.value.list!.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () => Get.toNamed(
'/favDetail',
arguments:
_favController.favFolderData.value.list![index],
parameters: {
'mediaId': _favController
.favFolderData.value.list![index].id
.toString(),
},
),
leading: const Icon(Icons.folder_special_outlined),
minLeadingWidth: 0,
title: Text(_favController
.favFolderData.value.list![index].title!),
subtitle: Text(
'${_favController.favFolderData.value.list![index].mediaCount}个内容',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize),
),
);
},
),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return Text('请求中');
}
},
),
);
}
}

View File

@ -0,0 +1,50 @@
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';
class FavDetailController extends GetxController {
FavFolderItemData? item;
Rx<FavDetailData> favDetailData = FavDetailData().obs;
int? mediaId;
@override
void onInit() {
item = Get.arguments;
if (Get.parameters.keys.isNotEmpty) {
mediaId = int.parse(Get.parameters['mediaId']!);
}
super.onInit();
}
Future<dynamic> queryUserFavFolderDetail() async {
var res = await await UserHttp.userFavFolderDetail(
pn: 1,
ps: 15,
mediaId: mediaId!,
);
favDetailData.value = res['data'];
return res;
}
onCancelFav(int id) async {
var result = await VideoHttp.favVideo(
aid: id.toString(), addIds: '', delIds: mediaId.toString());
if (result['status']) {
if (result['data']['prompt']) {
List<FavDetailItemData> dataList = favDetailData.value.medias!;
for (var i in dataList) {
if (i.id == id) {
dataList.remove(i);
break;
}
}
favDetailData.value.medias = dataList;
favDetailData.refresh();
SmartDialog.showToast('取消收藏');
}
}
}
}

View File

@ -0,0 +1,4 @@
library favdetail;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,219 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/favDetail/index.dart';
import 'widget/fav_video_card.dart';
class FavDetailPage extends StatefulWidget {
const FavDetailPage({super.key});
@override
State<FavDetailPage> createState() => _FavDetailPageState();
}
class _FavDetailPageState extends State<FavDetailPage> {
late final ScrollController _controller = ScrollController();
final FavDetailController _favDetailController =
Get.put(FavDetailController());
late StreamController<bool> titleStreamC; // a
@override
void initState() {
super.initState();
titleStreamC = StreamController<bool>();
_controller.addListener(
() {
if (_controller.offset > 160) {
titleStreamC.add(true);
} else if (_controller.offset <= 160) {
titleStreamC.add(false);
}
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _controller,
slivers: [
SliverAppBar(
expandedHeight: 260 - MediaQuery.of(context).padding.top,
pinned: true,
title: StreamBuilder(
stream: titleStreamC.stream,
initialData: false,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_favDetailController.item!.title!,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
'${_favDetailController.item!.mediaCount!}条视频',
style: Theme.of(context).textTheme.labelMedium,
)
],
)
],
),
);
},
),
// actions: [
// IconButton(
// onPressed: () {},
// icon: const Icon(Icons.more_vert),
// ),
// const SizedBox(width: 4)
// ],
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor.withOpacity(0.2),
),
),
),
padding: EdgeInsets.only(
top: kTextTabBarHeight +
MediaQuery.of(context).padding.top +
30,
left: 20,
right: 20),
child: SizedBox(
height: 200,
child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 180,
height: 110,
child: NetworkImgLayer(
width: 180,
height: 110,
src: _favDetailController.item!.cover,
),
),
const SizedBox(width: 14),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
_favDetailController.item!.title!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleMedium!
.fontSize,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
_favDetailController.item!.upper!.name!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.outline),
)
],
)
],
),
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx(
() => Text(
'${_favDetailController.favDetailData.value.medias != null ? _favDetailController.favDetailData.value.medias!.length : '-'}条视频',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
letterSpacing: 1),
),
),
),
),
FutureBuilder(
future: _favDetailController.queryUserFavFolderDetail(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
if (data['status']) {
if (_favDetailController.item!.mediaCount == 0) {
return const SliverToBoxAdapter(
child: SizedBox(
height: 300,
child: Center(child: Text('没有内容')),
),
);
} else {
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return FavVideoCardH(
videoItem: _favDetailController
.favDetailData.value.medias![index],
);
},
childCount: _favDetailController
.favDetailData.value.medias!.length),
),
);
}
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
return const SliverToBoxAdapter(
child: SizedBox(
height: 300,
child: Center(child: Text('加载中')),
),
);
}
},
),
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).padding.bottom + 20,
),
)
],
),
);
}
}

View File

@ -0,0 +1,165 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../controller.dart';
// 收藏视频卡片 - 水平布局
class FavVideoCardH extends StatelessWidget {
var videoItem;
final FavDetailController _favDetailController =
Get.put(FavDetailController());
FavVideoCardH({Key? key, required this.videoItem}) : super(key: key);
@override
Widget build(BuildContext context) {
int id = videoItem.id;
String heroTag = Utils.makeHeroTag(id);
return Dismissible(
movementDuration: const Duration(milliseconds: 300),
background: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.clear_all_rounded),
SizedBox(width: 6),
Text('取消收藏')
],
)),
direction: DismissDirection.endToStart,
key: ValueKey<int>(videoItem.id),
onDismissed: (DismissDirection direction) {
_favDetailController.onCancelFav(videoItem.id);
// widget.onDeleteNotice();
},
child: InkWell(
onTap: () async {
await Future.delayed(const Duration(milliseconds: 200));
Get.toNamed('/video?aid=$id',
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
},
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 5, 12, 5),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return SizedBox(
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
double PR =
MediaQuery.of(context).devicePixelRatio;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
// src: videoItem['pic'] +
// '@${(maxWidth * 2).toInt()}w',
src: videoItem.pic + '@.webp',
width: maxWidth,
height: maxHeight,
),
),
// Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,),
Positioned(
right: 4,
bottom: 4,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 1, horizontal: 6),
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(4),
color:
Colors.black54.withOpacity(0.4)),
child: Text(
Utils.timeFormat(videoItem.duration!),
style: const TextStyle(
fontSize: 11, color: Colors.white),
),
),
)
],
);
},
),
),
VideoContent(videoItem: videoItem)
],
),
);
},
),
),
],
),
),
);
}
}
class VideoContent extends StatelessWidget {
final videoItem;
const VideoContent({super.key, required this.videoItem});
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
videoItem.title,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
fontWeight: FontWeight.w500),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
videoItem.owner.name,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
Row(
children: [
StatView(
theme: 'gray',
view: videoItem.cntInfo['play'],
),
const SizedBox(width: 8),
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku'])
],
),
],
),
),
);
}
}

View File

@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/model_rec_video_item.dart';
class HomeController extends GetxController {
@ -17,28 +16,27 @@ class HomeController extends GetxController {
@override
void onInit() {
super.onInit();
queryRcmdFeed('init');
// queryRcmdFeed('init');
}
// 获取推荐
Future queryRcmdFeed(type) async {
var res = await Request().get(
Api.recommendList,
data: {'feed_version': "V3", 'ps': count, 'fresh_idx': _currentPage},
var res = await VideoHttp.rcmdVideoList(
ps: count,
freshIdx: _currentPage,
);
List<RecVideoItemModel> list = [];
for (var i in res.data['data']['item']) {
list.add(RecVideoItemModel.fromJson(i));
}
if (res['status']) {
if (type == 'init') {
videoList.value = list;
videoList.value = res['data'];
} else if (type == 'onRefresh') {
videoList.insertAll(0, list);
videoList.insertAll(0, res['data']);
} else if (type == 'onLoad') {
videoList.addAll(list);
videoList.addAll(res['data']);
}
_currentPage += 1;
}
isLoadingMore = false;
return res;
}
// 下拉刷新
@ -48,7 +46,6 @@ class HomeController extends GetxController {
// 上拉加载
Future onLoad() async {
await Future.delayed(const Duration(milliseconds: 500));
queryRcmdFeed('onLoad');
}

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_v.dart';
import './controller.dart';
import 'package:pilipala/common/constants.dart';
@ -18,6 +19,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage>
with AutomaticKeepAliveClientMixin {
final HomeController _homeController = Get.put(HomeController());
Future? _futureBuilderFuture;
List videoList = [];
@override
@ -26,6 +28,7 @@ class _HomePageState extends State<HomePage>
@override
void initState() {
super.initState();
_futureBuilderFuture = _homeController.queryRcmdFeed('init');
_homeController.videoList.listen((value) {
videoList = value;
setState(() {});
@ -71,37 +74,25 @@ class _HomePageState extends State<HomePage>
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(
StyleString.cardSpace, 0, StyleString.cardSpace, 8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
mainAxisSpacing: StyleString.cardSpace,
// 列间距
crossAxisSpacing: StyleString.cardSpace,
// 列数
crossAxisCount: _homeController.crossAxisCount,
mainAxisExtent: MediaQuery.of(context).size.width /
_homeController.crossAxisCount /
StyleString.aspectRatio +
72),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return videoList.isNotEmpty
? VideoCardV(
videoItem: videoList[index],
longPress: () {
_homeController.popupDialog =
_createPopupDialog(videoList[index]);
Overlay.of(context)
.insert(_homeController.popupDialog!);
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() => contentGrid(
_homeController, _homeController.videoList));
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return contentGrid(_homeController, []);
}
},
longPressEnd: () {
_homeController.popupDialog?.remove();
},
)
: const VideoCardVSkeleton();
},
childCount: videoList.isNotEmpty ? videoList.length : 10,
),
),
),
const LoadingMore()
@ -116,6 +107,42 @@ class _HomePageState extends State<HomePage>
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: videoItem),
));
}
Widget contentGrid(ctr, videoList) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
mainAxisSpacing: StyleString.cardSpace,
// 列间距
crossAxisSpacing: StyleString.cardSpace,
// 列数
crossAxisCount: ctr.crossAxisCount,
mainAxisExtent: MediaQuery.of(context).size.width /
ctr.crossAxisCount /
StyleString.aspectRatio +
70,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return videoList!.isNotEmpty
?
// VideoCardV(videoItem: videoList![index])
VideoCardV(
videoItem: videoList[index],
longPress: () {
_homeController.popupDialog =
_createPopupDialog(videoList[index]);
Overlay.of(context).insert(_homeController.popupDialog!);
},
longPressEnd: () {
_homeController.popupDialog?.remove();
},
)
: const VideoCardVSkeleton();
},
childCount: videoList!.isNotEmpty ? videoList!.length : 10,
),
);
}

View File

@ -1,5 +1,9 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/mine/view.dart';
class HomeAppBar extends StatelessWidget {
const HomeAppBar({super.key});
@ -9,11 +13,7 @@ class HomeAppBar extends StatelessWidget {
return SliverAppBar(
// forceElevated: true,
scrolledUnderElevation: 0,
toolbarHeight: Platform.isAndroid
? (MediaQuery.of(context).padding.top + 6)
: Platform.isIOS
? MediaQuery.of(context).padding.top - 2
: kToolbarHeight,
toolbarHeight: MediaQuery.of(context).padding.top,
expandedHeight: kToolbarHeight + MediaQuery.of(context).padding.top,
automaticallyImplyLeading: false,
pinned: true,
@ -29,19 +29,26 @@ class HomeAppBar extends StatelessWidget {
title: const Text(
'PiLiPaLa',
style: TextStyle(
fontSize: 18,
fontSize: 20,
fontWeight: FontWeight.bold,
letterSpacing: 1,
fontFamily: 'ArchivoNarrow',
),
),
actions: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.notifications_none_rounded),
icon: const Icon(CupertinoIcons.search, size: 22),
),
// IconButton(
// onPressed: () {},
// icon: const Icon(CupertinoIcons.bell, size: 22),
// ),
IconButton(
onPressed: () {},
icon: const Icon(Icons.search_rounded),
onPressed: () {
Get.bottomSheet(const MinePage());
},
icon: const Icon(CupertinoIcons.person, size: 22),
),
const SizedBox(width: 10)
],

View File

@ -1,8 +1,6 @@
import 'package:flutter/animation.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/model_hot_video_item.dart';
class HotController extends GetxController {
@ -14,31 +12,24 @@ class HotController extends GetxController {
bool flag = false;
OverlayEntry? popupDialog;
@override
void onInit() {
super.onInit();
queryHotFeed('init');
}
// 获取推荐
Future queryHotFeed(type) async {
var res = await Request().get(
Api.hotList,
data: {'pn': _currentPage, 'ps': _count},
var res = await VideoHttp.hotVideoList(
pn: _currentPage,
ps: _count,
);
List<HotVideoItemModel> list = [];
for (var i in res.data['data']['list']) {
list.add(HotVideoItemModel.fromJson(i));
}
if (res['status']) {
if (type == 'init') {
videoList.value = list;
videoList.value = res['data'];
} else if (type == 'onRefresh') {
videoList.insertAll(0, list);
videoList.insertAll(0, res['data']);
} else if (type == 'onLoad') {
videoList.addAll(list);
videoList.addAll(res['data']);
}
_currentPage += 1;
}
isLoadingMore = false;
return res;
}
// 下拉刷新

View File

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.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';
import 'package:pilipala/pages/hot/controller.dart';
import 'package:pilipala/pages/home/widgets/app_bar.dart';
@ -16,6 +18,7 @@ class HotPage extends StatefulWidget {
class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
final HotController _hotController = Get.put(HotController());
List videoList = [];
Future? _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@ -23,11 +26,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
@override
void initState() {
super.initState();
_hotController.videoList.listen((value) {
videoList = value;
setState(() {});
});
_futureBuilderFuture = _hotController.queryHotFeed('init');
_hotController.scrollController.addListener(
() {
if (_hotController.scrollController.position.pixels >=
@ -54,20 +53,46 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
controller: _hotController.scrollController,
slivers: [
const HomeAppBar(),
SliverList(
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return VideoCardH(
videoItem: videoList[index],
videoItem: _hotController.videoList[index],
longPress: () {
_hotController.popupDialog =
_createPopupDialog(videoList[index]);
Overlay.of(context).insert(_hotController.popupDialog!);
_hotController.popupDialog = _createPopupDialog(
_hotController.videoList[index]);
Overlay.of(context)
.insert(_hotController.popupDialog!);
},
longPressEnd: () {
_hotController.popupDialog?.remove();
},
);
}, childCount: videoList.length)),
}, childCount: _hotController.videoList.length),
),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 5),
);
}
},
),
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).padding.bottom + 10,

View File

@ -1,30 +1,97 @@
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/hot/view.dart';
import 'package:pilipala/pages/mine/view.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart';
class MainController extends GetxController {
List<Widget> pages = <Widget>[
const HomePage(),
const HotPage(),
const MinePage(),
const MediaPage(),
];
List navigationBars = [
RxList navigationBars = [
{
'icon': const Icon(Icons.home_outlined),
'selectedIcon': const Icon(Icons.home),
// 'icon': const Icon(Icons.home_outlined),
// 'selectedIcon': const Icon(Icons.home),
'icon': const Icon(
CupertinoIcons.square_favorites_alt,
size: 21,
),
'selectedIcon': const Icon(
CupertinoIcons.square_favorites_alt_fill,
size: 21,
),
'label': "推荐",
},
{
'icon': const Icon(Icons.whatshot_outlined),
'selectedIcon': const Icon(Icons.whatshot_rounded),
// 'icon': const Icon(Icons.whatshot_outlined),
// 'selectedIcon': const Icon(Icons.whatshot_rounded),
'icon': const Icon(
CupertinoIcons.flame,
size: 20,
),
'selectedIcon': const Icon(
CupertinoIcons.flame_fill,
size: 20,
),
'label': "热门",
},
// {
// 'icon': const Icon(
// CupertinoIcons.person,
// size: 21,
// ),
// 'selectedIcon': const Icon(
// CupertinoIcons.person_fill,
// size: 21,
// ),
// 'label': "我的",
// },
{
'icon': const Icon(Icons.person_outline),
'selectedIcon': const Icon(Icons.person),
'label': "我的",
// 'icon': const Icon(Icons.person_outline),
// 'selectedIcon': const Icon(Icons.person),
'icon': const Icon(
CupertinoIcons.folder,
size: 20,
),
'selectedIcon': const Icon(
CupertinoIcons.folder_fill,
size: 20,
),
'label': "媒体库",
}
];
].obs;
@override
void onInit() {
super.onInit();
// readuUserFace();
}
// 设置头像
// readuUserFace() async {
// Box user = GStrorage.user;
// if (user.get(UserBoxKey.userFace) != null) {
// navigationBars.last['icon'] =
// navigationBars.last['selectedIcon'] = NetworkImgLayer(
// width: 25,
// height: 25,
// type: 'avatar',
// src: user.get(UserBoxKey.userFace),
// );
// navigationBars.last['label'] = '我';
// }
// }
// 重置
// resetLast() {
// navigationBars.last['icon'] = const Icon(Icons.person_outline);
// navigationBars.last['selectedIcon'] = const Icon(Icons.person);
// navigationBars.last['label'] = '我的';
// }
}

View File

@ -15,6 +15,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
final MainController _mainController = Get.put(MainController());
final HomeController _homeController = Get.put(HomeController());
final HotController _hotController = Get.put(HotController());
PageController? _pageController;
late AnimationController? _animationController;
late Animation<double>? _fadeAnimation;
@ -36,6 +37,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
_slideAnimation =
Tween(begin: 0.8, end: 1.0).animate(_animationController!);
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
_pageController = PageController(initialPage: selectedIndex);
}
void setIndex(int value) async {
@ -47,7 +49,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
});
setState(() {});
}
_pageController!.jumpToPage(value);
var currentPage = _mainController.pages[value];
if (currentPage is HomePage) {
if (_homeController.flag) {
@ -98,13 +100,19 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
reverseCurve: Curves.linear,
),
),
child: IndexedStack(
index: selectedIndex,
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
onPageChanged: (index) {
selectedIndex = index;
setState(() {});
},
children: _mainController.pages,
),
),
),
bottomNavigationBar: NavigationBar(
bottomNavigationBar: Obx(
() => NavigationBar(
elevation: 1,
destinations: _mainController.navigationBars.map((e) {
return NavigationDestination(
@ -116,6 +124,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
selectedIndex: selectedIndex,
onDestinationSelected: (value) => setIndex(value),
),
),
);
}
}

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