import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/scaffold.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/sponsor_block.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/page_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show FilteringTextInputFormatter; import 'package:get/get.dart'; import 'package:hive_ce/hive.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class SponsorBlockPage extends StatefulWidget { const SponsorBlockPage({super.key}); @override State createState() => _SponsorBlockPageState(); } class _SponsorBlockPageState extends State { final _url = 'https://github.com/hanydd/BilibiliSponsorBlock'; final _textController = TextEditingController(); final _blockSettings = Pref.blockSettings; final List _blockColor = Pref.blockColor; String _userId = Pref.blockUserID; bool _blockToast = Pref.blockToast; String _blockServer = Pref.blockServer; final _serverStatus = Rxn(); Box setting = GStorage.setting; @override void initState() { super.initState(); _checkServerStatus(); } @override void dispose() { _textController.dispose(); super.dispose(); } Future _checkServerStatus() async { _serverStatus.value = (await SponsorBlock.uptimeStatus()).isSuccess; } Widget _aboutItem(TextStyle titleStyle, TextStyle subTitleStyle) => ListTile( dense: true, title: Text('关于空降助手', style: titleStyle), subtitle: Text(_url, style: subTitleStyle), onTap: () => PageUtils.launchURL(_url), ); Widget _userIdItem( ThemeData theme, TextStyle titleStyle, TextStyle subTitleStyle, ) => Builder( builder: (context) { return ListTile( dense: true, title: Text('用户ID', style: titleStyle), subtitle: Text(_userId, style: subTitleStyle), onTap: () { final key = GlobalKey>(); _textController.text = _userId; showDialog( context: context, builder: (_) { return AlertDialog( title: Text('用户ID', style: titleStyle), content: TextFormField( key: key, minLines: 1, maxLines: 4, autofocus: true, controller: _textController, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\d]+')), ], decoration: const InputDecoration(errorMaxLines: 2), validator: (value) { if ((value?.length ?? -1) < 30) { return '用户ID要求至少为30个字符长度的纯字符串'; } return null; }, ), actions: [ TextButton( onPressed: () { Get.back(); _userId = Digest( List.generate(16, (_) => Utils.random.nextInt(256)), ).toString(); setting.put(SettingBoxKey.blockUserID, _userId); (context as Element).markNeedsBuild(); }, child: const Text('随机'), ), TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: theme.colorScheme.outline, ), ), ), TextButton( onPressed: () { if (key.currentState?.validate() == true) { Get.back(); _userId = _textController.text; setting.put(SettingBoxKey.blockUserID, _userId); (context as Element).markNeedsBuild(); } }, child: const Text('确定'), ), ], ); }, ); }, ); }, ); Widget _blockToastItem(TextStyle titleStyle) => Builder( builder: (context) { void update() { _blockToast = !_blockToast; setting.put(SettingBoxKey.blockToast, _blockToast); (context as Element).markNeedsBuild(); } return ListTile( dense: true, onTap: update, title: Text( '显示跳过Toast', style: titleStyle, ), trailing: Transform.scale( alignment: Alignment.centerRight, scale: 0.8, child: Switch( value: _blockToast, onChanged: (val) => update(), ), ), ); }, ); Widget _blockServerItem( ThemeData theme, TextStyle titleStyle, TextStyle subTitleStyle, ) => Builder( builder: (context) { return ListTile( dense: true, onTap: () { _textController.text = _blockServer; showDialog( context: context, builder: (_) => AlertDialog( title: Text('服务器地址', style: titleStyle), content: TextFormField( keyboardType: TextInputType.url, controller: _textController, autofocus: true, ), actions: [ TextButton( onPressed: () { Get.back(); _blockServer = HttpString.sponsorBlockBaseUrl; setting.put(SettingBoxKey.blockServer, _blockServer); Request.accountManager.blockServer = _blockServer; (context as Element).markNeedsBuild(); }, child: const Text('重置'), ), TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: theme.colorScheme.outline, ), ), ), TextButton( onPressed: () { Get.back(); _blockServer = _textController.text; setting.put(SettingBoxKey.blockServer, _blockServer); Request.accountManager.blockServer = _blockServer; _checkServerStatus(); (context as Element).markNeedsBuild(); }, child: const Text('确定'), ), ], ), ); }, title: Text( '服务器地址', style: titleStyle, ), subtitle: Text( _blockServer, style: subTitleStyle, ), ); }, ); Widget _serverStatusItem(ThemeData theme, TextStyle titleStyle) => Obx( () { String status; Color? color; switch (_serverStatus.value) { case null: status = '——'; case true: status = '正常'; color = theme.colorScheme.primary; case false: status = '错误'; color = theme.colorScheme.error; } return ListTile( dense: true, onTap: () { _serverStatus.value = null; _checkServerStatus(); }, title: Text('服务器状态', style: titleStyle), trailing: Text( status, style: TextStyle(fontSize: 13, color: color), ), ); }, ); void onSelectColor( BuildContext context, int index, Color color, Pair item, ) { showDialog( context: context, builder: (_) => AlertDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 16), title: Text.rich( TextSpan( children: [ const TextSpan( text: 'Color Picker ', style: TextStyle(fontSize: 15), ), WidgetSpan( alignment: PlaceholderAlignment.middle, child: Container( height: 10, width: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: color, ), ), style: const TextStyle(fontSize: 13, height: 1), ), TextSpan( text: ' ${item.first.title}', style: const TextStyle(fontSize: 13, height: 1), ), ], ), ), content: SlideColorPicker( color: color, showResetBtn: true, onChanged: (Color? color) { _blockColor[index] = color ?? item.first.color; setting.put( SettingBoxKey.blockColor, _blockColor .map((item) => item.toARGB32().toRadixString(16).substring(2)) .toList(), ); (context as Element).markNeedsBuild(); }, ), ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); const titleStyle = TextStyle(fontSize: 15); final subTitleStyle = TextStyle( fontSize: 13, color: theme.colorScheme.outline, ); final divider = Divider( height: 1, color: theme.colorScheme.outline.withValues(alpha: 0.1), ); final dividerLarge = Divider( thickness: 16, color: theme.colorScheme.outline.withValues(alpha: 0.1), ); return scaffold( appBar: AppBar(title: const Text('空降助手')), body: ListView( padding: .only(bottom: 55 + MediaQuery.viewPaddingOf(context).bottom), children: [ dividerLarge, _serverStatusItem(theme, titleStyle), dividerLarge, divider, _blockToastItem(titleStyle), divider, divider, dividerLarge, ...List.generate( _blockSettings.length, (index) => _buildItem(theme, index, _blockSettings[index]), ) .expand((e) sync* { yield divider; yield e; }) .skip(1), dividerLarge, _userIdItem(theme, titleStyle, subTitleStyle), divider, _blockServerItem(theme, titleStyle, subTitleStyle), dividerLarge, _aboutItem(titleStyle, subTitleStyle), dividerLarge, ], ), ); } Widget _buildItem( ThemeData theme, int index, Pair item, ) { return Builder( builder: (context) { Color color = _blockColor[index]; final isDisable = item.second == SkipType.disable; return ListTile( dense: true, enabled: item.second != SkipType.disable, onTap: () => onSelectColor(context, index, color, item), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text.rich( TextSpan( children: [ WidgetSpan( alignment: PlaceholderAlignment.middle, child: Container( height: 10, width: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: color, ), ), style: const TextStyle(fontSize: 14, height: 1), ), TextSpan( text: ' ${item.first.title}', style: const TextStyle(fontSize: 14, height: 1), ), ], ), ), Builder( builder: (btnContext) { return PopupMenuButton( initialValue: item.second, onSelected: (e) { final updateItem = isDisable || e == SkipType.disable; item.second = e; setting.put( SettingBoxKey.blockSettings, _blockSettings.map((e) => e.second.index).toList(), ); if (updateItem) { (context as Element).markNeedsBuild(); } else { (btnContext as Element).markNeedsBuild(); } }, itemBuilder: (context) => SkipType.values .map( (item) => PopupMenuItem( value: item, child: Text(item.label), ), ) .toList(), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text.rich( style: TextStyle( height: 1, fontSize: 14, color: isDisable ? theme.colorScheme.outline.withValues( alpha: 0.7, ) : theme.colorScheme.secondary, ), strutStyle: const StrutStyle( height: 1, leading: 0, fontSize: 14, ), TextSpan( children: [ TextSpan(text: item.second.label), WidgetSpan( alignment: .middle, child: Icon( size: 14, MdiIcons.unfoldMoreHorizontal, color: isDisable ? theme.colorScheme.outline.withValues( alpha: 0.7, ) : theme.colorScheme.secondary, ), ), ], ), ), ), ); }, ), ], ), subtitle: Text( item.first.description, style: TextStyle( fontSize: 12, color: isDisable ? null : theme.colorScheme.outline, ), ), ); }, ); } }