feat: 我的订阅

This commit is contained in:
guozhigq
2024-02-25 12:12:54 +08:00
parent 4da6667b81
commit 078e4716b4
18 changed files with 1032 additions and 7 deletions

View File

@ -483,4 +483,10 @@ class Api {
/// 激活buvid3
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';
/// 我的订阅
static const userSubFolder = '/x/v3/fav/folder/collected/list';
/// 我的订阅详情
static const userSubFolderDetail = '/x/space/fav/season/list';
}

View File

@ -6,6 +6,8 @@ import '../models/user/fav_folder.dart';
import '../models/user/history.dart';
import '../models/user/info.dart';
import '../models/user/stat.dart';
import '../models/user/sub_detail.dart';
import '../models/user/sub_folder.dart';
import 'api.dart';
import 'init.dart';
@ -305,4 +307,46 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
// 我的订阅
static Future userSubFolder({
required int mid,
required int pn,
required int ps,
}) async {
var res = await Request().get(Api.userSubFolder, data: {
'up_mid': mid,
'ps': ps,
'pn': pn,
'platform': 'web',
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': SubFolderModelData.fromJson(res.data['data'])
};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
static Future userSubFolderDetail({
required int seasonId,
required int pn,
required int ps,
}) async {
var res = await Request().get(Api.userSubFolderDetail, data: {
'season_id': seasonId,
'ps': ps,
'pn': pn,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': SubDetailModelData.fromJson(res.data['data'])
};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
}

View File

@ -0,0 +1,123 @@
class SubDetailModelData {
DetailInfo? info;
List<SubDetailMediaItem>? medias;
SubDetailModelData({this.info, this.medias});
SubDetailModelData.fromJson(Map<String, dynamic> json) {
info = DetailInfo.fromJson(json['info']);
if (json['medias'] != null) {
medias = <SubDetailMediaItem>[];
json['medias'].forEach((v) {
medias!.add(SubDetailMediaItem.fromJson(v));
});
}
}
}
class SubDetailMediaItem {
int? id;
String? title;
String? cover;
String? pic;
int? duration;
int? pubtime;
String? bvid;
Map? upper;
Map? cntInfo;
int? enableVt;
String? vtDisplay;
SubDetailMediaItem({
this.id,
this.title,
this.cover,
this.pic,
this.duration,
this.pubtime,
this.bvid,
this.upper,
this.cntInfo,
this.enableVt,
this.vtDisplay,
});
SubDetailMediaItem.fromJson(Map<String, dynamic> json) {
id = json['id'];
title = json['title'];
cover = json['cover'];
pic = json['cover'];
duration = json['duration'];
pubtime = json['pubtime'];
bvid = json['bvid'];
upper = json['upper'];
cntInfo = json['cnt_info'];
enableVt = json['enable_vt'];
vtDisplay = json['vt_display'];
}
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
data['id'] = id;
data['title'] = title;
data['cover'] = cover;
data['duration'] = duration;
data['pubtime'] = pubtime;
data['bvid'] = bvid;
data['upper'] = upper;
data['cnt_info'] = cntInfo;
data['enable_vt'] = enableVt;
data['vt_display'] = vtDisplay;
return data;
}
}
class DetailInfo {
int? id;
int? seasonType;
String? title;
String? cover;
Map? upper;
Map? cntInfo;
int? mediaCount;
String? intro;
int? enableVt;
DetailInfo({
this.id,
this.seasonType,
this.title,
this.cover,
this.upper,
this.cntInfo,
this.mediaCount,
this.intro,
this.enableVt,
});
DetailInfo.fromJson(Map<String, dynamic> json) {
id = json['id'];
seasonType = json['season_type'];
title = json['title'];
cover = json['cover'];
upper = json['upper'];
cntInfo = json['cnt_info'];
mediaCount = json['media_count'];
intro = json['intro'];
enableVt = json['enable_vt'];
}
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
data['id'] = id;
data['season_type'] = seasonType;
data['title'] = title;
data['cover'] = cover;
data['upper'] = upper;
data['cnt_info'] = cntInfo;
data['media_count'] = mediaCount;
data['intro'] = intro;
data['enable_vt'] = enableVt;
return data;
}
}

View File

@ -0,0 +1,111 @@
class SubFolderModelData {
final int? count;
final List<SubFolderItemData>? list;
SubFolderModelData({
this.count,
this.list,
});
factory SubFolderModelData.fromJson(Map<String, dynamic> json) {
return SubFolderModelData(
count: json['count'],
list: json['list'] != null
? (json['list'] as List)
.map<SubFolderItemData>((i) => SubFolderItemData.fromJson(i))
.toList()
: null,
);
}
}
class SubFolderItemData {
final int? id;
final int? fid;
final int? mid;
final int? attr;
final String? title;
final String? cover;
final Upper? upper;
final int? coverType;
final String? intro;
final int? ctime;
final int? mtime;
final int? state;
final int? favState;
final int? mediaCount;
final int? viewCount;
final int? vt;
final int? playSwitch;
final int? type;
final String? link;
final String? bvid;
SubFolderItemData({
this.id,
this.fid,
this.mid,
this.attr,
this.title,
this.cover,
this.upper,
this.coverType,
this.intro,
this.ctime,
this.mtime,
this.state,
this.favState,
this.mediaCount,
this.viewCount,
this.vt,
this.playSwitch,
this.type,
this.link,
this.bvid,
});
factory SubFolderItemData.fromJson(Map<String, dynamic> json) {
return SubFolderItemData(
id: json['id'],
fid: json['fid'],
mid: json['mid'],
attr: json['attr'],
title: json['title'],
cover: json['cover'],
upper: json['upper'] != null ? Upper.fromJson(json['upper']) : null,
coverType: json['cover_type'],
intro: json['intro'],
ctime: json['ctime'],
mtime: json['mtime'],
state: json['state'],
favState: json['fav_state'],
mediaCount: json['media_count'],
viewCount: json['view_count'],
vt: json['vt'],
playSwitch: json['play_switch'],
type: json['type'],
link: json['link'],
bvid: json['bvid'],
);
}
}
class Upper {
final int? mid;
final String? name;
final String? face;
Upper({
this.mid,
this.name,
this.face,
});
factory Upper.fromJson(Map<String, dynamic> json) {
return Upper(
mid: json['mid'],
name: json['name'],
face: json['face'],
);
}
}

View File

@ -24,7 +24,7 @@ class FavController extends GetxController {
if (!hasMore.value) {
return;
}
var res = await await UserHttp.userfavFolder(
var res = await UserHttp.userfavFolder(
pn: currentPage,
ps: pageSize,
mid: userInfo!.mid!,

View File

@ -34,7 +34,7 @@ class FavDetailController extends GetxController {
return;
}
isLoadingMore = true;
var res = await await UserHttp.userFavFolderDetail(
var res = await UserHttp.userFavFolderDetail(
pn: currentPage,
ps: 20,
mediaId: mediaId!,

View File

@ -31,7 +31,6 @@ class _FavDetailPageState extends State<FavDetailPage> {
super.initState();
mediaId = Get.parameters['mediaId']!;
_futureBuilderFuture = _favDetailController.queryUserFavFolderDetail();
mediaId = Get.parameters['mediaId']!;
titleStreamC = StreamController<bool>();
_controller.addListener(
() {

View File

@ -70,10 +70,6 @@ class _HistoryPageState extends State<HistoryPage> {
child1: AppBar(
titleSpacing: 0,
centerTitle: false,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.arrow_back_outlined),
),
title: Text(
'观看记录',
style: Theme.of(context).textTheme.titleMedium,

View File

@ -28,6 +28,11 @@ class MediaController extends GetxController {
'title': '我的收藏',
'onTap': () => Get.toNamed('/fav'),
},
{
'icon': Icons.subscriptions_outlined,
'title': '我的订阅',
'onTap': () => Get.toNamed('/subscription'),
},
{
'icon': Icons.watch_later_outlined,
'title': '稍后再看',

View File

@ -0,0 +1,49 @@
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/http/user.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart';
import '../../models/user/sub_folder.dart';
class SubController extends GetxController {
final ScrollController scrollController = ScrollController();
Rx<SubFolderModelData> subFolderData = SubFolderModelData().obs;
Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo;
int currentPage = 1;
int pageSize = 20;
RxBool hasMore = true.obs;
Future<dynamic> querySubFolder({type = 'init'}) async {
userInfo = userInfoCache.get('userInfoCache');
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录'};
}
var res = await UserHttp.userSubFolder(
pn: currentPage,
ps: pageSize,
mid: userInfo!.mid!,
);
if (res['status']) {
if (type == 'init') {
subFolderData.value = res['data'];
} else {
if (res['data'].list.isNotEmpty) {
subFolderData.value.list!.addAll(res['data'].list);
subFolderData.update((val) {});
}
}
currentPage++;
} else {
SmartDialog.showToast(res['msg']);
}
return res;
}
Future onLoad() async {
querySubFolder(type: 'onload');
}
}

View File

@ -0,0 +1,4 @@
library sub;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,85 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/fav/widgets/item.dart';
import 'controller.dart';
import 'widgets/item.dart';
class SubPage extends StatefulWidget {
const SubPage({super.key});
@override
State<SubPage> createState() => _SubPageState();
}
class _SubPageState extends State<SubPage> {
final SubController _subController = Get.put(SubController());
late Future _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
super.initState();
_futureBuilderFuture = _subController.querySubFolder();
scrollController = _subController.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_subController.onLoad();
});
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
'我的订阅',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
return Obx(
() => ListView.builder(
controller: scrollController,
itemCount: _subController.subFolderData.value.list!.length,
itemBuilder: (context, index) {
return SubItem(
subFolderItem:
_subController.subFolderData.value.list![index]);
},
),
);
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data?['msg'],
fn: () => setState(() {}),
),
],
);
}
} else {
// 骨架屏
return const Text('请求中');
}
},
),
);
}
}

