Skip to content

Commit 3fcbf34

Browse files
committed
FIX: 修复网页词典加载与广告规则兼容
1 parent af779e6 commit 3fcbf34

11 files changed

Lines changed: 779 additions & 209 deletions

‎TASKS.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44
55
## 本次完成
66

7+
- [x] 修复 macOS 网页词典广告规则编译失败(2026-10-11):移除 WebKit 不支持的广告脚本 URL 正则分支,保留脚本路径匹配;增加规则集回归测试,确保交给原生拦截器的 URL 过滤器不含正则分支。定向 Widget 测试 2 项通过,相关 3 个 Dart 文件 `flutter analyze` 通过,`dart format` 与 `git diff --check` 通过。未运行 `scripts/check.sh`:改动只涉及网页词典广告规则。仍需在 macOS 重启 App 确认 WKContentRuleList 成功编译并正常显示词典页面。**完成时间**: 2026-10-11 12:17
8+
- [x] 恢复网页词典默认页面清理与 Cookie 同意脚本(2026-10-11):恢复隐藏常见页眉、页脚及 Cookie 横幅的默认 CSS,并自动点击 OneTrust 或通用 Cookie 接受按钮;在页面提交和加载完成时执行,保留提前展示内容的加载行为。回归测试确认 WebView 注入清理 CSS 并执行接受脚本;定向 `flutter analyze`、该测试、`dart format` 与 `git diff --check` 通过。未运行 `scripts/check.sh`:改动仅限网页词典视图。仍需在 macOS/移动端确认站点布局及 Cookie 提示表现。**完成时间**: 2026-10-11 12:14
9+
- [x] 恢复网页词典广告过滤(2026-10-11):将已有规则接回 WebView,Android/iOS/macOS 启用广告请求拦截与广告元素隐藏;所有平台在页面样式注入时隐藏匹配的广告元素,Windows 插件不支持网络请求拦截,因此仅隐藏页面广告。新增 Widget 回归测试确认规则传入 WebView。`flutter test test/widgets/dictionary/web_dictionary_ad_filter_test.dart`、相关文件定向 `flutter analyze`、`dart format` 与 `git diff --check` 通过;未运行 `scripts/check.sh`,改动只涉及网页词典广告过滤。仍需在 macOS/移动端实际确认广告展示效果。**完成时间**: 2026-10-11 12:08
10+
- [x] 升级应用版本至 1.0.40(2026-10-11):将 `pubspec.yaml` 中的版本从 1.0.39 提升至 1.0.40。已核对版本字段并通过 `git diff --check`;版本元数据变更无需运行测试,未运行 `scripts/check.sh`。**完成时间**: 2026-10-11 12:01
11+
- [x] 缩小网页词典弹窗左右留白(2026-10-11):网页词典面板左右内边距从 `24dp` 调至 `16dp`,其他词典面板保持原间距。`dart format`、定向 `flutter analyze lib/widgets/dictionary/dictionary_panel.dart` 与 `git diff --check` 通过;未运行测试和 `scripts/check.sh`。需用户在 macOS 确认视觉效果。**完成时间**: 2026-10-11 11:58
12+
- [x] 隐藏网页词典滚动条(2026-10-11):Android/iOS 关闭原生竖向滚动指示器,并在页面提交及加载完成时注入隐藏 WebKit 滚动条的 CSS,覆盖 macOS 页面滚动条。定向 `flutter analyze lib/widgets/dictionary/web_dictionary_view.dart` 与 `git diff --check` 通过;未运行测试和 `scripts/check.sh`。需用户在 macOS 确认视觉效果。**完成时间**: 2026-10-11 11:55
13+
- [x] 为网页词典设置移动 Safari User-Agent(2026-10-11):移动内容模式仍返回桌面布局后,为 WebView 配置移动 Safari UA;macOS 插件将该设置映射到 WKWebView 的 `customUserAgent`。定向 `flutter analyze lib/widgets/dictionary/web_dictionary_view.dart` 与 `git diff --check` 通过;未运行测试和 `scripts/check.sh`。仍需用户重启 App 实测。**完成时间**: 2026-10-11 11:52
14+
- [x] 网页词典显式请求移动版布局(2026-10-11):当前 WebView 未设置移动端内容模式,Apple WKWebView 默认模式可能让词典站点返回桌面布局。为 Android、iOS、macOS 设置 `preferredContentMode=MOBILE`,不改 URL 模板或手动伪造 UA。定向 `flutter analyze lib/widgets/dictionary/web_dictionary_view.dart` 与 `git diff --check` 通过;未运行测试和 `scripts/check.sh`。仍需在 macOS 实测确认页面布局。**完成时间**: 2026-10-11 11:49
15+
- [x] 更新有道词典移动版地址(2026-10-11):将旧版 `m.youdao.com/dict?le=eng&q=...` 改为 `dict.youdao.com/m/result?word=...&lang=en`,查询词继续使用既有 URL 编码。`dart format`、定向 `flutter analyze lib/services/dictionary/web_dictionary_source.dart` 与 `git diff --check` 通过;未运行测试,未运行 `scripts/check.sh`。**完成时间**: 2026-10-11 11:47
16+
- [x] 补充网页词典 URL 诊断日志(2026-10-11):主框架开始、提交、完成、失败和超时事件打印完整 URL;Android、iOS、macOS 额外记录已加载子资源的类型和 URL。定向 `flutter analyze lib/widgets/dictionary/web_dictionary_view.dart` 与 `git diff --check` 通过。未运行测试:本次只增加诊断日志;未运行 `scripts/check.sh`。**完成时间**: 2026-10-11 11:44
17+
- [x] 确认 macOS 网页词典黑闪与原生视图路径相关(2026-10-11):临时将 `AppKitView` 替换为占据同一布局的 Flutter 色块;用户实测黑闪消失,说明黑帧来自原生 WebView 创建/呈现路径。对照期间不创建 WebView,因此一直显示加载遮罩是预期结果;现已恢复网页加载。静态分析与 `git diff --check` 通过。精确到原生创建还是合成时序仍待继续排查。**完成时间**: 2026-10-11 10:55
18+
- [x] 统一放宽 Apple 平台 ATS 网络限制(2026-10-11):macOS 与 iOS 的 `Info.plist` 均配置 `NSAllowsArbitraryLoads=true`,允许应用通过 ATS 加载任意网络连接;macOS Debug/Release 沙盒客户端网络权限已存在,无需修改。两个 Info.plist 校验通过。未运行 `scripts/check.sh`:本次仅修改 Apple 平台网络策略配置,Plist 校验覆盖变更;仍需重新构建 macOS/iOS App 实测有道页面。**完成时间**: 2026-10-11 10:34
19+
- [x] 优化网页词典跨平台首屏加载(2026-10-11):WebView 全程挂载,上覆跟随 App 主题的加载遮罩;`onPageCommitVisible` 到达时提前展示页面,`onLoadStop` 为 Windows 等平台兜底。仅主框架错误显示重试界面,30 秒超时也会退出无限加载态;用导航 URL 和代际隔离过期回调与计时器。将 `underPageBackgroundColor` 限定配置到 iOS/macOS,并关闭透明背景;启用 Windows 内嵌 WebView。新增 9 项加载状态测试,连同网页词典弹窗高度测试共 15 项通过;4 个 Dart 文件定向分析通过,`git diff --check` 通过。未运行 `scripts/check.sh`:改动局限于网页词典加载链路,定向检查覆盖相关状态和弹窗。未运行 Maestro:需要用户在 macOS/iOS/Android/Windows 实机验证页面首次绘制,且当前 Maestro 首启 smoke flow 会清空应用数据。**完成时间**: 2026-10-11 10:02
720
- [x] 修正欧路词典词条链接(2026-10-11):将链接路径从 `/dicts/en/` 改为 `/mdicts/en/`,符合欧路词典的词条地址格式。定向测试 53 项通过,2 个 Dart 文件定向分析通过,`git diff --check` 通过。未运行 `scripts/check.sh`:改动仅涉及词条 URL 与其回归测试,定向检查已覆盖。**完成时间**: 2026-10-11 00:37
821
- [x] 移除词典广告过滤说明(2026-10-10):删除设置页说明及中英文文案;本地化生成、diff 检查通过,未运行测试。完成时间 22:37
922
- [x] 扩展网页词典广告过滤规则(2026-10-10):新增国际与中国广告域名、第三方广告路径和 CSS 隐藏规则;格式化、diff 检查通过,未运行测试。**完成时间**: 2026-10-10 22:17
@@ -53,6 +66,8 @@
5366

