mod: 首页推荐视频

This commit is contained in:
guozhigq
2023-04-19 08:12:08 +08:00
parent fbfdc2138b
commit 3a344843f9
22 changed files with 499 additions and 15 deletions

View File

@ -4,4 +4,5 @@ class StyleString {
static const double cardSpace = 8;
static BorderRadius mdRadius = BorderRadius.circular(6);
static const Radius imgRadius = Radius.circular(6);
static const double aspectRatio = 16 / 9;
}

View File

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
class NetworkImgLayer extends StatelessWidget {
final String? src;
final double? width;
final double? height;
final double? cacheW;
final double? cacheH;
final String? type;
final Duration? fadeOutDuration;
final Duration? fadeInDuration;
var onTap;
NetworkImgLayer(
{Key? key,
this.src,
required this.width,
required this.height,
this.cacheW,
this.cacheH,
this.type,
this.fadeOutDuration,
this.fadeInDuration,
this.onTap})
: super(key: key);
@override
Widget build(BuildContext context) {
return src != ''
? ClipRRect(
borderRadius: BorderRadius.circular(type == 'avatar' ? 50 : 4),
child: CachedNetworkImage(
imageUrl: src!,
width: width ?? double.infinity,
height: height ?? double.infinity,
maxWidthDiskCache: (cacheW ?? width!).toInt(),
maxHeightDiskCache: (cacheH ?? height!).toInt(),
memCacheWidth: (cacheW ?? width!).toInt(),
memCacheHeight: (cacheH ?? height!).toInt(),
fit: BoxFit.cover,
fadeOutDuration:
fadeOutDuration ?? const Duration(milliseconds: 200),
fadeInDuration:
fadeInDuration ?? const Duration(milliseconds: 200),
filterQuality: FilterQuality.high,
errorWidget: (context, url, error) => placeholder(context),
placeholder: (context, url) => placeholder(context),
),
)
: placeholder(context);
}
Widget placeholder(context) {
return SizedBox(
width: width ?? double.infinity,
height: height ?? double.infinity,
child: Center(
child: Image.asset(
'assets/images/loading.png',
width: 300,
height: 300,
)),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:pilipala/utils/utils.dart';
class StatDanMu extends StatelessWidget {
final String? theme;
final int? danmu;
final String? size;
const StatDanMu({Key? key, this.theme, this.danmu, this.size})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Image.asset(
'assets/images/dm_$theme.png',
width: size == 'medium' ? 16 : 14,
height: size == 'medium' ? 16 : 14,
),
const SizedBox(width: 2),
Text(
Utils.numFormat(danmu!),
style: TextStyle(
fontSize: size == 'medium' ? 12 : 11,
color: theme == 'white'
? Colors.white
: Theme.of(context).colorScheme.outline,
),
)
],
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:pilipala/utils/utils.dart';
class StatView extends StatelessWidget {
final String? theme;
final int? view;
final String? size;
const StatView({Key? key, this.theme, this.view, this.size}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Image.asset(
'assets/images/view_$theme.png',
width: size == 'medium' ? 16 : 14,
height: size == 'medium' ? 16 : 14,
),
const SizedBox(width: 2),
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,
),
),
],
);
}
}

View File

@ -0,0 +1,214 @@
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/pages/home/controller.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class VideoCardV extends StatelessWidget {
var videoItem;
VideoCardV({Key? key, required this.videoItem}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
elevation: 0.8,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: InkWell(
onTap: () async {
await Future.delayed(const Duration(milliseconds: 200));
Get.toNamed('/video?aid=${videoItem.id}',
arguments: {'videoItem': videoItem});
},
onLongPress: () {
print('长按');
},
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
),
child: 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: [
NetworkImgLayer(
// 指定图片尺寸
// src: videoItem['pic'] + '@${(maxWidth * 2).toInt() }w',
src: videoItem.pic + '@.webp',
width: maxWidth,
height: maxHeight,
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 200),
child: VideoStat(
view: videoItem.stat.view,
danmaku: videoItem.stat.danmaku,
duration: videoItem.duration,
),
),
)
],
);
}),
),
),
VideoContent(videoItem: videoItem)
],
),
),
);
}
}
class VideoContent extends StatelessWidget {
final videoItem;
const VideoContent({Key? key, required this.videoItem}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(8, 8, 6, 7),
// 单列
// padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
// fontSize:
// Theme.of(context).textTheme.titleSmall!.fontSize,
fontSize: 13,
fontWeight: FontWeight.w500),
maxLines: Get.find<HomeController>().crossAxisCount,
overflow: TextOverflow.ellipsis,
),
SizedBox(
height: 18,
child: Row(
children: [
if (videoItem.rcmdReason.content != '') ...[
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(
videoItem.rcmdReason.content,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelSmall!.fontSize,
color: Theme.of(context).colorScheme.primary,
),
),
),
const SizedBox(width: 4)
],
Expanded(
child: LayoutBuilder(builder:
(BuildContext context, BoxConstraints constraints) {
return SizedBox(
width: constraints.maxWidth,
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
);
}),
),
],
),
),
],
),
),
);
}
}
class VideoStat extends StatelessWidget {
final int? view;
final int? danmaku;
final int? duration;
const VideoStat(
{Key? key,
required this.view,
required this.danmaku,
required this.duration})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 45,
padding: const EdgeInsets.only(top: 22, left: 8, right: 8),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black54,
],
tileMode: TileMode.mirror,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
StatView(
theme: 'white',
view: view,
),
const SizedBox(width: 8),
StatDanMu(
theme: 'white',
danmu: danmaku,
),
],
),
Text(
Utils.timeFormat(duration!),
style: const TextStyle(fontSize: 11, color: Colors.white),
)
],
),
);
}
}