View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart';
import '../../../models/user/sub_folder.dart';
class SubItem extends StatelessWidget {
final SubFolderItemData subFolderItem;
const SubItem({super.key, required this.subFolderItem});
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(subFolderItem.id);
return InkWell(
onTap: () => Get.toNamed(
'/subDetail',
arguments: subFolderItem,
parameters: {
'heroTag': heroTag,
'seasonId': subFolderItem.id.toString(),
},
),
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 7, 12, 7),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return SizedBox(
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Hero(
tag: heroTag,
child: NetworkImgLayer(
src: subFolderItem.cover,
width: maxWidth,
height: maxHeight,
),
);
},
),
),
VideoContent(subFolderItem: subFolderItem)
],
),
);
},
),
),
);
}
}
class VideoContent extends StatelessWidget {
final SubFolderItemData subFolderItem;
const VideoContent({super.key, required this.subFolderItem});
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
subFolderItem.title!,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
),
const SizedBox(height: 2),
Text(
'合集 UP主${subFolderItem.upper!.name!}',
textAlign: TextAlign.start,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(height: 2),
Text(
'${subFolderItem.mediaCount}个视频',
textAlign: TextAlign.start,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:get/get.dart';
import 'package:pilipala/http/user.dart';
import '../../models/user/sub_detail.dart';
import '../../models/user/sub_folder.dart';
class SubDetailController extends GetxController {
late SubFolderItemData item;
late int seasonId;
late String heroTag;
int currentPage = 1;
bool isLoadingMore = false;
Rx<DetailInfo> subInfo = DetailInfo().obs;
RxList<SubDetailMediaItem> subList = <SubDetailMediaItem>[].obs;
RxString loadingText = '加载中...'.obs;
int mediaCount = 0;
@override
void onInit() {
item = Get.arguments;
if (Get.parameters.keys.isNotEmpty) {
seasonId = int.parse(Get.parameters['seasonId']!);
heroTag = Get.parameters['heroTag']!;
}
super.onInit();
}
Future<dynamic> queryUserSubFolderDetail({type = 'init'}) async {
if (type == 'onLoad' && subList.length >= mediaCount) {
loadingText.value = '没有更多了';
return;
}
isLoadingMore = true;
var res = await UserHttp.userSubFolderDetail(
seasonId: seasonId,
ps: 20,
pn: currentPage,
);
if (res['status']) {
subInfo.value = res['data'].info;
if (currentPage == 1 && type == 'init') {
subList.value = res['data'].medias;
mediaCount = res['data'].info.mediaCount;
} else if (type == 'onLoad') {
subList.addAll(res['data'].medias);
}
if (subList.length >= mediaCount) {
loadingText.value = '没有更多了';
}
}
currentPage += 1;
isLoadingMore = false;
return res;
}
onLoad() {
queryUserSubFolderDetail(type: 'onLoad');
}
}

View File

@ -0,0 +1,4 @@
library sub_detail;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,257 @@
import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import '../../models/user/sub_folder.dart';
import '../../utils/utils.dart';
import 'controller.dart';
import 'widget/sub_video_card.dart';
class SubDetailPage extends StatefulWidget {
const SubDetailPage({super.key});
@override
State<SubDetailPage> createState() => _SubDetailPageState();
}
class _SubDetailPageState extends State<SubDetailPage> {
late final ScrollController _controller = ScrollController();
final SubDetailController _subDetailController =
Get.put(SubDetailController());
late StreamController<bool> titleStreamC; // a
late Future _futureBuilderFuture;
late String seasonId;
@override
void initState() {
super.initState();
seasonId = Get.parameters['seasonId']!;
_futureBuilderFuture = _subDetailController.queryUserSubFolderDetail();
titleStreamC = StreamController<bool>();
_controller.addListener(
() {
if (_controller.offset > 160) {
titleStreamC.add(true);
} else if (_controller.offset <= 160) {
titleStreamC.add(false);
}
if (_controller.position.pixels >=
_controller.position.maxScrollExtent - 200) {
EasyThrottle.throttle('subDetail', const Duration(seconds: 1), () {
_subDetailController.onLoad();
});
}
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _controller,
slivers: [
SliverAppBar(
expandedHeight: 260 - MediaQuery.of(context).padding.top,
pinned: true,
titleSpacing: 0,
title: StreamBuilder(
stream: titleStreamC.stream,
initialData: false,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_subDetailController.item.title!,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
'${_subDetailController.item.mediaCount!}条视频',
style: Theme.of(context).textTheme.labelMedium,
)
],
)
],
),
);
},
),
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor.withOpacity(0.2),
),
),
),
padding: EdgeInsets.only(
top: kTextTabBarHeight +
MediaQuery.of(context).padding.top +
30,
left: 20,
right: 20),
child: SizedBox(
height: 200,
child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: _subDetailController.heroTag,
child: NetworkImgLayer(
width: 180,
height: 110,
src: _subDetailController.item.cover,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
_subDetailController.item.title!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleMedium!
.fontSize,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
GestureDetector(
onTap: () {
SubFolderItemData item =
_subDetailController.item;
Get.toNamed(
'/member?mid=${item.upper!.mid}',
arguments: {
'face': item.upper!.face,
},
);
},
child: Text(
_subDetailController.item.upper!.name!,
style: TextStyle(
color:
Theme.of(context).colorScheme.primary),
),
),
const SizedBox(height: 4),
Text(
'${Utils.numFormat(_subDetailController.item.viewCount)}次播放',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.outline),
),
],
),
),
],
),
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx(
() => Text(
'${_subDetailController.subList.length}条视频',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
letterSpacing: 1),
),
),
),
),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
if (data['status']) {
if (_subDetailController.item.mediaCount == 0) {
return const NoData();
} else {
List subList = _subDetailController.subList;
return Obx(
() => subList.isEmpty
? const SliverToBoxAdapter(child: SizedBox())
: SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
return SubVideoCardH(
videoItem: subList[index],
);
}, childCount: subList.length),
),
);
}
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
);
}
},
),
SliverToBoxAdapter(
child: Container(
height: MediaQuery.of(context).padding.bottom + 60,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom),
child: Center(
child: Obx(
() => Text(
_subDetailController.loadingText.value,
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: 13),
),
),
),
),
)
],
),
);
}
}

