diff --git a/assets/images/dm.svg b/assets/images/dm.svg
new file mode 100644
index 00000000..2690acd2
--- /dev/null
+++ b/assets/images/dm.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/assets/images/dm_gray.png b/assets/images/dm_gray.png
new file mode 100644
index 00000000..438cffc0
Binary files /dev/null and b/assets/images/dm_gray.png differ
diff --git a/assets/images/dm_white.png b/assets/images/dm_white.png
new file mode 100644
index 00000000..71fd28f9
Binary files /dev/null and b/assets/images/dm_white.png differ
diff --git a/assets/images/loading.gif b/assets/images/loading.gif
new file mode 100644
index 00000000..7c2d1f70
Binary files /dev/null and b/assets/images/loading.gif differ
diff --git a/assets/images/loading.png b/assets/images/loading.png
new file mode 100644
index 00000000..357ae73f
Binary files /dev/null and b/assets/images/loading.png differ
diff --git a/assets/images/play.svg b/assets/images/play.svg
new file mode 100644
index 00000000..0032f069
--- /dev/null
+++ b/assets/images/play.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/tv.svg b/assets/images/tv.svg
new file mode 100644
index 00000000..fdb077b1
--- /dev/null
+++ b/assets/images/tv.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/up.svg b/assets/images/up.svg
new file mode 100644
index 00000000..c63989c5
--- /dev/null
+++ b/assets/images/up.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/up_gray.png b/assets/images/up_gray.png
new file mode 100644
index 00000000..c6d7f4ab
Binary files /dev/null and b/assets/images/up_gray.png differ
diff --git a/assets/images/view.svg b/assets/images/view.svg
new file mode 100644
index 00000000..88fe609c
--- /dev/null
+++ b/assets/images/view.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/assets/images/view_gray.png b/assets/images/view_gray.png
new file mode 100644
index 00000000..fe2b3482
Binary files /dev/null and b/assets/images/view_gray.png differ
diff --git a/assets/images/view_white.png b/assets/images/view_white.png
new file mode 100644
index 00000000..d97b0e93
Binary files /dev/null and b/assets/images/view_white.png differ
diff --git a/lib/common/constants.dart b/lib/common/constants.dart
index ab9d0178..41bbf8c2 100644
--- a/lib/common/constants.dart
+++ b/lib/common/constants.dart
@@ -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;
}
diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart
new file mode 100644
index 00000000..02cf33b5
--- /dev/null
+++ b/lib/common/widgets/network_img_layer.dart
@@ -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,
+ )),
+ );
+ }
+}
diff --git a/lib/common/widgets/stat/danmu.dart b/lib/common/widgets/stat/danmu.dart
new file mode 100644
index 00000000..44f63b21
--- /dev/null
+++ b/lib/common/widgets/stat/danmu.dart
@@ -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,
+ ),
+ )
+ ],
+ );
+ }
+}
diff --git a/lib/common/widgets/stat/view.dart b/lib/common/widgets/stat/view.dart
new file mode 100644
index 00000000..6f6d1960
--- /dev/null
+++ b/lib/common/widgets/stat/view.dart
@@ -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,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart
new file mode 100644
index 00000000..32b9c927
--- /dev/null
+++ b/lib/common/widgets/video_card_v.dart
@@ -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().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: [
+ 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),
+ )
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/models/models_rec_video_item.dart b/lib/models/models_rec_video_item.dart
new file mode 100644
index 00000000..b37988a7
--- /dev/null
+++ b/lib/models/models_rec_video_item.dart
@@ -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 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 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 json) {
+ view = json["view"];
+ like = json["like"];
+ danmaku = json['danmaku'];
+ }
+}
+
+class RcmdReason {
+ RcmdReason({
+ this.reasonType,
+ this.content,
+ });
+
+ int? reasonType;
+ String? content = '';
+
+ RcmdReason.fromJson(Map json) {
+ reasonType = json["reason_type"];
+ content = json["content"] ?? '';
+ }
+}
diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart
index e8d98bf4..961d05c0 100644
--- a/lib/pages/home/controller.dart
+++ b/lib/pages/home/controller.dart
@@ -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 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 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;
diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart
index 56494278..2482e465 100644
--- a/lib/pages/home/view.dart
+++ b/lib/pages/home/view.dart
@@ -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
// 列数
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,
),
diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart
index 299fb77f..0d5918f5 100644
--- a/lib/utils/utils.dart
+++ b/lib/utils/utils.dart
@@ -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 '';
+ }
+ }
}
diff --git a/pubspec.yaml b/pubspec.yaml
index bb429eb2..6d3b671c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -74,10 +74,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
- # assets:
- # - images/a_dot_burr.jpeg
- # - images/a_dot_ham.jpeg
-
+ assets:
+ - assets/images/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware