Files
PiliPlus/lib/pages/setting/models/extra_settings.dart
KoishiMoe 836ab311d6 feat: add option to turn off dynamic interactions (#1798)
* add option to turn off dynamic interactions

* update

Signed-off-by: dom <githubaccount56556@proton.me>

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-09 11:35:47 +08:00

1257 lines
40 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io';
import 'dart:math' show pi, max;
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart'
show CustomGridView, ImageModel;
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
import 'package:PiliPlus/grpc/reply.dart';
import 'package:PiliPlus/http/fav.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/audio_normalization.dart';
import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart';
import 'package:PiliPlus/models/common/member/tab_type.dart';
import 'package:PiliPlus/models/common/reply/reply_sort_type.dart';
import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart';
import 'package:PiliPlus/models/common/super_resolution_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart'
show DynamicsDataModel, ItemModulesModel;
import 'package:PiliPlus/pages/common/slide/common_slide_page.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/setting/models/model.dart';
import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart';
import 'package:PiliPlus/pages/setting/widgets/slide_dialog.dart';
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart';
import 'package:PiliPlus/services/download/download_service.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/cache_manager.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/path_utils.dart';
import 'package:PiliPlus/utils/platform_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/update.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
List<SettingsModel> get extraSettings => [
if (PlatformUtils.isDesktop) ...[
SwitchModel(
title: '退出时最小化',
leading: const Icon(Icons.exit_to_app),
setKey: SettingBoxKey.minimizeOnExit,
defaultVal: true,
onChanged: (value) {
try {
Get.find<MainController>().minimizeOnExit = value;
} catch (_) {}
},
),
NormalModel(
title: '缓存路径',
getSubtitle: () => downloadPath,
leading: const Icon(Icons.storage),
onTap: (context, setState) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
onTap: () {
Get.back();
Utils.copyText(downloadPath);
},
dense: true,
title: const Text('复制', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () {
Get.back();
final defPath = defDownloadPath;
if (downloadPath == defPath) return;
downloadPath = defPath;
setState();
Get.find<DownloadService>().initDownloadList();
GStorage.setting.delete(SettingBoxKey.downloadPath);
},
dense: true,
title: const Text('重置', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () async {
Get.back();
final path = await FilePicker.platform.getDirectoryPath();
if (path == null || path == downloadPath) return;
downloadPath = path;
setState();
Get.find<DownloadService>().initDownloadList();
GStorage.setting.put(SettingBoxKey.downloadPath, path);
},
dense: true,
title: const Text('设置新路径', style: TextStyle(fontSize: 14)),
),
],
),
);
},
);
},
),
],
SwitchModel(
title: '空降助手',
subtitle: '点击配置',
setKey: SettingBoxKey.enableSponsorBlock,
defaultVal: false,
onTap: (context) => Get.toNamed('/sponsorBlock'),
leading: const Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Icon(Icons.shield_outlined),
Icon(Icons.play_arrow_rounded, size: 15),
],
),
),
NormalModel(
leading: const Icon(MdiIcons.debugStepOver),
title: '番剧片头/片尾跳过类型',
getTrailing: () => Builder(
builder: (context) {
final pgcSkipType = Pref.pgcSkipType;
final colorScheme = ColorScheme.of(context);
final color = pgcSkipType == SkipType.disable
? colorScheme.outline
: colorScheme.secondary;
return PopupMenuButton<SkipType>(
initialValue: pgcSkipType,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text.rich(
style: TextStyle(fontSize: 14, height: 1, color: color),
strutStyle: const StrutStyle(
leading: 0,
height: 1,
fontSize: 14,
),
TextSpan(
children: [
TextSpan(text: pgcSkipType.title),
WidgetSpan(
alignment: .middle,
child: Icon(
MdiIcons.unfoldMoreHorizontal,
size: 14,
color: color,
),
),
],
),
),
),
onSelected: (value) async {
await GStorage.setting.put(SettingBoxKey.pgcSkipType, value.index);
if (context.mounted) {
(context as Element).markNeedsBuild();
}
},
itemBuilder: (context) => SkipType.values
.map((e) => PopupMenuItem(value: e, child: Text(e.title)))
.toList(),
);
},
),
),
SwitchModel(
title: '检查未读动态',
subtitle: '点击设置检查周期(min)',
leading: const Icon(Icons.notifications_none),
setKey: SettingBoxKey.checkDynamic,
defaultVal: true,
onChanged: (value) {
Get.find<MainController>().checkDynamic = value;
},
onTap: (context) {
int dynamicPeriod = Pref.dynamicPeriod;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('检查周期'),
content: TextFormField(
autofocus: true,
initialValue: dynamicPeriod.toString(),
keyboardType: TextInputType.number,
onChanged: (value) {
dynamicPeriod = int.tryParse(value) ?? 5;
},
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const 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<MainController>().dynamicPeriod =
dynamicPeriod * 60 * 1000;
},
child: const Text('确定'),
),
],
);
},
);
},
),
SwitchModel(
title: '显示视频分段信息',
leading: Transform.rotate(
angle: pi / 2,
child: const Icon(MdiIcons.viewHeadline),
),
setKey: SettingBoxKey.showViewPoints,
defaultVal: true,
),
const SwitchModel(
title: '视频页显示相关视频',
leading: Icon(MdiIcons.motionPlayOutline),
setKey: SettingBoxKey.showRelatedVideo,
defaultVal: true,
),
const SwitchModel(
title: '显示视频评论',
leading: Icon(MdiIcons.commentTextOutline),
setKey: SettingBoxKey.showVideoReply,
defaultVal: true,
),
const SwitchModel(
title: '显示番剧评论',
leading: Icon(MdiIcons.commentTextOutline),
setKey: SettingBoxKey.showBangumiReply,
defaultVal: true,
),
const SwitchModel(
title: '默认展开视频简介',
leading: Icon(Icons.expand_more),
setKey: SettingBoxKey.alwaysExpandIntroPanel,
defaultVal: false,
),
const SwitchModel(
title: '横屏自动展开视频简介',
leading: Icon(Icons.expand_more),
setKey: SettingBoxKey.expandIntroPanelH,
defaultVal: false,
),
const SwitchModel(
title: '横屏分P/合集列表显示在Tab栏',
leading: Icon(Icons.format_list_numbered_rtl_sharp),
setKey: SettingBoxKey.horizontalSeasonPanel,
defaultVal: false,
),
const SwitchModel(
title: '横屏播放页在侧栏打开UP主页',
leading: Icon(Icons.account_circle_outlined),
setKey: SettingBoxKey.horizontalMemberPage,
defaultVal: false,
),
SwitchModel(
title: '横屏在侧栏打开图片预览',
leading: const Icon(Icons.photo_outlined),
setKey: SettingBoxKey.horizontalPreview,
defaultVal: false,
onChanged: (value) => CustomGridView.horizontalPreview = value,
),
NormalModel(
title: '评论折叠行数',
subtitle: '0行为不折叠',
leading: const Icon(Icons.compress),
getTrailing: () => Text(
'${ReplyItemGrpc.replyLengthLimit}',
style: Get.theme.textTheme.titleSmall,
),
onTap: (context, setState) {
String replyLengthLimit = ReplyItemGrpc.replyLengthLimit.toString();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('评论折叠行数'),
content: TextFormField(
autofocus: true,
initialValue: replyLengthLimit,
keyboardType: TextInputType.number,
onChanged: (value) {
replyLengthLimit = value;
},
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(suffixText: ''),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () async {
Get.back();
int length = int.tryParse(replyLengthLimit) ?? 6;
ReplyItemGrpc.replyLengthLimit = length == 0 ? null : length;
await GStorage.setting.put(
SettingBoxKey.replyLengthLimit,
length,
);
setState();
},
child: const Text('确定'),
),
],
);
},
);
},
),
NormalModel(
title: '弹幕行高',
subtitle: '默认1.6',
leading: const Icon(CustomIcons.dm_settings),
getTrailing: () => Text(
Pref.danmakuLineHeight.toString(),
style: Get.theme.textTheme.titleSmall,
),
onTap: (context, setState) {
String danmakuLineHeight = Pref.danmakuLineHeight.toString();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('弹幕行高'),
content: TextFormField(
autofocus: true,
initialValue: danmakuLineHeight,
keyboardType: const 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: const Text('确定'),
),
],
);
},
);
},
),
SwitchModel(
title: '显示视频警告/争议信息',
leading: const Icon(Icons.warning_amber_rounded),
setKey: SettingBoxKey.showArgueMsg,
defaultVal: true,
onChanged: (val) => ItemModulesModel.showArgueMsg = val,
),
const SwitchModel(
title: '分P/合集:倒序播放从首集开始播放',
subtitle: '开启则自动切换为倒序首集,否则保持当前集',
leading: Icon(MdiIcons.sort),
setKey: SettingBoxKey.reverseFromFirst,
defaultVal: true,
),
const SwitchModel(
title: '禁用 SSL 证书验证',
subtitle: '谨慎开启,禁用容易受到中间人攻击',
leading: Icon(Icons.security),
needReboot: true,
setKey: SettingBoxKey.badCertificateCallback,
),
const SwitchModel(
title: '显示继续播放分P提示',
leading: Icon(Icons.local_parking),
setKey: SettingBoxKey.continuePlayingPart,
defaultVal: true,
),
getBanWordModel(
title: '评论关键词过滤',
key: SettingBoxKey.banWordForReply,
onChanged: (value) {
ReplyGrpc.replyRegExp = value;
ReplyGrpc.enableFilter = value.pattern.isNotEmpty;
},
),
getBanWordModel(
title: '动态关键词过滤',
key: SettingBoxKey.banWordForDyn,
onChanged: (value) {
DynamicsDataModel.banWordForDyn = value;
DynamicsDataModel.enableFilter = value.pattern.isNotEmpty;
},
),
const SwitchModel(
title: '使用外部浏览器打开链接',
leading: Icon(Icons.open_in_browser),
setKey: SettingBoxKey.openInBrowser,
defaultVal: false,
),
NormalModel(
title: '刷新滑动距离',
leading: const Icon(Icons.refresh),
getSubtitle: () => '当前滑动距离: ${Pref.refreshDragPercentage}x',
onTap: (context, setState) async {
final result = await showDialog<double>(
context: context,
builder: (context) {
return SlideDialog(
title: '刷新滑动距离',
min: 0.1,
max: 0.5,
divisions: 8,
precise: 2,
value: Pref.refreshDragPercentage,
suffix: 'x',
);
},
);
if (result != null) {
kDragContainerExtentPercentage = result;
await GStorage.setting.put(SettingBoxKey.refreshDragPercentage, result);
Get.forceAppUpdate();
setState();
}
},
),
NormalModel(
title: '刷新指示器高度',
leading: const Icon(Icons.height),
getSubtitle: () => '当前指示器高度: ${Pref.refreshDisplacement}',
onTap: (context, setState) async {
final result = await showDialog<double>(
context: context,
builder: (context) {
return SlideDialog(
title: '刷新指示器高度',
min: 10.0,
max: 100.0,
divisions: 9,
value: Pref.refreshDisplacement,
);
},
);
if (result != null) {
displacement = result;
await GStorage.setting.put(SettingBoxKey.refreshDisplacement, result);
Get.forceAppUpdate();
setState();
}
},
),
const SwitchModel(
title: '显示会员彩色弹幕',
leading: Icon(MdiIcons.gradientHorizontal),
setKey: SettingBoxKey.showVipDanmaku,
defaultVal: true,
),
const SwitchModel(
title: '合并弹幕',
subtitle: '合并一段时间内获取到的相同弹幕',
leading: Icon(Icons.merge),
setKey: SettingBoxKey.mergeDanmaku,
defaultVal: false,
),
SwitchModel(
title: '显示热门推荐',
subtitle: '热门页面显示每周必看等推荐内容入口',
leading: const Icon(Icons.local_fire_department_outlined),
setKey: SettingBoxKey.showHotRcmd,
defaultVal: false,
onChanged: (value) {
try {
Get.find<HotController>().showHotRcmd.value = value;
} catch (_) {}
},
),
if (kDebugMode || Platform.isAndroid)
NormalModel(
title: '音量均衡',
leading: const Icon(Icons.multitrack_audio),
getSubtitle: () {
final audioNormalization = AudioNormalization.getTitleFromConfig(
Pref.audioNormalization,
);
String fallback = Pref.fallbackNormalization;
if (fallback == '0') {
fallback = '';
} else {
fallback =
',无参数时:「${AudioNormalization.getTitleFromConfig(fallback)}';
}
return '当前:「$audioNormalization$fallback';
},
onTap: audioNormalization,
),
NormalModel(
title: '超分辨率',
leading: const Icon(Icons.stay_current_landscape_outlined),
getSubtitle: () =>
'当前:「${Pref.superResolutionType.title}\n默认设置对番剧生效, 其他视频默认关闭\n超分辨率需要启用硬件解码, 若启用硬件解码后仍然不生效, 尝试切换硬件解码器为 auto-copy',
onTap: (context, setState) async {
final result = await showDialog<SuperResolutionType>(
context: context,
builder: (context) {
return SelectDialog<SuperResolutionType>(
title: '超分辨率',
value: Pref.superResolutionType,
values: SuperResolutionType.values
.map((e) => (e, e.title))
.toList(),
);
},
);
if (result != null) {
await GStorage.setting.put(
SettingBoxKey.superResolutionType,
result.index,
);
setState();
}
},
),
const SwitchModel(
title: '提前初始化播放器',
subtitle: '相对减少手动播放加载时间',
leading: Icon(Icons.play_circle_outlined),
setKey: SettingBoxKey.preInitPlayer,
defaultVal: false,
),
const SwitchModel(
title: '首页切换页面动画',
leading: Icon(Icons.home_outlined),
setKey: SettingBoxKey.mainTabBarView,
defaultVal: false,
needReboot: true,
),
const SwitchModel(
title: '搜索建议',
leading: Icon(Icons.search),
setKey: SettingBoxKey.searchSuggestion,
defaultVal: true,
),
const SwitchModel(
title: '记录搜索历史',
leading: Icon(Icons.history),
setKey: SettingBoxKey.recordSearchHistory,
defaultVal: true,
),
SwitchModel(
title: '展示头像/评论/动态装饰',
leading: const Icon(MdiIcons.stickerCircleOutline),
setKey: SettingBoxKey.showDynDecorate,
defaultVal: true,
onChanged: (value) => PendantAvatar.showDynDecorate = value,
),
SwitchModel(
title: '预览 Live Photo',
subtitle: '开启则以视频形式预览 Live Photo否则预览静态图片',
leading: const Icon(Icons.image_outlined),
setKey: SettingBoxKey.enableLivePhoto,
defaultVal: true,
onChanged: (value) => ImageModel.enableLivePhoto = value,
),
const SwitchModel(
title: '滑动跳转预览视频缩略图',
leading: Icon(Icons.preview_outlined),
setKey: SettingBoxKey.showSeekPreview,
defaultVal: true,
),
const SwitchModel(
title: '显示高能进度条',
subtitle: '高能进度条反应了在时域上,单位时间内弹幕发送量的变化趋势',
leading: Icon(Icons.show_chart),
setKey: SettingBoxKey.showDmChart,
defaultVal: false,
),
const SwitchModel(
title: '发评反诈',
subtitle: '发送评论后检查评论是否可见',
leading: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Icon(Icons.shield_outlined),
Icon(Icons.reply, size: 14),
],
),
setKey: SettingBoxKey.enableCommAntifraud,
defaultVal: false,
),
if (Platform.isAndroid)
const SwitchModel(
title: '使用「哔哩发评反诈」检查评论',
leading: Icon(
FontAwesomeIcons.b,
size: 22,
),
setKey: SettingBoxKey.biliSendCommAntifraud,
defaultVal: false,
),
const SwitchModel(
title: '发布/转发动态反诈',
subtitle: '发布/转发动态后检查动态是否可见',
leading: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Icon(Icons.shield_outlined),
Icon(Icons.motion_photos_on, size: 12),
],
),
setKey: SettingBoxKey.enableCreateDynAntifraud,
defaultVal: false,
),
SwitchModel(
title: '屏蔽带货动态',
leading: const Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Icon(Icons.shopping_bag_outlined, size: 14),
Icon(Icons.not_interested),
],
),
setKey: SettingBoxKey.antiGoodsDyn,
defaultVal: false,
onChanged: (value) {
DynamicsDataModel.antiGoodsDyn = value;
},
),
SwitchModel(
title: '屏蔽带货评论',
leading: const Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Icon(Icons.shopping_bag_outlined, size: 14),
Icon(Icons.not_interested),
],
),
setKey: SettingBoxKey.antiGoodsReply,
defaultVal: false,
onChanged: (value) {
ReplyGrpc.antiGoodsReply = value;
},
),
SwitchModel(
title: '侧滑关闭二级页面',
leading: Transform.rotate(
angle: pi * 1.5,
child: const Icon(Icons.touch_app),
),
setKey: SettingBoxKey.slideDismissReplyPage,
defaultVal: Platform.isIOS,
onChanged: (value) {
CommonSlideMixin.slideDismissReplyPage = value;
},
),
const SwitchModel(
title: '启用双指缩小视频',
leading: Icon(Icons.pinch),
setKey: SettingBoxKey.enableShrinkVideoSize,
defaultVal: true,
),
const SwitchModel(
title: '动态/专栏详情页展示底部操作栏',
leading: Icon(Icons.more_horiz),
setKey: SettingBoxKey.showDynActionBar,
defaultVal: true,
),
const SwitchModel(
title: '启用拖拽字幕调整底部边距',
leading: Icon(MdiIcons.dragVariant),
setKey: SettingBoxKey.enableDragSubtitle,
defaultVal: false,
),
const SwitchModel(
title: '展示追番时间表',
leading: Icon(MdiIcons.chartTimelineVariantShimmer),
setKey: SettingBoxKey.showPgcTimeline,
defaultVal: true,
needReboot: true,
),
SwitchModel(
title: '静默下载图片',
subtitle: '不显示下载 Loading 弹窗',
leading: const Icon(Icons.download_for_offline_outlined),
setKey: SettingBoxKey.silentDownImg,
defaultVal: false,
onChanged: (value) => ImageUtils.silentDownImg = value,
),
SwitchModel(
setKey: SettingBoxKey.feedBackEnable,
onChanged: (value) {
enableFeedback = value;
feedBack();
},
leading: const Icon(Icons.vibration_outlined),
title: '震动反馈',
subtitle: '请确定手机设置中已开启震动反馈',
),
const SwitchModel(
title: '大家都在搜',
subtitle: '是否展示「大家都在搜」',
leading: Icon(Icons.data_thresholding_outlined),
setKey: SettingBoxKey.enableHotKey,
defaultVal: true,
),
const SwitchModel(
title: '搜索发现',
subtitle: '是否展示「搜索发现」',
leading: Icon(Icons.search_outlined),
setKey: SettingBoxKey.enableSearchRcmd,
defaultVal: true,
),
SwitchModel(
title: '搜索默认词',
subtitle: '是否展示搜索框默认词',
leading: const Icon(Icons.whatshot_outlined),
setKey: SettingBoxKey.enableSearchWord,
defaultVal: false,
onChanged: (val) {
try {
final controller = Get.find<HomeController>()..enableSearchWord = val;
if (val) {
controller.querySearchDefault();
} else {
controller.defaultSearch.value = '';
}
} catch (_) {}
},
),
SwitchModel(
title: '快速收藏',
subtitle: '点击设置默认收藏夹\n点按收藏至默认,长按选择文件夹',
leading: const Icon(Icons.bookmark_add_outlined),
setKey: SettingBoxKey.enableQuickFav,
onTap: (context) async {
if (Accounts.main.isLogin) {
final res = await FavHttp.allFavFolders(Accounts.main.mid);
if (res case Success(:final response)) {
final list = response.list;
if (list == null || list.isEmpty) {
return;
}
final quickFavId = Pref.quickFavId;
if (!context.mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
title: const Text('选择默认收藏夹'),
contentPadding: const EdgeInsets.only(top: 5, bottom: 18),
content: SingleChildScrollView(
child: RadioGroup(
onChanged: (value) {
Get.back();
GStorage.setting.put(SettingBoxKey.quickFavId, value);
SmartDialog.showToast('设置成功');
},
groupValue: quickFavId,
child: Column(
children: list.map((item) {
return RadioListTile(
toggleable: true,
dense: true,
title: Text(item.title),
value: item.id,
);
}).toList(),
),
),
),
),
);
} else {
res.toast();
}
}
},
defaultVal: false,
),
SwitchModel(
title: '评论区搜索关键词',
subtitle: '展示评论区搜索关键词',
leading: const Icon(Icons.search_outlined),
setKey: SettingBoxKey.enableWordRe,
defaultVal: false,
onChanged: (value) {
ReplyItemGrpc.enableWordRe = value;
},
),
const SwitchModel(
title: '启用AI总结',
subtitle: '视频详情页开启AI总结',
leading: Icon(Icons.engineering_outlined),
setKey: SettingBoxKey.enableAi,
defaultVal: false,
),
const SwitchModel(
title: '消息页禁用"收到的赞"功能',
subtitle: '禁止打开入口,降低网络社交依赖',
leading: Icon(Icons.beach_access_outlined),
setKey: SettingBoxKey.disableLikeMsg,
defaultVal: false,
),
const SwitchModel(
title: '默认展示评论区',
subtitle: '在视频详情页默认切换至评论区页仅Tab型布局',
leading: Icon(Icons.mode_comment_outlined),
setKey: SettingBoxKey.defaultShowComment,
defaultVal: false,
),
const SwitchModel(
title: '启用HTTP/2',
leading: Icon(Icons.swap_horizontal_circle_outlined),
setKey: SettingBoxKey.enableHttp2,
defaultVal: false,
needReboot: true,
),
NormalModel(
title: '连接重试次数',
subtitle: '为0时禁用',
leading: const Icon(Icons.repeat),
onTap: (context, setState) async {
final result = await showDialog<double>(
context: context,
builder: (context) {
return SlideDialog(
title: '连接重试次数',
min: 0,
max: 8,
divisions: 8,
precise: 0,
value: Pref.retryCount.toDouble(),
);
},
);
if (result != null) {
await GStorage.setting.put(SettingBoxKey.retryCount, result.toInt());
setState();
SmartDialog.showToast('重启生效');
}
},
),
NormalModel(
title: '连接重试间隔',
subtitle: '实际间隔 = 间隔 * 第x次重试',
leading: const Icon(Icons.more_time_outlined),
onTap: (context, setState) async {
final result = await showDialog<double>(
context: context,
builder: (context) {
return SlideDialog(
title: '连接重试间隔',
min: 0,
max: 1000,
divisions: 10,
precise: 0,
value: Pref.retryDelay.toDouble(),
suffix: 'ms',
);
},
);
if (result != null) {
await GStorage.setting.put(SettingBoxKey.retryDelay, result.toInt());
setState();
SmartDialog.showToast('重启生效');
}
},
),
NormalModel(
title: '评论展示',
leading: const Icon(Icons.whatshot_outlined),
getSubtitle: () =>
'当前优先展示「${ReplySortType.values[Pref.replySortType].title}',
onTap: (context, setState) async {
final result = await showDialog<int>(
context: context,
builder: (context) {
return SelectDialog<int>(
title: '评论展示',
value: Pref.replySortType,
values: ReplySortType.values
.map((e) => (e.index, e.title))
.toList(),
);
},
);
if (result != null) {
await GStorage.setting.put(SettingBoxKey.replySortType, result);
setState();
}
},
),
NormalModel(
title: '动态展示',
leading: const Icon(Icons.dynamic_feed_rounded),
getSubtitle: () =>
'当前优先展示「${DynamicsTabType.values[Pref.defaultDynamicType].label}',
onTap: (context, setState) async {
final result = await showDialog<int>(
context: context,
builder: (context) {
return SelectDialog<int>(
title: '动态展示',
value: Pref.defaultDynamicType,
values: DynamicsTabType.values
.take(4)
.map((e) => (e.index, e.label))
.toList(),
);
},
);
if (result != null) {
await GStorage.setting.put(SettingBoxKey.defaultDynamicType, result);
setState();
}
},
),
SwitchModel(
title: '显示动态互动内容',
subtitle: '开启后则在动态卡片底部显示互动内容(如关注的人点赞、热评等)',
leading: const Icon(Icons.quickreply_outlined),
setKey: SettingBoxKey.showDynInteraction,
defaultVal: true,
onChanged: (val) => ItemModulesModel.showDynInteraction = val,
),
NormalModel(
title: '用户页默认展示TAB',
leading: const Icon(Icons.tab),
getSubtitle: () => '当前优先展示「${Pref.memberTab.title}',
onTap: (context, setState) async {
final result = await showDialog<MemberTabType>(
context: context,
builder: (context) {
return SelectDialog<MemberTabType>(
title: '用户页默认展示TAB',
value: Pref.memberTab,
values: MemberTabType.values.map((e) => (e, e.title)).toList(),
);
},
);
if (result != null) {
await GStorage.setting.put(SettingBoxKey.memberTab, result.index);
setState();
}
},
),
SwitchModel(
title: '显示UP主页小店TAB',
leading: const Icon(Icons.shop_outlined),
setKey: SettingBoxKey.showMemberShop,
defaultVal: false,
onChanged: (value) => MemberTabType.showMemberShop = value,
),
SwitchModel(
onTap: (context) {
String systemProxyHost = Pref.systemProxyHost;
String systemProxyPort = Pref.systemProxyPort;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('设置代理'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 6),
TextFormField(
initialValue: systemProxyHost,
decoration: const InputDecoration(
isDense: true,
labelText: '请输入Host使用 . 分割',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(6)),
),
),
onChanged: (e) => systemProxyHost = e,
),
const SizedBox(height: 10),
TextFormField(
initialValue: systemProxyPort,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
isDense: true,
labelText: '请输入Port',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(6)),
),
),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (e) => systemProxyPort = e,
),
],
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
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,
),
const SwitchModel(
title: '自动清除缓存',
subtitle: '每次启动时清除缓存',
leading: Icon(Icons.auto_delete_outlined),
setKey: SettingBoxKey.autoClearCache,
defaultVal: false,
),
NormalModel(
title: '最大缓存大小',
getSubtitle: () {
final num = Pref.maxCacheSize;
return '当前最大缓存大小: 「${num == 0 ? '无限' : CacheManager.formatSize(Pref.maxCacheSize)}';
},
onTap: (context, setState) {
showDialog(
context: context,
builder: (context) {
String valueStr = '';
return AlertDialog(
title: const Text('最大缓存大小'),
content: TextField(
autofocus: true,
onChanged: (value) => valueStr = value,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')),
],
decoration: const InputDecoration(suffixText: 'MB'),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () async {
Get.back();
num value = num.tryParse(valueStr) ?? 0;
await GStorage.setting.put(
SettingBoxKey.maxCacheSize,
value * 1024 * 1024,
);
setState();
},
child: const Text('确定'),
),
],
);
},
);
},
leading: const Icon(Icons.delete_outlined),
),
SwitchModel(
title: '检查更新',
subtitle: '每次启动时检查是否需要更新',
leading: const Icon(Icons.system_update_alt),
setKey: SettingBoxKey.autoUpdate,
defaultVal: true,
onChanged: (val) {
if (val) {
Update.checkUpdate(false);
}
},
),
];
Future<void> audioNormalization(
BuildContext context,
VoidCallback setState, {
bool fallback = false,
}) async {
final key = fallback
? SettingBoxKey.fallbackNormalization
: SettingBoxKey.audioNormalization;
final result = await showDialog<String>(
context: context,
builder: (context) {
String audioNormalization = fallback
? Pref.fallbackNormalization
: Pref.audioNormalization;
Set<String> values = {
'0',
'1',
if (!fallback) '2',
audioNormalization,
'3',
};
return SelectDialog<String>(
title: fallback ? '服务器无loudnorm配置时使用' : '音量均衡',
toggleable: true,
value: audioNormalization,
values: values
.map(
(e) => (
e,
switch (e) {
'0' => AudioNormalization.disable.title,
'1' => AudioNormalization.dynaudnorm.title,
'2' => AudioNormalization.loudnorm.title,
'3' => AudioNormalization.custom.title,
_ => e,
},
),
)
.toList(),
);
},
);
if (result != null && context.mounted) {
if (result == '3') {
String param = '';
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('自定义参数'),
content: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
const Text('等同于 --lavfi-complex="[aid1] 参数 [ao]"'),
TextField(
autofocus: true,
onChanged: (value) => param = value,
),
],
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: ColorScheme.of(context).outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
GStorage.setting.put(key, param);
if (!fallback &&
PlPlayerController.loudnormRegExp.hasMatch(param)) {
audioNormalization(context, setState, fallback: true);
}
setState();
},
child: const Text('确定'),
),
],
);
},
);
} else {
GStorage.setting.put(key, result);
if (result == '2') {
audioNormalization(context, setState, fallback: true);
}
setState();
}
}
}