Files
MookNote/lib/pages/movies/douban_webview_page.dart
2026-08-12 22:28:33 +08:00

400 lines
13 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:convert';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import '../../widgets/app_overlay.dart';
/// 豆瓣影视WebView页面 - 用于抓取影视信息
class DoubanWebViewPage extends StatefulWidget {
final String url;
const DoubanWebViewPage({super.key, required this.url});
@override
State<DoubanWebViewPage> createState() => _DoubanWebViewPageState();
}
class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
InAppWebViewController? _controller;
bool _isLoading = true;
bool _isExtracting = false; // 防止重复提取
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('豆瓣影视'),
leading: _buildBackButton(),
actions: [
// 提取按钮 - 始终显示
_buildActionButton(
colors: colors,
icon: Icons.auto_fix_high_outlined,
onPressed: _showExtractedInfo,
tooltip: '提取信息',
),
// 刷新按钮
_buildActionButton(
colors: colors,
icon: Icons.refresh,
onPressed: () => _controller?.reload(),
tooltip: '刷新',
),
const SizedBox(width: 8),
],
),
body: Stack(
children: [
InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(widget.url)),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
),
onWebViewCreated: (controller) {
_controller = controller;
},
onLoadStart: (_, __) {
if (mounted) {
setState(() {
_isLoading = true;
});
}
},
onLoadStop: (_, __) {
if (mounted) {
setState(() {
_isLoading = false;
});
}
},
),
// 加载指示器
if (_isLoading) const Center(
child: CircularProgressIndicator(),
),
],
),
);
}
/// 构建返回按钮
Widget _buildBackButton() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
// 停止加载并返回
_controller?.loadUrl(
urlRequest: URLRequest(url: WebUri('about:blank')));
Navigator.pop(context);
},
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(8),
child: const Icon(Icons.arrow_back, color: Colors.white, size: 22),
),
),
),
);
}
/// 构建右上角操作按钮
Widget _buildActionButton({
required ColorScheme colors,
required IconData icon,
required VoidCallback onPressed,
required String tooltip,
}) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(8),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(8),
child: Icon(icon, color: colors.onSurface, size: 22),
),
),
),
);
}
/// 显示提取的信息对话框
Future<void> _showExtractedInfo() async {
// 先提取信息
final movieInfo = await _extractMovieInfo();
if (movieInfo == null) return;
// 显示提取的信息
if (mounted) {
appDialog(
context: context,
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Text(
'提取的影视信息',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(colors, '标题', movieInfo['title']?.toString() ?? '未提取到'),
_buildInfoRow(colors, '导演', movieInfo['director']?.toString() ?? '未提取到'),
_buildInfoRow(colors, '类型', movieInfo['genres']?.toString() ?? '未提取到'),
_buildInfoRow(colors, '上映日期', movieInfo['releaseDate']?.toString() ?? '未提取到'),
if (movieInfo['summary'] != null)
_buildInfoRow(
colors,
'简介',
movieInfo['summary'].toString().substring(
0,
movieInfo['summary'].toString().length > 100
? 100
: movieInfo['summary'].toString().length) +
'...'),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(
'取消',
style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)),
),
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
Navigator.pop(context, movieInfo);
},
child: Text(
'使用此信息',
style: TextStyle(
color: colors.onSurface, fontWeight: FontWeight.w600),
),
),
],
);
},
);
}
}
/// 构建信息行
Widget _buildInfoRow(ColorScheme colors, String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 64,
child: Text(
label,
style: TextStyle(
fontSize: 14,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 14,
color: colors.onSurface,
),
),
),
],
),
);
}
/// 提取影视信息
Future<Map<String, dynamic>?> _extractMovieInfo() async {
// 检查是否已提取过,避免重复点击
if (_isExtracting || _controller == null) return null;
try {
_isExtracting = true;
// 显示加载提示
appDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => const Center(
child: CircularProgressIndicator(),
),
);
// 执行JavaScript代码提取页面信息
final result = await _controller!.evaluateJavascript(source: r'''
(function() {
const info = {};
// 获取标题 - 移动版页面
const titleEl = document.querySelector('.sub-title');
info.title = titleEl ? titleEl.textContent.trim() : '';
// 获取年份 - 从 original-title 中提取
const originalTitleEl = document.querySelector('.sub-original-title');
if (originalTitleEl) {
const yearMatch = originalTitleEl.textContent.match(/\((\d{4})\)/);
info.year = yearMatch ? yearMatch[1] : '';
} else {
info.year = '';
}
// 获取封面图 - 从 sub-cover 中的 img 标签获取
const coverEl = document.querySelector('.sub-cover img');
if (coverEl) {
let coverUrl = coverEl.src;
// 将 webp 转换为 jpg 格式,提高兼容性
if (coverUrl && coverUrl.includes('.webp')) {
coverUrl = coverUrl.replace('.webp', '.jpg');
}
info.coverUrl = coverUrl;
} else {
info.coverUrl = '';
}
// 获取评分 - 移动版可能在 mark-item 中
const ratingEl = document.querySelector('.rating-num') || document.querySelector('.score');
info.rating = ratingEl ? ratingEl.textContent.trim() : '';
// 获取导演 - 从演职员列表中找
const directorEl = document.querySelector('.movie-celebrities .item__celebrity .role');
if (directorEl && directorEl.textContent.includes('导演')) {
const nameEl = directorEl.closest('.item__celebrity').querySelector('.name');
info.director = nameEl ? nameEl.textContent.trim() : '';
} else {
info.director = '';
}
// 获取编剧 - 从演职员列表中找(匹配"编剧"或"剧本"
const writerEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
const writers = [];
writerEls.forEach(el => {
const roleEl = el.querySelector('.role');
if (roleEl && (roleEl.textContent.includes('编剧') || roleEl.textContent.includes('剧本'))) {
const nameEl = el.querySelector('.name');
if (nameEl) writers.push(nameEl.textContent.trim());
}
});
info.writers = writers;
// 获取主演- 从演职员列表中找前5个
const actorEls = document.querySelectorAll('.movie-celebrities .item__celebrity');
const actors = [];
actorEls.forEach(el => {
const roleEl = el
.querySelector('.role');
if (roleEl && (
roleEl.textContent.includes('配音') ||
roleEl.textContent.includes('主演') ||
roleEl.textContent.includes('演员') ||
roleEl.textContent.includes('参演') ||
roleEl.textContent.includes('饰')
)) {
const nameEl = el.querySelector('.name');
if (nameEl) actors.push(nameEl.textContent.trim());
}
});
info.actors = actors;
// 获取类型 - 从 sub-meta 或标签中提取
const metaEl = document.querySelector('.sub-meta');
if (metaEl) {
const metaText = metaEl.textContent;
const parts = metaText.split('/').map(s => s.trim());
// 过滤出类型(通常是中文,不是日期,不是时长)
info.genres = parts.filter(p =>
p && !p.match(/^\d{4}/) && !p.includes('分钟') && !p.includes('上映')
).join(',');
} else {
info.genres = '';
}
// 获取上映日期
if (metaEl) {
const dateMatch = metaEl.textContent.match(/(\d{4}-\d{2}-\d{2})/);
info.releaseDate = dateMatch ? dateMatch[1] : '';
} else {
info.releaseDate = '';
}
// 获取简介
const summaryEl = document.querySelector('.subject-intro p');
if (summaryEl) {
info.summary = summaryEl.textContent.trim().substring(0, 500);
} else {
info.summary = '';
}
// 获取别名 - 从 original-title 中提取(去掉年份)
if (originalTitleEl) {
const fullText = originalTitleEl.textContent.trim();
info.alternateTitles = [fullText.replace(/\s*\(\d{4}\)\s*$/, '')];
} else {
info.alternateTitles = [];
}
return JSON.stringify(info);
})()
''');
// 关闭加载提示
if (mounted) Navigator.pop(context);
// 解析提取的信息
// evaluateJavascript 返回 JS 值JSON.stringify 的结果是字符串
final String jsonStr = result?.toString() ?? '';
if (jsonStr.isEmpty) return null;
// result 是 JSON.stringify 的输出,可能带外层引号
final String cleanJson = jsonStr.startsWith('"') && jsonStr.endsWith('"')
? jsonDecode(jsonStr) as String
: jsonStr;
final Map<String, dynamic> movieInfo = jsonDecode(cleanJson) as Map<String, dynamic>;
return movieInfo;
} catch (e) {
// 关闭加载提示
if (mounted) Navigator.pop(context);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('提取信息失败: $e')),
);
}
return null;
} finally {
_isExtracting = false;
}
}
}