5467
## 优先处理
5568

69+
- [ ] 实测 WebView 移动内容模式与 User-Agent:macOS 完整重启 App 后确认有道等词典是否显示移动布局;如仍为桌面版,再收集页面实际 User-Agent 与 viewport 宽度。
70+
- [ ] 修复 macOS 网页词典首帧黑闪:继续隔离原生 WebView 创建与 Flutter 合成时序,保持内容尽早显示且不修改插件源码。
5671
- [ ] Android 离线 ASR 结束录音闪退:获取真机 `logcat` 与 `/data/tombstones`,定位并修复 Silero VAD native 崩溃。
5772
- [ ] 段落复述页面复用统一录音识别模块。
5873
- [ ] 意群操作栏切换收藏时其他按钮闪烁,继续定位根因。

‎lib/services/dictionary/web_dictionary_ad_block_rules.dart‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,9 @@ const _adDomains = <String>[
8686
const _thirdPartyAdPaths = <String>[
8787
r'/pagead/',
8888
r'/gampad/',
89-
r'/adsbygoogle\.js(?:[?#]|$)',
90-
r'/show_ads\.js(?:[?#]|$)',
89+
// WKContentRuleList 不支持正则分支;外层 `.*` 已覆盖 URL 查询参数。
90+
r'/adsbygoogle\.js',
91+
r'/show_ads\.js',
9192
r'/adserver/',
9293
r'/adserving/',
9394
r'/ad-delivery/',
@@ -103,7 +104,7 @@ const _adResourceTypes = <ContentBlockerTriggerResourceType>[
103104
];
104105

105106
/// 只隐藏具有明确广告语义的页面元素,避免使用 `.ad`、`.banner` 等宽泛规则。
106-
const _adSelectors = <String>[
107+
const webDictionaryAdCssSelectors = <String>[
107108
// 标准广告类名
108109
'.adsbygoogle',
109110
'.adsense',
@@ -206,6 +207,10 @@ const _adSelectors = <String>[
206207
'[aria-label="广告"]',
207208
];
208209

210+
/// 页面元素广告过滤所使用的 CSS 规则,供不支持网络拦截的平台注入。
211+
final String webDictionaryAdHidingCss =
212+
'${webDictionaryAdCssSelectors.join(', ')} { display: none !important; }';
213+
209214
/// 将广告域名转换为完整 URL 正则,并检查主机名边界。
210215
String _domainUrlPattern(String domain) {
211216
final escapedDomain = RegExp.escape(domain);
@@ -243,7 +248,7 @@ final List<ContentBlocker> webDictionaryCommonAdBlockers =
243248
trigger: ContentBlockerTrigger(urlFilter: '.*'),
244249
action: ContentBlockerAction(
245250
type: ContentBlockerActionType.CSS_DISPLAY_NONE,
246-
selector: _adSelectors.join(', '),
251+
selector: webDictionaryAdCssSelectors.join(', '),
247252
),
248253
),
249254
]);

‎lib/services/dictionary/web_dictionary_source.dart‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,5 +222,8 @@ String _forvoUrl(String w) => 'https://forvo.com/word/$w/#en';
222222
String _wordReferenceUrl(String w) =>
223223
'https://www.wordreference.com/definition/$w';
224224
String _etymonlineUrl(String w) => 'https://www.etymonline.com/search?q=$w';
225-
String _youdaoUrl(String w) => 'https://m.youdao.com/dict?le=eng&q=$w';
225+
226+
/// 构造有道移动网页版查询地址,`w` 已由调用方进行 URL 编码。
227+
String _youdaoUrl(String w) =>
228+
'https://dict.youdao.com/m/result?word=$w&lang=en';
226229
String _eudicUrl(String w) => 'https://dict.eudic.net/mdicts/en/$w';

‎lib/widgets/dictionary/dictionary_panel.dart‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -469,10 +469,10 @@ class _DictionaryPanelState extends ConsumerState<DictionaryPanel> {
469469
// 可拉伸源用显式高度(默认 3/5,可拖拽指示条调整);本地源按内容自适应。
470470
height: isResizable ? (_sheetHeight ?? _defaultSheetHeight) : null,
471471
child: Padding(
472-
padding: const EdgeInsets.fromLTRB(
473-
AppSpacing.l,
472+
padding: EdgeInsets.fromLTRB(
473+
isWeb ? AppSpacing.m : AppSpacing.l,
474474
6,
475-
AppSpacing.l,
475+
isWeb ? AppSpacing.m : AppSpacing.l,
476476
AppSpacing.s,
477477
),
478478
child: Column(

‎lib/widgets/dictionary/dictionary_result_view.dart‎

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,15 @@
66
library;
77

88
import 'package:flutter/material.dart';
9-
import 'package:flutter_riverpod/flutter_riverpod.dart';
10-
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
119

1210
import '../../models/dictionary/dictionary_lookup_result.dart';
1311
import '../../providers/dictionary/lookup_controller.dart';
14-
import '../../providers/dictionary/dictionary_settings_provider.dart';
15-
import '../../services/dictionary/web_dictionary_ad_block_rules.dart';
1612
import 'ai_dict_result_view.dart';
1713
import 'local_dict_result_view.dart';
1814
import 'web_dictionary_view.dart';
1915

2016
/// 结果分发视图
21-
class DictionaryResultView extends ConsumerWidget {
17+
class DictionaryResultView extends StatelessWidget {
2218
/// 当前选中源 id
2319
final String sourceId;
2420

@@ -48,7 +44,7 @@ class DictionaryResultView extends ConsumerWidget {
4844
});
4945

5046
@override
51-
Widget build(BuildContext context, WidgetRef ref) {
47+
Widget build(BuildContext context) {
5248
switch (sourceId) {
5349
case 'local':
5450
return LocalDictResultView(state: state, word: word);
@@ -60,40 +56,29 @@ class DictionaryResultView extends ConsumerWidget {
6056
onUpgrade: onUpgrade,
6157
);
6258
default:
63-
final adFilteringEnabled = ref
64-
.watch(dictionarySettingsNotifierProvider)
65-
.adFilteringEnabled;
66-
final adFilterBlockers = adFilteringEnabled
67-
? webDictionaryCommonAdBlockers
68-
: const <ContentBlocker>[];
6959
// 其余源(含全部网页词典 cambridge/oxford/... )走结果子类穷尽 switch
7060
// 兜底——新增结果子类需在此补分支。
7161
if (state case LookupLoaded(:final result)) {
72-
return _loadedFallback(result, adFilterBlockers, adFilteringEnabled);
62+
return _loadedFallback(result);
7363
}
7464
return const _Loading();
7565
}
7666
}
7767

7868
/// sealed 结果穷尽分发(新增源安全网)
79-
Widget _loadedFallback(
80-
DictionaryLookupResult result,
81-
List<ContentBlocker> adFilterBlockers,
82-
bool adFilteringEnabled,
83-
) => switch (result) {
69+
Widget _loadedFallback(DictionaryLookupResult result) => switch (result) {
8470
LocalDictResult() => LocalDictResultView(state: state, word: word),
8571
AiDictResult() => AiDictResultView(
8672
state: state,
8773
onRetry: onRetry,
8874
onSignIn: onSignIn,
8975
onUpgrade: onUpgrade,
9076
),
91-
// 切换词典源或过滤开关时重建 WebView,让初始过滤规则立即生效。
77+
// 查询新词时创建对应的原生 WebView。
9278
WebDictResult(:final sourceId, :final url) => WebDictionaryView(
93-
key: ValueKey('web_${sourceId}_$adFilteringEnabled'),
79+
key: ValueKey('web_${sourceId}_${url.toString()}'),
9480
sourceId: sourceId,
9581
url: url,
96-
contentBlockers: adFilterBlockers,
9782
),
9883
};
9984
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/// 网页词典主框架导航的加载阶段。
2+
enum WebDictionaryLoadPhase { loading, ready, error }
3+
4+
/// 维护当前主框架导航状态,并拒绝 URL 已过期的回调。
5+
class WebDictionaryLoadState {
6+
/// 创建加载状态快照。
7+
const WebDictionaryLoadState({
8+
required this.phase,
9+
required this.generation,
10+
required this.activeUrl,
11+
});
12+
13+
/// 创建首屏等待状态。
14+
factory WebDictionaryLoadState.initial(String url) => WebDictionaryLoadState(
15+
phase: WebDictionaryLoadPhase.loading,
16+
generation: 0,
17+
activeUrl: url,
18+
);
19+
20+
/// 当前主框架导航的阶段。
21+
final WebDictionaryLoadPhase phase;
22+
23+
/// 当前导航代际,用于隔离旧导航留下的计时器。
24+
final int generation;
25+
26+
/// 当前主框架 URL,用于忽略较早导航的回调。
27+
final String? activeUrl;
28+
29+
/// 页面是否仍在等待可见提交或加载结束。
30+
bool get isLoading => phase == WebDictionaryLoadPhase.loading;
31+
32+
/// 页面开始新导航,并使先前的超时计时器失效。
33+
WebDictionaryLoadState navigationStarted(String? url) =>
34+
WebDictionaryLoadState(
35+
phase: WebDictionaryLoadPhase.loading,
36+
generation: generation + 1,
37+
activeUrl: url ?? activeUrl,
38+
);
39+
40+
/// 用户请求重试当前 URL。
41+
WebDictionaryLoadState retryStarted() => WebDictionaryLoadState(
42+
phase: WebDictionaryLoadPhase.loading,
43+
generation: generation + 1,
44+
activeUrl: activeUrl,
45+
);
46+
47+
/// 页面已进入可显示阶段;不等待全部资源完成。
48+
WebDictionaryLoadState pageCommitted(String? url) =>
49+
_accepts(url) ? _withPhase(WebDictionaryLoadPhase.ready) : this;
50+
51+
/// 平台加载完成回调兜底,包括没有发送可见提交事件的平台。
52+
WebDictionaryLoadState loadStopped(String? url) =>
53+
_accepts(url) ? _withPhase(WebDictionaryLoadPhase.ready) : this;
54+
55+
/// 当前主框架导航失败;旧 URL 的迟到错误不影响新导航。
56+
WebDictionaryLoadState loadFailed(String? url) =>
57+
_accepts(url) ? _withPhase(WebDictionaryLoadPhase.error) : this;
58+
59+
/// 等待超时后结束加载态;旧代际的计时器回调不影响当前状态。
60+
WebDictionaryLoadState timedOut(int expectedGeneration) =>
61+
expectedGeneration == generation && isLoading
62+
? _withPhase(WebDictionaryLoadPhase.error)
63+
: this;
64+
65+
bool _accepts(String? url) =>
66+
url == null || activeUrl == null || url == activeUrl;
67+
68+
WebDictionaryLoadState _withPhase(WebDictionaryLoadPhase value) =>
69+
phase == value
70+
? this
71+
: WebDictionaryLoadState(
72+
phase: value,
73+
generation: generation,
74+
activeUrl: activeUrl,
75+
);
76+
}

0 commit comments

Comments
 (0)