diff --git a/.github/workflows/beta_ci.yml b/.github/workflows/beta_ci.yml index 14b51780..9c40de6b 100644 --- a/.github/workflows/beta_ci.yml +++ b/.github/workflows/beta_ci.yml @@ -12,7 +12,6 @@ on: - ".idea/**" - "!.github/workflows/**" - jobs: update_version: name: Read and update version @@ -96,7 +95,7 @@ jobs: if: steps.cache-flutter.outputs.cache-hit != 'true' uses: subosito/flutter-action@v2 with: - flutter-version: 3.16.5 + flutter-version: 3.19.6 channel: any - name: 下载项目依赖 diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index 78230645..f7c06d29 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -36,7 +36,7 @@ jobs: if: steps.cache-flutter.outputs.cache-hit != 'true' uses: subosito/flutter-action@v2 with: - flutter-version: 3.16.5 + flutter-version: 3.19.6 channel: any - name: 下载项目依赖 @@ -98,7 +98,7 @@ jobs: uses: subosito/flutter-action@v2.10.0 with: cache: true - flutter-version: 3.16.5 + flutter-version: 3.19.6 - name: flutter build ipa run: | diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f119eb1e..46b34c20 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,7 +12,6 @@ - @@ -20,7 +19,6 @@ "android.support.customtabs.action.CustomTabsService" /> - @@ -34,7 +32,6 @@ - + + + + + + + + + @@ -132,102 +141,55 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + \ No newline at end of file diff --git a/assets/images/pay/alipay.jpg b/assets/images/pay/alipay.jpg new file mode 100644 index 00000000..1c1fc4c6 Binary files /dev/null and b/assets/images/pay/alipay.jpg differ diff --git a/assets/images/pay/wechat.png b/assets/images/pay/wechat.png new file mode 100644 index 00000000..3aa3a6a2 Binary files /dev/null and b/assets/images/pay/wechat.png differ diff --git a/assets/images/video/dlna.png b/assets/images/video/dlna.png new file mode 100755 index 00000000..a5d65872 Binary files /dev/null and b/assets/images/video/dlna.png differ diff --git a/assets/images/video/fullscreen.png b/assets/images/video/fullscreen.png new file mode 100755 index 00000000..449f1425 Binary files /dev/null and b/assets/images/video/fullscreen.png differ diff --git a/assets/images/video/fullscreen_exit.png b/assets/images/video/fullscreen_exit.png new file mode 100755 index 00000000..9881ed1d Binary files /dev/null and b/assets/images/video/fullscreen_exit.png differ diff --git a/assets/images/video/pip.png b/assets/images/video/pip.png new file mode 100755 index 00000000..1b742125 Binary files /dev/null and b/assets/images/video/pip.png differ diff --git a/change_log/1.0.25.1010.md b/change_log/1.0.25.1010.md new file mode 100644 index 00000000..951efcb1 --- /dev/null +++ b/change_log/1.0.25.1010.md @@ -0,0 +1,39 @@ +## 1.0.25 + +### 功能 ++ 直播弹幕 ++ 稍后再看、收藏夹播放全部 ++ 收藏夹新建、编辑 ++ 评论删除 ++ 评论保存为图片 ++ 动态页滑动切换up ++ up投稿筛选充电视频 ++ 直播tab展示关注up ++ up主页专栏展示 + +### 优化 ++ 视频详情页一键三连 ++ 动态页标识充电视频 ++ 播放器亮度、音量调整百分比展示 ++ 封面预览时视频标题可复制 ++ 竖屏直播布局 ++ 图片预览 ++ 专栏渲染优化 ++ 私信图片查看 + +### 修复 ++ 收藏夹点击异常 ++ 搜索up异常 ++ 系统通知已读异常 ++ [赞了我的]展示错误 ++ 部分up合集无法打开 ++ 切换合集视频投币个数未重置 ++ 搜索条件筛选面板无法滚动 ++ 部分机型导航条未沉浸 ++ 专栏图片渲染问题 ++ 专栏浏览历史记录 ++ 直播间历史记录 + + +更多更新日志可在Github上查看 +问题反馈、功能建议请查看「关于」页面。 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a400600f..27baf9e5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - appscheme (1.0.4): + - app_links (0.0.2): - Flutter - audio_service (0.0.1): - Flutter @@ -27,6 +27,8 @@ PODS: - Flutter - GT3Captcha-iOS - GT3Captcha-iOS (0.15.8.3) + - image_picker_ios (0.0.1): + - Flutter - media_kit_libs_ios_video (1.0.4): - Flutter - media_kit_native_event_loop (1.0.0): @@ -66,7 +68,7 @@ PODS: - Flutter DEPENDENCIES: - - appscheme (from `.symlinks/plugins/appscheme/ios`) + - app_links (from `.symlinks/plugins/app_links/ios`) - audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - auto_orientation (from `.symlinks/plugins/auto_orientation/ios`) @@ -77,6 +79,7 @@ DEPENDENCIES: - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) @@ -102,8 +105,8 @@ SPEC REPOS: - Toast EXTERNAL SOURCES: - appscheme: - :path: ".symlinks/plugins/appscheme/ios" + app_links: + :path: ".symlinks/plugins/app_links/ios" audio_service: :path: ".symlinks/plugins/audio_service/ios" audio_session: @@ -124,6 +127,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluttertoast/ios" gt3_flutter_plugin: :path: ".symlinks/plugins/gt3_flutter_plugin/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" media_kit_libs_ios_video: :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" media_kit_native_event_loop: @@ -160,7 +165,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: - appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8 + app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_session: 4f3e461722055d21515cf3261b64c973c062f345 auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d @@ -173,6 +178,7 @@ SPEC CHECKSUMS: FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6 + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 1e7d9fed..24dceb17 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -65,44 +65,29 @@ CFBundleURLName - + bilibili CFBundleURLSchemes http https - - CFBundleURLTypes - - - CFBundleURLName - - CFBundleURLSchemes - - m.bilibili.com - bilibili.com - www.bilibili.com - bangumi.bilibili.com - bilibili.cn - www.bilibili.cn - bangumi.bilibili.cn - bilibili.tv - www.bilibili.tv - bangumi.bilibili.tv - miniapp.bilibili.com - live.bilibili.com - - - - - - - - CFBundleURLName - bilibili - CFBundleURLSchemes - bilibili + m.bilibili.com + bilibili.com + www.bilibili.com + bangumi.bilibili.com + bilibili.cn + www.bilibili.cn + bangumi.bilibili.cn + bilibili.tv + www.bilibili.tv + bangumi.bilibili.tv + miniapp.bilibili.com + live.bilibili.com + pili + pilipala + FlutterDeepLinkingEnabled + UIBackgroundModes diff --git a/lib/common/constants.dart b/lib/common/constants.dart index cac13688..0607206c 100644 --- a/lib/common/constants.dart +++ b/lib/common/constants.dart @@ -15,6 +15,4 @@ class Constants { // 59b43e04ad6965f34319062b478f83dd TV端 static const String appSec = '59b43e04ad6965f34319062b478f83dd'; static const String thirdSign = '04224646d1fea004e79606d3b038c84a'; - static const String thirdApi = - 'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png'; } diff --git a/lib/common/pages_bottom_sheet.dart b/lib/common/pages_bottom_sheet.dart index 49e9b4d8..49300949 100644 --- a/lib/common/pages_bottom_sheet.dart +++ b/lib/common/pages_bottom_sheet.dart @@ -1,35 +1,466 @@ import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/video_detail_res.dart'; +import 'package:pilipala/pages/video/detail/index.dart'; +import 'package:pilipala/utils/utils.dart'; +import 'package:scrollview_observer/scrollview_observer.dart'; import '../models/common/video_episode_type.dart'; +import 'widgets/badge.dart'; +import 'widgets/stat/danmu.dart'; +import 'widgets/stat/view.dart'; class EpisodeBottomSheet { final List episodes; final int currentCid; final dynamic dataType; - final BuildContext context; final Function changeFucCall; final int? cid; final double? sheetHeight; bool isFullScreen = false; + final UgcSeason? ugcSeason; + final int? currentEpisodeIndex; + final int? currentIndex; EpisodeBottomSheet({ required this.episodes, required this.currentCid, required this.dataType, - required this.context, required this.changeFucCall, this.cid, this.sheetHeight, this.isFullScreen = false, + this.ugcSeason, + this.currentEpisodeIndex, + this.currentIndex, }); - Widget buildEpisodeListItem( - dynamic episode, - int index, - bool isCurrentIndex, - ) { + Widget buildShowContent() { + return PagesBottomSheet( + episodes: episodes, + currentCid: currentCid, + dataType: dataType, + changeFucCall: changeFucCall, + cid: cid, + sheetHeight: sheetHeight, + isFullScreen: isFullScreen, + ugcSeason: ugcSeason, + currentEpisodeIndex: currentEpisodeIndex, + currentIndex: currentIndex, + ); + } + + PersistentBottomSheetController show(BuildContext context) { + final PersistentBottomSheetController btmSheetCtr = showBottomSheet( + context: context, + builder: (BuildContext context) { + return buildShowContent(); + }, + ); + return btmSheetCtr; + } +} + +class PagesBottomSheet extends StatefulWidget { + const PagesBottomSheet({ + super.key, + required this.episodes, + required this.currentCid, + required this.dataType, + required this.changeFucCall, + this.cid, + this.sheetHeight, + this.isFullScreen = false, + this.ugcSeason, + this.currentEpisodeIndex, + this.currentIndex, + }); + + final List episodes; + final int currentCid; + final dynamic dataType; + final Function changeFucCall; + final int? cid; + final double? sheetHeight; + final bool isFullScreen; + final UgcSeason? ugcSeason; + final int? currentEpisodeIndex; + final int? currentIndex; + + @override + State createState() => _PagesBottomSheetState(); +} + +class _PagesBottomSheetState extends State + with TickerProviderStateMixin { + final ScrollController _listScrollController = ScrollController(); + late ListObserverController _listObserverController; + late GridObserverController _gridObserverController; + final ScrollController _gridScrollController = ScrollController(); + late int currentIndex; + TabController? tabController; + List? _listObserverControllerList; + List? _listScrollControllerList; + final String heroTag = Get.arguments['heroTag']; + VideoDetailController? _videoDetailController; + RxInt isSubscribe = (-1).obs; + bool isVisible = false; + + @override + void initState() { + super.initState(); + currentIndex = widget.currentIndex ?? + widget.episodes.indexWhere((dynamic e) => e.cid == widget.currentCid); + _scrollToInit(); + _scrollPositionInit(); + if (widget.dataType == VideoEpidoesType.videoEpisode) { + _videoDetailController = Get.find(tag: heroTag); + _getSubscribeStatus(); + } + } + + String prefix() { + switch (widget.dataType) { + case VideoEpidoesType.videoEpisode: + return '选集'; + case VideoEpidoesType.videoPart: + return '分集'; + case VideoEpidoesType.bangumiEpisode: + return '选集'; + } + return '选集'; + } + + // 滚动器初始化 + void _scrollToInit() { + /// 单个 + _listObserverController = + ListObserverController(controller: _listScrollController); + + if (widget.dataType == VideoEpidoesType.videoEpisode && + widget.ugcSeason?.sections != null && + widget.ugcSeason!.sections!.length > 1) { + tabController = TabController( + length: widget.ugcSeason!.sections!.length, + vsync: this, + initialIndex: widget.currentEpisodeIndex ?? 0, + ); + + /// 多tab + _listScrollControllerList = List.generate( + widget.ugcSeason!.sections!.length, + (index) { + return ScrollController(); + }, + ); + _listObserverControllerList = List.generate( + widget.ugcSeason!.sections!.length, + (index) { + return ListObserverController( + controller: _listScrollControllerList![index], + ); + }, + ); + } else { + _gridObserverController = + GridObserverController(controller: _gridScrollController); + } + } + + // 滚动器位置初始化 + void _scrollPositionInit() { + if (widget.dataType == VideoEpidoesType.videoEpisode) { + // 单个 多tab + if (widget.ugcSeason?.sections != null) { + if (widget.ugcSeason!.sections!.length == 1) { + _listObserverController.initialIndexModel = + ObserverIndexPositionModel( + index: currentIndex, + isFixedHeight: true, + ); + } else { + _listObserverControllerList![widget.currentEpisodeIndex!] + .initialIndexModel = ObserverIndexPositionModel( + index: currentIndex, + isFixedHeight: true, + ); + } + } + } else { + _gridObserverController.initialIndexModel = ObserverIndexPositionModel( + index: currentIndex, + isFixedHeight: false, + ); + } + } + + // 获取订阅状态 + void _getSubscribeStatus() async { + var res = + await VideoHttp.getSubscribeStatus(bvid: _videoDetailController!.bvid); + if (res['status']) { + isSubscribe.value = res['data']['season_fav'] ? 1 : 0; + } + } + + // 更改订阅状态 + void _changeSubscribeStatus() async { + if (isSubscribe.value == -1) { + return; + } + dynamic result = await VideoHttp.seasonFav( + isFav: isSubscribe.value == 1, + seasonId: widget.ugcSeason!.id, + ); + if (result['status']) { + SmartDialog.showToast(isSubscribe.value == 1 ? '取消订阅成功' : '订阅成功'); + isSubscribe.value = isSubscribe.value == 1 ? 0 : 1; + } else { + SmartDialog.showToast(result['msg']); + } + } + + // 更改展开状态 + void _changeVisible() { + setState(() { + isVisible = !isVisible; + }); + } + + @override + void dispose() { + try { + _listObserverController.controller?.dispose(); + _gridObserverController.controller?.dispose(); + _listScrollController.dispose(); + _gridScrollController.dispose(); + for (var element in _listObserverControllerList!) { + element.controller?.dispose(); + } + for (var element in _listScrollControllerList!) { + element.dispose(); + } + } catch (_) {} + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SizedBox( + height: widget.sheetHeight, + child: Column( + children: [ + TitleBar( + title: '${prefix()}(${widget.episodes.length})', + isFullScreen: widget.isFullScreen, + ), + if (widget.ugcSeason != null) ...[ + UgcSeasonBuild( + ugcSeason: widget.ugcSeason!, + isSubscribe: isSubscribe, + isVisible: isVisible, + changeFucCall: _changeSubscribeStatus, + changeVisible: _changeVisible, + ), + ], + Expanded( + child: Material( + child: widget.dataType == VideoEpidoesType.videoEpisode + ? (widget.ugcSeason!.sections!.length == 1 + ? ListViewObserver( + controller: _listObserverController, + child: ListView.builder( + controller: _listScrollController, + itemCount: widget.episodes.length + 1, + itemBuilder: (BuildContext context, int index) { + bool isLastItem = + index == widget.episodes.length; + bool isCurrentIndex = currentIndex == index; + return isLastItem + ? SizedBox( + height: MediaQuery.of(context) + .padding + .bottom + + 20, + ) + : EpisodeListItem( + episode: widget.episodes[index], + index: index, + isCurrentIndex: isCurrentIndex, + dataType: widget.dataType, + changeFucCall: widget.changeFucCall, + isFullScreen: widget.isFullScreen, + ); + }, + ), + ) + : buildTabBar()) + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0), // 设置左右间距为12 + child: GridViewObserver( + controller: _gridObserverController, + child: GridView.count( + controller: _gridScrollController, + crossAxisCount: 2, + crossAxisSpacing: StyleString.safeSpace, + childAspectRatio: 2.6, + children: List.generate( + widget.episodes.length, + (index) { + bool isCurrentIndex = currentIndex == index; + return EpisodeGridItem( + episode: widget.episodes[index], + index: index, + isCurrentIndex: isCurrentIndex, + dataType: widget.dataType, + changeFucCall: widget.changeFucCall, + isFullScreen: widget.isFullScreen, + ); + }, + ), + ), + ), + ), + ), + ), + ], + ), + ); + }); + } + + Widget buildTabBar() { + return Column( + children: [ + // 背景色 + Container( + color: Theme.of(context).colorScheme.surface, + child: TabBar( + controller: tabController, + isScrollable: true, + indicatorSize: TabBarIndicatorSize.label, + tabAlignment: TabAlignment.start, + splashBorderRadius: BorderRadius.circular(4), + tabs: [ + ...widget.ugcSeason!.sections!.map((SectionItem section) { + return Tab( + text: section.title, + ); + }).toList() + ], + ), + ), + Expanded( + child: TabBarView( + controller: tabController, + children: [ + ...widget.ugcSeason!.sections!.map((SectionItem section) { + final int fIndex = widget.ugcSeason!.sections!.indexOf(section); + return ListViewObserver( + controller: _listObserverControllerList![fIndex], + child: ListView.builder( + controller: _listScrollControllerList![fIndex], + itemCount: section.episodes!.length + 1, + itemBuilder: (BuildContext context, int index) { + final bool isLastItem = index == section.episodes!.length; + return isLastItem + ? SizedBox( + height: + MediaQuery.of(context).padding.bottom + 20, + ) + : EpisodeListItem( + episode: section.episodes![index], // 调整索引 + index: index, // 调整索引 + isCurrentIndex: widget.currentCid == + section.episodes![index].cid, + dataType: widget.dataType, + changeFucCall: widget.changeFucCall, + isFullScreen: widget.isFullScreen, + ); + }, + ), + ); + }).toList() + ], + ), + ), + ], + ); + } +} + +class TitleBar extends StatelessWidget { + final String title; + final bool isFullScreen; + + const TitleBar({ + Key? key, + required this.title, + required this.isFullScreen, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppBar( + toolbarHeight: 45, + automaticallyImplyLeading: false, + centerTitle: false, + elevation: 1, + scrolledUnderElevation: 1, + title: Padding( + padding: const EdgeInsets.only(left: 12), + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + actions: !isFullScreen + ? [ + SizedBox( + width: 35, + height: 35, + child: IconButton( + icon: const Icon(Icons.close, size: 20), + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => Navigator.pop(context), + ), + ), + const SizedBox(width: 8), + ] + : null, + ); + } +} + +class EpisodeListItem extends StatelessWidget { + final dynamic episode; + final int index; + final bool isCurrentIndex; + final dynamic dataType; + final Function changeFucCall; + final bool isFullScreen; + + const EpisodeListItem({ + Key? key, + required this.episode, + required this.index, + required this.isCurrentIndex, + required this.dataType, + required this.changeFucCall, + required this.isFullScreen, + }) : super(key: key); + + @override + Widget build(BuildContext context) { Color primary = Theme.of(context).colorScheme.primary; Color onSurface = Theme.of(context).colorScheme.onSurface; @@ -45,128 +476,365 @@ class EpisodeBottomSheet { title = '第${episode.title}话 ${episode.longTitle!}'; break; } + return isFullScreen || episode?.cover == null || episode?.cover == '' - ? ListTile( - onTap: () { - SmartDialog.showToast('切换至「$title」'); - changeFucCall.call(episode, index); - }, - dense: false, - leading: isCurrentIndex - ? Image.asset( - 'assets/images/live.gif', - color: primary, - height: 12, + ? _buildListTile(context, title, primary, onSurface) + : _buildInkWell(context, title, primary, onSurface); + } + + Widget _buildListTile( + BuildContext context, String title, Color primary, Color onSurface) { + return ListTile( + onTap: () { + if (isCurrentIndex) { + return; + } + SmartDialog.showToast('切换至「$title」'); + changeFucCall.call(episode, index); + }, + dense: false, + leading: isCurrentIndex + ? Image.asset( + 'assets/images/live.png', + color: primary, + height: 12, + ) + : null, + title: Text( + title, + style: TextStyle( + fontSize: 14, + color: isCurrentIndex ? primary : onSurface, + ), + ), + ); + } + + Widget _buildInkWell( + BuildContext context, String title, Color primary, Color onSurface) { + return InkWell( + onTap: () { + if (isCurrentIndex) { + return; + } + SmartDialog.showToast('切换至「$title」'); + changeFucCall.call(episode, index); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB( + StyleString.safeSpace, 6, StyleString.safeSpace, 6), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints boxConstraints) { + const double width = 160; + 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: episode?.cover ?? '', + width: maxWidth, + height: maxHeight, + ), + if (episode.duration != 0) + PBadge( + text: Utils.timeFormat(episode.duration!), + right: 6.0, + bottom: 6.0, + type: 'gray', + ), + ], + ); + }, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 2, 6, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + episode.title as String, + textAlign: TextAlign.start, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w500, + color: isCurrentIndex ? primary : onSurface, + ), + ), + const Spacer(), + if (dataType != VideoEpidoesType.videoPart) ...[ + if (episode?.pubdate != null || + episode.pubTime != null) + Text( + Utils.dateFormat( + episode?.pubdate ?? episode.pubTime), + style: TextStyle( + fontSize: 11, + color: + Theme.of(context).colorScheme.outline), + ), + const SizedBox(height: 2), + if (episode.stat != null) + Row( + children: [ + StatView(view: episode.stat.view), + const SizedBox(width: 8), + StatDanMu(danmu: episode.stat.danmaku), + const Spacer(), + ], + ), + const SizedBox(height: 4), + ] + ], + ), + ), ) - : null, - title: Text(title, - style: TextStyle( - fontSize: 14, - color: isCurrentIndex ? primary : onSurface, - ))) - : InkWell( + ], + ), + ); + }, + ), + ), + ); + } +} + +class EpisodeGridItem extends StatelessWidget { + final dynamic episode; + final int index; + final bool isCurrentIndex; + final dynamic dataType; + final Function changeFucCall; + final bool isFullScreen; + + const EpisodeGridItem({ + Key? key, + required this.episode, + required this.index, + required this.isCurrentIndex, + required this.dataType, + required this.changeFucCall, + required this.isFullScreen, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + TextStyle textStyle = TextStyle( + color: isCurrentIndex ? colorScheme.primary : colorScheme.onSurface, + fontSize: 14, + ); + return Stack( + children: [ + Container( + width: double.infinity, + margin: const EdgeInsets.only(top: StyleString.safeSpace), + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: isCurrentIndex + ? colorScheme.primaryContainer.withOpacity(0.6) + : colorScheme.onInverseSurface.withOpacity(0.6), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isCurrentIndex + ? colorScheme.primary.withOpacity(0.8) + : Colors.transparent, + width: 1, + ), + ), + child: InkWell( + borderRadius: BorderRadius.circular(8), onTap: () { - SmartDialog.showToast('切换至「$title」'); + if (isCurrentIndex) { + return; + } + SmartDialog.showToast('切换至「${episode.title}」'); changeFucCall.call(episode, index); }, child: Padding( - padding: - const EdgeInsets.only(left: 14, right: 14, top: 8, bottom: 8), - child: Row( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, children: [ - NetworkImgLayer( - width: 130, height: 75, src: episode?.cover ?? ''), - const SizedBox(width: 10), - Expanded( - child: Text( - title, - maxLines: 2, - style: TextStyle( - fontSize: 14, - color: isCurrentIndex ? primary : onSurface, - ), - ), + Text( + dataType == VideoEpidoesType.bangumiEpisode + ? '第${index + 1}话' + : '第${index + 1}p', + style: textStyle), + const SizedBox(height: 1), + Text( + episode.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textStyle, ), ], ), ), - ); - } - - Widget buildTitle() { - return AppBar( - toolbarHeight: 45, - automaticallyImplyLeading: false, - centerTitle: false, - title: Text( - '合集(${episodes.length})', - style: Theme.of(context).textTheme.titleMedium, - ), - actions: !isFullScreen - ? [ - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: () => Navigator.pop(context), - ), - const SizedBox(width: 14), - ] - : null, + ), + ), + if (dataType == VideoEpidoesType.bangumiEpisode && + episode.badge != '' && + episode.badge != null) + Positioned( + right: 8, + top: 18, + child: Text( + episode.badge, + style: const TextStyle(fontSize: 11, color: Color(0xFFFF6699)), + ), + ) + ], ); } +} - Widget buildShowContent(BuildContext context) { - final ItemScrollController itemScrollController = ItemScrollController(); - int currentIndex = episodes.indexWhere((dynamic e) => e.cid == currentCid); - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - WidgetsBinding.instance.addPostFrameCallback((_) { - itemScrollController.jumpTo(index: currentIndex); - }); - return Container( - height: sheetHeight, - color: Theme.of(context).colorScheme.surface, - child: Column( - children: [ - buildTitle(), - Expanded( - child: Material( - child: PageStorage( - bucket: PageStorageBucket(), - child: ScrollablePositionedList.builder( - itemScrollController: itemScrollController, - itemCount: episodes.length + 1, - itemBuilder: (BuildContext context, int index) { - bool isLastItem = index == episodes.length; - bool isCurrentIndex = currentIndex == index; - return isLastItem - ? SizedBox( - height: - MediaQuery.of(context).padding.bottom + 20, - ) - : buildEpisodeListItem( - episodes[index], - index, - isCurrentIndex, - ); - }, +class UgcSeasonBuild extends StatelessWidget { + final UgcSeason ugcSeason; + final RxInt isSubscribe; + final bool isVisible; + final Function changeFucCall; + final Function changeVisible; + + const UgcSeasonBuild({ + Key? key, + required this.ugcSeason, + required this.isSubscribe, + required this.isVisible, + required this.changeFucCall, + required this.changeVisible, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final Color outline = theme.colorScheme.outline; + final Color surface = theme.colorScheme.surface; + final Color primary = theme.colorScheme.primary; + final Color onPrimary = theme.colorScheme.onPrimary; + final Color onInverseSurface = theme.colorScheme.onInverseSurface; + final TextStyle titleMedium = theme.textTheme.titleMedium!; + final TextStyle labelMedium = theme.textTheme.labelMedium!; + final Color dividerColor = theme.dividerColor.withOpacity(0.1); + + return isVisible + ? Container( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), + color: surface, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Divider(height: 1, thickness: 1, color: dividerColor), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: Text( + '合集:${ugcSeason.title}', + style: titleMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 10), + Obx( + () => isSubscribe.value == -1 + ? const SizedBox(height: 32) + : SizedBox( + height: 32, + child: FilledButton.tonal( + onPressed: () => changeFucCall.call(), + style: TextButton.styleFrom( + padding: + const EdgeInsets.only(left: 8, right: 8), + foregroundColor: isSubscribe.value == 1 + ? outline + : onPrimary, + backgroundColor: isSubscribe.value == 1 + ? onInverseSurface + : primary, + ), + child: + Text(isSubscribe.value == 1 ? '已订阅' : '订阅'), + ), + ), + ), + ], + ), + if (ugcSeason.intro != null && ugcSeason.intro != '') ...[ + const SizedBox(height: 4), + Text( + ugcSeason.intro!, + style: TextStyle(color: outline, fontSize: 12), ), + ], + const SizedBox(height: 4), + Text.rich( + TextSpan( + style: TextStyle( + fontSize: labelMedium.fontSize, color: outline), + children: [ + TextSpan( + text: '${Utils.numFormat(ugcSeason.stat!.view)}播放'), + const TextSpan(text: ' · '), + TextSpan( + text: + '${Utils.numFormat(ugcSeason.stat!.danmaku)}弹幕'), + ], + ), + ), + const SizedBox(height: 14), + Align( + alignment: Alignment.center, + child: Material( + color: surface, + child: InkWell( + onTap: () => changeVisible.call(), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 0), + child: Text( + '收起简介', + style: TextStyle(color: primary, fontSize: 12), + ), + ), + ), + ), + ), + Divider(height: 1, thickness: 1, color: dividerColor), + ], + ), + ) + : Align( + alignment: Alignment.center, + child: InkWell( + onTap: () => changeVisible.call(), + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 10, horizontal: 0), + child: Text( + '展开简介', + style: TextStyle(color: primary, fontSize: 12), ), ), ), - ], - ), - ); - }); - } - - /// The [BuildContext] of the widget that calls the bottom sheet. - PersistentBottomSheetController show(BuildContext context) { - final PersistentBottomSheetController btmSheetCtr = showBottomSheet( - context: context, - builder: (BuildContext context) { - return buildShowContent(context); - }, - ); - return btmSheetCtr; + ); } } diff --git a/lib/common/skeleton/media_bangumi.dart b/lib/common/skeleton/media_bangumi.dart index cf589254..98282cf0 100644 --- a/lib/common/skeleton/media_bangumi.dart +++ b/lib/common/skeleton/media_bangumi.dart @@ -3,14 +3,9 @@ import 'package:pilipala/common/constants.dart'; import 'skeleton.dart'; -class MediaBangumiSkeleton extends StatefulWidget { +class MediaBangumiSkeleton extends StatelessWidget { const MediaBangumiSkeleton({super.key}); - @override - State createState() => _MediaBangumiSkeletonState(); -} - -class _MediaBangumiSkeletonState extends State { @override Widget build(BuildContext context) { Color bgColor = Theme.of(context).colorScheme.onInverseSurface; @@ -35,25 +30,25 @@ class _MediaBangumiSkeletonState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - color: Theme.of(context).colorScheme.onInverseSurface, + color: bgColor, width: 200, height: 20, margin: const EdgeInsets.only(bottom: 15), ), Container( - color: Theme.of(context).colorScheme.onInverseSurface, + color: bgColor, width: 150, height: 13, margin: const EdgeInsets.only(bottom: 5), ), Container( - color: Theme.of(context).colorScheme.onInverseSurface, + color: bgColor, width: 150, height: 13, margin: const EdgeInsets.only(bottom: 5), ), Container( - color: Theme.of(context).colorScheme.onInverseSurface, + color: bgColor, width: 150, height: 13, ), @@ -64,7 +59,7 @@ class _MediaBangumiSkeletonState extends State { decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(20)), - color: Theme.of(context).colorScheme.onInverseSurface, + color: bgColor, ), ), ], diff --git a/lib/common/skeleton/user_list.dart b/lib/common/skeleton/user_list.dart new file mode 100644 index 00000000..cd9d4eb3 --- /dev/null +++ b/lib/common/skeleton/user_list.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import '../constants.dart'; + +class UserListSkeleton extends StatelessWidget { + const UserListSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + Color bgColor = Theme.of(context).colorScheme.onInverseSurface; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, vertical: 7), + child: Row( + children: [ + ClipOval( + child: Container(width: 42, height: 42, color: bgColor), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container(color: bgColor, width: 60, height: 13), + const SizedBox(width: 10), + Container(color: bgColor, width: 40, height: 13), + ], + ), + const SizedBox(height: 6), + Container( + color: bgColor, + width: 100, + height: 13, + ), + ], + ), + ), + ], + )); + } +} diff --git a/lib/common/skeleton/video_intro.dart b/lib/common/skeleton/video_intro.dart new file mode 100644 index 00000000..b7a5ec74 --- /dev/null +++ b/lib/common/skeleton/video_intro.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import '../constants.dart'; +import 'skeleton.dart'; + +class VideoIntroSkeleton extends StatelessWidget { + const VideoIntroSkeleton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Color bgColor = Theme.of(context).colorScheme.onInverseSurface; + return Skeleton( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 18), + Container( + width: double.infinity, + height: 20, + margin: const EdgeInsets.only(bottom: 6), + color: bgColor, + ), + Container( + width: 220, + height: 20, + margin: const EdgeInsets.only(bottom: 12), + color: bgColor, + ), + Row( + children: [ + Container( + width: 45, + height: 14, + color: bgColor, + ), + const SizedBox(width: 8), + Container( + width: 45, + height: 14, + color: bgColor, + ), + const SizedBox(width: 8), + Container( + width: 45, + height: 14, + color: bgColor, + ), + const Spacer(), + Container( + width: 35, + height: 14, + color: bgColor, + ), + const SizedBox(width: 4), + ], + ), + const SizedBox(height: 30), + LayoutBuilder(builder: (context, constraints) { + // 并列5个正方形 + double width = (constraints.maxWidth - 30) / 5; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: List.generate(5, (index) { + return Container( + width: width - 24, + height: width - 24, + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(16), + ), + ); + }), + ); + }), + const SizedBox(height: 20), + Container( + width: double.infinity, + height: 30, + margin: const EdgeInsets.symmetric(horizontal: 6), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(height: 20), + Row( + children: [ + ClipOval( + child: Container( + width: 44, + height: 44, + color: bgColor, + ), + ), + const SizedBox(width: 12), + Container( + width: 50, + height: 14, + color: bgColor, + ), + const SizedBox(width: 8), + Container( + width: 35, + height: 14, + color: bgColor, + ), + const Spacer(), + Container( + width: 55, + height: 30, + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(16), + ), + ), + const SizedBox(width: 2) + ], + ), + const SizedBox(height: 10), + ], + ), + ), + ); + } +} diff --git a/lib/common/widgets/custom_toast.dart b/lib/common/widgets/custom_toast.dart index f732fd85..93695e0d 100644 --- a/lib/common/widgets/custom_toast.dart +++ b/lib/common/widgets/custom_toast.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/utils/storage.dart'; -Box setting = GStrorage.setting; +Box setting = GStorage.setting; class CustomToast extends StatelessWidget { const CustomToast({super.key, required this.msg}); diff --git a/lib/common/widgets/drag_handle.dart b/lib/common/widgets/drag_handle.dart new file mode 100644 index 00000000..1143a732 --- /dev/null +++ b/lib/common/widgets/drag_handle.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class DragHandle extends StatelessWidget { + const DragHandle({super.key}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: Navigator.of(context).pop, + child: SizedBox( + height: 36, + child: Center( + child: Container( + width: 32, + height: 4, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.outline, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ); + } +} diff --git a/lib/common/widgets/http_error.dart b/lib/common/widgets/http_error.dart index 0381319e..51396c0b 100644 --- a/lib/common/widgets/http_error.dart +++ b/lib/common/widgets/http_error.dart @@ -4,9 +4,10 @@ import 'package:flutter_svg/flutter_svg.dart'; class HttpError extends StatelessWidget { const HttpError({ required this.errMsg, - required this.fn, + this.fn, this.btnText, this.isShowBtn = true, + this.isInSliver = true, super.key, }); @@ -14,46 +15,41 @@ class HttpError extends StatelessWidget { final Function()? fn; final String? btnText; final bool isShowBtn; + final bool isInSliver; @override Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: SizedBox( - height: 400, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - "assets/images/error.svg", - height: 200, - ), - const SizedBox(height: 30), - Text( - errMsg ?? '请求异常', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(height: 20), - if (isShowBtn) - FilledButton.tonal( - onPressed: () { - fn!(); - }, - style: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith((states) { - return Theme.of(context).colorScheme.primary.withAlpha(20); - }), - ), - child: Text( - btnText ?? '点击重试', - style: - TextStyle(color: Theme.of(context).colorScheme.primary), - ), + Color primary = Theme.of(context).colorScheme.primary; + final errorContent = SizedBox( + height: 400, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset("assets/images/error.svg", height: 200), + const SizedBox(height: 30), + Text( + errMsg ?? '请求异常', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 20), + if (isShowBtn) + FilledButton.tonal( + onPressed: () => fn?.call(), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) { + return primary.withAlpha(20); + }), ), - ], - ), + child: Text(btnText ?? '点击重试', style: TextStyle(color: primary)), + ), + ], ), ); + if (isInSliver) { + return SliverToBoxAdapter(child: errorContent); + } else { + return Align(alignment: Alignment.topCenter, child: errorContent); + } } } diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index 0b715a89..b7b5de7e 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -6,7 +6,7 @@ import 'package:pilipala/utils/global_data_cache.dart'; import '../../utils/storage.dart'; import '../constants.dart'; -Box setting = GStrorage.setting; +Box setting = GStorage.setting; class NetworkImgLayer extends StatelessWidget { const NetworkImgLayer({ @@ -20,6 +20,7 @@ class NetworkImgLayer extends StatelessWidget { // 图片质量 默认1% this.quality, this.origAspectRatio, + this.radius, }); final String? src; @@ -30,10 +31,26 @@ class NetworkImgLayer extends StatelessWidget { final Duration? fadeInDuration; final int? quality; final double? origAspectRatio; + final double? radius; + + BorderRadius getBorderRadius(String? type, double? radius) { + return BorderRadius.circular( + radius ?? + (type == 'avatar' + ? 50 + : type == 'emote' + ? 0 + : StyleString.imgRadius.x), + ); + } @override Widget build(BuildContext context) { - final int defaultImgQuality = GlobalDataCache().imgQuality; + int defaultImgQuality = 10; + try { + defaultImgQuality = GlobalDataCache.imgQuality; + } catch (_) {} + if (src == '' || src == null) { return placeholder(context); } @@ -68,13 +85,7 @@ class NetworkImgLayer extends StatelessWidget { return src != '' && src != null ? ClipRRect( clipBehavior: Clip.antiAlias, - borderRadius: BorderRadius.circular( - type == 'avatar' - ? 50 - : type == 'emote' - ? 0 - : StyleString.imgRadius.x, - ), + borderRadius: getBorderRadius(type, radius), child: CachedNetworkImage( imageUrl: imageUrl, width: width, @@ -103,11 +114,7 @@ class NetworkImgLayer extends StatelessWidget { clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4), - borderRadius: BorderRadius.circular(type == 'avatar' - ? 50 - : type == 'emote' - ? 0 - : StyleString.imgRadius.x), + borderRadius: getBorderRadius(type, radius), ), child: type == 'bg' ? const SizedBox() diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 78c4ba87..b662a3b0 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -12,6 +12,7 @@ import '../../http/video.dart'; import '../../utils/utils.dart'; import '../constants.dart'; import 'badge.dart'; +import 'drag_handle.dart'; import 'network_img_layer.dart'; import 'stat/danmu.dart'; import 'stat/view.dart'; @@ -373,27 +374,12 @@ class MorePanel extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), child: Column( mainAxisSize: MainAxisSize.min, children: [ - InkWell( - onTap: () => Get.back(), - child: Container( - height: 35, - padding: const EdgeInsets.only(bottom: 2), - child: Center( - child: Container( - width: 32, - height: 3, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.outline, - borderRadius: const BorderRadius.all(Radius.circular(3))), - ), - ), - ), - ), + const DragHandle(), ListTile( onTap: () async => await menuActionHandler('block'), minLeadingWidth: 0, diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 378c9f75..72cfb998 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -5,6 +5,7 @@ import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/image_save.dart'; import 'package:pilipala/utils/route_push.dart'; import '../../models/model_rec_video_item.dart'; +import 'drag_handle.dart'; import 'stat/danmu.dart'; import 'stat/view.dart'; import '../../http/dynamics.dart'; @@ -282,9 +283,10 @@ class VideoStat extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - StatView(view: videoItem.stat.view), + if (videoItem.stat.view != null) StatView(view: videoItem.stat.view), const SizedBox(width: 8), - StatDanMu(danmu: videoItem.stat.danmu), + if (videoItem.stat.danmu != null) + StatDanMu(danmu: videoItem.stat.danmu), if (videoItem is RecVideoItemModel) ...[ crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8), RichText( @@ -367,27 +369,12 @@ class MorePanel extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), child: Column( mainAxisSize: MainAxisSize.min, children: [ - InkWell( - onTap: () => Get.back(), - child: Container( - height: 35, - padding: const EdgeInsets.only(bottom: 2), - child: Center( - child: Container( - width: 32, - height: 3, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.outline, - borderRadius: const BorderRadius.all(Radius.circular(3))), - ), - ), - ), - ), + const DragHandle(), ListTile( onTap: () async => await menuActionHandler('block'), minLeadingWidth: 0, diff --git a/lib/http/api.dart b/lib/http/api.dart index ff49b314..379540a5 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -104,7 +104,7 @@ class Api { // 评论列表 // https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11 - static const String replyList = '/x/v2/reply'; + static const String replyList = '/x/v2/reply/main'; // 楼中楼 static const String replyReplyList = '/x/v2/reply/reply'; @@ -175,7 +175,7 @@ class Api { static const String delHistory = '/x/v2/history/delete'; // 搜索历史记录 - static const String searchHistory = '/x/web-goblin/history/search'; + static const String searchHistory = '/x/web-interface/history/search'; // 热搜 static const String hotSearchList = @@ -301,10 +301,6 @@ class Api { static const String bangumiList = '/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1©right=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1'; - // 我的订阅 - static const String bangumiFollow = - '/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15&ts=1691544359969'; - // 黑名单 static const String blackLst = '/x/relation/blacks'; @@ -499,7 +495,7 @@ class Api { static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi'; /// 获取字幕配置 - static const getSubtitleConfig = '/x/player/v2'; + static const getSubtitleConfig = '/x/player/wbi/v2'; /// 我的订阅 static const userSubFolder = '/x/v3/fav/folder/collected/list'; @@ -555,6 +551,10 @@ class Api { static const String messageSystemAPi = '${HttpString.messageBaseUrl}/x/sys-msg/query_unified_notify'; + /// 系统通知 个人 + static const String userMessageSystemAPi = + '${HttpString.messageBaseUrl}/x/sys-msg/query_user_notify'; + /// 系统通知标记已读 static const String systemMarkRead = '${HttpString.messageBaseUrl}/x/sys-msg/update_cursor'; @@ -579,6 +579,51 @@ class Api { /// 稍后再看&收藏夹视频列表 static const String mediaList = '/x/v2/medialist/resource/list'; + /// 用户专栏 + static const String opusList = '/x/polymer/web-dynamic/v1/opus/feed/space'; + /// static const String getViewInfo = '/x/article/viewinfo'; + + /// 直播间记录 + static const String liveRoomEntry = + '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/roomEntryAction'; + + /// 用户信息 + static const String accountInfo = '/x/member/web/account'; + + /// 更新用户信息 + static const String updateAccountInfo = '/x/member/web/update'; + + /// 删除评论 + static const String replyDel = '/x/v2/reply/del'; + + /// 图片上传 + static const String uploadImage = '/x/dynamic/feed/draw/upload_bfs'; + + /// 更新追番状态 + static const String updateBangumiStatus = '/pgc/web/follow/status/update'; + + /// 番剧点赞投币收藏状态 + static const String bangumiActionStatus = '/pgc/season/episode/community'; + + /// @我的 + static const String messageAtAPi = '/x/msgfeed/at?'; + + /// 订阅 + static const String confirmSub = '/x/v3/fav/season/fav'; + + /// 订阅状态 + static const String videoRelation = '/x/web-interface/archive/relation'; + + /// 获取空降区间 + static const String getSkipSegments = + '${HttpString.sponsorBlockBaseUrl}/api/skipSegments'; + + /// 视频标签 + static const String videoTag = '/x/tag/archive/tags'; + + /// 修复标题和海报 + // /api/view?id=${aid} /all/video/av${aid} /video/av${aid}/ + static const String fixTitleAndPic = '${HttpString.biliplusBaseUrl}/api/view'; } diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart index 91508682..d0c052d6 100644 --- a/lib/http/bangumi.dart +++ b/lib/http/bangumi.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; import '../models/bangumi/list.dart'; import 'index.dart'; +import 'package:html/parser.dart' as html_parser; +import 'package:html/dom.dart' as html_dom; class BangumiHttp { static Future bangumiList({int? page}) async { @@ -18,8 +21,19 @@ class BangumiHttp { } } - static Future bangumiFollow({int? mid}) async { - var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid}); + static Future getRecentBangumi({ + int? mid, + int type = 1, + int pn = 1, + int ps = 20, + }) async { + var res = await Request().get(Api.getRecentBangumiApi, data: { + 'vmid': mid, + 'type': type, + 'follow_status': 0, + 'pn': pn, + 'ps': ps, + }); if (res.data['code'] == 0) { return { 'status': true, @@ -33,4 +47,62 @@ class BangumiHttp { }; } } + + // 获取追番状态 + static Future bangumiStatus({required int seasonId}) async { + var res = await Request() + .get('https://www.bilibili.com/bangumi/play/ss$seasonId'); + html_dom.Document document = html_parser.parse(res.data); + // 查找 id 为 __NEXT_DATA__ 的 script 元素 + html_dom.Element? scriptElement = + document.querySelector('script#\\__NEXT_DATA__'); + if (scriptElement != null) { + // 提取 script 元素的内容 + String scriptContent = scriptElement.text; + final dynamic scriptContentJson = jsonDecode(scriptContent); + Map followState = scriptContentJson['props']['pageProps']['followState']; + return { + 'status': true, + 'data': { + 'isFollowed': followState['isFollowed'], + 'followStatus': followState['followStatus'] + } + }; + } else { + print('Script element with id "__NEXT_DATA__" not found.'); + } + } + + // 更新追番状态 + static Future updateBangumiStatus({ + required int seasonId, + required int status, + }) async { + var res = await Request().post(Api.updateBangumiStatus, data: { + 'season_id': seasonId, + 'status': status, + }); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 获取番剧点赞投币收藏状态 + static Future bangumiActionStatus({required int epId}) async { + var res = await Request().get( + Api.bangumiActionStatus, + data: {'ep_id': epId}, + ); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } } diff --git a/lib/http/black.dart b/lib/http/black.dart index 0c6a63ab..67356a92 100644 --- a/lib/http/black.dart +++ b/lib/http/black.dart @@ -28,7 +28,7 @@ class BlackHttp { static Future removeBlack({required int fid}) async { var res = await Request().post( Api.removeBlack, - queryParameters: { + data: { 'act': 6, 'csrf': await Request.getCsrf(), 'fid': fid, diff --git a/lib/http/common.dart b/lib/http/common.dart index d711a7e7..8dd1cc24 100644 --- a/lib/http/common.dart +++ b/lib/http/common.dart @@ -1,6 +1,15 @@ +import 'package:pilipala/models/common/invalid_video.dart'; +import 'dart:convert'; +import 'dart:math'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/models/sponsor_block/segment.dart'; + import 'index.dart'; class CommonHttp { + static final RegExp spmPrefixExp = + RegExp(r''); static Future unReadDynamic() async { var res = await Request().get(Api.getUnreadDynamic, data: {'alltype_offset': 0, 'video_offset': '', 'article_offset': 0}); @@ -14,4 +23,95 @@ class CommonHttp { }; } } + + static Future querySkipSegments({required String bvid}) async { + var res = await Request().getWithoutCookie(Api.getSkipSegments, data: { + 'videoID': bvid, + }); + if (res.data is List && res.data.isNotEmpty) { + try { + return { + 'status': true, + 'data': res.data + .map((e) => SegmentDataModel.fromJson(e)) + .toList(), + }; + } catch (err) { + return { + 'status': false, + 'data': [], + 'msg': 'sponsorBlock数据解析失败: $err', + }; + } + } else { + return { + 'status': false, + 'data': [], + }; + } + } + + static Future fixVideoPicAndTitle({required int aid}) async { + var res = await Request().getWithoutCookie(Api.fixTitleAndPic, data: { + 'id': aid, + }); + if (res != null) { + if (res.data['code'] == -404) { + return { + 'status': false, + 'data': null, + 'msg': '没有相关信息', + }; + } else { + return { + 'status': true, + 'data': InvalidVideoModel.fromJson(res.data), + }; + } + } else { + return { + 'status': false, + 'data': null, + 'msg': '没有相关信息', + }; + } + } + + static Future buvidActivate() async { + try { + // 获取 HTML 数据 + var html = await Request().get(Api.dynamicSpmPrefix); + + // 提取 spmPrefix + String spmPrefix = spmPrefixExp.firstMatch(html.data)?.group(1) ?? ''; + + // 生成随机 PNG 结束部分 + Random rand = Random(); + String randPngEnd = base64.encode( + List.generate(32, (_) => rand.nextInt(256)) + ..addAll(List.filled(4, 0)) + ..addAll([73, 69, 78, 68]) + ..addAll(List.generate(4, (_) => rand.nextInt(256))), + ); + + // 构建 JSON 数据 + String jsonData = json.encode({ + '3064': 1, + '39c8': '$spmPrefix.fp.risk', + '3c43': { + 'adca': 'Linux', + 'bfe9': randPngEnd.substring(randPngEnd.length - 50), + }, + }); + + // 发送 POST 请求 + await Request().post( + Api.activateBuvidApi, + data: {'payload': jsonData}, + options: Options(contentType: 'application/json'), + ); + } catch (err) { + debugPrint('buvidActivate error: $err'); + } + } } diff --git a/lib/http/constants.dart b/lib/http/constants.dart index b734c279..e7e031bb 100644 --- a/lib/http/constants.dart +++ b/lib/http/constants.dart @@ -7,6 +7,9 @@ class HttpString { static const String passBaseUrl = 'https://passport.bilibili.com'; static const String messageBaseUrl = 'https://message.bilibili.com'; static const String bangumiBaseUrl = 'https://bili.meark.me'; + static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top'; + static const String biliplusBaseUrl = 'https://www.biliplus.com'; + static const List validateStatusCodes = [ 302, 304, diff --git a/lib/http/danmaku.dart b/lib/http/danmaku.dart index 0b108755..7b4283ae 100644 --- a/lib/http/danmaku.dart +++ b/lib/http/danmaku.dart @@ -17,7 +17,9 @@ class DanmakaHttp { var response = await Request().get( Api.webDanmaku, data: params, - extra: {'resType': ResponseType.bytes}, + options: Options( + responseType: ResponseType.bytes, + ), ); return DmSegMobileReply.fromBuffer(response.data); } @@ -67,9 +69,6 @@ class DanmakaHttp { var response = await Request().post( Api.shootDanmaku, data: params, - options: Options( - contentType: Headers.formUrlEncodedContentType, - ), ); if (response.statusCode != 200) { return { diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart index 69619361..53ba6fc1 100644 --- a/lib/http/dynamics.dart +++ b/lib/http/dynamics.dart @@ -1,4 +1,5 @@ import 'dart:math'; +import 'package:dio/dio.dart'; import '../models/dynamics/result.dart'; import '../models/dynamics/up.dart'; import 'index.dart'; @@ -69,7 +70,7 @@ class DynamicsHttp { }) async { var res = await Request().post( Api.likeDynamic, - queryParameters: { + data: { 'dynamic_id': dynamicId, 'up': up, 'csrf': await Request.getCsrf(), @@ -91,7 +92,7 @@ class DynamicsHttp { // static Future dynamicDetail({ - String? id, + required String id, }) async { var res = await Request().get(Api.dynamicDetail, data: { 'timezone_offset': -480, @@ -175,27 +176,32 @@ class DynamicsHttp { 'revs_id': {'dyn_type': 8, 'rid': oid} }; } - var res = await Request().post(Api.dynamicCreate, queryParameters: { - 'platform': 'web', - 'csrf': await Request.getCsrf(), - 'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'}, - 'x-bili-web-req-json': {'spm_id': '333.999'}, - }, data: { - 'dyn_req': { - 'content': { - 'contents': [ - {'raw_text': rawText ?? '', 'type': 1, 'biz_id': ''} - ] - }, - 'scene': scene, - 'attach_card': null, - 'upload_id': uploadId, - 'meta': { - 'app_meta': {'from': 'create.dynamic.web', 'mobi_app': 'web'} - } + var res = await Request().post( + Api.dynamicCreate, + queryParameters: { + 'platform': 'web', + 'csrf': await Request.getCsrf(), + 'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'}, + 'x-bili-web-req-json': {'spm_id': '333.999'}, }, - 'web_repost_src': webRepostSrc - }); + data: { + 'dyn_req': { + 'content': { + 'contents': [ + {'raw_text': rawText ?? '', 'type': 1, 'biz_id': ''} + ] + }, + 'scene': scene, + 'attach_card': null, + 'upload_id': uploadId, + 'meta': { + 'app_meta': {'from': 'create.dynamic.web', 'mobi_app': 'web'} + } + }, + 'web_repost_src': webRepostSrc + }, + options: Options(contentType: 'application/json'), + ); if (res.data['code'] == 0) { return { 'status': true, diff --git a/lib/http/fav.dart b/lib/http/fav.dart index 6f49d68a..69577e7e 100644 --- a/lib/http/fav.dart +++ b/lib/http/fav.dart @@ -11,7 +11,7 @@ class FavHttp { }) async { var res = await Request().post( Api.editFavFolder, - queryParameters: { + data: { 'title': title, 'intro': intro, 'media_id': mediaId, @@ -43,7 +43,7 @@ class FavHttp { }) async { var res = await Request().post( Api.addFavFolder, - queryParameters: { + data: { 'title': title, 'intro': intro, 'cover': cover ?? '', diff --git a/lib/http/html.dart b/lib/http/html.dart index 100887e5..87adacb9 100644 --- a/lib/http/html.dart +++ b/lib/http/html.dart @@ -21,7 +21,6 @@ class HtmlHttp { } try { Document rootTree = parse(response.data); - // log(response.data.body.toString()); Element body = rootTree.body!; Element appDom = body.querySelector('#app')!; Element authorHeader = appDom.querySelector('.fixed-author-header')!; @@ -52,7 +51,6 @@ class HtmlHttp { .className .split(' ')[1] .split('-')[2]; - // List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img'); return { 'status': true, 'avatar': avatar, @@ -76,20 +74,10 @@ class HtmlHttp { Element body = rootTree.body!; Element appDom = body.querySelector('#app')!; Element authorHeader = appDom.querySelector('.up-left')!; - // 头像 - // String avatar = - // authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!; - // print(avatar); - // avatar = 'https:${avatar.split('@')[0]}'; String uname = authorHeader.querySelector('.up-name')!.text.trim(); // 动态详情 Element opusDetail = appDom.querySelector('.article-content')!; // 发布时间 - // String updateTime = - // opusDetail.querySelector('.opus-module-author__pub__text')!.text; - // print(updateTime); - - // String opusContent = opusDetail.querySelector('#read-article-holder')!.innerHtml; RegExp digitRegExp = RegExp(r'\d+'); diff --git a/lib/http/init.dart b/lib/http/init.dart index 6a90a87d..9c0f368c 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -1,19 +1,16 @@ // ignore_for_file: avoid_print import 'dart:async'; -import 'dart:convert'; import 'dart:developer'; import 'dart:io'; -import 'dart:math' show Random; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; -// import 'package:dio_http2_adapter/dio_http2_adapter.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/utils/id_utils.dart'; import '../utils/storage.dart'; import '../utils/utils.dart'; -import 'api.dart'; import 'constants.dart'; import 'interceptor.dart'; @@ -22,19 +19,17 @@ class Request { static late CookieManager cookieManager; static late final Dio dio; factory Request() => _instance; - Box setting = GStrorage.setting; - static Box localCache = GStrorage.localCache; + Box setting = GStorage.setting; + static Box localCache = GStorage.localCache; late bool enableSystemProxy; late String systemProxyHost; late String systemProxyPort; - static final RegExp spmPrefixExp = - RegExp(r''); static String? buvid; /// 设置cookie static setCookie() async { - Box userInfoCache = GStrorage.userInfo; - Box setting = GStrorage.setting; + Box userInfoCache = GStorage.userInfo; + Box setting = GStorage.setting; final String cookiePath = await Utils.getCookiePath(); final PersistCookieJar cookieJar = PersistCookieJar( ignoreExpires: true, @@ -44,7 +39,7 @@ class Request { dio.interceptors.add(cookieManager); final List cookie = await cookieManager.cookieJar .loadForRequest(Uri.parse(HttpString.baseUrl)); - final userInfo = userInfoCache.get('userInfoCache'); + final UserInfoData? userInfo = userInfoCache.get('userInfoCache'); if (userInfo != null && userInfo.mid != null) { final List cookie2 = await cookieManager.cookieJar .loadForRequest(Uri.parse(HttpString.tUrl)); @@ -62,11 +57,6 @@ class Request { baseUrlType = 'bangumi'; } setBaseUrl(type: baseUrlType); - try { - await buvidActivate(); - } catch (e) { - log("setCookie, ${e.toString()}"); - } final String cookieString = cookie .map((Cookie cookie) => '${cookie.name}=${cookie.value}') @@ -78,7 +68,7 @@ class Request { // 从cookie中获取 csrf token static Future getCsrf() async { List cookies = await cookieManager.cookieJar - .loadForRequest(Uri.parse(HttpString.apiBaseUrl)); + .loadForRequest(Uri.parse(HttpString.baseUrl)); String token = ''; if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) { token = cookies.firstWhere((e) => e.name == 'bili_jct').value; @@ -92,9 +82,12 @@ class Request { } final List cookies = await cookieManager.cookieJar - .loadForRequest(Uri.parse(HttpString.baseUrl)); - buvid = cookies.firstWhere((cookie) => cookie.name == 'buvid3').value; - if (buvid == null) { + .loadForRequest(Uri.parse(HttpString.apiBaseUrl)); + buvid = cookies + .firstWhere((cookie) => cookie.name == 'buvid3', + orElse: () => Cookie('buvid3', '')) + .value; + if (buvid == null || buvid!.isEmpty) { try { var result = await Request().get( "${HttpString.apiBaseUrl}/x/frontend/finger/spi", @@ -122,30 +115,6 @@ class Request { dio.options.headers['referer'] = 'https://www.bilibili.com/'; } - static Future buvidActivate() async { - var html = await Request().get(Api.dynamicSpmPrefix); - String spmPrefix = spmPrefixExp.firstMatch(html.data)!.group(1)!; - Random rand = Random(); - String rand_png_end = base64.encode( - List.generate(32, (_) => rand.nextInt(256)) + - List.filled(4, 0) + - [73, 69, 78, 68] + - List.generate(4, (_) => rand.nextInt(256))); - - String jsonData = json.encode({ - '3064': 1, - '39c8': '${spmPrefix}.fp.risk', - '3c43': { - 'adca': 'Linux', - 'bfe9': rand_png_end.substring(rand_png_end.length - 50), - }, - }); - - await Request().post(Api.activateBuvidApi, - data: {'payload': jsonData}, - options: Options(contentType: 'application/json')); - } - /* * config it and create */ @@ -171,15 +140,6 @@ class Request { dio = Dio(options); - /// fix 第三方登录 302重定向 跟iOS代理问题冲突 - // ..httpClientAdapter = Http2Adapter( - // ConnectionManager( - // idleTimeout: const Duration(milliseconds: 10000), - // onClientCreate: (_, ClientSetting config) => - // config.onBadCertificate = (_) => true, - // ), - // ); - /// 设置代理 if (enableSystemProxy) { dio.httpClientAdapter = IOHttpClientAdapter( @@ -217,18 +177,15 @@ class Request { /* * get请求 */ - get(url, {data, options, cancelToken, extra}) async { + get(url, {data, Options? options, cancelToken, extra}) async { Response response; - final Options options = Options(); - ResponseType resType = ResponseType.json; if (extra != null) { - resType = extra!['resType'] ?? ResponseType.json; if (extra['ua'] != null) { - options.headers = {'user-agent': headerUa(type: extra['ua'])}; + options ??= Options(); + options.headers ??= {}; + options.headers?['user-agent'] = headerUa(type: extra['ua']); } } - options.responseType = resType; - try { response = await dio.get( url, @@ -238,32 +195,44 @@ class Request { ); return response; } on DioException catch (e) { - Response errResponse = Response( - data: { - 'message': await ApiInterceptor.dioError(e) - }, // 将自定义 Map 数据赋值给 Response 的 data 属性 + return Response( + data: {'message': await ApiInterceptor.dioError(e)}, statusCode: 200, requestOptions: RequestOptions(), ); - return errResponse; } } + /* + * get请求 + */ + getWithoutCookie(url, {data}) { + return get( + url, + data: data, + options: Options( + headers: { + 'cookie': 'buvid3= ; b_nut= ; sid= ', + 'user-agent': headerUa(type: 'pc'), + }, + ), + ); + } + /* * post请求 */ post(url, {data, queryParameters, options, cancelToken, extra}) async { - // print('post-data: $data'); Response response; try { response = await dio.post( url, data: data, queryParameters: queryParameters, - options: options, + options: + options ?? Options(contentType: Headers.formUrlEncodedContentType), cancelToken: cancelToken, ); - // print('post success: ${response.data}'); return response; } on DioException catch (e) { Response errResponse = Response( @@ -319,7 +288,7 @@ class Request { } } else { headerUa = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15'; + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36'; } return headerUa; } diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart index 259a3bf9..b33d18df 100644 --- a/lib/http/interceptor.dart +++ b/lib/http/interceptor.dart @@ -3,8 +3,7 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:hive/hive.dart'; -import '../utils/storage.dart'; +import 'package:pilipala/utils/login.dart'; class ApiInterceptor extends Interceptor { @override @@ -19,20 +18,9 @@ class ApiInterceptor extends Interceptor { @override void onResponse(Response response, ResponseInterceptorHandler handler) { try { - if (response.statusCode == 302) { - final List locations = response.headers['location']!; - if (locations.isNotEmpty) { - if (locations.first.startsWith('https://www.mcbbs.net')) { - final Uri uri = Uri.parse(locations.first); - final String? accessKey = uri.queryParameters['access_key']; - final String? mid = uri.queryParameters['mid']; - try { - Box localCache = GStrorage.localCache; - localCache.put(LocalCacheKey.accessKey, - {'mid': mid, 'value': accessKey}); - } catch (_) {} - } - } + // 在响应之后处理数据 + if (response.data is Map && response.data['code'] == -101) { + LoginUtils.loginOut(); } } catch (err) { print('ApiInterceptor: $err'); diff --git a/lib/http/live.dart b/lib/http/live.dart index f5fd2a43..259f86fc 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -89,23 +89,26 @@ class LiveHttp { // 发送弹幕 static Future sendDanmaku({roomId, msg}) async { - var res = await Request().post(Api.sendLiveMsg, queryParameters: { - 'bubble': 0, - 'msg': msg, - 'color': 16777215, // 颜色 - 'mode': 1, // 模式 - 'room_type': 0, - 'jumpfrom': 71001, // 直播间来源 - 'reply_mid': 0, - 'reply_attr': 0, - 'replay_dmid': '', - 'statistics': {"appId": 100, "platform": 5}, - 'fontsize': 25, // 字体大小 - 'rnd': DateTime.now().millisecondsSinceEpoch ~/ 1000, // 时间戳 - 'roomid': roomId, - 'csrf': await Request.getCsrf(), - 'csrf_token': await Request.getCsrf(), - }); + var res = await Request().post( + Api.sendLiveMsg, + data: { + 'bubble': 0, + 'msg': msg, + 'color': 16777215, // 颜色 + 'mode': 1, // 模式 + 'room_type': 0, + 'jumpfrom': 71001, // 直播间来源 + 'reply_mid': 0, + 'reply_attr': 0, + 'replay_dmid': '', + 'statistics': {"appId": 100, "platform": 5}, + 'fontsize': 25, // 字体大小 + 'rnd': DateTime.now().millisecondsSinceEpoch ~/ 1000, // 时间戳 + 'roomid': roomId, + 'csrf': await Request.getCsrf(), + 'csrf_token': await Request.getCsrf(), + }, + ); if (res.data['code'] == 0) { return { 'status': true, @@ -142,4 +145,18 @@ class LiveHttp { }; } } + + // 直播历史记录 + static Future liveRoomEntry({required int roomId}) async { + await Request().post( + Api.liveRoomEntry, + data: { + 'room_id': roomId, + 'platform': 'pc', + 'csrf_token': await Request.getCsrf(), + 'csrf': await Request.getCsrf(), + 'visit_id': '', + }, + ); + } } diff --git a/lib/http/login.dart b/lib/http/login.dart index 2437b72a..80f58803 100644 --- a/lib/http/login.dart +++ b/lib/http/login.dart @@ -71,9 +71,6 @@ class LoginHttp { var res = await Request().post( Api.webSmsCode, data: formData, - options: Options( - contentType: Headers.formUrlEncodedContentType, - ), ); if (res.data['code'] == 0) { return { @@ -106,9 +103,6 @@ class LoginHttp { var res = await Request().post( Api.webSmsLogin, data: formData, - options: Options( - contentType: Headers.formUrlEncodedContentType, - ), ); if (res.data['code'] == 0) { return { @@ -155,9 +149,6 @@ class LoginHttp { var res = await Request().post( Api.appSmsCode, data: data, - options: Options( - contentType: Headers.formUrlEncodedContentType, - ), ); print(res); } @@ -208,9 +199,6 @@ class LoginHttp { var res = await Request().post( Api.loginInByPwdApi, data: data, - options: Options( - contentType: Headers.formUrlEncodedContentType, - ), ); print(res); } @@ -239,17 +227,27 @@ class LoginHttp { var res = await Request().post( Api.loginInByWebPwd, data: formData, - options: Options( - contentType: Headers.formUrlEncodedContentType, - ), ); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': res.data['data'], - }; + if (res.data['data']['status'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'code': 1, + 'data': res.data['data'], + 'msg': res.data['data']['message'], + }; + } } else { - return {'status': false, 'data': [], 'msg': res.data['message']}; + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; } } diff --git a/lib/http/member.dart b/lib/http/member.dart index e87aa42e..107a9379 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -1,6 +1,11 @@ +import 'dart:convert'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:hive/hive.dart'; +import 'package:html/parser.dart'; +import 'package:pilipala/models/member/article.dart'; import 'package:pilipala/models/member/like.dart'; +import 'package:pilipala/models/user/info.dart'; +import 'package:pilipala/utils/global_data_cache.dart'; import '../common/constants.dart'; import '../models/dynamics/result.dart'; import '../models/follow/result.dart'; @@ -16,14 +21,20 @@ import 'index.dart'; class MemberHttp { static Future memberInfo({ - int? mid, + required int mid, String token = '', }) async { + String? wWebid; + if ((await getWWebid(mid: mid))['status']) { + wWebid = GlobalDataCache.wWebid; + } + Map params = await WbiSign().makSign({ 'mid': mid, 'token': token, 'platform': 'web', 'web_location': 1550101, + ...wWebid != null ? {'w_webid': wWebid} : {}, }); var res = await Request().get( Api.memberInfo, @@ -195,13 +206,15 @@ class MemberHttp { // 设置分组 static Future addUsers(int? fids, String? tagids) async { - var res = await Request().post(Api.addUsers, queryParameters: { - 'fids': fids, - 'tagids': tagids ?? '0', - 'csrf': await Request.getCsrf(), - }, data: { - 'cross_domain': true - }); + var res = await Request().post( + Api.addUsers, + data: { + 'fids': fids, + 'tagids': tagids ?? '0', + 'csrf': await Request.getCsrf(), + }, + queryParameters: {'cross_domain': true}, + ); if (res.data['code'] == 0) { return {'status': true, 'data': [], 'msg': '操作成功'}; } else { @@ -419,11 +432,14 @@ class MemberHttp { static Future cookieToKey() async { var authCodeRes = await getTVCode(); if (authCodeRes['status']) { - var res = await Request().post(Api.cookieToKey, queryParameters: { - 'auth_code': authCodeRes['data'], - 'build': 708200, - 'csrf': await Request.getCsrf(), - }); + var res = await Request().post( + Api.cookieToKey, + data: { + 'auth_code': authCodeRes['data'], + 'build': 708200, + 'csrf': await Request.getCsrf(), + }, + ); await Future.delayed(const Duration(milliseconds: 300)); await qrcodePoll(authCodeRes['data']); if (res.data['code'] == 0) { @@ -455,11 +471,11 @@ class MemberHttp { SmartDialog.dismiss(); if (res.data['code'] == 0) { String accessKey = res.data['data']['access_token']; - Box localCache = GStrorage.localCache; - Box userInfoCache = GStrorage.userInfo; - var userInfo = userInfoCache.get('userInfoCache'); + Box localCache = GStorage.localCache; + Box userInfoCache = GStorage.userInfo; + final UserInfoData? userInfo = userInfoCache.get('userInfoCache'); localCache.put( - LocalCacheKey.accessKey, {'mid': userInfo.mid, 'value': accessKey}); + LocalCacheKey.accessKey, {'mid': userInfo!.mid, 'value': accessKey}); return {'status': true, 'data': [], 'msg': '操作成功'}; } else { return { @@ -556,4 +572,60 @@ class MemberHttp { }; } } + + static Future getWWebid({required int mid}) async { + String? wWebid = GlobalDataCache.wWebid; + if (wWebid != null) { + return {'status': true, 'data': wWebid}; + } + var res = await Request().get('https://space.bilibili.com/$mid/article'); + String? headContent = parse(res.data).head?.outerHtml; + final regex = RegExp( + r''); + if (headContent != null) { + final match = regex.firstMatch(headContent); + if (match != null && match.groupCount >= 1) { + final content = match.group(1); + String decodedString = Uri.decodeComponent(content!); + Map map = jsonDecode(decodedString); + GlobalDataCache.wWebid = map['access_id']; + return {'status': true, 'data': map['access_id']}; + } else { + return {'status': false, 'data': '请检查登录状态'}; + } + } + return {'status': false, 'data': '请检查登录状态'}; + } + + // 获取用户专栏 + static Future getMemberArticle({ + required int mid, + required int pn, + String? offset, + }) async { + String? wWebid; + if ((await getWWebid(mid: mid))['status']) { + wWebid = GlobalDataCache.wWebid; + } + Map params = await WbiSign().makSign({ + 'host_mid': mid, + 'page': pn, + 'offset': offset, + 'web_location': 333.999, + ...wWebid != null ? {'w_webid': wWebid} : {}, + }); + var res = await Request().get(Api.opusList, data: params); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': MemberArticleDataModel.fromJson(res.data['data']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'] ?? '请求异常', + }; + } + } } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 2de9cd49..65156e03 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math'; -import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/models/msg/at.dart'; import 'package:pilipala/models/msg/like.dart'; import 'package:pilipala/models/msg/reply.dart'; import 'package:pilipala/models/msg/system.dart'; @@ -64,7 +65,7 @@ class MsgHttp { .toList(), }; } catch (err) { - print('err🔟: $err'); + debugPrint('err: $err'); } } else { return { @@ -158,9 +159,6 @@ class MsgHttp { 'csrf_token': csrf, 'csrf': csrf, }, - options: Options( - contentType: Headers.formUrlEncodedContentType, - ), ); if (res.data['code'] == 0) { return { @@ -282,10 +280,10 @@ class MsgHttp { 'data': MessageLikeModel.fromJson(res.data['data']), }; } catch (err) { - return {'status': false, 'date': [], 'msg': err.toString()}; + return {'status': false, 'data': [], 'msg': err.toString()}; } } else { - return {'status': false, 'date': [], 'msg': res.data['message']}; + return {'status': false, 'data': [], 'msg': res.data['message']}; } } @@ -330,4 +328,47 @@ class MsgHttp { }; } } + + static Future messageSystemAccount() async { + var res = await Request().get(Api.userMessageSystemAPi, data: { + 'csrf': await Request.getCsrf(), + 'page_size': 20, + 'build': 0, + 'mobi_app': 'web', + }); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': res.data['data']['system_notify_list'] + .map((e) => MessageSystemModel.fromJson(e)) + .toList(), + }; + } catch (err) { + return {'status': false, 'date': [], 'msg': err.toString()}; + } + } else { + return {'status': false, 'date': [], 'msg': res.data['message']}; + } + } + + // @我的 + static Future messageAt() async { + var res = await Request().get(Api.messageAtAPi, data: { + 'build': 0, + 'mobi_app': 'web', + }); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': MessageAtModel.fromJson(res.data['data']), + }; + } catch (err) { + return {'status': false, 'data': [], 'msg': err.toString()}; + } + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } } diff --git a/lib/http/read.dart b/lib/http/read.dart index 68e72e59..f2542936 100644 --- a/lib/http/read.dart +++ b/lib/http/read.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:dio/dio.dart'; import 'package:html/parser.dart'; import 'package:pilipala/models/read/opus.dart'; import 'package:pilipala/models/read/read.dart'; @@ -65,6 +66,11 @@ class ReadHttp { var res = await Request().get( 'https://www.bilibili.com/read/cv$id', extra: {'ua': 'pc'}, + options: Options( + headers: { + 'cookie': 'opus-goback=1', + }, + ), ); String scriptContent = extractScriptContents(parse(res.data).body!.outerHtml)[0]; diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 880f9072..846ef45b 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -1,3 +1,8 @@ +import 'dart:convert'; + +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:image_picker/image_picker.dart'; import '../models/video/reply/data.dart'; import '../models/video/reply/emote.dart'; import 'api.dart'; @@ -6,17 +11,16 @@ import 'init.dart'; class ReplyHttp { static Future replyList({ required int oid, - required int pageNum, + required String nextOffset, required int type, int? ps, int sort = 1, }) async { var res = await Request().get(Api.replyList, data: { 'oid': oid, - 'pn': pageNum, 'type': type, - 'sort': sort, - 'ps': ps ?? 20 + 'pagination_str': jsonEncode({'offset': nextOffset}), + 'mode': sort + 2, }); if (res.data['code'] == 0) { return { @@ -52,19 +56,13 @@ class ReplyHttp { if (res.data['code'] == 0) { return { 'status': true, - 'data': ReplyData.fromJson(res.data['data']), + 'data': ReplyReplyData.fromJson(res.data['data']), }; } else { - Map errMap = { - -400: '请求错误', - -404: '无此项', - 12002: '评论区已关闭', - 12009: '评论主体的type不合法', - }; return { 'status': false, 'date': [], - 'msg': errMap[res.data['code']] ?? '请求异常', + 'msg': res.data['message'], }; } } @@ -78,7 +76,7 @@ class ReplyHttp { }) async { var res = await Request().post( Api.likeReply, - queryParameters: { + data: { 'type': type, 'oid': oid, 'rpid': rpid, @@ -115,4 +113,65 @@ class ReplyHttp { }; } } + + static Future replyDel({ + required int type, //replyType + required int oid, + required int rpid, + }) async { + var res = await Request().post( + Api.replyDel, + queryParameters: { + 'type': type, //type.index + 'oid': oid, + 'rpid': rpid, + 'csrf': await Request.getCsrf(), + }, + ); + if (res.data['code'] == 0) { + return {'status': true, 'msg': '删除成功'}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + // 图片上传 + static Future uploadImage( + {required XFile xFile, String type = 'new_dyn'}) async { + var formData = FormData.fromMap({ + 'file_up': await xFileToMultipartFile(xFile), + 'biz': type, + 'csrf': await Request.getCsrf(), + 'category': 'daily', + }); + var res = await Request().post( + Api.uploadImage, + data: formData, + ); + if (res.data['code'] == 0) { + var data = res.data['data']; + data['img_src'] = data['image_url']; + data['img_width'] = data['image_width']; + data['img_height'] = data['image_height']; + data.remove('image_url'); + data.remove('image_width'); + data.remove('image_height'); + return { + 'status': true, + 'data': data, + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': res.data['message'], + }; + } + } + + static Future xFileToMultipartFile(XFile xFile) async { + var file = File(xFile.path); + var bytes = await file.readAsBytes(); + return MultipartFile.fromBytes(bytes, filename: xFile.name); + } } diff --git a/lib/http/search.dart b/lib/http/search.dart index 00e51497..a61ff406 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -11,7 +11,7 @@ import '../utils/storage.dart'; import 'index.dart'; class SearchHttp { - static Box setting = GStrorage.setting; + static Box setting = GStorage.setting; static Future hotSearchList() async { var res = await Request().get(Api.hotSearchList); if (res.data is String) { diff --git a/lib/http/user.dart b/lib/http/user.dart index f4535905..99888aea 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,10 +1,8 @@ import 'dart:convert'; -import 'dart:developer'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:dio/dio.dart'; import 'package:html/parser.dart'; import 'package:pilipala/models/video/later.dart'; -import '../common/constants.dart'; import '../models/model_hot_video_item.dart'; import '../models/user/fav_detail.dart'; import '../models/user/fav_folder.dart'; @@ -153,7 +151,7 @@ class UserHttp { // 暂停switchStatus传true 否则false var res = await Request().post( Api.pauseHistory, - queryParameters: { + data: { 'switch': switchStatus, 'jsonp': 'jsonp', 'csrf': await Request.getCsrf(), @@ -172,7 +170,7 @@ class UserHttp { static Future clearHistory() async { var res = await Request().post( Api.clearHistory, - queryParameters: { + data: { 'jsonp': 'jsonp', 'csrf': await Request.getCsrf(), }, @@ -190,7 +188,7 @@ class UserHttp { } var res = await Request().post( Api.toViewLater, - queryParameters: data, + data: data, ); if (res.data['code'] == 0) { return {'status': true, 'msg': 'yeah!稍后再看'}; @@ -209,7 +207,7 @@ class UserHttp { params[aid != null ? 'aid' : 'viewed'] = aid ?? true; var res = await Request().post( Api.toViewDel, - queryParameters: params, + data: params, ); if (res.data['code'] == 0) { return {'status': true, 'msg': 'yeah!成功移除'}; @@ -218,30 +216,11 @@ class UserHttp { } } - // 获取用户凭证 失效 - static Future thirdLogin() async { - var res = await Request().get( - 'https://passport.bilibili.com/login/app/third', - data: { - 'appkey': Constants.appKey, - 'api': Constants.thirdApi, - 'sign': Constants.thirdSign, - }, - ); - try { - if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) { - Request().get(res.data['data']['confirm_uri']); - } - } catch (err) { - SmartDialog.showNotify(msg: '获取用户凭证: $err', notifyType: NotifyType.error); - } - } - // 清空稍后再看 static Future toViewClear() async { var res = await Request().post( Api.toViewClear, - queryParameters: { + data: { 'jsonp': 'jsonp', 'csrf': await Request.getCsrf(), }, @@ -257,7 +236,7 @@ class UserHttp { static Future delHistory(kid) async { var res = await Request().post( Api.delHistory, - queryParameters: { + data: { 'kid': kid, 'jsonp': 'jsonp', 'csrf': await Request.getCsrf(), @@ -283,30 +262,6 @@ class UserHttp { return {'status': false, 'msg': res.data['message']}; } } - // // 相互关系查询 - // static Future relationSearch(int mid) async { - // Map params = await WbiSign().makSign({ - // 'mid': mid, - // 'token': '', - // 'platform': 'web', - // 'web_location': 1550101, - // }); - // var res = await Request().get( - // Api.relationSearch, - // data: { - // 'mid': mid, - // 'w_rid': params['w_rid'], - // 'wts': params['wts'], - // }, - // ); - // if (res.data['code'] == 0) { - // // relation 主动状态 - // // 被动状态 - // return {'status': true, 'data': res.data['data']}; - // } else { - // return {'status': false, 'msg': res.data['message']}; - // } - // } // 搜索历史记录 static Future searchHistory( @@ -406,7 +361,7 @@ class UserHttp { static Future cancelSub({required int seasonId}) async { var res = await Request().post( Api.cancelSub, - queryParameters: { + data: { 'platform': 'web', 'season_id': seasonId, 'csrf': await Request.getCsrf(), @@ -423,7 +378,7 @@ class UserHttp { static Future delFavFolder({required int mediaIds}) async { var res = await Request().post( Api.delFavFolder, - queryParameters: { + data: { 'media_ids': mediaIds, 'platform': 'web', 'csrf': await Request.getCsrf(), @@ -436,31 +391,6 @@ class UserHttp { } } - // 稍后再看播放全部 - // 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'