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