diff --git a/lib/http/constants.dart b/lib/http/constants.dart index a4a12a96..03059ee2 100644 --- a/lib/http/constants.dart +++ b/lib/http/constants.dart @@ -8,6 +8,7 @@ class HttpString { static const String messageBaseUrl = 'https://message.bilibili.com'; static const String dynamicShareBaseUrl = 'https://t.bilibili.com'; static const String spaceBaseUrl = 'https://space.bilibili.com'; + static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top'; static const List validateStatusCodes = [ 302, 304, diff --git a/lib/http/init.dart b/lib/http/init.dart index a84cfefd..d458346e 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -224,7 +224,8 @@ class Request { /* * post请求 */ - post(url, {data, queryParameters, options, cancelToken, extra}) async { + Future post(url, + {data, queryParameters, options, cancelToken, extra}) async { // debugPrint('post-data: $data'); Response response; try { diff --git a/lib/pages/setting/sponsor_block_page.dart b/lib/pages/setting/sponsor_block_page.dart index 213af8b3..2e8d164a 100644 --- a/lib/pages/setting/sponsor_block_page.dart +++ b/lib/pages/setting/sponsor_block_page.dart @@ -8,6 +8,7 @@ import 'package:PiliPalaX/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:uuid/uuid.dart'; class SponsorBlockPage extends StatefulWidget { const SponsorBlockPage({super.key}); @@ -17,10 +18,11 @@ class SponsorBlockPage extends StatefulWidget { } class _SponsorBlockPageState extends State { + final _url = 'https://github.com/hanydd/BilibiliSponsorBlock'; late double _blockLimit; late List> _blockSettings; late List _blockColor; - final _url = 'https://github.com/hanydd/BilibiliSponsorBlock'; + late String _userId; @override void initState() { @@ -28,8 +30,139 @@ class _SponsorBlockPageState extends State { _blockLimit = GStorage.blockLimit; _blockSettings = GStorage.blockSettings; _blockColor = GStorage.blockColor; + _userId = GStorage.blockUserID; } + TextStyle get _titleStyle => TextStyle(fontSize: 15); + TextStyle get _subTitleStyle => + TextStyle(color: Theme.of(context).colorScheme.outline); + + Widget get _blockLimitItem => ListTile( + onTap: () { + final textController = + TextEditingController(text: _blockLimit.toString()); + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('最短片段时长'), + content: TextFormField( + keyboardType: TextInputType.numberWithOptions(decimal: true), + controller: textController, + autofocus: true, + decoration: InputDecoration(suffixText: 's'), + ), + actions: [ + TextButton( + onPressed: Get.back, + child: Text( + '取消', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + TextButton( + onPressed: () async { + Get.back(); + _blockLimit = + max(0.0, double.tryParse(textController.text) ?? 0.0); + await GStorage.setting + .put(SettingBoxKey.blockLimit, _blockLimit); + setState(() {}); + }, + child: Text('确定'), + ) + ], + ); + }, + ); + }, + title: Text('最短片段时长', style: _titleStyle), + subtitle: Text( + '忽略短于此时长的片段', + style: _subTitleStyle, + ), + trailing: Text( + '${_blockLimit}s', + style: TextStyle(fontSize: 13), + ), + ); + + Widget get _aboudItem => ListTile( + title: Text('关于 SponsorBlock', style: _titleStyle), + subtitle: Text(_url, style: _subTitleStyle), + onTap: () => Utils.launchURL(_url), + ); + + Widget get _userIdItem => ListTile( + title: Text('用户ID', style: _titleStyle), + subtitle: Text(_userId, style: _subTitleStyle), + onTap: () { + final key = GlobalKey(); + final textController = TextEditingController(text: _userId); + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('用户ID'), + content: Form( + key: key, + child: TextFormField( + minLines: 1, + maxLines: 4, + autofocus: true, + controller: textController, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\d]+')), + ], + validator: (value) { + if ((value?.length ?? -1) < 30) { + return '用户ID要求至少为30个字符长度的纯字符串'; + } + return null; + }, + ), + ), + actions: [ + TextButton( + onPressed: () async { + Get.back(); + _userId = Uuid().v4().replaceAll('-', ''); + await GStorage.setting + .put(SettingBoxKey.blockUserID, _userId); + setState(() {}); + }, + child: Text('随机'), + ), + TextButton( + onPressed: Get.back, + child: Text( + '取消', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + TextButton( + onPressed: () async { + if (key.currentState?.validate() == true) { + Get.back(); + _userId = textController.text; + await GStorage.setting + .put(SettingBoxKey.blockUserID, _userId); + setState(() {}); + } + }, + child: Text('确定'), + ) + ], + ); + }, + ); + }, + ); + @override Widget build(BuildContext context) { return Scaffold( @@ -41,198 +174,155 @@ class _SponsorBlockPageState extends State { style: Theme.of(context).textTheme.titleMedium, ), ), - body: ListView.separated( - itemCount: _blockSettings.length + 2, - itemBuilder: (_, index) => index == _blockSettings.length + 1 - ? ListTile( - leading: Icon(Icons.code), - title: const Text('About'), - subtitle: Text(_url), - onTap: () => Utils.launchURL(_url), - ) - : index == 0 - ? ListTile( - onTap: () { - final textController = - TextEditingController(text: _blockLimit.toString()); - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Block Limit'), - content: TextFormField( - keyboardType: TextInputType.numberWithOptions( - decimal: true), - controller: textController, - autofocus: true, - decoration: InputDecoration(suffixText: 's'), - ), - actions: [ - TextButton( - onPressed: Get.back, - child: Text( - '取消', - style: TextStyle( - color: - Theme.of(context).colorScheme.outline, - ), + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: _blockLimitItem), + SliverToBoxAdapter(child: Divider(height: 1)), + SliverToBoxAdapter(child: _userIdItem), + SliverToBoxAdapter(child: Divider(height: 1)), + SliverList.separated( + itemCount: _blockSettings.length, + itemBuilder: (_, index) => ListTile( + enabled: _blockSettings[index].second != SkipType.disable, + onTap: () { + showDialog( + context: context, + builder: (_) => AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.symmetric(vertical: 16), + title: Text.rich( + style: TextStyle(height: 1), + strutStyle: StrutStyle(height: 1, leading: 0), + TextSpan( + children: [ + TextSpan( + text: 'Color Picker\n', + style: TextStyle(fontSize: 18, height: 1.5), + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: + MediaQuery.textScalerOf(context).scale(16), + width: MediaQuery.textScalerOf(context).scale(16), + alignment: Alignment.center, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _blockColor[index], ), ), - TextButton( - onPressed: () async { - Get.back(); - _blockLimit = max( - 0.0, - double.tryParse(textController.text) ?? - 0.0); - await GStorage.setting.put( - SettingBoxKey.blockLimit, _blockLimit); - setState(() {}); - }, - child: Text('确定'), - ) - ], - ); - }, - ); - }, - leading: Icon(Icons.av_timer), - title: const Text('Block Limit'), - trailing: Text( - '${_blockLimit}s', - style: TextStyle(fontSize: 13), - ), - ) - : ListTile( - enabled: - _blockSettings[index - 1].second != SkipType.disable, - onTap: () { - showDialog( - context: context, - builder: (_) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: - const EdgeInsets.symmetric(vertical: 16), - title: Text.rich( - style: TextStyle(height: 1), - strutStyle: StrutStyle(height: 1, leading: 0), - TextSpan( - children: [ - TextSpan( - text: 'Color Picker\n', - style: TextStyle(fontSize: 18, height: 1.5), - ), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Container( - height: MediaQuery.textScalerOf(context) - .scale(16), - width: MediaQuery.textScalerOf(context) - .scale(16), - alignment: Alignment.center, - child: Container( - height: 10, - width: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _blockColor[index - 1], - ), - ), - ), - style: TextStyle(fontSize: 16, height: 1), - ), - TextSpan( - text: - ' ${_blockSettings[index - 1].first.name}', - style: TextStyle(fontSize: 16, height: 1), - ), - ], ), + style: TextStyle(fontSize: 16, height: 1), ), - content: SlideColorPicker( - color: _blockColor[index - 1], - callback: (Color? color) async { - _blockColor[index - 1] = color ?? - _blockSettings[index - 1].first.color; - await GStorage.setting.put( - SettingBoxKey.blockColor, - _blockColor - .map((item) => item.value - .toRadixString(16) - .substring(2)) - .toList()); - setState(() {}); - }, + TextSpan( + text: ' ${_blockSettings[index].first.title}', + style: TextStyle(fontSize: 16, height: 1), ), - ), - ); - }, - leading: Container( - height: 24, - width: 24, - alignment: Alignment.center, - child: Container( - height: 10, - width: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _blockColor[index - 1], - ), - ), - ), - title: Text( - _blockSettings[index - 1].first.name, - style: - _blockSettings[index - 1].second == SkipType.disable - ? TextStyle( - color: Theme.of(context).colorScheme.outline, - ) - : null, - ), - trailing: PopupMenuButton( - initialValue: _blockSettings[index - 1].second, - onSelected: (item) async { - _blockSettings[index - 1].second = item; - await GStorage.setting.put( - SettingBoxKey.blockSettings, - _blockSettings - .map((item) => item.second.index) - .toList()); - setState(() {}); - }, - itemBuilder: (context) => SkipType.values - .map((item) => PopupMenuItem( - value: item, - child: Text(item.title), - )) - .toList(), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _blockSettings[index - 1].second.title, - style: TextStyle( - fontSize: 13, - color: _blockSettings[index - 1].second == - SkipType.disable - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.primary, - ), - ), - Icon( - size: 20, - Icons.keyboard_arrow_right, - color: _blockSettings[index - 1].second == - SkipType.disable - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.primary, - ) ], ), ), + content: SlideColorPicker( + color: _blockColor[index], + callback: (Color? color) async { + _blockColor[index] = + color ?? _blockSettings[index].first.color; + await GStorage.setting.put( + SettingBoxKey.blockColor, + _blockColor + .map((item) => + item.value.toRadixString(16).substring(2)) + .toList()); + setState(() {}); + }, + ), ), - separatorBuilder: (_, index) => Divider(height: 1), + ); + }, + title: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: MediaQuery.textScalerOf(context).scale(15), + width: 10, + alignment: Alignment.center, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _blockColor[index], + ), + ), + ), + style: TextStyle(fontSize: 15), + ), + TextSpan( + text: ' ${_blockSettings[index].first.title}', + style: TextStyle(fontSize: 15), + ), + ], + ), + ), + subtitle: Text( + _blockSettings[index].first.description, + style: _blockSettings[index].second == SkipType.disable + ? null + : TextStyle( + color: Theme.of(context).colorScheme.outline, + ), + ), + trailing: PopupMenuButton( + initialValue: _blockSettings[index].second, + onSelected: (item) async { + _blockSettings[index].second = item; + await GStorage.setting.put(SettingBoxKey.blockSettings, + _blockSettings.map((item) => item.second.index).toList()); + setState(() {}); + }, + itemBuilder: (context) => SkipType.values + .map((item) => PopupMenuItem( + value: item, + child: Text(item.title), + )) + .toList(), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _blockSettings[index].second.title, + style: TextStyle( + fontSize: 13, + color: _blockSettings[index].second == SkipType.disable + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.primary, + ), + ), + Icon( + size: 20, + Icons.keyboard_arrow_right, + color: _blockSettings[index].second == SkipType.disable + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.primary, + ) + ], + ), + ), + ), + separatorBuilder: (_, index) => Divider(height: 1), + ), + SliverToBoxAdapter(child: Divider(height: 1)), + SliverToBoxAdapter(child: _aboudItem), + SliverToBoxAdapter( + child: SizedBox( + height: 25 + MediaQuery.paddingOf(context).bottom, + )), + ], ), ); } diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 2393146c..6d118c0d 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -33,24 +33,49 @@ enum SegmentType { preview, music_offtopic, poi_highlight, - chapter, filler, exclusive_access } extension SegmentTypeExt on SegmentType { + /// from https://github.com/hanydd/BilibiliSponsorBlock/*/public/_locales/zh_CN/messages.json + String get title => [ + '赞助广告', //sponsor + '无偿/自我推广', //selfpromo + '三连/订阅提醒', //interaction + '过场/开场动画', //intro + '鸣谢/结束画面', //outro + '回顾/概要', //preview + '音乐:非音乐部分', //music_offtopic + '精彩时刻/重点', //poi_highlight + '离题闲聊/玩笑', //filler + '品牌合作', //exclusive_access + ][index]; + + String get description => [ + '付费推广、付费推荐和直接广告。不是自我推广或免费提及他们喜欢的商品/创作者/网站/产品。', //sponsor + '类似于 “赞助广告” ,但无报酬或是自我推广。包括有关商品、捐赠的部分或合作者的信息。', //selfpromo + '视频中间简短提醒观众来一键三连或关注。 如果片段较长,或是有具体内容,则应分类为自我推广。', //interaction + '没有实际内容的间隔片段。可以是暂停、静态帧或重复动画。不适用于包含内容的过场。', //intro + '致谢画面或片尾画面。不包含内容的结尾。', //outro + '展示此视频或同系列视频将出现的画面集锦,片段中所有内容都将在之后的正片中再次出现。', //preview + '仅用于音乐视频。此分类只能用于音乐视频中未包括于其他分类的部分。', //music_offtopic + '大部分人都在寻找的空降时间。类似于“封面在12:34”的评论。', //poi_highlight + "仅作为填充内容或增添趣味而添加的离题片段,这些内容对理解视频的主要内容并非必需。这不包括提供背景信息或上下文的片段。这是一个非常激进的分类,适用于当你不想看'娱乐性'内容的时候。", //filler + '仅用于对整个视频进行标记。适用于展示UP主免费或获得补贴后使用的产品、服务或场地的视频。', //exclusive_access + ][index]; + Color get color => [ - Colors.amber, - Colors.blue, - Colors.red, - Colors.indigo, - Colors.pink, - Colors.purple, - Colors.lightGreen, - Colors.teal, - Colors.cyan, - Colors.yellow, - Colors.orange + Color(0xFF00d400), //sponsor + Color(0xFFffff00), //selfpromo + Color(0xFFcc00ff), //interaction + Color(0xFF00ffff), //intro + Color(0xFF0202ed), //outro + Color(0xFF008fd6), //preview + Color(0xFFff9900), //music_offtopic + Color(0xFFff1684), //poi_highlight + Color(0xFF7300FF), //filler + Color(0xFF008a5c), //exclusive_access ][index]; } @@ -62,11 +87,15 @@ extension SkipTypeExt on SkipType { class SegmentModel { SegmentModel({ + // ignore: non_constant_identifier_names + required this.UUID, required this.segmentType, required this.segment, required this.skipType, required this.hasSkipped, }); + // ignore: non_constant_identifier_names + String UUID; SegmentType segmentType; Pair segment; SkipType skipType; @@ -215,13 +244,256 @@ class VideoDetailController extends GetxController double? _blockLimit; List>? _blockSettings; List? _blockColor; - List? _segmentList; + RxList segmentList = [].obs; List? _segmentProgressList; + Color _getColor(SegmentType segment) => + _blockColor?[segment.index] ?? segment.color; + + Future _vote(String uuid, int type) async { + Request() + .post( + '${HttpString.sponsorBlockBaseUrl}/api/voteOnSponsorTime', + queryParameters: { + 'UUID': uuid, + 'userID': GStorage.blockUserID, + 'type': 1, + }, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ) + .then((res) { + SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败'); + }); + } + + void _showCategoryDialog(BuildContext context, SegmentModel segment) { + showDialog( + context: context, + builder: (_) => AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: SegmentType.values + .map((item) => ListTile( + dense: true, + onTap: () { + Get.back(); + Request().post( + '${HttpString.sponsorBlockBaseUrl}/api/voteOnSponsorTime', + queryParameters: { + 'UUID': segment.UUID, + 'userID': GStorage.blockUserID, + 'category': item.name, + }, + ).then((res) { + SmartDialog.showToast( + '类别更改${res.statusCode == 200 ? '成功' : '失败'}'); + }); + }, + title: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: + MediaQuery.textScalerOf(context).scale(14), + width: 10, + alignment: Alignment.center, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _getColor(item), + ), + ), + ), + style: TextStyle(fontSize: 14), + ), + TextSpan( + text: ' ${item.title}', + style: TextStyle(fontSize: 14), + ), + ], + ), + ), + )) + .toList(), + ), + ), + ), + ); + } + + void _showVoteDialog(BuildContext context, SegmentModel segment) { + showDialog( + context: context, + builder: (_) => AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + dense: true, + title: Text( + '赞成票', + style: TextStyle(fontSize: 14), + ), + onTap: () { + Get.back(); + _vote(segment.UUID, 1); + }, + ), + ListTile( + dense: true, + title: Text( + '反对票', + style: TextStyle(fontSize: 14), + ), + onTap: () { + Get.back(); + _vote(segment.UUID, 0); + }, + ), + ListTile( + dense: true, + title: Text( + '更改类别', + style: TextStyle(fontSize: 14), + ), + onTap: () { + Get.back(); + _showCategoryDialog(context, segment); + }, + ), + ], + ), + ), + ), + ); + } + + void showSponsorBlock(BuildContext context) { + showDialog( + context: context, + builder: (_) => AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: segmentList + .map( + (item) => ListTile( + onTap: () { + Get.back(); + _showVoteDialog(context, item); + }, + dense: true, + title: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: + MediaQuery.textScalerOf(context).scale(14), + width: 10, + alignment: Alignment.center, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _getColor(item.segmentType), + ), + ), + ), + style: TextStyle(fontSize: 14), + ), + TextSpan( + text: ' ${item.segmentType.title}', + style: TextStyle(fontSize: 14), + ), + ], + ), + ), + contentPadding: EdgeInsets.only(left: 16, right: 8), + subtitle: Text( + '${Utils.formatDuration(item.segment.first)} 至 ${Utils.formatDuration(item.segment.second)}', + style: TextStyle(fontSize: 13), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + item.skipType.title, + style: TextStyle(fontSize: 13), + ), + if (item.skipType == SkipType.showOnly) + SizedBox( + width: 36, + height: 36, + child: IconButton( + tooltip: '跳转至此片段', + onPressed: () async { + Get.back(); + try { + plPlayerController.danmakuController?.clear(); + await plPlayerController.videoPlayerController + ?.seek(Duration( + seconds: item.segment.first)); + SmartDialog.showToast( + '已跳至${Utils.formatDuration(item.segment.first)}', + displayType: SmartToastType.normal, + ); + } catch (e) { + SmartDialog.showToast( + '跳转失败: $e', + displayType: SmartToastType.normal, + ); + } + }, + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + icon: Icon( + Icons.my_location, + size: 18, + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.7), + ), + ), + ) + else + const SizedBox(width: 10), + ], + ), + ), + ) + .toList(), + ), + ), + ), + ); + } Future _sponsorBlock() async { dynamic result = await Request().get( - 'https://www.bsbsb.top/api/skipSegments', - data: {'videoID': bvid}, + '${HttpString.sponsorBlockBaseUrl}/api/skipSegments', + data: { + 'videoID': bvid, + 'cid': cid.value, + }, options: Options( headers: { 'env': '', @@ -243,10 +515,10 @@ class VideoDetailController extends GetxController .toList() .map((item) => item.first.name) .toList(); - _segmentList = (result.data as List) + segmentList.value = (result.data as List) .where((item) => enableList.contains(item['category']) && - item['segment'][1] > 0 && + // item['segment'][1] > 0 && item['segment'][1] >= item['segment'][0]) .map( (item) { @@ -260,6 +532,7 @@ class VideoDetailController extends GetxController } } return SegmentModel( + UUID: item['UUID'], segmentType: segmentType, segment: Pair( first: _convert(item['segment'][0]), @@ -270,7 +543,7 @@ class VideoDetailController extends GetxController ); }, ).toList(); - _segmentProgressList = _segmentList?.map((item) { + _segmentProgressList = segmentList.map((item) { double start = (item.segment.first / ((data.timeLength ?? 0) / 1000)) .clamp(0.0, 1.0); double end = (item.segment.second / ((data.timeLength ?? 0) / 1000)) @@ -278,11 +551,11 @@ class VideoDetailController extends GetxController return Segment( start, start == end ? (end + 0.01).clamp(0.0, 1.0) : end, - _blockColor?[item.segmentType.index] ?? item.segmentType.color, + _getColor(item.segmentType), ); }).toList(); } catch (e) { - debugPrint(e.toString()); + debugPrint('filed to parse sponsorblock: $e'); } } } @@ -296,14 +569,14 @@ class VideoDetailController extends GetxController } void _initSkip() { - if (_segmentList != null && _segmentList!.isNotEmpty) { + if (segmentList.isNotEmpty) { positionSubscription = plPlayerController .videoPlayerController?.stream.position .listen((position) async { int currentPos = position.inSeconds; if (currentPos != _lastPos) { _lastPos = currentPos; - for (SegmentModel item in _segmentList!) { + for (SegmentModel item in segmentList) { // debugPrint( // '${position.inSeconds},,${item.segment.first},,${item.segment.second},,${item.skipType.name},,${item.hasSkipped}'); if (item.segment.first == position.inSeconds) { @@ -315,11 +588,17 @@ class VideoDetailController extends GetxController ?.seek(Duration(seconds: item.segment.second)); // await plPlayerController // .seekTo(Duration(seconds: item.segment.second)); - SmartDialog.showToast('已跳过${item.segmentType.name}'); + SmartDialog.showToast( + '已跳过${item.segmentType.title}片段', + displayType: SmartToastType.normal, + ); item.hasSkipped = true; } catch (e) { debugPrint('failed to skip: $e'); - SmartDialog.showToast('${item.segmentType.name}跳过失败'); + SmartDialog.showToast( + '${item.segmentType.title}片段跳过失败', + displayType: SmartToastType.normal, + ); } } break; diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 105e0ba0..3f7f6d01 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -1454,6 +1454,27 @@ class _HeaderControlState extends State { // ), // fuc: () => _.screenshot(), // ), + Obx( + () => widget.videoDetailCtr?.segmentList.isNotEmpty == true + ? SizedBox( + width: 42, + height: 34, + child: IconButton( + tooltip: 'SponsorBlock', + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => + widget.videoDetailCtr?.showSponsorBlock(context), + icon: const Icon( + Icons.block, + size: 19, + color: Colors.white, + ), + ), + ) + : const SizedBox.shrink(), + ), SizedBox( width: 42, height: 34, diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 24b2588c..cf5175f6 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -12,6 +12,7 @@ import 'package:PiliPalaX/models/model_owner.dart'; import 'package:PiliPalaX/models/search/hot.dart'; import 'package:PiliPalaX/models/user/info.dart'; import 'global_data.dart'; +import 'package:uuid/uuid.dart'; class GStorage { static late final Box userInfo; @@ -49,6 +50,16 @@ class GStorage { static double get blockLimit => setting.get(SettingBoxKey.blockLimit, defaultValue: 0.0); + static String get blockUserID { + String blockUserID = + setting.get(SettingBoxKey.blockUserID, defaultValue: ''); + if (blockUserID.isEmpty) { + blockUserID = Uuid().v4().replaceAll('-', ''); + setting.put(SettingBoxKey.blockUserID, blockUserID); + } + return blockUserID; + } + static ThemeMode get themeMode { switch (setting.get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)) { @@ -225,6 +236,7 @@ class SettingBoxKey { blockSettings = 'blockSettings', blockLimit = 'blockLimit', blockColor = 'blockColor', + blockUserID = 'blockUserID', // 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 字体粗细 danmakuWeight = 'danmakuWeight', diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 1ac0a642..07452d1f 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -602,6 +602,22 @@ class Utils { return v.toString() + random.nextInt(9999).toString(); } + static String formatDuration(int seconds) { + int hours = seconds ~/ 3600; + int minutes = (seconds % 3600) ~/ 60; + int remainingSeconds = seconds % 60; + + String minutesStr = minutes.toString().padLeft(2, '0'); + String secondsStr = remainingSeconds.toString().padLeft(2, '0'); + + if (hours > 0) { + String hoursStr = hours.toString().padLeft(2, '0'); + return "$hoursStr:$minutesStr:$secondsStr"; + } else { + return "$minutesStr:$secondsStr"; + } + } + static int duration(String duration) { List timeList = duration.split(':'); int len = timeList.length;