import 'dart:io'; import 'dart:math'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart' show kDragContainerExtentPercentage, displacement; import 'package:PiliPlus/http/interceptor.dart'; import 'package:PiliPlus/models/common/audio_normalization.dart'; import 'package:PiliPlus/models/common/dynamic_badge_mode.dart'; import 'package:PiliPlus/models/common/dynamics_type.dart'; import 'package:PiliPlus/models/common/nav_bar_config.dart'; import 'package:PiliPlus/models/common/rcmd_type.dart'; import 'package:PiliPlus/models/common/reply_sort_type.dart'; import 'package:PiliPlus/models/common/super_resolution_type.dart'; import 'package:PiliPlus/models/common/theme_type.dart'; import 'package:PiliPlus/models/common/up_panel_position.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/video/play/CDN.dart'; import 'package:PiliPlus/models/video/play/quality.dart'; import 'package:PiliPlus/models/video/play/subtitle.dart'; import 'package:PiliPlus/pages/home/controller.dart'; import 'package:PiliPlus/pages/hot/controller.dart'; import 'package:PiliPlus/pages/main/controller.dart'; import 'package:PiliPlus/pages/member/new/controller.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/pages/rcmd/controller.dart'; import 'package:PiliPlus/pages/setting/pages/color_select.dart'; import 'package:PiliPlus/pages/setting/widgets/multi_select_dialog.dart'; import 'package:PiliPlus/pages/setting/widgets/normal_item.dart'; import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart'; import 'package:PiliPlus/pages/setting/widgets/slide_dialog.dart'; import 'package:PiliPlus/pages/setting/widgets/switch_item.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart'; import 'package:PiliPlus/plugin/pl_player/models/fullscreen_mode.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/recommend_filter.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:auto_orientation/auto_orientation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; extension SettingsModelExt on SettingsModel { Widget get widget => switch (settingsType) { SettingsType.divider => const Divider(height: 1), SettingsType.normal => NormalItem( title: title, getTitle: getTitle, subtitle: subtitle, getSubtitle: getSubtitle, setKey: setKey, defaultVal: defaultVal, onChanged: onChanged, needReboot: needReboot, leading: leading, getTrailing: getTrailing, onTap: onTap, contentPadding: contentPadding, titleStyle: titleStyle, ), SettingsType.sw1tch => SetSwitchItem( title: title, subtitle: subtitle, setKey: setKey, defaultVal: defaultVal, onChanged: onChanged, needReboot: needReboot, leading: leading, onTap: onTap, contentPadding: contentPadding, titleStyle: titleStyle, ), }; } class SettingsModel { final SettingsType settingsType; final String? title; final Function? getTitle; final String? subtitle; final Function? getSubtitle; final String? setKey; final bool? defaultVal; final ValueChanged? onChanged; final bool? needReboot; final Widget? leading; final Function? getTrailing; final Function? onTap; final EdgeInsetsGeometry? contentPadding; final TextStyle? titleStyle; final bool? enableFeedback; SettingsModel({ required this.settingsType, this.title, this.getTitle, this.subtitle, this.getSubtitle, this.setKey, this.defaultVal, this.onChanged, this.needReboot, this.leading, this.getTrailing, this.onTap, this.contentPadding, this.titleStyle, this.enableFeedback, }); } enum SettingsType { normal, sw1tch, divider } List get styleSettings => [ SettingsModel( settingsType: SettingsType.sw1tch, title: '横屏适配', subtitle: '启用横屏布局与逻辑,平板、折叠屏等可开启;建议全屏方向设为【不改变当前方向】', leading: const Icon(Icons.phonelink_outlined), setKey: SettingBoxKey.horizontalScreen, defaultVal: false, onChanged: (value) { if (value) { autoScreen(); // SmartDialog.showToast('已开启横屏适配'); } else { AutoOrientation.portraitUpMode(); // SmartDialog.showToast('已关闭横屏适配'); } }), SettingsModel( settingsType: SettingsType.sw1tch, title: '改用侧边栏', subtitle: '开启后底栏与顶栏被替换,且相关设置失效', leading: Icon(Icons.chrome_reader_mode_outlined), setKey: SettingBoxKey.useSideBar, defaultVal: false, needReboot: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: 'MD3样式底栏', subtitle: 'Material You设计规范底栏,关闭可变窄', leading: Icon(Icons.design_services_outlined), setKey: SettingBoxKey.enableMYBar, defaultVal: true, needReboot: true, ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) async { double? result = await showDialog( context: Get.context!, builder: (context) { return SlideDialog( title: '小卡最大列宽度(默认240dp)', value: GStorage.smallCardWidth, min: 150.0, max: 500.0, divisions: 35, suffix: 'dp', ); }); if (result != null) { await GStorage.setting.put(SettingBoxKey.smallCardWidth, result); SmartDialog.showToast('重启生效'); setState(); } }, leading: const Icon(Icons.calendar_view_week_outlined), title: '小卡列表宽度(dp)限制', getSubtitle: () => '当前:${GStorage.smallCardWidth.toInt()}dp,屏幕宽度:${MediaQuery.of(Get.context!).size.width.toPrecision(2)}dp。宽度越小列数越多。', ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) async { double? result = await showDialog( context: Get.context!, builder: (context) { return SlideDialog( title: '中卡最大列宽度(默认280dp)', value: GStorage.mediumCardWidth, min: 150.0, max: 500.0, divisions: 35, suffix: 'dp', ); }); if (result != null) { await GStorage.setting.put(SettingBoxKey.mediumCardWidth, result); SmartDialog.showToast('重启生效'); setState(); } }, leading: const Icon(Icons.calendar_view_week_outlined), title: '中卡列表宽度(dp)限制', getSubtitle: () => '当前:${GStorage.mediumCardWidth.toInt()}dp,屏幕宽度:${MediaQuery.of(Get.context!).size.width.toPrecision(2)}dp。宽度越小列数越多。', ), SettingsModel( settingsType: SettingsType.sw1tch, title: '播放页移除安全边距', subtitle: '隐藏状态栏、撑满屏幕,但播放控件仍处于安全域内', leading: Icon(Icons.fit_screen_outlined), setKey: SettingBoxKey.videoPlayerRemoveSafeArea, defaultVal: false, needReboot: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '动态页启用瀑布流', subtitle: '关闭会显示为单列', leading: Icon(Icons.view_array_outlined), setKey: SettingBoxKey.dynamicsWaterfallFlow, defaultVal: true, needReboot: true, ), SettingsModel( settingsType: SettingsType.normal, title: '动态页UP主显示位置', leading: const Icon(Icons.person_outlined), getSubtitle: () => '当前:${GStorage.upPanelPosition.labels}', onTap: (setState) async { UpPanelPosition? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '动态页UP主显示位置', value: GStorage.upPanelPosition, values: UpPanelPosition.values.map((e) { return {'title': e.labels, 'value': e}; }).toList(), ); }, ); if (result != null) { await GStorage.setting .put(SettingBoxKey.upPanelPosition, result.index); SmartDialog.showToast('重启生效'); setState(); } }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '动态页显示所有已关注UP主', subtitle: '并以最常访问排序UP', leading: Icon(Icons.people_alt_outlined), setKey: SettingBoxKey.dynamicsShowAllFollowedUp, defaultVal: false, ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) async { DynamicBadgeMode? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '动态未读标记', value: GStorage.dynamicBadgeType, values: DynamicBadgeMode.values.map((e) { return {'title': e.description, 'value': e}; }).toList(), ); }, ); if (result != null) { MainController mainController = Get.put(MainController()); mainController.dynamicBadgeMode = DynamicBadgeMode.values[result.index]; if (mainController.dynamicBadgeMode != DynamicBadgeMode.hidden) { mainController.getUnreadDynamic(); } await GStorage.setting .put(SettingBoxKey.dynamicBadgeMode, result.index); SmartDialog.showToast('设置成功'); setState(); } }, title: '动态未读标记', leading: const Icon(Icons.motion_photos_on_outlined), getSubtitle: () => '当前标记样式:${GStorage.dynamicBadgeType.description}', ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) async { DynamicBadgeMode? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '消息未读标记', value: GStorage.msgBadgeMode, values: DynamicBadgeMode.values.map((e) { return {'title': e.description, 'value': e}; }).toList(), ); }, ); if (result != null) { MainController mainController = Get.put(MainController()); mainController.msgBadgeMode = DynamicBadgeMode.values[result.index]; if (mainController.msgBadgeMode != DynamicBadgeMode.hidden) { mainController.queryUnreadMsg(); } else { mainController.msgUnReadCount.value = ''; } await GStorage.setting .put(SettingBoxKey.msgBadgeMode, result.index); SmartDialog.showToast('设置成功'); setState(); } }, title: '消息未读标记', leading: Icon(MdiIcons.bellBadgeOutline), getSubtitle: () => '当前标记样式:${GStorage.msgBadgeMode.description}', ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) async { List? result = await showDialog( context: Get.context!, builder: (context) { return MultiSelectDialog( title: '消息未读类型', initValues: GStorage.msgUnReadTypeV2, values: MsgUnReadType.values.map((e) { return {'title': e.title, 'value': e}; }).toList(), ); }, ); if (result != null) { MainController mainController = Get.put(MainController()); mainController.msgUnReadTypes = result; if (mainController.msgBadgeMode != DynamicBadgeMode.hidden) { mainController.queryUnreadMsg(); } await GStorage.setting.put(SettingBoxKey.msgUnReadTypeV2, result.map((item) => item.index).toList()..sort()); SmartDialog.showToast('设置成功'); setState(); } }, title: '消息未读类型', leading: Icon(MdiIcons.bellCogOutline), getSubtitle: () => '当前消息类型:${GStorage.msgUnReadTypeV2.map((item) => item.title).join('、')}', ), SettingsModel( settingsType: SettingsType.sw1tch, title: '首页顶栏收起', subtitle: '首页列表滑动时,收起顶栏', leading: Icon(Icons.vertical_align_top_outlined), setKey: SettingBoxKey.hideSearchBar, defaultVal: true, needReboot: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '首页底栏收起', subtitle: '首页列表滑动时,收起底栏', leading: Icon(Icons.vertical_align_bottom_outlined), setKey: SettingBoxKey.hideTabBar, defaultVal: true, needReboot: true, ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) { _showQualityDialog( context: Get.context!, title: '图片质量', initValue: GStorage.picQuality, callback: (picQuality) async { GlobalData().imgQuality = picQuality; await GStorage.setting .put(SettingBoxKey.defaultPicQa, picQuality); setState(); }, ); }, title: '图片质量', subtitle: '选择合适的图片清晰度,上限100%', leading: const Icon(Icons.image_outlined), getTrailing: () => Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( '${GStorage.picQuality}%', style: Theme.of(Get.context!).textTheme.titleSmall, ), ), ), // preview quality SettingsModel( settingsType: SettingsType.normal, onTap: (setState) { _showQualityDialog( context: Get.context!, title: '查看大图质量', initValue: GStorage.previewQ, callback: (picQuality) async { await GStorage.setting .put(SettingBoxKey.previewQuality, picQuality); setState(); }, ); }, title: '查看大图质量', subtitle: '选择合适的图片清晰度,上限100%', leading: const Icon(Icons.image_outlined), getTrailing: () => Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( '${GStorage.previewQ}%', style: Theme.of(Get.context!).textTheme.titleSmall, ), ), ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) async { double? result = await showDialog( context: Get.context!, builder: (context) { return SlideDialog( title: 'Toast不透明度', value: GStorage.toastOpacity, min: 0.0, max: 1.0, divisions: 10, ); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.defaultToastOp, result); SmartDialog.showToast('设置成功'); setState(); } }, leading: const Icon(Icons.opacity_outlined), title: '气泡提示不透明度', subtitle: '自定义气泡提示(Toast)不透明度', getTrailing: () => Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( GStorage.toastOpacity.toStringAsFixed(1), style: Theme.of(Get.context!).textTheme.titleSmall, ), ), ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) async { ThemeType? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '主题模式', value: GStorage.themeType, values: ThemeType.values.map( (e) { return {'title': e.description, 'value': e}; }, ).toList()); }, ); if (result != null) { GStorage.setting.put(SettingBoxKey.themeMode, result.index); Get.put(ColorSelectController()).themeType.value = result; Get.forceAppUpdate(); } }, leading: const Icon(Icons.flashlight_on_outlined), title: '主题模式', getSubtitle: () => '当前模式:${GStorage.themeType.description}', ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) => Get.toNamed('/colorSetting'), leading: const Icon(Icons.color_lens_outlined), title: '应用主题', getSubtitle: () => '当前主题:${Get.put(ColorSelectController()).type.value == 0 ? '动态取色' : '指定颜色'}', ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) async { int? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '首页启动页', value: GStorage.defaultHomePage, values: defaultNavigationBars.map((e) { return {'title': e['label'], 'value': e['id']}; }).toList(), ); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.defaultHomePage, result); SmartDialog.showToast('设置成功,重启生效'); setState(); } }, leading: const Icon(Icons.home_outlined), title: '默认启动页', getSubtitle: () => '当前启动页:${defaultNavigationBars.firstWhere((e) => e['id'] == GStorage.defaultHomePage)['label']}', ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) async { dynamic result = await Get.toNamed('/fontSizeSetting'); if (result != null) { Get.put(ColorSelectController()).currentTextScale.value = result; } }, title: '字体大小', leading: const Icon(Icons.format_size_outlined), getSubtitle: () => Get.put(ColorSelectController()).currentTextScale.value == 1.0 ? '默认' : Get.put(ColorSelectController()) .currentTextScale .value .toString(), ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) => Get.toNamed('/tabbarSetting'), title: '首页标签页', subtitle: '删除或调换首页标签页', leading: const Icon(Icons.toc_outlined), ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) => Get.toNamed('/navbarSetting'), title: 'Navbar编辑', leading: const Icon(Icons.toc_outlined), ), if (Platform.isAndroid) SettingsModel( settingsType: SettingsType.normal, onTap: (setState) => Get.toNamed('/displayModeSetting'), title: '屏幕帧率', leading: const Icon(Icons.autofps_select_outlined), ), ]; void _showQualityDialog({ required BuildContext context, required String title, required int initValue, required ValueChanged callback, }) { showDialog( context: context, builder: (context) { int picQuality = initValue; return AlertDialog( title: Text(title), contentPadding: const EdgeInsets.only(top: 20, left: 8, right: 8, bottom: 8), content: SizedBox( height: 40, child: Builder( builder: (context) => Slider( value: picQuality.toDouble(), min: 10, max: 100, divisions: 9, label: '$picQuality%', onChanged: (double val) { picQuality = val.toInt(); (context as Element).markNeedsBuild(); }, ), ), ), actions: [ TextButton( onPressed: () => Get.back(), child: Text('取消', style: TextStyle(color: Theme.of(context).colorScheme.outline))), TextButton( onPressed: () { Get.back(); callback(picQuality); SmartDialog.showToast('设置成功'); }, child: const Text('确定'), ) ], ); }, ); } List get playSettings => [ SettingsModel( settingsType: SettingsType.sw1tch, title: '弹幕开关', subtitle: '是否展示弹幕', leading: Icon(Icons.subtitles_outlined), setKey: SettingBoxKey.enableShowDanmaku, defaultVal: true, ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) => Get.toNamed('/playSpeedSet'), leading: const Icon(Icons.speed_outlined), title: '倍速设置', subtitle: '设置视频播放速度', ), SettingsModel( settingsType: SettingsType.sw1tch, title: '自动播放', subtitle: '进入详情页自动播放', leading: Icon(Icons.motion_photos_auto_outlined), setKey: SettingBoxKey.autoPlayEnable, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '双击快退/快进', subtitle: '左侧双击快退/右侧双击快进,关闭则双击均为暂停/播放', leading: Icon(Icons.touch_app_outlined), setKey: SettingBoxKey.enableQuickDouble, defaultVal: true, ), SettingsModel( settingsType: SettingsType.normal, title: '自动启用字幕', leading: const Icon(Icons.closed_caption_outlined), getSubtitle: () => '当前选择偏好:${SubtitlePreferenceCode.fromCode(GStorage.defaultSubtitlePreference)!.description}', onTap: (setState) async { String? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '字幕选择偏好', value: GStorage.setting.get(SettingBoxKey.subtitlePreference, defaultValue: SubtitlePreference.values.first.code), values: SubtitlePreference.values.map((e) { return {'title': e.description, 'value': e.code}; }).toList()); }, ); if (result != null) { await GStorage.setting .put(SettingBoxKey.subtitlePreference, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '竖屏扩大展示', subtitle: '小屏竖屏视频宽高比由16:9扩大至1:1(不支持收起);横屏适配时,扩大至9:16', leading: Icon(Icons.expand_outlined), setKey: SettingBoxKey.enableVerticalExpand, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '自动全屏', subtitle: '视频开始播放时进入全屏', leading: Icon(Icons.fullscreen_outlined), setKey: SettingBoxKey.enableAutoEnter, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '自动退出全屏', subtitle: '视频结束播放时退出全屏', leading: Icon(Icons.fullscreen_exit_outlined), setKey: SettingBoxKey.enableAutoExit, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '延长播放控件显示时间', subtitle: '开启后延长至30秒,便于屏幕阅读器滑动切换控件焦点', leading: Icon(Icons.timer_outlined), setKey: SettingBoxKey.enableLongShowControl, defaultVal: false), SettingsModel( settingsType: SettingsType.sw1tch, title: '全向旋转', subtitle: '小屏可受重力转为临时全屏,若系统锁定旋转仍触发请关闭,关闭会影响横屏适配', leading: Icon(Icons.screen_rotation_alt_outlined), setKey: SettingBoxKey.allowRotateScreen, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '后台播放', subtitle: '进入后台时继续播放', leading: Icon(Icons.motion_photos_pause_outlined), setKey: SettingBoxKey.continuePlayInBackground, defaultVal: false, ), if (Platform.isAndroid) SettingsModel( settingsType: SettingsType.sw1tch, title: '后台画中画', subtitle: '进入后台时以小窗形式(PiP)播放', leading: const Icon(Icons.picture_in_picture_outlined), setKey: SettingBoxKey.autoPiP, defaultVal: false, onChanged: (val) { if (val && !GStorage.setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: true)) { SmartDialog.showToast('建议开启后台音频服务'); } }), if (Platform.isAndroid) SettingsModel( settingsType: SettingsType.sw1tch, title: '画中画不加载弹幕', subtitle: '当弹幕开关开启时,小窗屏蔽弹幕以获得较好的体验', leading: Icon(Icons.subtitles_off_outlined), setKey: SettingBoxKey.pipNoDanmaku, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '全屏手势反向', subtitle: '默认播放器中部向上滑动进入全屏,向下退出\n开启后向下全屏,向上退出', leading: Icon(Icons.swap_vert_outlined), setKey: SettingBoxKey.fullScreenGestureReverse, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '观看人数', subtitle: '展示同时在看人数', leading: Icon(Icons.people_outlined), setKey: SettingBoxKey.enableOnlineTotal, defaultVal: false, ), SettingsModel( settingsType: SettingsType.normal, title: '默认全屏方向', leading: const Icon(Icons.open_with_outlined), getSubtitle: () => '当前全屏方向:${FullScreenModeCode.fromCode(GStorage.defaultFullScreenMode)!.description}', onTap: (setState) async { int? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '默认全屏方向', value: GStorage.defaultFullScreenMode, values: FullScreenMode.values.map((e) { return {'title': e.description, 'value': e.code}; }).toList()); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.fullScreenMode, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.normal, title: '底部进度条展示', leading: const Icon(Icons.border_bottom_outlined), getSubtitle: () => '当前展示方式:${BtmProgresBehaviorCode.fromCode(GStorage.defaultBtmProgressBehavior)!.description}', onTap: (setState) async { int? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '底部进度条展示', value: GStorage.defaultBtmProgressBehavior, values: BtmProgressBehavior.values.map((e) { return {'title': e.description, 'value': e.code}; }).toList()); }, ); if (result != null) { await GStorage.setting .put(SettingBoxKey.btmProgressBehavior, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '后台音频服务', subtitle: '避免画中画没有播放暂停功能', leading: Icon(Icons.volume_up_outlined), setKey: SettingBoxKey.enableBackgroundPlay, defaultVal: true, ), ]; List get videoSettings => [ SettingsModel( settingsType: SettingsType.sw1tch, title: '开启硬解', subtitle: '以较低功耗播放视频,若异常卡死请关闭', leading: Icon(Icons.flash_on_outlined), setKey: SettingBoxKey.enableHA, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '免登录1080P', subtitle: '免登录查看1080P视频', leading: Icon(Icons.hd_outlined), setKey: SettingBoxKey.p1080, defaultVal: true, ), SettingsModel( settingsType: SettingsType.normal, title: 'B站定向流量支持', subtitle: '若套餐含B站定向流量,则会自动使用。可查阅运营商的流量记录确认。', leading: const Icon(Icons.perm_data_setting_outlined), getTrailing: () => Transform.scale( alignment: Alignment.centerRight, scale: 0.8, child: Switch( thumbIcon: WidgetStateProperty.resolveWith( (Set states) { if (states.isNotEmpty && states.first == WidgetState.selected) { return const Icon(Icons.lock_outline_rounded); } return null; }), value: true, onChanged: (_) {}, ), ), ), SettingsModel( settingsType: SettingsType.normal, title: 'CDN 设置', leading: Icon(MdiIcons.cloudPlusOutline), getSubtitle: () => '当前使用:${CDNServiceCode.fromCode(GStorage.defaultCDNService)!.description},部分 CDN 可能失效,如无法播放请尝试切换', onTap: (setState) async { String? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: 'CDN 设置', value: GStorage.defaultCDNService, values: CDNService.values.map((e) { return {'title': e.description, 'value': e.code}; }).toList()); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.CDNService, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: 'CDN 测速', leading: const Icon(Icons.speed), subtitle: '测速通过模拟加载视频实现,注意流量消耗,结果仅供参考', setKey: SettingBoxKey.cdnSpeedTest, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '音频不跟随 CDN 设置', subtitle: '直接采用备用 URL,可解决部分视频无声', leading: Icon(MdiIcons.musicNotePlus), setKey: SettingBoxKey.disableAudioCDN, defaultVal: true, ), SettingsModel( settingsType: SettingsType.normal, title: '默认画质', leading: const Icon(Icons.video_settings_outlined), getSubtitle: () => '当前画质:${VideoQualityCode.fromCode(GStorage.defaultVideoQa)!.description!}', onTap: (setState) async { int? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '默认画质', value: GStorage.defaultVideoQa, values: VideoQuality.values.reversed.map((e) { return {'title': e.description, 'value': e.code}; }).toList(), ); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.defaultVideoQa, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.normal, title: '蜂窝网络画质', leading: const Icon(Icons.video_settings_outlined), getSubtitle: () => '当前画质:${VideoQualityCode.fromCode(GStorage.defaultVideoQaCellular)!.description!}', onTap: (setState) async { int? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '蜂窝网络画质', value: GStorage.defaultVideoQaCellular, values: VideoQuality.values.reversed.map((e) { return {'title': e.description, 'value': e.code}; }).toList(), ); }, ); if (result != null) { await GStorage.setting .put(SettingBoxKey.defaultVideoQaCellular, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.normal, title: '默认音质', leading: const Icon(Icons.music_video_outlined), getSubtitle: () => '当前音质:${AudioQualityCode.fromCode(GStorage.defaultAudioQa)!.description!}', onTap: (setState) async { int? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '默认音质', value: GStorage.defaultAudioQa, values: AudioQuality.values.reversed.map((e) { return {'title': e.description, 'value': e.code}; }).toList(), ); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.defaultAudioQa, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.normal, title: '蜂窝网络音质', leading: const Icon(Icons.music_video_outlined), getSubtitle: () => '当前音质:${AudioQualityCode.fromCode(GStorage.defaultAudioQaCellular)!.description!}', onTap: (setState) async { int? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '蜂窝网络音质', value: GStorage.defaultAudioQaCellular, values: AudioQuality.values.reversed.map((e) { return {'title': e.description, 'value': e.code}; }).toList(), ); }, ); if (result != null) { await GStorage.setting .put(SettingBoxKey.defaultAudioQaCellular, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.normal, title: '首选解码格式', leading: const Icon(Icons.movie_creation_outlined), getSubtitle: () => '首选解码格式:${VideoDecodeFormatsCode.fromCode(GStorage.defaultDecode)!.description!},请根据设备支持情况与需求调整', onTap: (setState) async { String? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '默认解码格式', value: GStorage.defaultDecode, values: VideoDecodeFormats.values.map((e) { return {'title': e.description, 'value': e.code}; }).toList()); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.defaultDecode, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.normal, title: '次选解码格式', getSubtitle: () => '非杜比视频次选:${VideoDecodeFormatsCode.fromCode(GStorage.secondDecode)!.description!},仍无则选择首个提供的解码格式', leading: const Icon(Icons.swap_horizontal_circle_outlined), onTap: (setState) async { String? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '次选解码格式', value: GStorage.secondDecode, values: VideoDecodeFormats.values.map((e) { return {'title': e.description, 'value': e.code}; }).toList()); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.secondDecode, result); setState(); } }, ), if (Platform.isAndroid) SettingsModel( settingsType: SettingsType.sw1tch, title: '优先使用 OpenSL ES 输出音频', leading: Icon(Icons.speaker_outlined), subtitle: '关闭则优先使用AudioTrack输出音频(此项即mpv的--ao),若遇系统音效丢失、无声、音画不同步等问题请尝试关闭。', setKey: SettingBoxKey.useOpenSLES, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '扩大缓冲区', leading: Icon(Icons.storage_outlined), subtitle: '默认缓冲区为视频4MB/直播16MB,开启后为32MB/64MB,加载时间变长', setKey: SettingBoxKey.expandBuffer, defaultVal: false, ), //video-sync SettingsModel( settingsType: SettingsType.normal, title: '视频同步', leading: const Icon(Icons.view_timeline_outlined), getSubtitle: () => '当前:${GStorage.videoSync}(此项即mpv的--video-sync)', onTap: (setState) async { String? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '视频同步', value: GStorage.videoSync, values: [ 'audio', 'display-resample', 'display-resample-vdrop', 'display-resample-desync', 'display-tempo', 'display-vdrop', 'display-adrop', 'display-desync', 'desync' ].map((e) { return {'title': e, 'value': e}; }).toList()); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.videoSync, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.normal, title: '硬解模式', leading: const Icon(Icons.memory_outlined), getSubtitle: () => '当前:${GStorage.hardwareDecoding}(此项即mpv的--hwdec)', onTap: (setState) async { String? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '硬解模式', value: GStorage.hardwareDecoding, values: ['auto', 'auto-copy', 'auto-safe', 'no', 'yes'].map((e) { return {'title': e, 'value': e}; }).toList()); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.hardwareDecoding, result); setState(); } }, ), ]; List get recommendSettings => [ SettingsModel( settingsType: SettingsType.normal, title: '首页推荐类型', leading: const Icon(Icons.model_training_outlined), getSubtitle: () => '当前使用「${GStorage.defaultRcmdType}端」推荐¹', onTap: (setState) async { String? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '推荐类型', value: GStorage.defaultRcmdType, values: RcmdType.values.map((e) { return {'title': e.labels, 'value': e.values}; }).toList(), ); }, ); if (result != null) { if (result == 'app') { if (GStorage.isLogin.not) { SmartDialog.showToast('尚未登录,无法收到个性化推荐'); } } await GStorage.setting.put(SettingBoxKey.defaultRcmdType, result); SmartDialog.showToast('下次启动时生效'); setState(); } }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '推荐动态', subtitle: '是否在推荐内容中展示动态(仅app端)', leading: Icon(Icons.motion_photos_on_outlined), setKey: SettingBoxKey.enableRcmdDynamic, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '保留首页推荐刷新', subtitle: '下拉刷新时保留上次内容', leading: Icon(Icons.refresh), setKey: SettingBoxKey.enableSaveLastData, defaultVal: false, onChanged: (value) { try { Get.find().enableSaveLastData = value; } catch (e) { debugPrint('$e'); } }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '显示上次看到位置提示', subtitle: '保留上次推荐时,在上次刷新位置显示提示', leading: Icon(Icons.tips_and_updates_outlined), setKey: SettingBoxKey.savedRcmdTip, defaultVal: true, onChanged: (value) { try { RcmdController ctr = Get.find(); ctr.savedRcmdTip = value; if (value.not) { ctr.lastRefreshAt = null; } } catch (e) { debugPrint('$e'); } }, ), SettingsModel(settingsType: SettingsType.divider), SettingsModel( settingsType: SettingsType.normal, leading: const Icon(Icons.thumb_up_outlined), title: '点赞率过滤', getSubtitle: () => '过滤掉点赞数/播放量「小于${GStorage.minLikeRatioForRecommend}%」的推荐视频(仅web端)', onTap: (setState) async { int? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '选择点赞率(0即不过滤)', value: GStorage.minLikeRatioForRecommend, values: [0, 1, 2, 3, 4].map( (e) { return {'title': '$e %', 'value': e}; }, ).toList()); }, ); if (result != null) { await GStorage.setting .put(SettingBoxKey.minLikeRatioForRecommend, result); RecommendFilter.update(); setState(); } }, ), getBanwordModel( title: '标题关键词过滤', key: SettingBoxKey.banWordForRecommend, getBanWord: () => GStorage.banWordForRecommend, ), getBanwordModel( title: '推荐(app端)/热门/分区: 视频分区关键词过滤', key: SettingBoxKey.banWordForZone, getBanWord: () => GStorage.banWordForZone, ), SettingsModel( settingsType: SettingsType.normal, title: '视频时长过滤', leading: const Icon(Icons.timelapse_outlined), getSubtitle: () => '过滤掉时长「小于${GStorage.minDurationForRcmd}秒」的推荐视频', onTap: (setState) async { const List defDurations = [0, 30, 60, 90, 120]; int? result = await showDialog( context: Get.context!, builder: (context) { int minDurationForRcmd = GStorage.minDurationForRcmd; return SelectDialog( title: '选择时长(0即不过滤)', value: minDurationForRcmd, values: [ ...[ ...defDurations, if (defDurations.contains(minDurationForRcmd).not) minDurationForRcmd, ]..sort(), -1, ].map((e) { if (e == -1) { return {'title': '自定义', 'value': e}; } return {'title': '$e 秒', 'value': e}; }).toList()); }, ); if (result != null) { void updateDuration(int value) async { await GStorage.setting .put(SettingBoxKey.minDurationForRcmd, value); RecommendFilter.update(); setState(); } if (result == -1) { showDialog( context: Get.context!, builder: (context) { String duration = ''; return AlertDialog( title: const Text('自定义时长'), content: TextField( autofocus: true, onChanged: (value) => duration = value, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'\d+')), ], decoration: const InputDecoration(suffixText: 's'), ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () { Get.back(); updateDuration(int.tryParse(duration) ?? 0); }, child: Text('确定'), ), ], ); }, ); } else { updateDuration(result); } } }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '已关注UP豁免推荐过滤', subtitle: '推荐中已关注用户发布的内容不会被过滤', leading: const Icon(Icons.favorite_border_outlined), setKey: SettingBoxKey.exemptFilterForFollowed, defaultVal: true, onChanged: (_) => {RecommendFilter.update}, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '过滤器也应用于相关视频', subtitle: '视频详情页的相关视频也进行过滤²', leading: const Icon(Icons.explore_outlined), setKey: SettingBoxKey.applyFilterToRelatedVideos, defaultVal: true, onChanged: (_) => {RecommendFilter.update}, ), ]; List get privacySettings => [ SettingsModel( settingsType: SettingsType.normal, onTap: (setState) { if (GStorage.isLogin.not) { SmartDialog.showToast('登录后查看'); return; } Get.toNamed('/blackListPage'); }, title: '黑名单管理', subtitle: '已拉黑用户', leading: const Icon(Icons.block), ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) { MineController.onChangeAnonymity(Get.context!); setState(); }, leading: const Icon(Icons.privacy_tip_outlined), getTitle: () => MineController.anonymity.value ? '退出无痕模式' : '进入无痕模式', getSubtitle: () => MineController.anonymity.value ? '已进入无痕模式,搜索、观看视频/直播不携带Cookie与CSRF,其余操作不受影响' : '未开启无痕模式,将使用账户信息提供完整服务', ), SettingsModel( settingsType: SettingsType.normal, onTap: (setState) { showDialog( context: Get.context!, builder: (context) { return AlertDialog( title: const Text('查看详情'), content: Text(ApiInterceptor.anonymityList.join('\n')), actions: [ TextButton( onPressed: () async { Get.back(); }, child: const Text('确认'), ) ], ); }, ); }, leading: const Icon(Icons.flag_outlined), title: '了解无痕模式', subtitle: '查看无痕模式作用的API列表', ), ]; List get extraSettings => [ SettingsModel( settingsType: SettingsType.sw1tch, title: '空降助手', subtitle: '点击配置', setKey: SettingBoxKey.enableSponsorBlock, defaultVal: false, onTap: () => Get.toNamed('/sponsorBlock'), leading: Stack( alignment: Alignment.center, children: [ const Icon(Icons.shield_outlined), Icon( Icons.play_arrow_rounded, size: 15, ), ], ), ), SettingsModel( settingsType: SettingsType.sw1tch, title: '检查未读动态', subtitle: '点击设置检查周期(min)', leading: const Icon(Icons.notifications_none), setKey: SettingBoxKey.checkDynamic, defaultVal: true, onChanged: (value) { Get.find().checkDynamic = value; }, onTap: () { int dynamicPeriod = GStorage.dynamicPeriod; showDialog( context: Get.context!, builder: (context) { return AlertDialog( title: const Text('检查周期'), content: TextFormField( autofocus: true, initialValue: dynamicPeriod.toString(), keyboardType: TextInputType.numberWithOptions(), onChanged: (value) { dynamicPeriod = int.tryParse(value) ?? 5; }, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'\d+')), ], decoration: InputDecoration(suffixText: 'min'), ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), ), ), TextButton( onPressed: () { Get.back(); GStorage.setting .put(SettingBoxKey.dynamicPeriod, dynamicPeriod); Get.find().dynamicPeriod = dynamicPeriod; }, child: Text('确定'), ) ], ); }, ); }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '使用gRPC加载评论', subtitle: '如无法加载评论,可关闭\n非gRPC楼中楼无法定位评论、按热度/时间排序、查看对话', leading: SizedBox( height: 24, width: 24, child: Icon(MdiIcons.google, size: 20), ), setKey: SettingBoxKey.grpcReply, defaultVal: true, onChanged: (value) { GlobalData().grpcReply = value; }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '显示视频分段信息', leading: Transform.rotate( angle: pi / 2, child: Icon(MdiIcons.viewHeadline), ), setKey: SettingBoxKey.showViewPoints, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '视频页显示相关视频', leading: Icon(MdiIcons.motionPlayOutline), setKey: SettingBoxKey.showRelatedVideo, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '显示视频评论', leading: Icon(MdiIcons.commentTextOutline), setKey: SettingBoxKey.showVideoReply, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '显示番剧评论', leading: Icon(MdiIcons.commentTextOutline), setKey: SettingBoxKey.showBangumiReply, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '默认展开视频简介', leading: const Icon(Icons.expand_more), setKey: SettingBoxKey.alwaysExapndIntroPanel, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '横屏自动展开视频简介', leading: const Icon(Icons.expand_more), setKey: SettingBoxKey.exapndIntroPanelH, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '横屏分P/合集列表显示在Tab栏', leading: const Icon(Icons.format_list_numbered_rtl_sharp), setKey: SettingBoxKey.horizontalSeasonPanel, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '横屏播放页在侧栏打开UP主页', leading: const Icon(Icons.account_circle_outlined), setKey: SettingBoxKey.horizontalMemberPage, defaultVal: false, ), SettingsModel( settingsType: SettingsType.normal, title: '评论折叠行数', subtitle: '0行为不折叠', leading: const Icon(Icons.compress), setKey: SettingBoxKey.replyLengthLimit, getTrailing: () => Text( '${GlobalData().replyLengthLimit}行', style: Theme.of(Get.context!).textTheme.titleSmall, ), onTap: (setState) { String replyLengthLimit = GlobalData().replyLengthLimit.toString(); showDialog( context: Get.context!, builder: (context) { return AlertDialog( title: const Text('评论折叠行数'), content: TextFormField( autofocus: true, initialValue: replyLengthLimit, keyboardType: TextInputType.numberWithOptions(), onChanged: (value) { replyLengthLimit = value; }, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'\d+')), ], decoration: InputDecoration(suffixText: '行'), ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), ), ), TextButton( onPressed: () async { Get.back(); GlobalData().replyLengthLimit = int.tryParse(replyLengthLimit) ?? 6; await GStorage.setting.put( SettingBoxKey.replyLengthLimit, GlobalData().replyLengthLimit, ); setState(); }, child: Text('确定'), ) ], ); }, ); }, ), SettingsModel( settingsType: SettingsType.normal, title: '弹幕行高', subtitle: '默认1.6', setKey: SettingBoxKey.danmakuLineHeight, leading: const Icon(Icons.subtitles_outlined), getTrailing: () => Text( GStorage.danmakuLineHeight.toString(), style: Theme.of(Get.context!).textTheme.titleSmall, ), onTap: (setState) { String danmakuLineHeight = GStorage.danmakuLineHeight.toString(); showDialog( context: Get.context!, builder: (context) { return AlertDialog( title: const Text('弹幕行高'), content: TextFormField( autofocus: true, initialValue: danmakuLineHeight, keyboardType: TextInputType.numberWithOptions(decimal: true), onChanged: (value) { danmakuLineHeight = value; }, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')), ], ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), ), ), TextButton( onPressed: () async { Get.back(); await GStorage.setting.put( SettingBoxKey.danmakuLineHeight, max( 1.0, double.tryParse(danmakuLineHeight)?.toPrecision(1) ?? 1.6, ), ); setState(); }, child: Text('确定'), ) ], ); }, ); }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '显示视频警告/争议信息', leading: const Icon(Icons.warning_amber_rounded), setKey: SettingBoxKey.showArgueMsg, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '分P/合集:倒序播放从首集开始播放', subtitle: '开启则自动切换为倒序首集,否则保持当前集', leading: Icon(MdiIcons.sort), setKey: SettingBoxKey.reverseFromFirst, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '禁用 SSL 证书验证', subtitle: '谨慎开启,禁用容易受到中间人攻击', leading: Icon(Icons.security), needReboot: true, setKey: SettingBoxKey.badCertificateCallback, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '显示继续播放分P提示', leading: Icon(Icons.local_parking), setKey: SettingBoxKey.continuePlayingPart, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '横屏在侧栏打开图片预览', leading: Icon(Icons.photo_outlined), setKey: SettingBoxKey.horizontalPreview, defaultVal: false, ), getBanwordModel( title: '评论关键词过滤', key: SettingBoxKey.banWordForReply, getBanWord: () => GStorage.banWordForReply, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '使用外部浏览器打开链接', leading: Icon(Icons.open_in_browser), setKey: SettingBoxKey.openInBrowser, defaultVal: false, ), SettingsModel( settingsType: SettingsType.normal, title: '刷新滑动距离', leading: Icon(Icons.refresh), setKey: SettingBoxKey.refreshDragPercentage, getSubtitle: () => '当前滑动距离: ${GStorage.refreshDragPercentage}x', onTap: (setState) async { double? result = await showDialog( context: Get.context!, builder: (context) { return SlideDialog( title: '刷新滑动距离', min: 0.1, max: 0.5, divisions: 8, precise: 2, value: GStorage.refreshDragPercentage, suffix: 'x', ); }, ); if (result != null) { kDragContainerExtentPercentage = result; await GStorage.setting .put(SettingBoxKey.refreshDragPercentage, result); Get.forceAppUpdate(); setState(); } }, ), SettingsModel( settingsType: SettingsType.normal, title: '刷新指示器高度', leading: Icon(Icons.height), setKey: SettingBoxKey.refreshDisplacement, getSubtitle: () => '当前指示器高度: ${GStorage.refreshDisplacement}', onTap: (setState) async { double? result = await showDialog( context: Get.context!, builder: (context) { return SlideDialog( title: '刷新指示器高度', min: 10.0, max: 100.0, divisions: 9, value: GStorage.refreshDisplacement, ); }, ); if (result != null) { displacement = result; await GStorage.setting .put(SettingBoxKey.refreshDisplacement, result); Get.forceAppUpdate(); setState(); } }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '显示会员彩色弹幕', leading: Icon(MdiIcons.gradientHorizontal), setKey: SettingBoxKey.showVipDanmaku, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '合并弹幕', subtitle: '合并一段时间内获取到的相同弹幕', leading: Icon(Icons.merge), setKey: SettingBoxKey.mergeDanmaku, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '显示热门推荐', subtitle: '热门页面显示每周必看等推荐内容入口', leading: Icon(Icons.local_fire_department_outlined), setKey: SettingBoxKey.showHotRcmd, defaultVal: false, onChanged: (value) { try { Get.find().showHotRcmd.value = value; } catch (_) {} }, ), SettingsModel( settingsType: SettingsType.normal, title: '音量均衡', setKey: SettingBoxKey.audioNormalization, leading: const Icon(Icons.multitrack_audio), getSubtitle: () { String audioNormalization = GStorage.audioNormalization; audioNormalization = switch (audioNormalization) { '0' => AudioNormalization.disable.title, '1' => AudioNormalization.dynaudnorm.title, '2' => AudioNormalization.loudnorm.title, _ => audioNormalization, }; return '当前:「$audioNormalization」'; }, onTap: (setState) async { String? result = await showDialog( context: Get.context!, builder: (context) { String audioNormalization = GStorage.audioNormalization; Set values = {'0', '1', '2', audioNormalization, '3'}; return SelectDialog( title: '音量均衡', value: audioNormalization, values: values.map((e) { return { 'title': switch (e) { '0' => AudioNormalization.disable.title, '1' => AudioNormalization.dynaudnorm.title, '2' => AudioNormalization.loudnorm.title, '3' => AudioNormalization.custom.title, _ => e, }, 'value': e, }; }).toList()); }, ); if (result != null) { if (result == '3') { String param = ''; showDialog( context: Get.context!, builder: (context) { return AlertDialog( title: const Text('自定义参数'), content: TextField( autofocus: true, onChanged: (value) => param = value, ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), ), ), TextButton( onPressed: () async { Get.back(); await GStorage.setting .put(SettingBoxKey.audioNormalization, param); setState(); }, child: Text('确定'), ), ], ); }); } else { await GStorage.setting .put(SettingBoxKey.audioNormalization, result); setState(); } } }, ), SettingsModel( settingsType: SettingsType.normal, title: '超分辨率', leading: const Icon(Icons.stay_current_landscape_outlined), getSubtitle: () => '当前:「${SuperResolutionType.values[GStorage.superResolutionType].title}」\n默认设置对番剧生效, 其他视频默认关闭\n超分辨率需要启用硬件解码, 若启用硬件解码后仍然不生效, 尝试切换硬件解码器为 auto-copy', onTap: (setState) async { SuperResolutionType? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '超分辨率', value: SuperResolutionType.values[GStorage.superResolutionType], values: SuperResolutionType.values.map((e) { return {'title': e.title, 'value': e}; }).toList()); }, ); if (result != null) { await GStorage.setting .put(SettingBoxKey.superResolutionType, result.index); setState(); } }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '提前初始化播放器', subtitle: '相对减少手动播放加载时间', leading: Icon(Icons.play_circle_outlined), setKey: SettingBoxKey.preInitPlayer, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '首页切换页面动画', leading: Icon(Icons.home_outlined), setKey: SettingBoxKey.mainTabBarView, defaultVal: false, needReboot: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '搜索建议', leading: Icon(Icons.search), setKey: SettingBoxKey.searchSuggestion, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '展示头像/评论/动态装饰', leading: Icon(MdiIcons.stickerCircleOutline), setKey: SettingBoxKey.showDynDecorate, defaultVal: true, onChanged: (value) => ModuleAuthorModel.showDynDecorate = value, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '预览 Live Photo', subtitle: '开启则以视频形式预览 Live Photo,否则预览静态图片', leading: Icon(Icons.image_outlined), setKey: SettingBoxKey.enableLivePhoto, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '滑动跳转预览视频缩略图', leading: Icon(Icons.preview_outlined), setKey: SettingBoxKey.showSeekPreview, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '显示高能进度条', subtitle: '高能进度条反应了在时域上,单位时间内弹幕发送量的变化趋势', leading: Icon(Icons.show_chart), setKey: SettingBoxKey.showDmChart, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '发评反诈', subtitle: '发送评论后检查评论是否可见', leading: Stack( alignment: Alignment.center, children: [ const Icon(Icons.shield), Icon( Icons.reply, size: 16, color: Theme.of(Get.context!).colorScheme.surface, ), ], ), setKey: SettingBoxKey.enableCommAntifraud, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, enableFeedback: true, setKey: SettingBoxKey.feedBackEnable, onChanged: (value) { feedBack(); }, leading: const Icon(Icons.vibration_outlined), title: '震动反馈', subtitle: '请确定手机设置中已开启震动反馈', ), SettingsModel( settingsType: SettingsType.sw1tch, title: '大家都在搜', subtitle: '是否展示「大家都在搜」', leading: Icon(Icons.data_thresholding_outlined), setKey: SettingBoxKey.enableHotKey, defaultVal: true, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '搜索默认词', subtitle: '是否展示搜索框默认词', leading: const Icon(Icons.whatshot_outlined), setKey: SettingBoxKey.enableSearchWord, defaultVal: true, onChanged: (val) { Get.find().defaultSearch.value = ''; }, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '快速收藏', subtitle: '点按收藏至默认,长按选择文件夹', leading: Icon(Icons.bookmark_add_outlined), setKey: SettingBoxKey.enableQuickFav, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '评论区搜索关键词', subtitle: '展示评论区搜索关键词', leading: Icon(Icons.search_outlined), setKey: SettingBoxKey.enableWordRe, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '启用AI总结', subtitle: '视频详情页开启AI总结', leading: Icon(Icons.engineering_outlined), setKey: SettingBoxKey.enableAi, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '消息页禁用"收到的赞"功能', subtitle: '禁止打开入口,降低网络社交依赖', leading: Icon(Icons.beach_access_outlined), setKey: SettingBoxKey.disableLikeMsg, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '默认展示评论区', subtitle: '在视频详情页默认切换至评论区页(仅Tab型布局)', leading: Icon(Icons.mode_comment_outlined), setKey: SettingBoxKey.defaultShowComment, defaultVal: false, ), SettingsModel( settingsType: SettingsType.normal, title: '评论展示', setKey: SettingBoxKey.replySortType, leading: const Icon(Icons.whatshot_outlined), getSubtitle: () => '当前优先展示「${ReplySortType.values[GStorage.defaultReplySort].title}」', onTap: (setState) async { int? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '评论展示', value: GStorage.defaultReplySort, values: ReplySortType.values.map((e) { return {'title': e.title, 'value': e.index}; }).toList(), ); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.replySortType, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.normal, title: '动态展示', setKey: SettingBoxKey.defaultDynamicType, leading: const Icon(Icons.dynamic_feed_outlined), getSubtitle: () => '当前优先展示「${DynamicsType.values[GStorage.defaultDynamicType].labels}」', onTap: (setState) async { int? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '动态展示', value: GStorage.defaultDynamicType, values: DynamicsType.values.sublist(0, 4).map((e) { return {'title': e.labels, 'value': e.index}; }).toList()); }, ); if (result != null) { await GStorage.setting .put(SettingBoxKey.defaultDynamicType, result); setState(); } }, ), SettingsModel( settingsType: SettingsType.normal, title: '用户页默认展示TAB', setKey: SettingBoxKey.memberTab, leading: const Icon(Icons.tab), getSubtitle: () => '当前优先展示「${GStorage.memberTab.title}」', onTap: (setState) async { MemberTabType? result = await showDialog( context: Get.context!, builder: (context) { return SelectDialog( title: '用户页默认展示TAB', value: GStorage.memberTab, values: MemberTabType.values.map((e) { return {'title': e.title, 'value': e}; }).toList()); }, ); if (result != null) { await GStorage.setting.put(SettingBoxKey.memberTab, result.index); setState(); } }, ), SettingsModel( settingsType: SettingsType.sw1tch, onTap: () { String systemProxyHost = GStorage.defaultSystemProxyHost; String systemProxyPort = GStorage.defaultSystemProxyPort; showDialog( context: Get.context!, builder: (context) { return AlertDialog( title: const Text('设置代理'), content: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 6), TextField( decoration: InputDecoration( isDense: true, labelText: systemProxyHost != '' ? systemProxyHost : '请输入Host,使用 . 分割', border: OutlineInputBorder( borderRadius: BorderRadius.circular(6.0), ), hintText: systemProxyHost, ), onChanged: (e) { systemProxyHost = e; }, ), const SizedBox(height: 10), TextField( keyboardType: TextInputType.number, decoration: InputDecoration( isDense: true, labelText: systemProxyPort != '' ? systemProxyPort : '请输入Port', border: OutlineInputBorder( borderRadius: BorderRadius.circular(6.0), ), hintText: systemProxyPort, ), onChanged: (e) { systemProxyPort = e; }, ), ], ), actions: [ TextButton( onPressed: () async { Get.back(); }, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () async { Get.back(); GStorage.setting .put(SettingBoxKey.systemProxyHost, systemProxyHost); GStorage.setting .put(SettingBoxKey.systemProxyPort, systemProxyPort); }, child: const Text('确认'), ) ], ); }, ); }, leading: const Icon(Icons.airplane_ticket_outlined), title: '设置代理', subtitle: '设置代理 host:port', setKey: SettingBoxKey.enableSystemProxy, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '自动清除缓存', subtitle: '每次启动时清除缓存', leading: Icon(Icons.auto_delete_outlined), setKey: SettingBoxKey.autoClearCache, defaultVal: false, ), SettingsModel( settingsType: SettingsType.sw1tch, title: '检查更新', subtitle: '每次启动时检查是否需要更新', leading: const Icon(Icons.system_update_alt_outlined), setKey: SettingBoxKey.autoUpdate, defaultVal: true, onChanged: (val) { if (val) { Utils.checkUpdate(false); } }, ), ]; SettingsModel getBanwordModel({ required String title, required String key, required Function getBanWord, }) { return SettingsModel( settingsType: SettingsType.normal, leading: const Icon(Icons.filter_alt_outlined), title: title, getSubtitle: () { String banWord = getBanWord(); return banWord.isEmpty ? "点击添加" : banWord; }, onTap: (setState) async { String banWord = getBanWord(); await showDialog( context: Get.context!, builder: (context) { return AlertDialog( title: Text(title), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('使用|隔开,如:尝试|测试'), TextFormField( autofocus: true, initialValue: banWord, textInputAction: TextInputAction.newline, minLines: 1, maxLines: 4, onChanged: (value) => banWord = value, ) ], ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( child: const Text('保存'), onPressed: () async { Get.back(); await GStorage.setting.put(key, banWord); setState(); SmartDialog.showToast('已保存'); }, ), ], ); }, ); }, ); }