diff --git a/lib/common/widgets/dialog/report.dart b/lib/common/widgets/dialog/report.dart index 413c8359e..8f2504d18 100644 --- a/lib/common/widgets/dialog/report.dart +++ b/lib/common/widgets/dialog/report.dart @@ -260,7 +260,7 @@ class ReportOptions { 4: '辱骂引战', 5: '政治敏感', 6: '青少年不良信息', - 7: '其他 ', // avoid show form + 7: '其他', // avoid show form }, }; } diff --git a/lib/common/widgets/image/flutter_svg_provider.dart b/lib/common/widgets/image/flutter_svg_provider.dart deleted file mode 100644 index dabe06cd5..000000000 --- a/lib/common/widgets/image/flutter_svg_provider.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show rootBundle; -import 'package:flutter_svg/flutter_svg.dart'; - -/// https://github.com/yang-f/flutter_svg_provider - -/// Rasterizes given svg picture for displaying in [Image] widget: -/// -/// ```dart -/// Image( -/// width: 32, -/// height: 32, -/// image: Svg('assets/my_icon.svg'), -/// ) -/// ``` -class SvgImageProvider extends ImageProvider { - /// Path to svg file or asset - final String path; - - /// Size in logical pixels to render. - /// Useful for [DecorationImage]. - /// If not specified, will use size from [Image]. - /// If [Image] not specifies size too, will use default size 100x100. - final Size? size; - - /// Color to tint the SVG - final Color? color; - - /// Image scale. - final double? scale; - - /// Width and height can also be specified from [Image] constructor. - /// Default size is 100x100 logical pixels. - /// Different size can be specified in [Image] parameters - const SvgImageProvider( - this.path, { - this.size, - this.scale, - this.color, - }); - - @override - Future obtainKey(ImageConfiguration configuration) { - final Color color = this.color ?? Colors.transparent; - final double scale = this.scale ?? configuration.devicePixelRatio ?? 1.0; - final double logicWidth = size?.width ?? configuration.size?.width ?? 100; - final double logicHeight = - size?.height ?? configuration.size?.height ?? 100; - - return SynchronousFuture( - SvgImageKey( - path: path, - scale: scale, - color: color, - pixelWidth: (logicWidth * scale).round(), - pixelHeight: (logicHeight * scale).round(), - ), - ); - } - - @override - ImageStreamCompleter loadImage(SvgImageKey key, ImageDecoderCallback decode) { - return OneFrameImageStreamCompleter( - _loadAsync(key, getFilterColor(color)), - ); - } - - static Future _loadAsync(SvgImageKey key, Color color) async { - final rawSvg = await rootBundle.loadString(key.path); - final pictureInfo = await vg.loadPicture( - SvgStringLoader(rawSvg, theme: SvgTheme(currentColor: color)), - null, - clipViewbox: false, - ); - - try { - final image = pictureInfo.picture.toImageSync( - pictureInfo.size.width.round(), - pictureInfo.size.height.round(), - ); - return ImageInfo(image: image); - } finally { - // Dispose of the Picture to release resources - pictureInfo.picture.dispose(); - } - } - - // Note: == and hashCode not overrided as changes in properties - // (width, height and scale) are not observable from the here. - // [SvgImageKey] instances will be compared instead. - @override - String toString() => '$runtimeType(${describeIdentity(path)})'; - - // Running on web with Colors.transparent may throws the exception `Expected a value of type 'SkDeletable', but got one of type 'Null'`. - static Color getFilterColor(Color? color) { - if (kIsWeb && color == Colors.transparent) { - return const Color(0x01ffffff); - } else { - return color ?? Colors.transparent; - } - } -} - -@immutable -class SvgImageKey { - const SvgImageKey({ - required this.path, - required this.pixelWidth, - required this.pixelHeight, - required this.scale, - this.color, - }); - - /// Path to svg asset. - final String path; - - /// Width in physical pixels. - /// Used when raterizing. - final int pixelWidth; - - /// Height in physical pixels. - /// Used when raterizing. - final int pixelHeight; - - /// Color to tint the SVG - final Color? color; - - /// Used to calculate logical size from physical, i.e. - /// logicalWidth = [pixelWidth] / [scale], - /// logicalHeight = [pixelHeight] / [scale]. - /// Should be equal to [MediaQueryData.devicePixelRatio]. - final double scale; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) { - return false; - } - - return other is SvgImageKey && - other.path == path && - other.pixelWidth == pixelWidth && - other.pixelHeight == pixelHeight && - other.scale == scale && - other.color == color; - } - - @override - int get hashCode => Object.hash( - path, - pixelWidth, - pixelHeight, - scale, - color, - ); -} diff --git a/lib/http/live.dart b/lib/http/live.dart index 8e438e031..bceca1459 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -667,13 +667,16 @@ abstract final class LiveHttp { } } - static Future> liveDmReport({ - required Object roomId, - required int mid, + static Future> liveDmReport({ + required int roomId, + required Object mid, required String msg, required String reason, required int reasonId, - required String id, + required int dmType, + required Object idStr, + required Object ts, + required Object sign, }) async { final csrf = Accounts.main.csrf; final data = { @@ -682,25 +685,21 @@ abstract final class LiveHttp { 'tuid': mid, 'msg': msg, 'reason': reason, - 'sign': '', + 'ts': ts, + 'sign': sign, 'reason_id': reasonId, 'token': '', - 'dm_type': '0', - 'id_str': id, + 'dm_type': dmType, + 'id_str': idStr, 'csrf_token': csrf, 'csrf': csrf, 'visit_id': '', - 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000, }; final res = await Request().post( Api.liveDmReport, data: data, options: Options(contentType: Headers.formUrlEncodedContentType), ); - if (res.data['code'] == 0) { - return const Success(null); // {"id": num} - } else { - return Error(res.data['message']); - } + return res.data as Map; } } diff --git a/lib/pages/danmaku/dnamaku_model.dart b/lib/pages/danmaku/dnamaku_model.dart index 991ff05aa..80f3ec463 100644 --- a/lib/pages/danmaku/dnamaku_model.dart +++ b/lib/pages/danmaku/dnamaku_model.dart @@ -21,7 +21,17 @@ class LiveDanmaku extends DanmakuExtra { final Object id; @override final Object mid; - final String uname; - const LiveDanmaku({required this.id, required this.mid, required this.uname}); + final int dmType; + + final Object ts; + final Object ct; + + const LiveDanmaku({ + required this.id, + required this.mid, + required this.dmType, + required this.ts, + required this.ct, + }); } diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 99cc41c7c..f57a319d0 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -345,16 +345,19 @@ class LiveRoomController extends GetxController { content['extra'], ); final user = content['user']; + // final midHash = first[7]; final uid = user['uid']; + final name = user['base']['name']; + final msg = info[1]; BaseEmote? uemote; if (first[13] case Map map) { uemote = BaseEmote.fromJson(map); } messages.add( DanmakuMsg( - name: user['base']['name'], + name: name, uid: uid, - text: info[1], + text: msg, emots: (extra['emots'] as Map?)?.map( (k, v) => MapEntry(k, BaseEmote.fromJson(v)), ), @@ -363,9 +366,10 @@ class LiveRoomController extends GetxController { ); if (plPlayerController.showDanmaku) { - plPlayerController.danmakuController?.addDanmaku( + final checkInfo = info[9]; + danmakuController?.addDanmaku( DanmakuContentItem( - extra['content'], + msg, color: plPlayerController.blockColorful ? Colors.white : DmUtils.decimalToColor(extra['color']), @@ -374,7 +378,9 @@ class LiveRoomController extends GetxController { extra: LiveDanmaku( id: extra['id_str'], mid: uid, - uname: user['base']['name'], + dmType: extra['dm_type'], + ts: checkInfo['ts'], + ct: checkInfo['ct'], ), ), ); diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index c8cf54d51..03d4572b8 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -11,6 +11,7 @@ import 'package:PiliPlus/common/widgets/marquee.dart'; import 'package:PiliPlus/http/danmaku.dart'; import 'package:PiliPlus/http/danmaku_block.dart'; import 'package:PiliPlus/http/init.dart'; +import 'package:PiliPlus/http/live.dart'; import 'package:PiliPlus/models/common/super_resolution_type.dart'; import 'package:PiliPlus/models/common/video/audio_quality.dart'; import 'package:PiliPlus/models/common/video/cdn_type.dart'; @@ -105,10 +106,10 @@ class HeaderControl extends StatefulWidget { } static Future reportDanmaku( - VideoDanmaku extra, - BuildContext context, - PlPlayerController ctr, - ) { + BuildContext context, { + required VideoDanmaku extra, + required PlPlayerController ctr, + }) { if (Accounts.main.isLogin) { return autoWrapReportDialog( context, @@ -132,7 +133,51 @@ class HeaderControl extends StatefulWidget { reason: reasonType == 0 ? 11 : reasonType, cid: ctr.cid!, id: extra.id, - content: reasonDesc, + content: reasonType == 0 ? reasonDesc : null, + ); + }, + ); + } else { + return SmartDialog.showToast('请先登录'); + } + } + + static Future reportLiveDanmaku( + BuildContext context, { + required int roomId, + required String msg, + required LiveDanmaku extra, + required PlPlayerController ctr, + }) { + if (Accounts.main.isLogin) { + return autoWrapReportDialog( + context, + ReportOptions.liveDanmakuReport, + (reasonType, reasonDesc, banUid) { + // if (banUid) { + // final filter = ctr.filters; + // if (filter.dmUid.add(extra.mid)) { + // filter.count++; + // GStorage.localCache.put( + // LocalCacheKey.danmakuFilterRules, + // filter, + // ); + // } + // DanmakuFilterHttp.danmakuFilterAdd( + // filter: extra.mid, + // type: 2, + // ); + // } + return LiveHttp.liveDmReport( + roomId: roomId, + mid: extra.mid, + msg: msg, + reason: ReportOptions.liveDanmakuReport['']![reasonType]!, + reasonId: reasonType, + dmType: extra.dmType, + idStr: extra.id, + ts: extra.ts, + sign: extra.ct, ); }, ); @@ -2040,9 +2085,9 @@ class HeaderControlState extends State { else iconButton( onPressed: () => HeaderControl.reportDanmaku( - extra, context, - plPlayerController, + extra: extra, + ctr: plPlayerController, ), icon: const Icon(Icons.report_problem_outlined), ), @@ -2326,7 +2371,7 @@ class HeaderControlState extends State { ) : const SizedBox.shrink(), ), - if (isFSOrPip) ...[ + if (isFSOrPip || Utils.isDesktop) ...[ SizedBox( width: 42, height: 34, diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 065a88477..1adc068bb 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -7,7 +7,6 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/gesture/immediate_tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/gesture/mouse_interactive_viewer.dart'; -import 'package:PiliPlus/common/widgets/image/flutter_svg_provider.dart'; import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.dart'; @@ -26,6 +25,8 @@ import 'package:PiliPlus/models_new/video/video_detail/ugc_season.dart'; import 'package:PiliPlus/models_new/video/video_shot/data.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart'; +import 'package:PiliPlus/pages/live_room/widgets/bottom_control.dart' + as live_bottom; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart'; import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart'; @@ -62,6 +63,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart' hide ContextExtensionss; @@ -2217,40 +2219,15 @@ class _PLVideoPlayerState extends State ); } - BoxDecoration _getDmTipBg(DanmakuItem item, double dx) { + String _getDmTipBg(DanmakuItem item) { const offset = 65; - const size = Size(_overlayWidth, _overlayHeight); if (item.xPosition >= maxWidth - offset) { - return const BoxDecoration( - image: DecorationImage( - filterQuality: FilterQuality.low, - image: SvgImageProvider( - 'assets/images/dm_tip/player_dm_tip_right.svg', - size: size, - ), - ), - ); + return 'right'; } if (item.xPosition + item.width <= offset) { - return const BoxDecoration( - image: DecorationImage( - filterQuality: FilterQuality.low, - image: SvgImageProvider( - 'assets/images/dm_tip/player_dm_tip_left.svg', - size: size, - ), - ), - ); + return 'left'; } - return const BoxDecoration( - image: DecorationImage( - filterQuality: FilterQuality.low, - image: SvgImageProvider( - 'assets/images/dm_tip/player_dm_tip_center.svg', - size: size, - ), - ), - ); + return 'center'; } Widget _buildDmAction( @@ -2276,8 +2253,12 @@ class _PLVideoPlayerState extends State maxWidth - _overlaySpacing, ); - // TODO LiveDanmaku - final extra = item.content.extra as VideoDanmaku; + if (right > (maxWidth - item.xPosition)) { + _removeDmAction(); + return const Positioned(left: 0, top: 0, child: SizedBox.shrink()); + } + + final extra = item.content.extra; return Positioned( right: right, @@ -2285,63 +2266,113 @@ class _PLVideoPlayerState extends State child: SizedBox( width: _overlayWidth, height: _overlayHeight, - child: DecoratedBox( - decoration: _getDmTipBg(item, dx), - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _dmActionItem( - Icon( - size: 20, - extra.isLike - ? CustomIcons.player_dm_tip_like_solid - : CustomIcons.player_dm_tip_like, - color: Colors.white, - ), - onTap: () => HeaderControl.likeDanmaku( - extra, - plPlayerController.cid!, - ), - ), - _dmActionItem( - const Icon( - size: 19, - CustomIcons.player_dm_tip_copy, - color: Colors.white, - ), - onTap: () => Utils.copyText(item.content.text), - ), - if (item.content.selfSend) - _dmActionItem( - const Icon( - size: 20, - CustomIcons.player_dm_tip_recall, - color: Colors.white, - ), - onTap: () => HeaderControl.deleteDanmaku( - extra.id, - plPlayerController.cid!, - ), - ) - else - _dmActionItem( - const Icon( - size: 20, - CustomIcons.player_dm_tip_back, - color: Colors.white, - ), - onTap: () => HeaderControl.reportDanmaku( - extra, - context, - plPlayerController, - ), - ), - ], + child: Stack( + children: [ + SvgPicture.asset( + 'assets/images/dm_tip/player_dm_tip_${_getDmTipBg(item)}.svg', + clipBehavior: Clip.none, + width: _overlayWidth, + height: _overlayHeight, ), - ), + Positioned.fill( + top: 4, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: extra is VideoDanmaku + ? [ + _dmActionItem( + extra.isLike + ? const Icon( + size: 20, + CustomIcons.player_dm_tip_like_solid, + color: Colors.white, + ) + : const Icon( + size: 20, + CustomIcons.player_dm_tip_like, + color: Colors.white, + ), + onTap: () => HeaderControl.likeDanmaku( + extra, + plPlayerController.cid!, + ), + ), + _dmActionItem( + const Icon( + size: 19, + CustomIcons.player_dm_tip_copy, + color: Colors.white, + ), + onTap: () => Utils.copyText(item.content.text), + ), + if (item.content.selfSend) + _dmActionItem( + const Icon( + size: 20, + CustomIcons.player_dm_tip_recall, + color: Colors.white, + ), + onTap: () => HeaderControl.deleteDanmaku( + extra.id, + plPlayerController.cid!, + ), + ) + else + _dmActionItem( + const Icon( + size: 20, + CustomIcons.player_dm_tip_back, + color: Colors.white, + ), + onTap: () => HeaderControl.reportDanmaku( + context, + extra: extra, + ctr: plPlayerController, + ), + ), + ] + : extra is LiveDanmaku + ? [ + _dmActionItem( + const Icon( + size: 20, + MdiIcons.accountOutline, + color: Colors.white, + ), + onTap: () => Get.toNamed('/member?mid=${extra.mid}'), + ), + _dmActionItem( + const Icon( + size: 19, + CustomIcons.player_dm_tip_copy, + color: Colors.white, + ), + onTap: () => Utils.copyText(item.content.text), + ), + _dmActionItem( + const Icon( + size: 20, + CustomIcons.player_dm_tip_back, + color: Colors.white, + ), + onTap: () => HeaderControl.reportLiveDanmaku( + context, + roomId: + (widget.bottomControl + as live_bottom.BottomControl) + .liveRoomCtr + .roomId, + msg: item.content.text, + extra: extra, + ctr: plPlayerController, + ), + ), + ] + : throw UnimplementedError(), + ), + ), + ], ), ), );