QinGang_interested/lib/customWidget/remote_file_page.dart

263 lines
9.4 KiB
Dart
Raw Normal View History

2025-12-12 09:11:30 +08:00
import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:pdfx/pdfx.dart';
import 'package:path_provider/path_provider.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:dio/dio.dart';
import 'package:qhd_prevention/tools/tools.dart';
class RemoteFilePage extends StatefulWidget {
final String fileUrl;
final int countdownSeconds;
const RemoteFilePage({
Key? key,
required this.fileUrl,
this.countdownSeconds = 3,
}) : super(key: key);
@override
_RemoteFilePageState createState() => _RemoteFilePageState();
}
class _RemoteFilePageState extends State<RemoteFilePage> {
String? _localPath;
bool _isLoading = true;
bool _hasScrolledToBottom = false;
bool _timerFinished = false;
late int _secondsRemaining;
Timer? _countdownTimer;
PdfControllerPinch? _pdfController;
int _totalPages = 0;
// ========== 新增用于方案1的字段 ==========
Size? _viewportSize; // 当前 viewport 大小,用于计算 scale
final List<double> _pageTopOffsets = []; // 每页顶部偏移(相对于内容起点)
double _totalContentHeight = 0; // 计算得到的滚动内容总高度
double _pageSpacing = 0.0; // 如果你在 PdfViewPinch 布局里有页间距,可设置
// ======================================
@override
void initState() {
super.initState();
_secondsRemaining = widget.countdownSeconds;
_startCountdown();
_downloadAndLoad();
LoadingDialogHelper.hide();
}
Future<void> _downloadAndLoad() async {
try {
final url = widget.fileUrl;
final filename = url.split('/').last;
final dir = await getTemporaryDirectory();
final filePath = '${dir.path}/$filename';
final dio = Dio();
final response = await dio.get<List<int>>(
url,
options: Options(responseType: ResponseType.bytes),
);
final file = File(filePath);
await file.writeAsBytes(response.data!);
// 直接传 Future<PdfDocument> 给 controller不要 await
final futureDoc = PdfDocument.openFile(filePath); // Future<PdfDocument>
_pdfController = PdfControllerPinch(document: futureDoc);
// 注册 pageListenable listenerpageListenable 是 ValueListenable<int>1-based
_pdfController!.pageListenable.addListener(_onPageListenableChanged);
if (!mounted) return;
setState(() {
_localPath = filePath;
_isLoading = false;
});
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
ToastUtil.showNormal(context, '文件加载失败: $e');
}
}
}
void _onPageListenableChanged() {
// pageListenable.value 是当前页1-based
final controller = _pdfController;
if (controller == null) return;
final currentPage = controller.pageListenable.value;
// 如果 totalPages 已知且到达最后一页,则标记为已看完
if (_totalPages > 0 && currentPage >= _totalPages - 3) {
if (!_hasScrolledToBottom && mounted) {
setState(() {
_hasScrolledToBottom = true;
});
}
}
}
void _startCountdown() {
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) return;
setState(() {
if (_secondsRemaining > 1) {
_secondsRemaining--;
} else {
_secondsRemaining = 0;
_timerFinished = true;
_countdownTimer?.cancel();
}
});
});
}
@override
void dispose() {
_countdownTimer?.cancel();
// 移除 listener 再释放 controller注意 controller 和其 listenable 可能为 null
try {
if (_pdfController != null) {
_pdfController!.pageListenable.removeListener(_onPageListenableChanged);
_pdfController!.dispose();
}
} catch (e) {
// ignore dispose errors
}
super.dispose();
}
// 当滚动通知到达底部(或接近底部)时调用 — 作为备选方案/调试用
bool _handleScrollNotification(ScrollNotification notification) {
final metrics = notification.metrics;
// 优先使用我们计算的 totalContentHeight + viewport 判断(更准确)
if (_totalContentHeight > 0 && _viewportSize != null) {
final viewportH = _viewportSize!.height;
final threshold = math.min(50.0, viewportH * 0.1); // 灵活阈值
final scrolledToBottom = (metrics.pixels + viewportH) >= (_totalContentHeight - threshold);
if (scrolledToBottom) {
if (!_hasScrolledToBottom && mounted) {
setState(() {
_hasScrolledToBottom = true;
});
}
}
} else {
// 兜底:使用 maxScrollExtent 判断(保留你原来的逻辑)
if ((metrics.maxScrollExtent - metrics.pixels) <= 5.0 ||
(metrics.atEdge && metrics.pixels == metrics.maxScrollExtent)) {
if (!_hasScrolledToBottom && mounted) {
setState(() {
_hasScrolledToBottom = true;
});
}
}
}
return false; // 让事件继续传递
}
@override
Widget build(BuildContext context) {
final isButtonEnabled = _timerFinished && _hasScrolledToBottom;
return Scaffold(
appBar: MyAppbar(title: '资料学习'),
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: LayoutBuilder(builder: (context, constraints) {
// 保存 viewport size用于计算 scale
_viewportSize = Size(constraints.maxWidth, constraints.maxHeight);
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: PdfViewPinch(
controller: _pdfController!,
scrollDirection: Axis.vertical,
onDocumentLoaded: (document) async {
// 原有行为:保存页数
_totalPages = document.pagesCount;
// ======= 新增:计算每页渲染高度并累加 totalContentHeight =======
// 注意document.getPage(i) 后要 close()
try {
final viewportW = _viewportSize?.width ?? MediaQuery.of(context).size.width;
final viewportH = _viewportSize?.height ?? MediaQuery.of(context).size.height;
_pageTopOffsets.clear();
double acc = 0;
for (int i = 1; i <= _totalPages; i++) {
final page = await document.getPage(i);
// page.width / page.height 可能是 num/double
final pw = (page.width is num) ? (page.width as num).toDouble() : page.width.toDouble();
final ph = (page.height is num) ? (page.height as num).toDouble() : page.height.toDouble();
// 模拟 contained 缩放行为scale = min(viewportW / pw, viewportH / ph)
final scale = math.min(viewportW / pw, viewportH / ph);
final renderedH = ph * scale;
_pageTopOffsets.add(acc);
acc += renderedH + _pageSpacing;
await page.close();
}
_totalContentHeight = acc;
} catch (e) {
// 计算失败则保留 _totalContentHeight 为 0使用兜底判断
_totalContentHeight = 0;
}
// ==============================================================
// 保留你原来:如果页少,直接视为已看完
if (_totalPages <= 3) {
if (!_hasScrolledToBottom && mounted) {
setState(() => _hasScrolledToBottom = true);
}
}
if (mounted) setState(() {});
},
// 作为兜底:当 page 到最后一页时也标记为已看完(注意 page 是 1-based
onPageChanged: (page) {
if (_totalPages > 0 && page == _totalPages) {
if (!_hasScrolledToBottom && mounted) {
setState(() => _hasScrolledToBottom = true);
}
}
},
),
);
}),
),
Padding(
padding: const EdgeInsets.all(16),
child: CustomButton(
backgroundColor: isButtonEnabled ? Colors.blue : Colors.grey,
text: isButtonEnabled
? '我已学习完毕'
: _secondsRemaining == 0 ? '我已学习完毕' : '$_secondsRemaining s我已学习完毕',
onPressed: isButtonEnabled
? () {
Navigator.pop(context, true);
}
: null,
),
),
],
),
),
);
}
}