Compare commits

...

3 Commits

Author SHA1 Message Date
dom
bac0769933 add split settings model
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-18 12:28:40 +08:00
dom
24f2cfa4e9 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-18 12:28:40 +08:00
0x535A
970ee679f1 fix: reset Dio adapters on iOS network switch (#1885)
* fix reset Dio adapters on iOS network switch

* improve iOS connectivity adapter reset safeguards

* refa

---------

Co-authored-by: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com>
2026-04-18 12:26:24 +08:00
5 changed files with 234 additions and 93 deletions

View File

@@ -16,10 +16,11 @@ import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:archive/archive.dart'; import 'package:archive/archive.dart';
import 'package:brotli/brotli.dart'; import 'package:brotli/brotli.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart'; import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/foundation.dart' show kDebugMode, listEquals;
class Request { class Request {
static const _gzipDecoder = GZipDecoder(); static const _gzipDecoder = GZipDecoder();
@@ -115,28 +116,24 @@ class Request {
return h11; return h11;
} }
/* static Timer? _networkChangeDebounce;
* config it and create
*/
Request._internal() {
//BaseOptions、Options、RequestOptions 都可以配置参数,优先级别依次递增,且可以根据优先级别覆盖参数
BaseOptions options = BaseOptions(
//请求基地址,可以包含子路径
baseUrl: HttpString.apiBaseUrl,
//连接服务器超时时间,单位是毫秒.
connectTimeout: const Duration(milliseconds: 10000),
//响应流上前后两次接受到数据的间隔,单位为毫秒。
receiveTimeout: const Duration(milliseconds: 10000),
//Http请求头.
headers: {
'user-agent': 'Dart/3.6 (dart:io)', // Http2Adapter不会自动添加标头
if (!_enableHttp2) 'connection': 'keep-alive',
'accept-encoding': 'br,gzip',
},
responseDecoder: _responseDecoder, // Http2Adapter没有自动解压
persistentConnection: true,
);
static void _onConnectivityChanged(List<ConnectivityResult> result) {
if (listEquals(result, const [ConnectivityResult.none])) {
return;
}
_networkChangeDebounce?.cancel();
_networkChangeDebounce = Timer(
const Duration(milliseconds: 500),
_resetAdaptersForNetworkChange,
);
}
static void _watchConnectivity() {
Connectivity().onConnectivityChanged.skip(1).listen(_onConnectivityChanged);
}
static (IOHttpClientAdapter, ConnectionManager?) _createPool() {
final bool enableSystemProxy; final bool enableSystemProxy;
late final String systemProxyHost; late final String systemProxyHost;
late final int? systemProxyPort; late final int? systemProxyPort;
@@ -160,30 +157,72 @@ class Request {
..autoUncompress = false, // Http2Adapter没有自动解压, 统一行为 ..autoUncompress = false, // Http2Adapter没有自动解压, 统一行为
); );
dio = Dio(options) final connectionManager = _enableHttp2
..httpClientAdapter = _enableHttp2 ? ConnectionManager(
? Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(seconds: 15), idleTimeout: const Duration(seconds: 15),
onClientCreate: enableSystemProxy onClientCreate: enableSystemProxy
? (_, config) { ? (_, config) => config
config
..proxy = Uri( ..proxy = Uri(
scheme: 'http', scheme: 'http',
host: systemProxyHost, host: systemProxyHost,
port: systemProxyPort, port: systemProxyPort,
) )
..onBadCertificate = (_) => true; ..onBadCertificate = (_) => true
}
: Pref.badCertificateCallback : Pref.badCertificateCallback
? (_, config) { ? (_, config) => config.onBadCertificate = (_) => true
config.onBadCertificate = (_) => true;
}
: null, : null,
),
fallbackAdapter: http11Adapter,
) )
: http11Adapter; : null;
return (http11Adapter, connectionManager);
}
@pragma('vm:notify-debugger-on-exception')
static void _resetAdaptersForNetworkChange() {
try {
final (h11, connectionManager) = _createPool();
if (connectionManager != null) {
(dio.httpClientAdapter as Http2Adapter)
..connectionManager.close(force: true)
..connectionManager = connectionManager
..fallbackAdapter.close(force: true)
..fallbackAdapter = h11;
_http11Dio?.httpClientAdapter = h11;
} else {
dio
..httpClientAdapter.close(force: true)
..httpClientAdapter = h11;
}
} catch (_) {}
}
/*
* config it and create
*/
Request._internal() {
//BaseOptions、Options、RequestOptions 都可以配置参数,优先级别依次递增,且可以根据优先级别覆盖参数
BaseOptions options = BaseOptions(
//请求基地址,可以包含子路径
baseUrl: HttpString.apiBaseUrl,
//连接服务器超时时间,单位是毫秒.
connectTimeout: const Duration(milliseconds: 10000),
//响应流上前后两次接受到数据的间隔,单位为毫秒。
receiveTimeout: const Duration(milliseconds: 10000),
//Http请求头.
headers: {
'user-agent': 'Dart/3.6 (dart:io)', // Http2Adapter不会自动添加标头
if (!_enableHttp2) 'connection': 'keep-alive',
'accept-encoding': 'br,gzip',
},
responseDecoder: _responseDecoder, // Http2Adapter没有自动解压
persistentConnection: true,
);
final (h11, connectionManager) = _createPool();
dio = Dio(options)
..httpClientAdapter = _enableHttp2
? Http2Adapter(connectionManager, fallbackAdapter: h11)
: h11;
// 先于其他Interceptor // 先于其他Interceptor
if (Pref.retryCount != 0) { if (Pref.retryCount != 0) {
@@ -208,6 +247,8 @@ class Request {
..options.validateStatus = (int? status) { ..options.validateStatus = (int? status) {
return status != null && status >= 200 && status < 300; return status != null && status >= 200 && status < 300;
}; };
if (Platform.isIOS) _watchConnectivity();
} }
/* /*

View File

@@ -70,13 +70,11 @@ List<SettingsModel> get extraSettings => [
onTap: _showDownPathDialog, onTap: _showDownPathDialog,
), ),
], ],
SwitchModel( SplitModel(
normalModel: const NormalModel.split(
title: '空降助手', title: '空降助手',
subtitle: '点击配置', subtitle: '点击配置',
setKey: SettingBoxKey.enableSponsorBlock, leading: Stack(
defaultVal: false,
onTap: (context) => Get.toNamed('/sponsorBlock'),
leading: const Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
@@ -85,6 +83,12 @@ List<SettingsModel> get extraSettings => [
], ],
), ),
), ),
switchModel: SwitchModel.split(
defaultVal: false,
setKey: SettingBoxKey.enableSponsorBlock,
onTap: (context) => Get.toNamed('/sponsorBlock'),
),
),
PopupModel<SkipType>( PopupModel<SkipType>(
title: '番剧片头/片尾跳过类型', title: '番剧片头/片尾跳过类型',
leading: const Icon(MdiIcons.debugStepOver), leading: const Icon(MdiIcons.debugStepOver),
@@ -94,15 +98,19 @@ List<SettingsModel> get extraSettings => [
.put(SettingBoxKey.pgcSkipType, value.index) .put(SettingBoxKey.pgcSkipType, value.index)
.whenComplete(setState), .whenComplete(setState),
), ),
SwitchModel( SplitModel(
normalModel: const NormalModel.split(
title: '检查未读动态', title: '检查未读动态',
subtitle: '点击设置检查周期(min)', subtitle: '点击设置检查周期(min)',
leading: const Icon(Icons.notifications_none), leading: Icon(Icons.notifications_none),
setKey: SettingBoxKey.checkDynamic, ),
switchModel: SwitchModel.split(
defaultVal: true, defaultVal: true,
setKey: SettingBoxKey.checkDynamic,
onChanged: (value) => Get.find<MainController>().checkDynamic = value, onChanged: (value) => Get.find<MainController>().checkDynamic = value,
onTap: _showDynDialog, onTap: _showDynDialog,
), ),
),
SwitchModel( SwitchModel(
title: '显示视频分段信息', title: '显示视频分段信息',
leading: Transform.rotate( leading: Transform.rotate(
@@ -615,13 +623,18 @@ List<SettingsModel> get extraSettings => [
defaultVal: false, defaultVal: false,
onChanged: (value) => MemberTabType.showMemberShop = value, onChanged: (value) => MemberTabType.showMemberShop = value,
), ),
const SwitchModel( const SplitModel(
leading: Icon(Icons.airplane_ticket_outlined), normalModel: NormalModel.split(
title: '设置代理', title: '设置代理',
subtitle: '设置代理 host:port', subtitle: '设置代理 host:port',
leading: Icon(Icons.airplane_ticket_outlined),
),
switchModel: SwitchModel.split(
defaultVal: false,
setKey: SettingBoxKey.enableSystemProxy, setKey: SettingBoxKey.enableSystemProxy,
onTap: _showProxyDialog, onTap: _showProxyDialog,
), ),
),
const SwitchModel( const SwitchModel(
title: '自动清除缓存', title: '自动清除缓存',
subtitle: '每次启动时清除缓存', subtitle: '每次启动时清除缓存',

View File

@@ -30,6 +30,43 @@ sealed class SettingsModel {
}); });
} }
class SplitModel extends SettingsModel {
const SplitModel({
super.contentPadding,
super.titleStyle,
required this.normalModel,
required this.switchModel,
});
@override
String? get effectiveSubtitle => normalModel.effectiveSubtitle;
@override
String get effectiveTitle => normalModel.effectiveTitle;
@override
String? get title => normalModel.title;
final NormalModel normalModel;
final SwitchModel switchModel;
@override
Widget get widget => SetSwitchItem(
title: effectiveTitle,
subtitle: effectiveSubtitle,
setKey: switchModel.setKey,
defaultVal: switchModel.defaultVal,
onChanged: switchModel.onChanged,
needReboot: switchModel.needReboot,
leading: normalModel.leading,
onTap: switchModel.onTap,
contentPadding: contentPadding,
titleStyle: titleStyle,
isSplit: true,
);
}
class PopupModel<T extends EnumWithLabel> extends SettingsModel { class PopupModel<T extends EnumWithLabel> extends SettingsModel {
const PopupModel({ const PopupModel({
required this.title, required this.title,
@@ -88,6 +125,18 @@ class NormalModel extends SettingsModel {
this.onTap, this.onTap,
}) : assert(title != null || getTitle != null); }) : assert(title != null || getTitle != null);
const NormalModel.split({
super.subtitle,
super.leading,
super.contentPadding,
super.titleStyle,
this.title,
this.getTitle,
this.getSubtitle,
this.getTrailing,
}) : onTap = null,
assert(title != null || getTitle != null);
@override @override
String get effectiveTitle => title ?? getTitle!(); String get effectiveTitle => title ?? getTitle!();
@override @override
@@ -109,7 +158,7 @@ class NormalModel extends SettingsModel {
class SwitchModel extends SettingsModel { class SwitchModel extends SettingsModel {
@override @override
final String title; final String? title;
final String setKey; final String setKey;
final bool defaultVal; final bool defaultVal;
final ValueChanged<bool>? onChanged; final ValueChanged<bool>? onChanged;
@@ -121,7 +170,7 @@ class SwitchModel extends SettingsModel {
super.leading, super.leading,
super.contentPadding, super.contentPadding,
super.titleStyle, super.titleStyle,
required this.title, required String this.title,
required this.setKey, required this.setKey,
this.defaultVal = false, this.defaultVal = false,
this.onChanged, this.onChanged,
@@ -129,14 +178,22 @@ class SwitchModel extends SettingsModel {
this.onTap, this.onTap,
}); });
const SwitchModel.split({
required this.setKey,
this.defaultVal = false,
this.needReboot = false,
this.onChanged,
this.onTap,
}) : title = null;
@override @override
String get effectiveTitle => title; String get effectiveTitle => title!;
@override @override
String? get effectiveSubtitle => subtitle; String? get effectiveSubtitle => subtitle;
@override @override
Widget get widget => SetSwitchItem( Widget get widget => SetSwitchItem(
title: title, title: title!,
subtitle: subtitle, subtitle: subtitle,
setKey: setKey, setKey: setKey,
defaultVal: defaultVal, defaultVal: defaultVal,

View File

@@ -82,15 +82,19 @@ List<SettingsModel> get styleSettings => [
defaultVal: false, defaultVal: false,
needReboot: true, needReboot: true,
), ),
SwitchModel( SplitModel(
normalModel: const NormalModel.split(
title: 'App字体字重', title: 'App字体字重',
subtitle: '点击设置', subtitle: '点击设置',
setKey: SettingBoxKey.appFontWeight, leading: Icon(Icons.text_fields),
),
switchModel: SwitchModel.split(
defaultVal: false, defaultVal: false,
leading: const Icon(Icons.text_fields), setKey: SettingBoxKey.appFontWeight,
onChanged: (_) => Get.updateMyAppTheme(), onChanged: (_) => Get.updateMyAppTheme(),
onTap: _showFontWeightDialog, onTap: _showFontWeightDialog,
), ),
),
NormalModel( NormalModel(
title: '界面缩放', title: '界面缩放',
getSubtitle: () => '当前缩放比例:${Pref.uiScale.toStringAsFixed(2)}', getSubtitle: () => '当前缩放比例:${Pref.uiScale.toStringAsFixed(2)}',

View File

@@ -17,8 +17,10 @@ class SetSwitchItem extends StatefulWidget {
final void Function(BuildContext context)? onTap; final void Function(BuildContext context)? onTap;
final EdgeInsetsGeometry? contentPadding; final EdgeInsetsGeometry? contentPadding;
final TextStyle? titleStyle; final TextStyle? titleStyle;
final bool isSplit;
const SetSwitchItem({ const SetSwitchItem({
super.key,
required this.title, required this.title,
this.subtitle, this.subtitle,
required this.setKey, required this.setKey,
@@ -29,7 +31,7 @@ class SetSwitchItem extends StatefulWidget {
this.onTap, this.onTap,
this.contentPadding, this.contentPadding,
this.titleStyle, this.titleStyle,
super.key, this.isSplit = false,
}); });
@override @override
@@ -103,7 +105,17 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
final subTitleStyle = theme.textTheme.labelMedium!.copyWith( final subTitleStyle = theme.textTheme.labelMedium!.copyWith(
color: theme.colorScheme.outline, color: theme.colorScheme.outline,
); );
return ListTile(
final switchBtn = Transform.scale(
scale: 0.8,
alignment: .centerRight,
child: Switch(
value: val,
onChanged: switchChange,
),
);
Widget child(Widget? trailing) => ListTile(
contentPadding: widget.contentPadding, contentPadding: widget.contentPadding,
enabled: widget.onTap == null ? true : val, enabled: widget.onTap == null ? true : val,
onTap: widget.onTap == null ? switchChange : () => widget.onTap!(context), onTap: widget.onTap == null ? switchChange : () => widget.onTap!(context),
@@ -112,14 +124,28 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
? Text(widget.subtitle!, style: subTitleStyle) ? Text(widget.subtitle!, style: subTitleStyle)
: null, : null,
leading: widget.leading, leading: widget.leading,
trailing: Transform.scale( trailing: trailing,
scale: 0.8, );
alignment: .centerRight,
child: Switch( if (widget.isSplit) {
value: val, return Row(
onChanged: switchChange, children: [
Expanded(child: child(null)),
SizedBox(
height: 25,
child: VerticalDivider(
width: 1,
color: theme.colorScheme.outline.withValues(alpha: .3),
), ),
), ),
Padding(
padding: const .only(left: 4, right: 24),
child: switchBtn,
),
],
); );
} }
return child(switchBtn);
}
} }