diff --git a/lib/http/api.dart b/lib/http/api.dart index b47272d21..b056b7cd4 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -1,12 +1,72 @@ class Api { // 推荐视频 - static const String recommendList = '/x/web-interface/index/top/feed/rcmd'; + static const String recommendList = '/x/web-interface/index/top/rcmd'; + // 热门视频 static const String hotList = '/x/web-interface/popular'; + // 视频详情 // 竖屏 https://api.bilibili.com/x/web-interface/view?aid=527403921 // https://api.bilibili.com/x/web-interface/view/detail 获取视频超详细信息(web端) static const String videoIntro = '/x/web-interface/view'; + // 视频详情 超详细 + // https://api.bilibili.com/x/web-interface/view/detail?aid=527403921 + + /// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/action.md + // 点赞 Post + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + /// like num 操作方式 必要 1:点赞 2:取消赞 + // csrf str CSRF Token(位于cookie) 必要 + // https://api.bilibili.com/x/web-interface/archive/like + static const String likeVideo = '/x/web-interface/archive/like'; + + //判断视频是否被点赞(双端)Get + // access_key str APP登录Token APP方式必要 + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + // https://api.bilibili.com/x/web-interface/archive/has/like + static const String hasLikeVideo = '/x/web-interface/archive/has/like'; + + // 视频点踩 web端不支持 + + // 投币视频(web端)POST + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + /// multiply num 投币数量 必要 上限为2 + /// select_like num 是否附加点赞 非必要 0:不点赞 1:同时点赞 默认为0 + // csrf str CSRF Token(位于cookie) 必要 + // https://api.bilibili.com/x/web-interface/coin/add + static const String coinVideo = '/x/web-interface/coin/add'; + + // 判断视频是否被投币(双端)GET + // access_key str APP登录Token APP方式必要 + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + /// https://api.bilibili.com/x/web-interface/archive/coins + static const String hasCoinVideo = '/x/web-interface/archive/coins'; + + // 收藏视频(双端)POST + // access_key str APP登录Token APP方式必要 + /// rid num 稿件avid 必要 + /// type num 必须为2 必要 + /// add_media_ids nums 需要加入的收藏夹mlid 非必要 同时添加多个,用,(%2C)分隔 + /// del_media_ids nums 需要取消的收藏夹mlid 非必要 同时取消多个,用,(%2C)分隔 + // csrf str CSRF Token(位于cookie) Cookie方式必要 + // https://api.bilibili.com/medialist/gateway/coll/resource/deal + // https://api.bilibili.com/x/v3/fav/resource/deal + static const String favVideo = '/medialist/gateway/coll/resource/deal'; + + // 判断视频是否被收藏(双端)GET + /// aid + // https://api.bilibili.com/x/v2/fav/video/favoured + static const String hasFavVideo = '/x/v2/fav/video/favoured'; + + // 分享视频 (Web端) POST + // https://api.bilibili.com/x/web-interface/share/add + // aid num 稿件avid 必要(可选) avid与bvid任选一个 + // bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + // csrf str CSRF Token(位于cookie) 必要 // 视频详情页 相关视频 static const String relatedList = '/x/web-interface/archive/related'; @@ -40,4 +100,7 @@ class Api { /// tid int 分区id // https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0 static const String userFavFolderDetail = '/x/v3/fav/resource/list'; + + // 正在直播的up & 关注的up + // https://api.bilibili.com/x/polymer/web-dynamic/v1/portal } diff --git a/lib/http/init.dart b/lib/http/init.dart index 87ab8c595..eca0a6d41 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -55,6 +55,17 @@ class Request { dio.interceptors.add(cookieManager); } + // 从cookie中获取 csrf token + static Future getCsrf() async { + var cookies = await cookieManager.cookieJar + .loadForRequest(Uri.parse(HttpString.baseApiUrl)); + // for (var i in cookies) { + // print(i); + // } + var token = cookies.firstWhere((e) => e.name == 'bili_jct').value; + return token; + } + /* * config it and create */ diff --git a/lib/http/video.dart b/lib/http/video.dart index 686bbc2df..128b69933 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1,3 +1,7 @@ +import 'dart:io'; + +import 'package:dio_cookie_manager/dio_cookie_manager.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/models/model_hot_video_item.dart'; @@ -86,8 +90,75 @@ class VideoHttp { list.add(HotVideoItemModel.fromJson(i)); } return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': []}; + } + } + + // 获取点赞状态 + static Future hasLikeVideo({required String aid}) async { + var res = await Request().get(Api.hasLikeVideo, data: {'aid': aid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } + + // 获取投币状态 + static Future hasCoinVideo({required String aid}) async { + var res = await Request().get(Api.hasCoinVideo, data: {'aid': aid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; } else { return {'status': true, 'data': []}; } } + + // 获取收藏状态 + static Future hasFavVideo({required String aid}) async { + var res = await Request().get(Api.hasFavVideo, data: {'aid': aid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } + + // 一键三连 + // (取消)点赞 + static Future likeVideo({required String aid, required bool type}) async { + var res = await Request().post( + Api.likeVideo, + data: { + 'aid': aid, + 'like': type ? 1 : 2, + 'csrf': await Request.getCsrf(), + }, + ); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + + // (取消)收藏 + static Future favVideo( + {required String aid, required bool type, required String ids}) async { + Map data = {'rid': aid, 'type': 2}; + // type true 添加收藏 false 取消收藏 + if (type) { + data['add_media_ids'] = ids; + } else { + data['del_media_ids'] = ids; + } + var res = await Request() + .post(Api.favVideo, data: {'aid': aid, 'like': type ? 1 : 2}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } } diff --git a/lib/pages/fav/controller.dart b/lib/pages/fav/controller.dart index ff14f9f9d..0321ea396 100644 --- a/lib/pages/fav/controller.dart +++ b/lib/pages/fav/controller.dart @@ -1,3 +1,18 @@ import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/utils/storage.dart'; -class FavController extends GetxController {} +class FavController extends GetxController { + Rx favFolderData = FavFolderData().obs; + + Future queryFavFolder() async { + var res = await await UserHttp.userfavFolder( + pn: 1, + ps: 10, + mid: GStrorage.user.get(UserBoxKey.userMid), + ); + favFolderData.value = res['data']; + return res; + } +} diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart index 762fcf182..3f23f4514 100644 --- a/lib/pages/fav/view.dart +++ b/lib/pages/fav/view.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/pages/fav/index.dart'; class FavPage extends StatefulWidget { const FavPage({super.key}); @@ -8,12 +11,64 @@ class FavPage extends StatefulWidget { } class _FavPageState extends State { + final FavController _favController = Get.put(FavController()); + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: false, - title: Text('我的收藏'), + title: const Text('我的收藏'), + ), + body: FutureBuilder( + future: _favController.queryFavFolder(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx( + () => ListView.builder( + itemCount: _favController.favFolderData.value.list!.length, + itemBuilder: (context, index) { + return ListTile( + onTap: () => Get.toNamed( + '/favDetail', + arguments: + _favController.favFolderData.value.list![index], + parameters: { + 'mediaId': _favController + .favFolderData.value.list![index].id + .toString(), + }, + ), + leading: const Icon(Icons.folder_special_outlined), + minLeadingWidth: 0, + title: Text(_favController + .favFolderData.value.list![index].title!), + subtitle: Text( + '${_favController.favFolderData.value.list![index].mediaCount}个内容', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize), + ), + ); + }, + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return Text('请求中'); + } + }, ), ); } diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 25b219e70..7066cd9fd 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -56,12 +56,12 @@ class MainController extends GetxController { // 'icon': const Icon(Icons.person_outline), // 'selectedIcon': const Icon(Icons.person), 'icon': const Icon( - CupertinoIcons.tray_full, - size: 21, + CupertinoIcons.folder, + size: 20, ), 'selectedIcon': const Icon( - CupertinoIcons.tray_full_fill, - size: 21, + CupertinoIcons.folder_fill, + size: 20, ), 'label': "媒体库", } diff --git a/lib/pages/media/controller.dart b/lib/pages/media/controller.dart index 0c0f31f80..f32ca4c34 100644 --- a/lib/pages/media/controller.dart +++ b/lib/pages/media/controller.dart @@ -10,12 +10,12 @@ class MediaController extends GetxController { { 'icon': Icons.file_download_outlined, 'title': '离线缓存', - 'onTap': () => Get.toNamed('/fav'), + 'onTap': () {}, }, { 'icon': Icons.history, 'title': '观看记录', - 'onTap': () => Get.toNamed('/fav'), + 'onTap': () {}, }, { 'icon': Icons.star_border, @@ -24,8 +24,8 @@ class MediaController extends GetxController { }, { 'icon': Icons.watch_later_outlined, - 'title': '稍后再看', - 'onTap': () => Get.toNamed('/fav'), + 'title': '稍候再看', + 'onTap': () => {}, }, ]; diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart index c4474a28f..6557000cb 100644 --- a/lib/pages/media/view.dart +++ b/lib/pages/media/view.dart @@ -56,8 +56,13 @@ class _MediaPageState extends State color: primary, ), ), + contentPadding: + const EdgeInsets.only(left: 15, top: 2, bottom: 2), minLeadingWidth: 0, - title: Text(i['title']), + title: Text( + i['title'], + style: const TextStyle(fontSize: 15), + ), ), ], favFolder() @@ -105,13 +110,11 @@ class _MediaPageState extends State ), ), ), - trailing: Padding( - padding: const EdgeInsets.only(right: 10), - child: Text( - '查看全部', - style: TextStyle( - fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline), + trailing: IconButton( + onPressed: () => _mediaController.queryFavFolder(), + icon: const Icon( + Icons.refresh, + size: 20, ), ), ), diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 1ddaf5301..c7e7b8319 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/video.dart'; @@ -26,6 +28,13 @@ class VideoIntroController extends GetxController { // up主粉丝数 Map userStat = {'follower': '-'}; + // 是否点赞 + RxBool hasLike = false.obs; + // 是否投币 + RxBool hasCoin = false.obs; + // 是否收藏 + RxBool hasFav = false.obs; + @override void onInit() { super.onInit(); @@ -57,6 +66,13 @@ class VideoIntroController extends GetxController { } // 获取到粉丝数再返回 await queryUserStat(); + // 获取点赞状态 + queryHasLikeVideo(); + // 获取投币状态 + queryHasCoinVideo(); + // 获取收藏状态 + queryHasFavVideo(); + return result; } @@ -67,4 +83,51 @@ class VideoIntroController extends GetxController { userStat = result['data']; } } + + // 获取点赞状态 + Future queryHasLikeVideo() async { + var result = await VideoHttp.hasLikeVideo(aid: aid); + // data num 被点赞标志 0:未点赞 1:已点赞 + hasLike.value = result["data"] == 1 ? true : false; + } + + // 获取投币状态 + Future queryHasCoinVideo() async { + var result = await VideoHttp.hasCoinVideo(aid: aid); + hasCoin.value = result["data"]['multiply'] == 0 ? false : true; + } + + // 获取收藏状态 + Future queryHasFavVideo() async { + var result = await VideoHttp.hasFavVideo(aid: aid); + hasFav.value = result["data"]['favoured']; + } + + // 一键三连 + + // (取消)点赞 + Future actionLikeVideo() async { + var result = await VideoHttp.likeVideo(aid: aid, type: !hasLike.value); + if (result['status']) { + hasLike.value = result["data"] == 1 ? true : false; + } else { + SmartDialog.showToast(result['msg']); + } + } + + // 投币 + Future actionCoinVideo() async { + print('投币'); + } + + // (取消)收藏 + Future actionFavVideo() async { + print('(取消)收藏'); + // var result = await VideoHttp.favVideo(aid: aid, type: true, ids: ''); + } + + // 分享视频 + Future actionShareVideo() async { + print('分享视频'); + } } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 4bef011be..f9027ceeb 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -4,6 +4,8 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/pages/fav/index.dart'; +import 'package:pilipala/pages/favDetail/index.dart'; import 'package:pilipala/pages/video/detail/widgets/expandable_section.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/stat/danmu.dart'; @@ -100,6 +102,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { /// 手动控制 late Animation? _manualAnimation; + final FavController _favController = Get.put(FavController()); + @override void initState() { super.initState(); @@ -113,6 +117,102 @@ class _VideoInfoState extends State with TickerProviderStateMixin { Tween(begin: 0.5, end: 1.5).animate(_manualController!); } + showFavBottomSheet() { + Get.bottomSheet( + useRootNavigator: true, + isScrollControlled: true, + Container( + height: 450, + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + AppBar( + toolbarHeight: 50, + automaticallyImplyLeading: false, + centerTitle: false, + elevation: 1, + title: Text( + '选择文件夹', + style: Theme.of(context).textTheme.titleMedium, + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('完成'), + ), + const SizedBox(width: 6), + ], + ), + Expanded( + child: Material( + child: FutureBuilder( + future: _favController.queryFavFolder(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx( + () => ListView.builder( + itemCount: _favController + .favFolderData.value.list!.length + + 1, + itemBuilder: (context, index) { + if (index == 0) { + return const SizedBox(height: 15); + } else { + return ListTile( + onTap: () {}, + dense: true, + leading: + const Icon(Icons.folder_special_outlined), + minLeadingWidth: 0, + title: Text(_favController.favFolderData.value + .list![index - 1].title!), + subtitle: Text( + '${_favController.favFolderData.value.list![index - 1].mediaCount}个内容', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .outline, + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize), + ), + trailing: Transform.scale( + scale: 0.9, + child: Checkbox( + value: false, + onChanged: (bool? checkValue) {}, + ), + ), + ); + } + }, + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return Text('请求中'); + } + }, + ), + ), + ), + ], + ), + ), + persistent: false, + backgroundColor: Theme.of(context).bottomSheetTheme.backgroundColor, + ); + } + @override Widget build(BuildContext context) { return SliverPadding( @@ -293,7 +393,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), ), const SizedBox(height: 5), - _actionGrid(context), + _actionGrid(context, widget.videoIntroController), ], ) : const Center(child: CircularProgressIndicator()), @@ -302,7 +402,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { } // 喜欢 投币 分享 - Widget _actionGrid(BuildContext context) { + Widget _actionGrid(BuildContext context, videoIntroController) { return LayoutBuilder(builder: (context, constraints) { return SizedBox( height: constraints.maxWidth / 5 * 0.8, @@ -312,39 +412,50 @@ class _VideoInfoState extends State with TickerProviderStateMixin { crossAxisCount: 5, childAspectRatio: 1.25, children: [ - ActionItem( - icon: const Icon(FontAwesomeIcons.thumbsUp), - onTap: () => {}, - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.like!.toString() - : '-'), + Obx( + () => ActionItem( + icon: const Icon(FontAwesomeIcons.thumbsUp), + selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), + onTap: () => videoIntroController.actionLikeVideo(), + selectStatus: videoIntroController.hasLike.value, + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.like!.toString() + : '-'), + ), ActionItem( icon: const Icon(FontAwesomeIcons.thumbsDown), + selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown), onTap: () => {}, selectStatus: false, loadingStatus: widget.loadingStatus, text: '不喜欢'), - ActionItem( - icon: const Icon(FontAwesomeIcons.b), - onTap: () => {}, - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.coin!.toString() - : '-'), - ActionItem( - icon: const Icon(FontAwesomeIcons.star), - onTap: () => {}, - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.favorite!.toString() - : '-'), + Obx( + () => ActionItem( + icon: const Icon(FontAwesomeIcons.b), + selectIcon: const Icon(FontAwesomeIcons.b), + onTap: () => videoIntroController.actionCoinVideo(), + selectStatus: videoIntroController.hasCoin.value, + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.coin!.toString() + : '-'), + ), + Obx( + () => ActionItem( + icon: const Icon(FontAwesomeIcons.star), + selectIcon: const Icon(FontAwesomeIcons.star), + // onTap: () => videoIntroController.actionFavVideo(), + onTap: () => showFavBottomSheet(), + selectStatus: videoIntroController.hasFav.value, + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.favorite!.toString() + : '-'), + ), ActionItem( icon: const Icon(FontAwesomeIcons.shareFromSquare), - onTap: () => {}, + onTap: () => videoIntroController.actionShareVideo(), selectStatus: false, loadingStatus: widget.loadingStatus, text: !widget.loadingStatus @@ -359,6 +470,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { class ActionItem extends StatelessWidget { Icon? icon; + Icon? selectIcon; Function? onTap; bool? loadingStatus; String? text; @@ -367,6 +479,7 @@ class ActionItem extends StatelessWidget { ActionItem({ Key? key, this.icon, + this.selectIcon, this.onTap, this.loadingStatus, this.text, @@ -378,16 +491,17 @@ class ActionItem extends StatelessWidget { return Material( child: Ink( child: InkWell( - onTap: () {}, + onTap: () => onTap!(), borderRadius: StyleString.mdRadius, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon!.icon!, - size: 21, - color: selectStatus - ? Theme.of(context).primaryColor - : Theme.of(context).colorScheme.outline), + const SizedBox(height: 4), + selectStatus + ? Icon(selectIcon!.icon!, + size: 21, color: Theme.of(context).primaryColor) + : Icon(icon!.icon!, + size: 21, color: Theme.of(context).colorScheme.outline), const SizedBox(height: 4), AnimatedOpacity( opacity: loadingStatus! ? 0 : 1,