diff --git a/lib/http/api.dart b/lib/http/api.dart index 2e758439..736030b2 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -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'; } diff --git a/lib/http/user.dart b/lib/http/user.dart index c1f86285..7d3def4e 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -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']}; + } + } } diff --git a/lib/models/user/sub_detail.dart b/lib/models/user/sub_detail.dart new file mode 100644 index 00000000..a1e52e55 --- /dev/null +++ b/lib/models/user/sub_detail.dart @@ -0,0 +1,123 @@ +class SubDetailModelData { + DetailInfo? info; + List? medias; + + SubDetailModelData({this.info, this.medias}); + + SubDetailModelData.fromJson(Map json) { + info = DetailInfo.fromJson(json['info']); + if (json['medias'] != null) { + medias = []; + 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 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 toJson() { + final data = {}; + 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 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 toJson() { + final data = {}; + 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; + } +} diff --git a/lib/models/user/sub_folder.dart b/lib/models/user/sub_folder.dart new file mode 100644 index 00000000..d496a1cf --- /dev/null +++ b/lib/models/user/sub_folder.dart @@ -0,0 +1,111 @@ +class SubFolderModelData { + final int? count; + final List? list; + + SubFolderModelData({ + this.count, + this.list, + }); + + factory SubFolderModelData.fromJson(Map json) { + return SubFolderModelData( + count: json['count'], + list: json['list'] != null + ? (json['list'] as List) + .map((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 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 json) { + return Upper( + mid: json['mid'], + name: json['name'], + face: json['face'], + ); + } +} diff --git a/lib/pages/fav/controller.dart b/lib/pages/fav/controller.dart index c3f76186..a5f94525 100644 --- a/lib/pages/fav/controller.dart +++ b/lib/pages/fav/controller.dart @@ -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!, diff --git a/lib/pages/fav_detail/controller.dart b/lib/pages/fav_detail/controller.dart index 95130be6..69cc939e 100644 --- a/lib/pages/fav_detail/controller.dart +++ b/lib/pages/fav_detail/controller.dart @@ -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!, diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index 787fa96e..27d7182b 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -31,7 +31,6 @@ class _FavDetailPageState extends State { super.initState(); mediaId = Get.parameters['mediaId']!; _futureBuilderFuture = _favDetailController.queryUserFavFolderDetail(); - mediaId = Get.parameters['mediaId']!; titleStreamC = StreamController(); _controller.addListener( () { diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart index d8fc60f0..92e1eee7 100644 --- a/lib/pages/history/view.dart +++ b/lib/pages/history/view.dart @@ -70,10 +70,6 @@ class _HistoryPageState extends State { 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, diff --git a/lib/pages/media/controller.dart b/lib/pages/media/controller.dart index 8b875511..757d5ac9 100644 --- a/lib/pages/media/controller.dart +++ b/lib/pages/media/controller.dart @@ -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': '稍后再看', diff --git a/lib/pages/subscription/controller.dart b/lib/pages/subscription/controller.dart new file mode 100644 index 00000000..bf0c593c --- /dev/null +++ b/lib/pages/subscription/controller.dart @@ -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 subFolderData = SubFolderModelData().obs; + Box userInfoCache = GStrorage.userInfo; + UserInfoData? userInfo; + int currentPage = 1; + int pageSize = 20; + RxBool hasMore = true.obs; + + Future 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'); + } +} diff --git a/lib/pages/subscription/index.dart b/lib/pages/subscription/index.dart new file mode 100644 index 00000000..4d034396 --- /dev/null +++ b/lib/pages/subscription/index.dart @@ -0,0 +1,4 @@ +library sub; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/subscription/view.dart b/lib/pages/subscription/view.dart new file mode 100644 index 00000000..b2a4965b --- /dev/null +++ b/lib/pages/subscription/view.dart @@ -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 createState() => _SubPageState(); +} + +class _SubPageState extends State { + 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('请求中'); + } + }, + ), + ); + } +} diff --git a/lib/pages/subscription/widgets/item.dart b/lib/pages/subscription/widgets/item.dart new file mode 100644 index 00000000..fd08ffa5 --- /dev/null +++ b/lib/pages/subscription/widgets/item.dart @@ -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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/subscription_detail/controller.dart b/lib/pages/subscription_detail/controller.dart new file mode 100644 index 00000000..6ecb894e --- /dev/null +++ b/lib/pages/subscription_detail/controller.dart @@ -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 subInfo = DetailInfo().obs; + RxList subList = [].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 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'); + } +} diff --git a/lib/pages/subscription_detail/index.dart b/lib/pages/subscription_detail/index.dart new file mode 100644 index 00000000..71df4b24 --- /dev/null +++ b/lib/pages/subscription_detail/index.dart @@ -0,0 +1,4 @@ +library sub_detail; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/subscription_detail/view.dart b/lib/pages/subscription_detail/view.dart new file mode 100644 index 00000000..d56125cd --- /dev/null +++ b/lib/pages/subscription_detail/view.dart @@ -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 createState() => _SubDetailPageState(); +} + +class _SubDetailPageState extends State { + late final ScrollController _controller = ScrollController(); + final SubDetailController _subDetailController = + Get.put(SubDetailController()); + late StreamController titleStreamC; // a + late Future _futureBuilderFuture; + late String seasonId; + + @override + void initState() { + super.initState(); + seasonId = Get.parameters['seasonId']!; + _futureBuilderFuture = _subDetailController.queryUserSubFolderDetail(); + titleStreamC = StreamController(); + _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), + ), + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/pages/subscription_detail/widget/sub_video_card.dart b/lib/pages/subscription_detail/widget/sub_video_card.dart new file mode 100644 index 00000000..11aebc39 --- /dev/null +++ b/lib/pages/subscription_detail/widget/sub_video_card.dart @@ -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 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(), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 45d7cad1..6ebaa638 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -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()), ]; }