diff --git a/lib/common/skeleton/video_card_v.dart b/lib/common/skeleton/video_card_v.dart index fc247e56..d13eaee3 100644 --- a/lib/common/skeleton/video_card_v.dart +++ b/lib/common/skeleton/video_card_v.dart @@ -45,11 +45,6 @@ class VideoCardVSkeleton extends StatelessWidget { margin: const EdgeInsets.only(bottom: 12), color: Theme.of(context).colorScheme.onInverseSurface, ), - Container( - width: 80, - height: 12, - color: Theme.of(context).colorScheme.onInverseSurface, - ), ], ), ), diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index eb37d0e1..c81c878b 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -15,12 +15,14 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; // 视频卡片 - 垂直布局 class VideoCardV extends StatelessWidget { final dynamic videoItem; + final int crossAxisCount; final Function()? longPress; final Function()? longPressEnd; const VideoCardV({ Key? key, required this.videoItem, + required this.crossAxisCount, this.longPress, this.longPressEnd, }) : super(key: key); @@ -77,7 +79,7 @@ class VideoCardV extends StatelessWidget { Widget build(BuildContext context) { String heroTag = Utils.makeHeroTag(videoItem.id); return Card( - elevation: 1, + elevation: crossAxisCount == 1 ? 0 : 1, clipBehavior: Clip.hardEdge, margin: EdgeInsets.zero, child: GestureDetector( @@ -100,17 +102,27 @@ class VideoCardV extends StatelessWidget { child: LayoutBuilder(builder: (context, boxConstraints) { double maxWidth = boxConstraints.maxWidth; double maxHeight = boxConstraints.maxHeight; - return Hero( - tag: heroTag, - child: NetworkImgLayer( - src: videoItem.pic, - width: maxWidth, - height: maxHeight, - ), + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: videoItem.pic, + width: maxWidth, + height: maxHeight, + ), + ), + if (crossAxisCount == 1) + PBadge( + bottom: 10, + right: 10, + text: videoItem.duration, + ) + ], ); }), ), - VideoContent(videoItem: videoItem) + VideoContent(videoItem: videoItem, crossAxisCount: crossAxisCount) ], ), ), @@ -121,22 +133,47 @@ class VideoCardV extends StatelessWidget { class VideoContent extends StatelessWidget { final dynamic videoItem; - const VideoContent({Key? key, required this.videoItem}) : super(key: key); + final int crossAxisCount; + const VideoContent( + {Key? key, required this.videoItem, required this.crossAxisCount}) + : super(key: key); @override Widget build(BuildContext context) { return Expanded( + flex: crossAxisCount == 1 ? 0 : 1, child: Padding( - padding: const EdgeInsets.fromLTRB(9, 8, 9, 4), + padding: crossAxisCount == 1 + ? const EdgeInsets.fromLTRB(9, 9, 9, 4) + : const EdgeInsets.fromLTRB(9, 8, 9, 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - videoItem.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, + Row( + children: [ + Expanded( + child: Text( + videoItem.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + if (videoItem.goto == 'av' && crossAxisCount == 1) ...[ + const SizedBox(width: 10), + WatchLater( + size: 32, + iconSize: 18, + callFn: () async { + int aid = videoItem.param; + var res = + await UserHttp.toViewLater(bvid: IdUtils.av2bv(aid)); + SmartDialog.showToast(res['msg']); + }, + ), + ], + ], ), - + if (crossAxisCount == 1) const SizedBox(height: 6), Row( children: [ if (videoItem.goto == 'bangumi') ...[ @@ -167,6 +204,7 @@ class VideoContent extends StatelessWidget { ) ], Expanded( + flex: crossAxisCount == 1 ? 0 : 1, child: Text( videoItem.owner.name, maxLines: 1, @@ -177,95 +215,33 @@ class VideoContent extends StatelessWidget { ), ), ), - if (videoItem.goto == 'av') - SizedBox( - width: 24, - height: 24, - child: PopupMenuButton( - padding: EdgeInsets.zero, - tooltip: '稍后再看', - icon: Icon( - Icons.more_vert_outlined, - color: Theme.of(context).colorScheme.outline, - size: 14, - ), - position: PopupMenuPosition.under, - // constraints: const BoxConstraints(maxHeight: 35), - onSelected: (String type) {}, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - onTap: () async { - int aid = videoItem.param; - var res = await UserHttp.toViewLater( - bvid: IdUtils.av2bv(aid)); - SmartDialog.showToast(res['msg']); - }, - value: 'pause', - height: 35, - child: const Row( - children: [ - Icon(Icons.watch_later_outlined, size: 16), - SizedBox(width: 6), - Text('稍后再看', style: TextStyle(fontSize: 13)) - ], - ), - ), - ], + if (crossAxisCount == 1) ...[ + Text( + ' • ', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, ), ), + VideoStat( + videoItem: videoItem, + ) + ], + const Spacer(), + if (videoItem.goto == 'av' && crossAxisCount != 1) + WatchLater( + size: 24, + iconSize: 14, + callFn: () async { + int aid = videoItem.param; + var res = + await UserHttp.toViewLater(bvid: IdUtils.av2bv(aid)); + SmartDialog.showToast(res['msg']); + }, + ), ], ), - // Row( - // children: [ - // const SizedBox(width: 1), - // StatView( - // theme: 'gray', - // view: videoItem.stat.view, - // ), - // const SizedBox(width: 10), - // StatDanMu( - // theme: 'gray', - // danmu: videoItem.stat.danmaku, - // ), - // const Spacer(), - // SizedBox( - // width: 24, - // height: 24, - // child: PopupMenuButton( - // padding: EdgeInsets.zero, - // tooltip: '稍后再看', - // icon: Icon( - // Icons.more_vert_outlined, - // color: Theme.of(context).colorScheme.outline, - // size: 14, - // ), - // position: PopupMenuPosition.under, - // // constraints: const BoxConstraints(maxHeight: 35), - // onSelected: (String type) {}, - // itemBuilder: (BuildContext context) => - // >[ - // PopupMenuItem( - // onTap: () async { - // var res = - // await UserHttp.toViewLater(bvid: videoItem.bvid); - // SmartDialog.showToast(res['msg']); - // }, - // value: 'pause', - // height: 35, - // child: const Row( - // children: [ - // Icon(Icons.watch_later_outlined, size: 16), - // SizedBox(width: 6), - // Text('稍后再看', style: TextStyle(fontSize: 13)) - // ], - // ), - // ), - // ], - // ), - // ), - // ], - // ), ], ), ), @@ -274,53 +250,77 @@ class VideoContent extends StatelessWidget { } class VideoStat extends StatelessWidget { - final int? view; - final int? danmaku; - final int? duration; + final dynamic videoItem; - const VideoStat( - {Key? key, - required this.view, - required this.danmaku, - required this.duration}) - : super(key: key); + const VideoStat({ + Key? key, + required this.videoItem, + }) : super(key: key); @override Widget build(BuildContext context) { - return Container( - height: 48, - padding: const EdgeInsets.only(top: 22, left: 6, right: 6), - 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: 6), - StatDanMu( - theme: 'white', - danmu: danmaku, - ), - ], + return Row( + children: [ + Text( + '${videoItem.stat.view}次观看', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + Text( + ' • ${videoItem.stat.danmu}条弹幕', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + ); + } +} + +class WatchLater extends StatelessWidget { + final double? size; + final double? iconSize; + final Function? callFn; + + const WatchLater({ + Key? key, + required this.size, + required this.iconSize, + this.callFn, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: size, + height: size, + child: PopupMenuButton( + padding: EdgeInsets.zero, + tooltip: '稍后再看', + icon: Icon( + Icons.more_vert_outlined, + color: Theme.of(context).colorScheme.outline, + size: iconSize, + ), + position: PopupMenuPosition.under, + // constraints: const BoxConstraints(maxHeight: 35), + onSelected: (String type) {}, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + onTap: () => callFn!(), + value: 'pause', + height: 35, + child: const Row( + children: [ + Icon(Icons.watch_later_outlined, size: 16), + SizedBox(width: 6), + Text('稍后再看', style: TextStyle(fontSize: 13)) + ], + ), ), - Text( - Utils.timeFormat(duration!), - style: const TextStyle(fontSize: 11, color: Colors.white), - ) ], ), ); diff --git a/lib/pages/live/controller.dart b/lib/pages/live/controller.dart index 2779659a..8cdf53a7 100644 --- a/lib/pages/live/controller.dart +++ b/lib/pages/live/controller.dart @@ -1,17 +1,27 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/http/live.dart'; import 'package:pilipala/models/live/item.dart'; +import 'package:pilipala/utils/storage.dart'; class LiveController extends GetxController { final ScrollController scrollController = ScrollController(); int count = 12; int _currentPage = 1; - int crossAxisCount = 2; + RxInt crossAxisCount = 2.obs; RxList liveList = [LiveItemModel()].obs; bool isLoadingMore = false; bool flag = false; OverlayEntry? popupDialog; + Box setting = GStrorage.setting; + + @override + void onInit() { + super.onInit(); + crossAxisCount.value = + setting.get(SettingBoxKey.enableSingleRow, defaultValue: false) ? 1 : 2; + } // 获取推荐 Future queryLiveList(type) async { diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index 1acf0dc7..5e3e68a1 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -129,14 +129,15 @@ class _LivePageState extends State { } Widget contentGrid(ctr, liveList) { - double maxWidth = Get.size.width; - int baseWidth = 500; - int step = 300; - int crossAxisCount = - maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2; - if (maxWidth < 300) { - crossAxisCount = 1; - } + // double maxWidth = Get.size.width; + // int baseWidth = 500; + // int step = 300; + // int crossAxisCount = + // maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2; + // if (maxWidth < 300) { + // crossAxisCount = 1; + // } + int crossAxisCount = ctr.crossAxisCount.value; return SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( // 行间距 @@ -147,13 +148,15 @@ class _LivePageState extends State { crossAxisCount: crossAxisCount, mainAxisExtent: Get.size.width / crossAxisCount / StyleString.aspectRatio + - 68 * MediaQuery.of(context).textScaleFactor, + (crossAxisCount == 1 ? 48 : 68) * + MediaQuery.of(context).textScaleFactor, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return liveList!.isNotEmpty ? LiveCardV( liveItem: liveList[index], + crossAxisCount: crossAxisCount, longPress: () { _liveController.popupDialog = _createPopupDialog(liveList[index]); diff --git a/lib/pages/live/widgets/live_item.dart b/lib/pages/live/widgets/live_item.dart index a8185be7..48a4356e 100644 --- a/lib/pages/live/widgets/live_item.dart +++ b/lib/pages/live/widgets/live_item.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; -import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; @@ -9,12 +8,14 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; // 视频卡片 - 垂直布局 class LiveCardV extends StatelessWidget { final LiveItemModel liveItem; + final int crossAxisCount; final Function()? longPress; final Function()? longPressEnd; const LiveCardV({ Key? key, required this.liveItem, + required this.crossAxisCount, this.longPress, this.longPressEnd, }) : super(key: key); @@ -23,7 +24,7 @@ class LiveCardV extends StatelessWidget { Widget build(BuildContext context) { String heroTag = Utils.makeHeroTag(liveItem.roomId); return Card( - elevation: 1, + elevation: crossAxisCount == 1 ? 0 : 1, clipBehavior: Clip.hardEdge, margin: EdgeInsets.zero, child: GestureDetector( @@ -45,12 +46,7 @@ class LiveCardV extends StatelessWidget { child: Column( children: [ ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: StyleString.imgRadius, - topRight: StyleString.imgRadius, - bottomLeft: StyleString.imgRadius, - bottomRight: StyleString.imgRadius, - ), + borderRadius: const BorderRadius.all(StyleString.imgRadius), child: AspectRatio( aspectRatio: StyleString.aspectRatio, child: LayoutBuilder(builder: (context, boxConstraints) { @@ -66,24 +62,25 @@ class LiveCardV extends StatelessWidget { height: maxHeight, ), ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: AnimatedOpacity( - opacity: 1, - duration: const Duration(milliseconds: 200), - child: VideoStat( - liveItem: liveItem, + if (crossAxisCount != 1) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: AnimatedOpacity( + opacity: 1, + duration: const Duration(milliseconds: 200), + child: VideoStat( + liveItem: liveItem, + ), ), ), - ), ], ); }), ), ), - LiveContent(liveItem: liveItem) + LiveContent(liveItem: liveItem, crossAxisCount: crossAxisCount) ], ), ), @@ -94,13 +91,18 @@ class LiveCardV extends StatelessWidget { class LiveContent extends StatelessWidget { final dynamic liveItem; - const LiveContent({Key? key, required this.liveItem}) : super(key: key); + final int crossAxisCount; + const LiveContent( + {Key? key, required this.liveItem, required this.crossAxisCount}) + : super(key: key); @override Widget build(BuildContext context) { return Expanded( + flex: crossAxisCount == 1 ? 0 : 1, child: Padding( - // 多列 - padding: const EdgeInsets.fromLTRB(9, 9, 9, 8), + padding: crossAxisCount == 1 + ? const EdgeInsets.fromLTRB(9, 9, 9, 4) + : const EdgeInsets.fromLTRB(9, 8, 9, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -112,29 +114,40 @@ class LiveContent extends StatelessWidget { fontWeight: FontWeight.w500, letterSpacing: 0.3, ), - maxLines: 2, + maxLines: crossAxisCount == 1 ? 1 : 2, overflow: TextOverflow.ellipsis, ), + if (crossAxisCount == 1) const SizedBox(height: 4), Row( children: [ - const PBadge( - text: 'UP', - size: 'small', - stack: 'normal', - fs: 9, + Text( + liveItem.uname, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - Expanded( - child: Text( - liveItem.uname, - textAlign: TextAlign.start, + if (crossAxisCount == 1) ...[ + Text( + ' • ${liveItem!.areaName!}', style: TextStyle( fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ) + Text( + ' • ${liveItem!.watchedShow!['text_small']}人观看', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + ] ], ), ], diff --git a/lib/pages/rcmd/controller.dart b/lib/pages/rcmd/controller.dart index df7aa2e7..183b79bf 100644 --- a/lib/pages/rcmd/controller.dart +++ b/lib/pages/rcmd/controller.dart @@ -12,10 +12,14 @@ class RcmdController extends GetxController { bool isLoadingMore = true; OverlayEntry? popupDialog; Box recVideo = GStrorage.recVideo; + Box setting = GStrorage.setting; + RxInt crossAxisCount = 2.obs; @override void onInit() { super.onInit(); + crossAxisCount.value = + setting.get(SettingBoxKey.enableSingleRow, defaultValue: false) ? 1 : 2; if (recVideo.get('cacheList') != null && recVideo.get('cacheList').isNotEmpty) { List list = []; diff --git a/lib/pages/rcmd/view.dart b/lib/pages/rcmd/view.dart index 0ed66ac9..ab4d7f55 100644 --- a/lib/pages/rcmd/view.dart +++ b/lib/pages/rcmd/view.dart @@ -142,31 +142,34 @@ class _RcmdPageState extends State } Widget contentGrid(ctr, videoList) { - double maxWidth = Get.size.width; - int baseWidth = 500; - int step = 300; - int crossAxisCount = - maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2; - if (maxWidth < 300) { - crossAxisCount = 1; - } + // double maxWidth = Get.size.width; + // int baseWidth = 500; + // int step = 300; + // int crossAxisCount = + // maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2; + // if (maxWidth < 300) { + // crossAxisCount = 1; + // } + int crossAxisCount = ctr.crossAxisCount.value; + double mainAxisExtent = + (Get.size.width / crossAxisCount / StyleString.aspectRatio) + + 68 * MediaQuery.of(context).textScaleFactor; return SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( // 行间距 - mainAxisSpacing: StyleString.cardSpace + 4, + mainAxisSpacing: StyleString.safeSpace, // 列间距 - crossAxisSpacing: StyleString.cardSpace + 4, + crossAxisSpacing: StyleString.safeSpace, // 列数 crossAxisCount: crossAxisCount, - mainAxisExtent: - (Get.size.width / crossAxisCount / StyleString.aspectRatio) + - 68 * MediaQuery.of(context).textScaleFactor, + mainAxisExtent: mainAxisExtent, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return videoList!.isNotEmpty ? VideoCardV( videoItem: videoList[index], + crossAxisCount: crossAxisCount, longPress: () { _rcmdController.popupDialog = _createPopupDialog(videoList[index]); diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart index 73cad841..c502beeb 100644 --- a/lib/pages/setting/style_setting.dart +++ b/lib/pages/setting/style_setting.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/models/common/theme_type.dart'; @@ -75,6 +76,13 @@ class _StyleSettingState extends State { setKey: SettingBoxKey.iosTransition, defaultVal: false, ), + SetSwitchItem( + title: '首页单列', + subTitle: '每行展示一个内容卡片', + setKey: SettingBoxKey.enableSingleRow, + defaultVal: false, + callFn: (val) => {SmartDialog.showToast('下次启动时生效')}, + ), ListTile( dense: false, onTap: () { diff --git a/lib/pages/setting/widgets/switch_item.dart b/lib/pages/setting/widgets/switch_item.dart index 1b2cc620..0e091b9a 100644 --- a/lib/pages/setting/widgets/switch_item.dart +++ b/lib/pages/setting/widgets/switch_item.dart @@ -8,12 +8,14 @@ class SetSwitchItem extends StatefulWidget { final String? subTitle; final String? setKey; final bool? defaultVal; + final Function? callFn; const SetSwitchItem({ this.title, this.subTitle, this.setKey, this.defaultVal, + this.callFn, Key? key, }) : super(key: key); @@ -32,12 +34,15 @@ class _SetSwitchItemState extends State { val = Setting.get(widget.setKey, defaultValue: widget.defaultVal ?? false); } - void switchChange(value) { + void switchChange(value) async { val = value ?? !val; - Setting.put(widget.setKey, val); + await Setting.put(widget.setKey, val); if (widget.setKey == SettingBoxKey.autoUpdate && value == true) { Utils.checkUpdata(); } + if (widget.callFn != null) { + widget.callFn!.call(val); + } setState(() {}); } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index a117760e..d92384a0 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -112,6 +112,7 @@ class SettingBoxKey { static const String dynamicColor = 'dynamicColor'; // bool static const String customColor = 'customColor'; // 自定义主题色 static const String iosTransition = 'iosTransition'; // ios路由 + static const String enableSingleRow = 'enableSingleRow'; // 首页单列 } class LocalCacheKey {