mod: 首页推荐视频
This commit is contained in:
@ -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;
|
||||
}
|
||||
|
66
lib/common/widgets/network_img_layer.dart
Normal file
66
lib/common/widgets/network_img_layer.dart
Normal 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,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
34
lib/common/widgets/stat/danmu.dart
Normal file
34
lib/common/widgets/stat/danmu.dart
Normal 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,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
34
lib/common/widgets/stat/view.dart
Normal file
34
lib/common/widgets/stat/view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
214
lib/common/widgets/video_card_v.dart
Normal file
214
lib/common/widgets/video_card_v.dart
Normal 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),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
97
lib/models/models_rec_video_item.dart
Normal file
97
lib/models/models_rec_video_item.dart
Normal 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"] ?? '';
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user