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/assets/images/coin.png b/assets/images/coin.png index bc2952a7..afca87b2 100644 Binary files a/assets/images/coin.png and b/assets/images/coin.png differ diff --git a/change_log/1.0.24.0626.md b/change_log/1.0.24.0626.md new file mode 100644 index 00000000..d9a8892f --- /dev/null +++ b/change_log/1.0.24.0626.md @@ -0,0 +1,23 @@ +## 1.0.24 + +### 功能 ++ 私信功能 ++ 回复我的、收到的赞查看 ++ 新的登录方式 ++ 全屏选集 ++ 一键三连 ++ 按分区搜索 + +### 优化 ++ 页面跳转动画 ++ 评论区跳转 + +### 修复 ++ 音画不同步问题 ++ 分集字幕未同步 ++ 多语言字幕 ++ 弹幕设置未生效 ++ + + +问题反馈、功能建议请查看「关于」页面。 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/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 70693e4a..dfa078c3 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ import UIKit import Flutter +import AVFoundation @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -8,6 +9,14 @@ import Flutter didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) + + // 设置音频会话类别,确保在静音模式下播放音频 + do { + try AVAudioSession.sharedInstance().setCategory(.playback, options: [.duckOthers]) + } catch { + print("Failed to set audio session category: \(error)") + } + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } -} +} \ No newline at end of file diff --git a/lib/common/widgets/badge.dart b/lib/common/widgets/badge.dart index a8f2fc67..1e518f39 100644 --- a/lib/common/widgets/badge.dart +++ b/lib/common/widgets/badge.dart @@ -66,7 +66,7 @@ class PBadge extends StatelessWidget { border: Border.all(color: borderColor), ), child: Text( - text!, + text ?? '', style: TextStyle(fontSize: fs ?? fontSize, color: color), ), ); diff --git a/lib/common/widgets/html_render.dart b/lib/common/widgets/html_render.dart index bf58d78c..b2aa75ff 100644 --- a/lib/common/widgets/html_render.dart +++ b/lib/common/widgets/html_render.dart @@ -1,7 +1,9 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:get/get.dart'; -import 'network_img_layer.dart'; +import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart'; +import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart'; +import 'package:pilipala/utils/highlight.dart'; // ignore: must_be_immutable class HtmlRender extends StatelessWidget { @@ -22,6 +24,20 @@ class HtmlRender extends StatelessWidget { data: htmlContent, onLinkTap: (String? url, Map buildContext, attributes) {}, extensions: [ + TagExtension( + tagsToExtend: {'pre'}, + builder: (ExtensionContext extensionContext) { + final Map attributes = extensionContext.attributes; + final String lang = attributes['data-lang'] as String; + final String code = attributes['codecontent'] as String; + List selectedLanguages = [lang.split('@').first]; + TextSpan? result = highlightExistingText(code, selectedLanguages); + if (result == null) { + return const Center(child: Text('代码块渲染失败')); + } + return SelectableText.rich(result); + }, + ), TagExtension( tagsToExtend: {'img'}, builder: (ExtensionContext extensionContext) { @@ -44,20 +60,52 @@ class HtmlRender extends StatelessWidget { if (isMall) { return const SizedBox(); } - // bool inTable = - // extensionContext.element!.previousElementSibling == null || - // extensionContext.element!.nextElementSibling == null; - // imgUrl = Utils().imageUrl(imgUrl!); - // return Image.network( - // imgUrl, - // width: isEmote ? 22 : null, - // height: isEmote ? 22 : null, - // ); - return NetworkImgLayer( - width: isEmote ? 22 : Get.size.width - 24, - height: isEmote ? 22 : 200, - src: imgUrl, + return InkWell( + onTap: () { + Navigator.of(context).push( + HeroDialogRoute( + builder: (BuildContext context) => + InteractiveviewerGallery( + sources: imgList ?? [imgUrl], + initIndex: imgList?.indexOf(imgUrl) ?? 0, + itemBuilder: ( + BuildContext context, + int index, + bool isFocus, + bool enablePageView, + ) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (enablePageView) { + Navigator.of(context).pop(); + } + }, + child: Center( + child: Hero( + tag: imgList?[index] ?? imgUrl, + child: CachedNetworkImage( + fadeInDuration: + const Duration(milliseconds: 0), + imageUrl: imgList?[index] ?? imgUrl, + fit: BoxFit.contain, + ), + ), + ), + ); + }, + onPageChanged: (int pageIndex) {}, + ), + ), + ); + }, + child: CachedNetworkImage(imageUrl: imgUrl), ); + // return NetworkImgLayer( + // width: isEmote ? 22 : Get.size.width - 24, + // height: isEmote ? 22 : 200, + // src: imgUrl, + // ); } catch (err) { return const SizedBox(); } @@ -66,7 +114,7 @@ class HtmlRender extends StatelessWidget { ], style: { 'html': Style( - fontSize: FontSize.medium, + fontSize: FontSize.large, lineHeight: LineHeight.percent(140), ), 'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero), @@ -78,7 +126,7 @@ class HtmlRender extends StatelessWidget { margin: Margins.only(bottom: 10), ), 'span': Style( - fontSize: FontSize.medium, + fontSize: FontSize.large, height: Height(1.65), ), 'div': Style(height: Height.auto()), diff --git a/lib/common/widgets/http_error.dart b/lib/common/widgets/http_error.dart index cbc6659b..0381319e 100644 --- a/lib/common/widgets/http_error.dart +++ b/lib/common/widgets/http_error.dart @@ -2,12 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class HttpError extends StatelessWidget { - const HttpError( - {required this.errMsg, required this.fn, this.btnText, super.key}); + const HttpError({ + required this.errMsg, + required this.fn, + this.btnText, + this.isShowBtn = true, + super.key, + }); final String? errMsg; final Function()? fn; final String? btnText; + final bool isShowBtn; @override Widget build(BuildContext context) { @@ -29,20 +35,22 @@ class HttpError extends StatelessWidget { style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 20), - FilledButton.tonal( - onPressed: () { - fn!(); - }, - style: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith((states) { - return Theme.of(context).colorScheme.primary.withAlpha(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), + ), ), - child: Text( - btnText ?? '点击重试', - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - ), ], ), ), diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index d2772478..fbedfbba 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/utils/extension.dart'; -import 'package:pilipala/utils/global_data.dart'; +import 'package:pilipala/utils/global_data_cache.dart'; import '../../utils/storage.dart'; import '../constants.dart'; @@ -33,7 +33,11 @@ class NetworkImgLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final int defaultImgQuality = GlobalData().imgQuality; + int defaultImgQuality = 10; + try { + defaultImgQuality = GlobalDataCache().imgQuality; + } catch (_) {} + if (src == '' || src == null) { return placeholder(context); } diff --git a/lib/common/widgets/pull_to_refresh_header.dart b/lib/common/widgets/pull_to_refresh_header.dart deleted file mode 100644 index 46db5138..00000000 --- a/lib/common/widgets/pull_to_refresh_header.dart +++ /dev/null @@ -1,130 +0,0 @@ -// ignore_for_file: depend_on_referenced_packages - -import 'dart:math'; -import 'dart:ui' as ui show Image; - -import 'package:extended_image/extended_image.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; - -double get maxDragOffset => 100; -double hideHeight = maxDragOffset / 2.3; -double refreshHeight = maxDragOffset / 1.5; - -class PullToRefreshHeader extends StatelessWidget { - const PullToRefreshHeader( - this.info, - this.lastRefreshTime, { - this.color, - super.key, - }); - - final PullToRefreshScrollNotificationInfo? info; - final DateTime? lastRefreshTime; - final Color? color; - - @override - Widget build(BuildContext context) { - final PullToRefreshScrollNotificationInfo? infos = info; - if (infos == null) { - return const SizedBox(); - } - String text = ''; - if (infos.mode == PullToRefreshIndicatorMode.armed) { - text = 'Release to refresh'; - } else if (infos.mode == PullToRefreshIndicatorMode.refresh || - infos.mode == PullToRefreshIndicatorMode.snap) { - text = 'Loading...'; - } else if (infos.mode == PullToRefreshIndicatorMode.done) { - text = 'Refresh completed.'; - } else if (infos.mode == PullToRefreshIndicatorMode.drag) { - text = 'Pull to refresh'; - } else if (infos.mode == PullToRefreshIndicatorMode.canceled) { - text = 'Cancel refresh'; - } - - final TextStyle ts = const TextStyle( - color: Colors.grey, - ).copyWith(fontSize: 14); - - final double dragOffset = info?.dragOffset ?? 0.0; - - final DateTime time = lastRefreshTime ?? DateTime.now(); - final double top = -hideHeight + dragOffset; - return Container( - height: dragOffset, - color: color ?? Colors.transparent, - // padding: EdgeInsets.only(top: dragOffset / 3), - // padding: EdgeInsets.only(bottom: 5.0), - child: Stack( - children: [ - Positioned( - left: 0.0, - right: 0.0, - top: top, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 12.0), - child: RefreshImage(top, null), - ), - ), - Column( - children: [ - Text(text, style: ts), - Text( - 'Last updated:${DateFormat('yyyy-MM-dd hh:mm').format(time)}', - style: ts.copyWith(fontSize: 14), - ) - ], - ), - const Spacer(), - ], - ), - ) - ], - ), - ); - } -} - -class RefreshImage extends StatelessWidget { - const RefreshImage(this.top, Key? key) : super(key: key); - - final double top; - - @override - Widget build(BuildContext context) { - const double imageSize = 30; - return ExtendedImage.asset( - 'assets/flutterCandies_grey.png', - width: imageSize, - height: imageSize, - afterPaintImage: (Canvas canvas, Rect rect, ui.Image image, Paint paint) { - final double imageHeight = image.height.toDouble(); - final double imageWidth = image.width.toDouble(); - final Size size = rect.size; - final double y = - (1 - min(top / (refreshHeight - hideHeight), 1)) * imageHeight; - - canvas.drawImageRect( - image, - Rect.fromLTWH(0.0, y, imageWidth, imageHeight - y), - Rect.fromLTWH(rect.left, rect.top + y / imageHeight * size.height, - size.width, (imageHeight - y) / imageHeight * size.height), - Paint() - ..colorFilter = - const ColorFilter.mode(Color(0xFFea5504), BlendMode.srcIn) - ..isAntiAlias = false - ..filterQuality = FilterQuality.low, - ); - - //canvas.restore(); - }, - ); - } -} diff --git a/lib/common/widgets/stat/danmu.dart b/lib/common/widgets/stat/danmu.dart index 511839a0..9ea05301 100644 --- a/lib/common/widgets/stat/danmu.dart +++ b/lib/common/widgets/stat/danmu.dart @@ -6,7 +6,7 @@ class StatDanMu extends StatelessWidget { final dynamic danmu; final String? size; - const StatDanMu({Key? key, this.theme, this.danmu, this.size}) + const StatDanMu({Key? key, this.theme = 'gray', this.danmu, this.size}) : super(key: key); @override @@ -17,21 +17,46 @@ class StatDanMu extends StatelessWidget { 'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8), }; Color color = colorObject[theme]!; + return StatIconText( + icon: Icons.subtitles_outlined, + text: Utils.numFormat(danmu!), + color: color, + size: size, + ); + } +} + +class StatIconText extends StatelessWidget { + final IconData icon; + final String text; + final Color color; + final String? size; + + const StatIconText({ + Key? key, + required this.icon, + required this.text, + required this.color, + this.size, + }) : super(key: key); + + @override + Widget build(BuildContext context) { return Row( children: [ Icon( - Icons.subtitles_outlined, + icon, size: 14, color: color, ), const SizedBox(width: 2), Text( - Utils.numFormat(danmu!), + text, style: TextStyle( fontSize: size == 'medium' ? 12 : 11, color: color, ), - ) + ), ], ); } diff --git a/lib/common/widgets/stat/view.dart b/lib/common/widgets/stat/view.dart index 5359c979..85bec816 100644 --- a/lib/common/widgets/stat/view.dart +++ b/lib/common/widgets/stat/view.dart @@ -6,8 +6,12 @@ class StatView extends StatelessWidget { final dynamic view; final String? size; - const StatView({Key? key, this.theme, this.view, this.size}) - : super(key: key); + const StatView({ + Key? key, + this.theme = 'gray', + this.view, + this.size, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -17,16 +21,41 @@ class StatView extends StatelessWidget { 'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8), }; Color color = colorObject[theme]!; + return StatIconText( + icon: Icons.play_circle_outlined, + text: Utils.numFormat(view!), + color: color, + size: size, + ); + } +} + +class StatIconText extends StatelessWidget { + final IconData icon; + final String text; + final Color color; + final String? size; + + const StatIconText({ + Key? key, + required this.icon, + required this.text, + required this.color, + this.size, + }) : super(key: key); + + @override + Widget build(BuildContext context) { return Row( children: [ Icon( - Icons.play_circle_outlined, + icon, size: 13, color: color, ), const SizedBox(width: 2), Text( - Utils.numFormat(view!), + text, style: TextStyle( fontSize: size == 'medium' ? 12 : 11, color: color, diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 1265477f..78c4ba87 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -266,17 +266,11 @@ class VideoContent extends StatelessWidget { Row( children: [ if (showView) ...[ - StatView( - theme: 'gray', - view: videoItem.stat.view as int, - ), + StatView(view: videoItem.stat.view as int), const SizedBox(width: 8), ], if (showDanmaku) - StatDanMu( - theme: 'gray', - danmu: videoItem.stat.danmaku as int, - ), + StatDanMu(danmu: videoItem.stat.danmaku as int), const Spacer(), if (source == 'normal') SizedBox( diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 14476cdf..378c9f75 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -60,17 +60,13 @@ class VideoCardV extends StatelessWidget { // 动态 case 'picture': try { - String dynamicType = 'picture'; String uri = videoItem.uri; - String id = ''; if (videoItem.uri.startsWith('bilibili://article/')) { // https://www.bilibili.com/read/cv27063554 - dynamicType = 'read'; RegExp regex = RegExp(r'\d+'); Match match = regex.firstMatch(videoItem.uri)!; String matchedNumber = match.group(0)!; videoItem.param = int.parse(matchedNumber); - id = 'cv${videoItem.param}'; } if (uri.startsWith('http')) { String path = Uri.parse(uri).path; @@ -88,11 +84,10 @@ class VideoCardV extends StatelessWidget { return; } } - Get.toNamed('/htmlRender', parameters: { - 'url': uri, + Get.toNamed('/read', parameters: { 'title': videoItem.title, - 'id': id, - 'dynamicType': dynamicType + 'id': videoItem.param, + 'articleType': 'read' }); } catch (err) { SmartDialog.showToast(err.toString()); @@ -215,9 +210,8 @@ class VideoContent extends StatelessWidget { children: [ if (videoItem.goto == 'bangumi') _buildBadge(videoItem.bangumiBadge, 'line', 9), - if (videoItem.rcmdReason?.content != null && - videoItem.rcmdReason.content != '') - _buildBadge(videoItem.rcmdReason.content, 'color'), + if (videoItem.rcmdReason != null) + _buildBadge(videoItem.rcmdReason, 'color'), if (videoItem.goto == 'picture') _buildBadge('动态', 'line', 9), if (videoItem.isFollowed == 1) _buildBadge('已关注', 'color'), Expanded( @@ -288,9 +282,9 @@ class VideoStat extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - StatView(theme: 'gray', view: videoItem.stat.view), + StatView(view: videoItem.stat.view), const SizedBox(width: 8), - StatDanMu(theme: 'gray', danmu: videoItem.stat.danmu), + StatDanMu(danmu: videoItem.stat.danmu), if (videoItem is RecVideoItemModel) ...[ crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8), RichText( diff --git a/lib/http/api.dart b/lib/http/api.dart index f20b8bcf..d7d60160 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -487,6 +487,8 @@ class Api { static const getSeasonDetailApi = '/x/polymer/web-space/seasons_archives_list'; + static const getSeriesDetailApi = '/x/series/archives'; + /// 获取未读动态数 static const getUnreadDynamic = '/x/web-interface/dynamic/entrance'; @@ -548,4 +550,49 @@ class Api { /// 收到的赞 static const String messageLikeAPi = '/x/msgfeed/like'; + + /// 系统通知 + 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'; + + /// 编辑收藏夹 + static const String editFavFolder = '/x/v3/fav/folder/edit'; + + /// 新建收藏夹 + static const String addFavFolder = '/x/v3/fav/folder/add'; + + /// 直播间弹幕信息 + static const String getDanmuInfo = + '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getDanmuInfo'; + + /// 直播间发送弹幕 + static const String sendLiveMsg = '${HttpString.liveBaseUrl}/msg/send'; + + /// 我的关注 - 正在直播 + static const String getFollowingLive = + '${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following'; + + /// 稍后再看&收藏夹视频列表 + 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 replyDel = '/x/v2/reply/del'; } 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/constants.dart b/lib/http/constants.dart index 3d749ee8..b734c279 100644 --- a/lib/http/constants.dart +++ b/lib/http/constants.dart @@ -5,6 +5,8 @@ class HttpString { static const String appBaseUrl = 'https://app.bilibili.com'; static const String liveBaseUrl = 'https://api.live.bilibili.com'; 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 List validateStatusCodes = [ 302, 304, diff --git a/lib/http/danmaku.dart b/lib/http/danmaku.dart index 0b108755..2ed2a415 100644 --- a/lib/http/danmaku.dart +++ b/lib/http/danmaku.dart @@ -67,9 +67,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..5ba5675e 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(), @@ -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 new file mode 100644 index 00000000..69577e7e --- /dev/null +++ b/lib/http/fav.dart @@ -0,0 +1,67 @@ +import 'index.dart'; + +class FavHttp { + /// 编辑收藏夹 + static Future editFolder({ + required String title, + required String intro, + required String mediaId, + String? cover, + int? privacy, + }) async { + var res = await Request().post( + Api.editFavFolder, + data: { + 'title': title, + 'intro': intro, + 'media_id': mediaId, + 'cover': cover ?? '', + 'privacy': privacy ?? 0, + '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 addFolder({ + required String title, + required String intro, + String? cover, + int? privacy, + }) async { + var res = await Request().post( + Api.addFavFolder, + data: { + 'title': title, + 'intro': intro, + 'cover': cover ?? '', + 'privacy': privacy ?? 0, + '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'], + }; + } + } +} diff --git a/lib/http/init.dart b/lib/http/init.dart index a0b36369..abe8d019 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -27,11 +27,14 @@ class Request { late bool enableSystemProxy; late String systemProxyHost; late String systemProxyPort; - static final RegExp spmPrefixExp = RegExp(r''); + static final RegExp spmPrefixExp = + RegExp(r''); + static String? buvid; /// 设置cookie static setCookie() async { Box userInfoCache = GStrorage.userInfo; + Box setting = GStrorage.setting; final String cookiePath = await Utils.getCookiePath(); final PersistCookieJar cookieJar = PersistCookieJar( ignoreExpires: true, @@ -54,7 +57,11 @@ class Request { } } setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null); - + String baseUrlType = 'default'; + if (setting.get(SettingBoxKey.enableGATMode, defaultValue: false)) { + baseUrlType = 'bangumi'; + } + setBaseUrl(type: baseUrlType); try { await buvidActivate(); } catch (e) { @@ -64,6 +71,7 @@ class Request { final String cookieString = cookie .map((Cookie cookie) => '${cookie.name}=${cookie.value}') .join('; '); + dio.options.headers['cookie'] = cookieString; } @@ -78,6 +86,30 @@ class Request { return token; } + static Future getBuvid() async { + if (buvid != null) { + return buvid!; + } + + final List cookies = await cookieManager.cookieJar + .loadForRequest(Uri.parse(HttpString.baseUrl)); + buvid = cookies.firstWhere((cookie) => cookie.name == 'buvid3').value; + if (buvid == null) { + try { + var result = await Request().get( + "${HttpString.apiBaseUrl}/x/frontend/finger/spi", + ); + buvid = result["data"]["b_3"].toString(); + } catch (e) { + // 处理请求错误 + buvid = ''; + print("Error fetching buvid: $e"); + } + } + + return buvid!; + } + static setOptionsHeaders(userInfo, bool status) { if (status) { dio.options.headers['x-bili-mid'] = userInfo.mid.toString(); @@ -95,11 +127,10 @@ class Request { 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)) - ); + 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, @@ -110,11 +141,9 @@ class Request { }, }); - await Request().post( - Api.activateBuvidApi, - data: {'payload': jsonData}, - options: Options(contentType: 'application/json') - ); + await Request().post(Api.activateBuvidApi, + data: {'payload': jsonData}, + options: Options(contentType: 'application/json')); } /* @@ -188,12 +217,13 @@ class Request { /* * get请求 */ - get(url, {data, options, cancelToken, extra}) async { + get(url, {data, Options? options, cancelToken, extra}) async { Response response; - final Options options = Options(); + options ??= Options(); // 如果 options 为 null,则初始化一个新的 Options 对象 ResponseType resType = ResponseType.json; + if (extra != null) { - resType = extra!['resType'] ?? ResponseType.json; + resType = extra['resType'] ?? ResponseType.json; if (extra['ua'] != null) { options.headers = {'user-agent': headerUa(type: extra['ua'])}; } @@ -209,14 +239,11 @@ 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; } } @@ -231,7 +258,8 @@ class Request { url, data: data, queryParameters: queryParameters, - options: options, + options: + options ?? Options(contentType: Headers.formUrlEncodedContentType), cancelToken: cancelToken, ); // print('post success: ${response.data}'); @@ -294,4 +322,17 @@ class Request { } return headerUa; } + + static setBaseUrl({String type = 'default'}) { + switch (type) { + case 'default': + dio.options.baseUrl = HttpString.apiBaseUrl; + break; + case 'bangumi': + dio.options.baseUrl = HttpString.bangumiBaseUrl; + break; + default: + dio.options.baseUrl = HttpString.apiBaseUrl; + } + } } diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart index 10b5aee5..259a3bf9 100644 --- a/lib/http/interceptor.dart +++ b/lib/http/interceptor.dart @@ -46,7 +46,8 @@ class ApiInterceptor extends Interceptor { // 处理网络请求错误 // handler.next(err); String url = err.requestOptions.uri.toString(); - if (!url.contains('heartbeat')) { + final excludedPatterns = RegExp(r'heartbeat|seg\.so|online/total'); + if (!excludedPatterns.hasMatch(url)) { SmartDialog.showToast( await dioError(err), displayType: SmartToastType.onlyRefresh, diff --git a/lib/http/live.dart b/lib/http/live.dart index e624120e..259f86fc 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -1,3 +1,5 @@ +import 'package:pilipala/models/live/follow.dart'; + import '../models/live/item.dart'; import '../models/live/room_info.dart'; import '../models/live/room_info_h5.dart'; @@ -65,4 +67,96 @@ class LiveHttp { }; } } + + // 获取弹幕信息 + static Future liveDanmakuInfo({roomId}) async { + var res = await Request().get(Api.getDanmuInfo, data: { + 'id': roomId, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 发送弹幕 + static Future sendDanmaku({roomId, msg}) async { + 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, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 我的关注 正在直播 + static Future liveFollowing({int? pn, int? ps}) async { + var res = await Request().get(Api.getFollowingLive, data: { + 'page': pn, + 'page_size': ps, + 'platform': 'web', + 'ignoreRecord': 1, + 'hit_ab': true, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': LiveFollowingModel.fromJson(res.data['data']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 直播历史记录 + 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 1af0f9a4..fc99c987 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -1,5 +1,10 @@ +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/utils/global_data_cache.dart'; import '../common/constants.dart'; import '../models/dynamics/result.dart'; import '../models/follow/result.dart'; @@ -15,14 +20,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, @@ -95,7 +106,14 @@ class MemberHttp { 'dm_img_str': dmImgStr.substring(0, dmImgStr.length - 2), 'dm_cover_img_str': dmCoverImgStr.substring(0, dmCoverImgStr.length - 2), 'dm_img_inter': '{"ds":[],"wh":[0,0,0],"of":[0,0,0]}', + ...order == 'charge' + ? { + 'order': 'pubdate', + 'special_type': 'charging', + } + : {} }); + var res = await Request().get( Api.memberArchive, data: params, @@ -187,13 +205,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 { @@ -328,7 +348,9 @@ class MemberHttp { if (res.data['code'] == 0) { return { 'status': true, - 'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists']) + 'data': res.data['data']['list'] + .map((e) => MemberLikeDataModel.fromJson(e)) + .toList(), }; } else { return { @@ -409,11 +431,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) { @@ -510,4 +535,96 @@ class MemberHttp { }; } } + + static Future getSeriesDetail({ + required int mid, + required int currentMid, + required int seriesId, + required int pn, + }) async { + var res = await Request().get( + Api.getSeriesDetailApi, + data: { + 'mid': mid, + 'series_id': seriesId, + 'only_normal': true, + 'sort': 'desc', + 'pn': pn, + 'ps': 30, + 'current_mid': currentMid, + }, + ); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': MemberSeasonsDataModel.fromJson(res.data['data']) + }; + } catch (err) { + print(err); + } + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + 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 7c168230..6426a6f2 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'dart:math'; -import 'package:dio/dio.dart'; import 'package:pilipala/models/msg/like.dart'; import 'package:pilipala/models/msg/reply.dart'; +import 'package:pilipala/models/msg/system.dart'; import '../models/msg/account.dart'; import '../models/msg/session.dart'; import '../utils/wbi_sign.dart'; @@ -149,7 +149,7 @@ class MsgHttp { 'msg[msg_status]': 0, 'msg[content]': jsonEncode(content), 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, - 'msg[new_face_version]': 0, + 'msg[new_face_version]': 1, 'msg[dev_id]': getDevId(), 'from_firework': 0, 'build': 0, @@ -157,9 +157,6 @@ class MsgHttp { 'csrf_token': csrf, 'csrf': csrf, }, - options: Options( - contentType: Headers.formUrlEncodedContentType, - ), ); if (res.data['code'] == 0) { return { @@ -287,4 +284,69 @@ class MsgHttp { return {'status': false, 'date': [], 'msg': res.data['message']}; } } + + static Future messageSystem() async { + var res = await Request().get(Api.messageSystemAPi, 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 systemMarkRead(int cursor) async { + String csrf = await Request.getCsrf(); + var res = await Request().get(Api.systemMarkRead, data: { + 'csrf': csrf, + 'cursor': cursor, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + }; + } else { + return { + 'status': false, + 'msg': res.data['message'], + }; + } + } + + 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']}; + } + } } diff --git a/lib/http/read.dart b/lib/http/read.dart new file mode 100644 index 00000000..68e72e59 --- /dev/null +++ b/lib/http/read.dart @@ -0,0 +1,116 @@ +import 'dart:convert'; +import 'package:html/parser.dart'; +import 'package:pilipala/models/read/opus.dart'; +import 'package:pilipala/models/read/read.dart'; +import 'package:pilipala/utils/wbi_sign.dart'; +import 'index.dart'; + +class ReadHttp { + static List extractScriptContents(String htmlContent) { + RegExp scriptRegExp = RegExp(r'