diff --git a/lib/common/widgets/dialog/dialog.dart b/lib/common/widgets/dialog/dialog.dart index b5325c398..b018a239e 100644 --- a/lib/common/widgets/dialog/dialog.dart +++ b/lib/common/widgets/dialog/dialog.dart @@ -7,6 +7,7 @@ Future showConfirmDialog({ dynamic content, required VoidCallback onConfirm, }) { + assert(content is String? || content is Widget); return showDialog( context: context, builder: (context) { diff --git a/lib/http/danmaku_block.dart b/lib/http/danmaku_block.dart index 583368311..aae2c2b47 100644 --- a/lib/http/danmaku_block.dart +++ b/lib/http/danmaku_block.dart @@ -1,26 +1,21 @@ import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/init.dart'; +import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/user/danmaku_block.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:dio/dio.dart'; class DanmakuFilterHttp { - static Future danmakuFilter() async { + static Future> danmakuFilter() async { var res = await Request().get(Api.danmakuFilter); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': DanmakuBlockDataModel.fromJson(res.data['data']), - }; + return Success(DanmakuBlockDataModel.fromJson(res.data['data'])); } else { - return { - 'status': false, - 'msg': res.data['message'], - }; + return Error(res.data['message']); } } - static Future danmakuFilterDel({required int ids}) async { + static Future> danmakuFilterDel({required int ids}) async { var res = await Request().post( Api.danmakuFilterDel, data: { @@ -30,16 +25,13 @@ class DanmakuFilterHttp { options: Options(contentType: Headers.formUrlEncodedContentType), ); if (res.data['code'] == 0) { - return {'status': true}; + return const Success(null); } else { - return { - 'status': false, - 'msg': res.data['message'], - }; + return Error(res.data['message']); } } - static Future danmakuFilterAdd({ + static Future> danmakuFilterAdd({ required String filter, required int type, }) async { @@ -53,15 +45,9 @@ class DanmakuFilterHttp { options: Options(contentType: Headers.formUrlEncodedContentType), ); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': SimpleRule.fromJson(res.data['data']), - }; + return Success(SimpleRule.fromJson(res.data['data'])); } else { - return { - 'status': false, - 'msg': res.data['message'], - }; + return Error(res.data['message']); } } } diff --git a/lib/http/video.dart b/lib/http/video.dart index c25d4285c..c9cb01624 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1068,11 +1068,10 @@ class VideoHttp { required int playurlType, // ugc 1, pgc 2 int? qn, }) async { - final accessKey = Accounts.accountMode[AccountType.video.index].accessKey; + final accessKey = Accounts.get(AccountType.video).accessKey; final params = { 'access_key': ?accessKey, 'actionKey': 'appkey', - 'appkey': Constants.appKey, 'cid': cid, 'fourk': 1, 'is_proj': 1, diff --git a/lib/pages/danmaku_block/controller.dart b/lib/pages/danmaku_block/controller.dart index c481c2913..85ec1cf11 100644 --- a/lib/pages/danmaku_block/controller.dart +++ b/lib/pages/danmaku_block/controller.dart @@ -32,10 +32,10 @@ class DanmakuBlockController extends GetxController Future queryDanmakuFilter() async { SmartDialog.showLoading(msg: '正在同步弹幕屏蔽规则……'); - var result = await DanmakuFilterHttp.danmakuFilter(); + final result = await DanmakuFilterHttp.danmakuFilter(); SmartDialog.dismiss(); - if (result['status']) { - DanmakuBlockDataModel data = result['data']; + if (result.isSuccess) { + final data = result.data; rules[0].addAll(data.rule); rules[1].addAll(data.rule1); rules[2].addAll(data.rule2); @@ -43,19 +43,19 @@ class DanmakuBlockController extends GetxController SmartDialog.showToast(data.toast!); } } else { - SmartDialog.showToast(result['msg']); + result.toast(); } } Future danmakuFilterDel(int tabIndex, int itemIndex, int id) async { SmartDialog.showLoading(msg: '正在删除弹幕屏蔽规则……'); - var result = await DanmakuFilterHttp.danmakuFilterDel(ids: id); + final result = await DanmakuFilterHttp.danmakuFilterDel(ids: id); SmartDialog.dismiss(); - if (result['status']) { + if (result.isSuccess) { rules[tabIndex].removeAt(itemIndex); SmartDialog.showToast('删除成功'); } else { - SmartDialog.showToast(result['msg']); + result.toast(); } } @@ -67,17 +67,16 @@ class DanmakuBlockController extends GetxController filter = Crc32Xz().convert(utf8.encode(filter)).toRadixString(16); } SmartDialog.showLoading(msg: '正在添加弹幕屏蔽规则……'); - var result = await DanmakuFilterHttp.danmakuFilterAdd( + final result = await DanmakuFilterHttp.danmakuFilterAdd( filter: filter, type: type, ); SmartDialog.dismiss(); - if (result['status']) { - SimpleRule rule = result['data']; - rules[type].add(rule); + if (result.isSuccess) { + rules[type].add(result.data); SmartDialog.showToast('添加成功'); } else { - SmartDialog.showToast(result['msg']); + result.toast(); } } } diff --git a/lib/pages/danmaku_block/view.dart b/lib/pages/danmaku_block/view.dart index cfd14b057..25dba20dc 100644 --- a/lib/pages/danmaku_block/view.dart +++ b/lib/pages/danmaku_block/view.dart @@ -78,7 +78,7 @@ class _DanmakuBlockPageState extends State { ); } - Widget tabViewBuilder(int tabIndex, List list) { + Widget tabViewBuilder(final int tabIndex, List list) { if (list.isEmpty) { return scrollErrorWidget(); } @@ -89,31 +89,54 @@ class _DanmakuBlockPageState extends State { ), itemBuilder: (context, itemIndex) { final SimpleRule item = list[itemIndex]; + final child = IconButton( + icon: const Icon(Icons.delete_outlined), + onPressed: () => showConfirmDialog( + context: context, + title: '确定删除该规则?', + onConfirm: () => _controller.danmakuFilterDel( + tabIndex, + itemIndex, + item.id, + ), + ), + ); return ListTile( title: Text( item.filter, style: Theme.of(context).textTheme.bodyMedium, ), - trailing: IconButton( - icon: const Icon(Icons.delete_outlined), - onPressed: () => showConfirmDialog( - context: context, - title: '确定删除该规则?', - onConfirm: () => _controller.danmakuFilterDel( - tabIndex, - itemIndex, - item.id, - ), - ), - ), + trailing: tabIndex == 2 + ? child + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit_outlined), + onPressed: () => _showAddDialog( + DmBlockType.values[_controller.tabController.index], + initFilter: item.filter, + itemIndex: itemIndex, + itemId: item.id, + ), + ), + child, + ], + ), ); }, ); } - void _showAddDialog(DmBlockType type) { - String filter = ''; - String hintText = switch (type) { + void _showAddDialog( + DmBlockType type, { + String initFilter = '', + int? itemIndex, + int? itemId, + }) { + assert((itemIndex == null) == (itemId == null)); + String filter = initFilter; + final hintText = switch (type) { DmBlockType.keyword => '输入过滤的关键词,其它类别请切换标签页后添加', DmBlockType.regex => '输入//之间的正则表达式,无需包含头尾的"/"', DmBlockType.uid => '输入用户UID', @@ -123,7 +146,7 @@ class _DanmakuBlockPageState extends State { context: context, builder: (context) { return AlertDialog( - title: Text('添加新的${type.label}规则'), + title: Text('${itemId != null ? "编辑" : "添加新的"}${type.label}规则'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -150,15 +173,24 @@ class _DanmakuBlockPageState extends State { ), TextButton( child: const Text('添加'), - onPressed: () { - if (filter.isNotEmpty) { + onPressed: () async { + if (filter != initFilter) { Get.back(); - _controller.danmakuFilterAdd( + if (itemId != null) { + await _controller.danmakuFilterDel( + type.index, + itemIndex!, + itemId, + ); + } + await _controller.danmakuFilterAdd( filter: filter, type: type.index, ); } else { - SmartDialog.showToast('输入内容不能为空'); + SmartDialog.showToast( + '输入内容${filter.isEmpty ? "不能为空" : "与上次相同"}', + ); } }, ), diff --git a/lib/pages/login/view.dart b/lib/pages/login/view.dart index db661fca9..90106a3c3 100644 --- a/lib/pages/login/view.dart +++ b/lib/pages/login/view.dart @@ -78,7 +78,7 @@ class _LoginPageState extends State { if (kDebugMode || Utils.isMobile) TextButton.icon( onPressed: () => PageUtils.launchURL( - _loginPageCtr.codeInfo.value.data.url, + 'bilibili://browser?url=${Uri.encodeComponent(_loginPageCtr.codeInfo.value.data.url)}', mode: LaunchMode.externalNonBrowserApplication, ), icon: const Icon(Icons.open_in_browser_outlined), diff --git a/lib/pages/sponsor_block/view.dart b/lib/pages/sponsor_block/view.dart index 4eee7e80d..9253b4c5e 100644 --- a/lib/pages/sponsor_block/view.dart +++ b/lib/pages/sponsor_block/view.dart @@ -3,9 +3,12 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/init.dart'; +import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; import 'package:PiliPlus/pages/setting/slide_color_picker.dart'; +import 'package:PiliPlus/utils/duration_utils.dart'; +import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; @@ -35,7 +38,8 @@ class _SponsorBlockPageState extends State { bool _blockToast = Pref.blockToast; String _blockServer = Pref.blockServer; bool _blockTrack = Pref.blockTrack; - final Rx _serverStatus = Rx(null); + final _serverStatus = Rxn(); + final _userInfo = LoadingState<_UserInfo>.loading().obs; Box setting = GStorage.setting; @@ -43,6 +47,7 @@ class _SponsorBlockPageState extends State { void initState() { super.initState(); _checkServerStatus(); + _getUserInfo(); } @override @@ -60,6 +65,22 @@ class _SponsorBlockPageState extends State { }); } + Future _getUserInfo() async { + final params = { + 'userID': _userId, + 'values': '["viewCount","minutesSaved","segmentCount"]', + }; + final res = await Request().get( + '$_blockServer/api/userInfo', + queryParameters: params, + ); + if (res.statusCode == 200) { + _userInfo.value = Success(_UserInfo.fromJson(res.data)); + } else { + _userInfo.value = Error(res.data['message']); + } + } + Widget _blockLimitItem( ThemeData theme, TextStyle titleStyle, @@ -270,6 +291,37 @@ class _SponsorBlockPageState extends State { }, ); + Widget _blockUserInfo( + ThemeData theme, + TextStyle titleStyle, + TextStyle subTitleStyle, + ) => Obx( + () { + return ListTile( + dense: true, + onTap: () { + _userInfo.value = LoadingState.loading(); + _getUserInfo(); + }, + title: Text( + '您的信息', + style: titleStyle, + ), + subtitle: switch (_userInfo.value) { + Loading() => const SizedBox.shrink(), + Success<_UserInfo>(:final response) => Text( + response.toString(), + style: subTitleStyle, + ), + Error(:final errMsg) => Text( + errMsg ?? '服务器错误', + style: subTitleStyle.copyWith(color: theme.colorScheme.error), + ), + }, + ); + }, + ); + Widget _blockServerItem( ThemeData theme, TextStyle titleStyle, @@ -316,6 +368,8 @@ class _SponsorBlockPageState extends State { _blockServer = _textController.text; setting.put(SettingBoxKey.blockServer, _blockServer); Request.accountManager.blockServer = _blockServer; + _checkServerStatus(); + _getUserInfo(); (context as Element).markNeedsBuild(); }, child: const Text('确定'), @@ -461,6 +515,10 @@ class _SponsorBlockPageState extends State { SliverToBoxAdapter(child: _blockToastItem(titleStyle)), sliverDivider, SliverToBoxAdapter(child: _blockTrackItem(titleStyle, subTitleStyle)), + sliverDivider, + SliverToBoxAdapter( + child: _blockUserInfo(theme, titleStyle, subTitleStyle), + ), dividerL, SliverList.separated( itemCount: _blockSettings.length, @@ -599,3 +657,34 @@ class _SponsorBlockPageState extends State { ); } } + +class _UserInfo { + final int viewCount; + final double minutesSaved; + final int segmentCount; + + const _UserInfo({ + required this.viewCount, + required this.minutesSaved, + required this.segmentCount, + }); + + factory _UserInfo.fromJson(Map json) => _UserInfo( + viewCount: json['viewCount'], + minutesSaved: (json['minutesSaved'] as num).toDouble(), + segmentCount: json['segmentCount'], + ); + + @override + String toString() { + String minutes = DurationUtils.formatTimeDuration( + Duration(minutes: minutesSaved.round()), + ); + if (minutes.isEmpty) { + minutes = '0分钟'; + } + return ('您提交了 ${NumUtils.formatPositiveDecimal(segmentCount)} 片段\n' + '您为大家节省了 ${NumUtils.formatPositiveDecimal(viewCount)} 片段\n' + '($minutes 的生命)'); + } +} diff --git a/lib/utils/accounts.dart b/lib/utils/accounts.dart index 954177a9b..3f210082a 100644 --- a/lib/utils/accounts.dart +++ b/lib/utils/accounts.dart @@ -124,6 +124,7 @@ abstract class Accounts { } } + @pragma("vm:prefer-inline") static Account get(AccountType key) { return accountMode[key.index]; } diff --git a/lib/utils/accounts/account_manager/account_mgr.dart b/lib/utils/accounts/account_manager/account_mgr.dart index 61a51a36e..a39751cf6 100644 --- a/lib/utils/accounts/account_manager/account_mgr.dart +++ b/lib/utils/accounts/account_manager/account_mgr.dart @@ -110,6 +110,7 @@ class AccountManager extends Interceptor { Api.ugcUrl, Api.pgcUrl, Api.pugvUrl, + Api.tvPlayUrl, }, }; diff --git a/lib/utils/duration_utils.dart b/lib/utils/duration_utils.dart index 81bd03043..502bbccb5 100644 --- a/lib/utils/duration_utils.dart +++ b/lib/utils/duration_utils.dart @@ -1,6 +1,6 @@ import 'dart:math' show pow; -abstract class DurationUtils { +abstract final class DurationUtils { static String formatDuration(num? seconds) { if (seconds == null || seconds == 0) { return '00:00'; @@ -30,10 +30,10 @@ abstract class DurationUtils { return duration; } - static String formatDurationBetween(int startMillis, int endMillis) { - int diffMillis = endMillis - startMillis; - final duration = Duration(milliseconds: diffMillis); + static String formatDurationBetween(int startMillis, int endMillis) => + formatTimeDuration(Duration(milliseconds: endMillis - startMillis)); + static String formatTimeDuration(Duration duration) { final inDays = duration.inDays; final daysLeft = inDays % 365; final years = inDays ~/ 365; @@ -42,14 +42,14 @@ abstract class DurationUtils { final hours = duration.inHours % 24; final minutes = duration.inMinutes % 60; - var format = ''; + final format = StringBuffer(); - if (years > 0) format += '$years年'; - if (months > 0) format += '$months月'; - if (days > 0) format += '$days天'; - if (hours > 0) format += '$hours小时'; - if (minutes > 0) format += '$minutes分钟'; + if (years > 0) format.write('$years年'); + if (months > 0) format.write('$months月'); + if (days > 0) format.write('$days天'); + if (hours > 0) format.write('$hours小时'); + if (minutes > 0) format.write('$minutes分钟'); - return format; + return format.toString(); } } diff --git a/lib/utils/num_utils.dart b/lib/utils/num_utils.dart index fd1e1983a..b919fa6b0 100644 --- a/lib/utils/num_utils.dart +++ b/lib/utils/num_utils.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart' show kDebugMode, debugPrint; import 'package:get/get_utils/get_utils.dart'; -abstract class NumUtils { +abstract final class NumUtils { static final _numRegExp = RegExp(r'([\d\.]+)([千万亿])?'); static int _getUnit(String? unit) { @@ -59,4 +59,24 @@ abstract class NumUtils { return number.toString(); } } + + static String formatPositiveDecimal(int number) { + if (number < 1000) return number.toString(); + + final numStr = number.toString(); + final length = numStr.length; + final sb = StringBuffer(); + + int firstLength = length % 3; + if (firstLength == 0) firstLength = 3; + + sb.write(numStr.substring(0, firstLength)); + for (int i = firstLength; i < length; i += 3) { + sb + ..write(',') + ..write(numStr.substring(i, i + 3)); + } + + return sb.toString(); + } }