View File

@ -0,0 +1,97 @@
class RecVideoItemModel {
RecVideoItemModel({
this.id,
this.bvid,
this.cid,
this.goto,
this.uri,
this.pic,
this.title,
this.duration,
this.pubdate,
this.owner,
this.stat,
this.rcmdReason,
});
int? id = -1;
String? bvid = '';
int? cid = -1;
String? goto = '';
String? uri = '';
String? pic = '';
String? title = '';
int? duration = -1;
int? pubdate = -1;
Onwer? owner;
Stat? stat;
RcmdReason? rcmdReason;
RecVideoItemModel.fromJson(Map<String, dynamic> json) {
id = json["id"];
bvid = json["bvid"];
cid = json["cid"];
goto = json["goto"];
uri = json["uri"];
pic = json["pic"];
title = json["title"];
duration = json["duration"];
pubdate = json["pubdate"];
owner = Onwer.fromJson(json["owner"]);
stat = Stat.fromJson(json["stat"]);
rcmdReason = json["rcmd_reason"] != null
? RcmdReason.fromJson(json["rcmd_reason"])
: RcmdReason(content: '');
}
}
class Onwer {
Onwer({
this.mid,
this.name,
this.face,
});
int? mid;
String? name;
String? face;
Onwer.fromJson(Map<String, dynamic> json) {
mid = json["mid"];
name = json["name"];
face = json['face'];
}
}
class Stat {
Stat({
this.view,
this.like,
this.danmaku,
});
int? view;
int? like;
int? danmaku;
Stat.fromJson(Map<String, dynamic> json) {
view = json["view"];
like = json["like"];
danmaku = json['danmaku'];
}
}
class RcmdReason {
RcmdReason({
this.reasonType,
this.content,
});
int? reasonType;
String? content = '';
RcmdReason.fromJson(Map<String, dynamic> json) {
reasonType = json["reason_type"];
content = json["content"] ?? '';
}
}

View File

@ -2,14 +2,16 @@ 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/models/models_rec_video_item.dart';
class HomeController extends GetxController {
final ScrollController scrollController = ScrollController();
int count = 12;
int _currentPage = 1;
int crossAxisCount = 2;
RxList videoList = [].obs;
RxList<RecVideoItemModel> videoList = [RecVideoItemModel()].obs;
bool isLoadingMore = false;
@override
void onInit() {
super.onInit();
@ -22,13 +24,17 @@ class HomeController extends GetxController {
Api.recommendList,
data: {'feed_version': "V3", 'ps': count, 'fresh_idx': _currentPage},
);
var data = res.data['data']['item'];
List<RecVideoItemModel> list = [];
for (var i in res.data['data']['item']) {
print(i);
list.add(RecVideoItemModel.fromJson(i));
}
if (type == 'init') {
videoList.value = data;
videoList.value = list;
} else if (type == 'onRefresh') {
videoList.insertAll(0, data);
videoList.insertAll(0, list);
} else if (type == 'onLoad') {
videoList.addAll(data);
videoList.addAll(list);
}
_currentPage += 1;
isLoadingMore = false;

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/video_card_v.dart';
import './controller.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/pages/home/widgets/app_bar.dart';
@ -76,15 +77,14 @@ class _HomePageState extends State<HomePage>
// 列数
crossAxisCount: _homeController.crossAxisCount,
mainAxisExtent: MediaQuery.of(context).size.width /
_homeController.crossAxisCount *
(10 / 16) +
_homeController.crossAxisCount /
StyleString.aspectRatio +
72),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Text(index.toString()),
);
return videoList.isNotEmpty
? VideoCardV(videoItem: videoList[index])
: const Text('加载中');
},
childCount: videoList.isNotEmpty ? videoList.length : 10,
),

View File

@ -1,5 +1,6 @@
// 工具函数
import 'dart:io';
import 'package:get/get_utils/get_utils.dart';
import 'package:path_provider/path_provider.dart';
class Utils {
@ -13,4 +14,28 @@ class Utils {
}
return tempPath;
}
static String numFormat(int number) {
String res = (number / 10000).toString();
if (int.parse(res.split('.')[0]) >= 1) {
return '${(number / 10000).toPrecision(1)}';
} else {
return number.toString();
}
}
static String timeFormat(int time) {
// 1小时内
if (time < 3600) {
int minute = time ~/ 60;
double res = time / 60;
if (minute != res) {
return '$minute:${(time - minute * 60) < 10 ? '0${(time - minute * 60)}' : (time - minute * 60)}';
} else {
return minute.toString();
}
} else {
return '';
}
}
}