View File

@ -0,0 +1,168 @@
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/http/search.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../../../common/widgets/badge.dart';
import '../../../models/user/sub_detail.dart';
// 收藏视频卡片 - 水平布局
class SubVideoCardH extends StatelessWidget {
final SubDetailMediaItem videoItem;
final int? searchType;
const SubVideoCardH({
Key? key,
required this.videoItem,
this.searchType,
}) : super(key: key);
@override
Widget build(BuildContext context) {
int id = videoItem.id!;
String bvid = videoItem.bvid!;
String heroTag = Utils.makeHeroTag(id);
return InkWell(
onTap: () async {
int cid = await SearchHttp.ab2c(bvid: bvid);
Map<String, String> parameters = {
'bvid': bvid,
'cid': cid.toString(),
};
Get.toNamed('/video', parameters: parameters, arguments: {
'videoItem': videoItem,
'heroTag': heroTag,
'videoType': SearchType.video,
});
},
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return SizedBox(
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.cover,
width: maxWidth,
height: maxHeight,
),
),
PBadge(
text: Utils.timeFormat(videoItem.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
// if (videoItem.ogv != null) ...[
// PBadge(
// text: videoItem.ogv['type_name'],
// top: 6.0,
// right: 6.0,
// bottom: null,
// left: null,
// ),
// ],
],
);
},
),
),
VideoContent(
videoItem: videoItem,
searchType: searchType,
)
],
),
);
},
),
),
],
),
);
}
}
class VideoContent extends StatelessWidget {
final dynamic videoItem;
final int? searchType;
const VideoContent({
super.key,
required this.videoItem,
this.searchType,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
Utils.dateFormat(videoItem.pubtime),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.outline),
),
Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
StatView(
theme: 'gray',
view: videoItem.cntInfo['play'],
),
const SizedBox(width: 8),
StatDanMu(
theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
const Spacer(),
],
),
),
],
),
],
),
),
);
}
}

View File

@ -44,6 +44,8 @@ import '../pages/setting/recommend_setting.dart';
import '../pages/setting/play_setting.dart';
import '../pages/setting/privacy_setting.dart';
import '../pages/setting/style_setting.dart';
import '../pages/subscription/index.dart';
import '../pages/subscription_detail/index.dart';
import '../pages/video/detail/index.dart';
import '../pages/video/detail/reply_reply/index.dart';
import '../pages/webview/index.dart';
@ -160,6 +162,10 @@ class Routes {
CustomGetPage(name: '/logs', page: () => const LogsPage()),
// 搜索关注
CustomGetPage(name: '/followSearch', page: () => const FollowSearchPage()),
// 订阅
CustomGetPage(name: '/subscription', page: () => const SubPage()),
// 订阅详情
CustomGetPage(name: '/subDetail', page: () => const SubDetailPage()),
];
}