diff --git a/lib/common/widgets/watch_later_list.dart b/lib/common/widgets/watch_later_list.dart new file mode 100644 index 000000000..324e0263e --- /dev/null +++ b/lib/common/widgets/watch_later_list.dart @@ -0,0 +1,225 @@ +import 'package:PiliPalaX/common/widgets/stat/danmu.dart'; +import 'package:PiliPalaX/common/widgets/stat/view.dart'; +import 'package:PiliPalaX/utils/extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:PiliPalaX/common/constants.dart'; +import 'package:PiliPalaX/common/widgets/badge.dart'; +import 'package:PiliPalaX/common/widgets/network_img_layer.dart'; +import 'package:PiliPalaX/http/search.dart'; +import 'package:PiliPalaX/http/user.dart'; +import 'package:PiliPalaX/models/video/later.dart'; +import 'package:PiliPalaX/utils/utils.dart'; + +class MediaListPanel extends StatefulWidget { + const MediaListPanel({ + this.sheetHeight, + required this.mediaList, + this.changeMediaList, + this.panelTitle, + this.bvid, + this.mediaId, + this.hasMore = false, + super.key, + }); + + final double? sheetHeight; + final List mediaList; + final Function? changeMediaList; + final String? panelTitle; + final String? bvid; + final int? mediaId; + final bool hasMore; + + @override + State createState() => _MediaListPanelState(); +} + +class _MediaListPanelState extends State { + RxList mediaList = [].obs; + bool _isEnd = false; + + @override + void initState() { + super.initState(); + mediaList.value = widget.mediaList; + } + + void loadMore() async { + var res = await UserHttp.getMediaList( + type: 3, + bizId: widget.mediaId!, + ps: 20, + oid: mediaList.last.id, + ); + if (res['status']) { + if (res['data'].isNotEmpty) { + mediaList.addAll(res['data']); + } else { + _isEnd = true; + } + } else { + SmartDialog.showToast(res['msg']); + } + } + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + children: [ + AppBar( + toolbarHeight: 45, + automaticallyImplyLeading: false, + titleSpacing: 16, + title: Text(widget.panelTitle ?? '稍后再看'), + actions: [ + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: Get.back, + ), + const SizedBox(width: 14), + ], + ), + Expanded( + child: Material( + color: Theme.of(context).colorScheme.surface, + child: Obx( + () => ListView.builder( + itemCount: mediaList.length, + itemBuilder: ((context, index) { + var item = mediaList[index]; + if (index == widget.mediaList.length - 1 && _isEnd.not) { + loadMore(); + } + return InkWell( + onTap: () async { + Get.back(); + String bvid = item.bvid!; + int? aid = item.id; + String cover = item.cover ?? ''; + final int cid = + await SearchHttp.ab2c(aid: aid, bvid: bvid); + widget.changeMediaList?.call(bvid, cid, aid, cover); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + child: LayoutBuilder( + builder: (context, boxConstraints) { + const double width = 120; + return Container( + constraints: const BoxConstraints(minHeight: 88), + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (BuildContext context, + BoxConstraints boxConstraints) { + final double maxWidth = + boxConstraints.maxWidth; + final double maxHeight = + boxConstraints.maxHeight; + return Stack( + children: [ + NetworkImgLayer( + src: item.cover ?? '', + width: maxWidth, + height: maxHeight, + ), + PBadge( + text: Utils.timeFormat( + item.duration!), + right: 6.0, + bottom: 6.0, + type: 'gray', + ), + ], + ); + }, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB( + 10, 0, 6, 0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + item.title as String, + textAlign: TextAlign.start, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w500, + color: item.bvid == widget.bvid + ? Theme.of(context) + .colorScheme + .primary + : null, + ), + ), + const Spacer(), + Text( + item.upper?.name as String, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize, + color: Theme.of(context) + .colorScheme + .outline, + ), + ), + const SizedBox(height: 2), + Row( + children: [ + statView( + context: context, + theme: 'gray', + view: item.cntInfo!['play'] + as int, + ), + const SizedBox(width: 8), + statDanMu( + context: context, + theme: 'gray', + danmu: item.cntInfo!['danmaku'] + as int, + ), + ], + ), + ], + ), + ), + ) + ], + ), + ); + }, + ), + ), + ); + }), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/http/api.dart b/lib/http/api.dart index e8566f027..5cd2c4a8d 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -693,4 +693,7 @@ class Api { static const String videoRelation = '/x/web-interface/archive/relation'; static const String seasonFav = '/x/v3/fav/season/'; // + fav unfav + + /// 稍后再看&收藏夹视频列表 + static const String mediaList = '/x/v2/medialist/resource/list'; } diff --git a/lib/http/user.dart b/lib/http/user.dart index 6474a716b..7649412f3 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,6 +1,10 @@ +import 'dart:convert'; + import 'package:PiliPalaX/http/loading_state.dart'; +import 'package:PiliPalaX/models/video/later.dart'; import 'package:dio/dio.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:html/parser.dart'; import '../common/constants.dart'; import '../models/model_hot_video_item.dart'; import '../models/user/fav_detail.dart'; @@ -471,4 +475,105 @@ class UserHttp { return {'status': false}; } } + + // 稍后再看播放全部 + // static Future toViewPlayAll({required int oid, required String bvid}) async { + // var res = await Request().get( + // Api.watchLaterHtml, + // data: { + // 'oid': oid, + // 'bvid': bvid, + // }, + // ); + // String scriptContent = + // extractScriptContents(parse(res.data).body!.outerHtml)[0]; + // int startIndex = scriptContent.indexOf('{'); + // int endIndex = scriptContent.lastIndexOf('};'); + // String jsonContent = scriptContent.substring(startIndex, endIndex + 1); + // // 解析JSON字符串为Map + // Map jsonData = json.decode(jsonContent); + // // 输出解析后的数据 + // return { + // 'status': true, + // 'data': jsonData['resourceList'] + // .map((e) => MediaVideoItemModel.fromJson(e)) + // .toList() + // }; + // } + static List extractScriptContents(String htmlContent) { + RegExp scriptRegExp = RegExp(r'