import 'dart:async'; import 'dart:io'; import 'package:battery_plus/battery_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dmzj/app/app_color.dart'; import 'package:flutter_dmzj/app/app_constant.dart'; import 'package:flutter_dmzj/app/app_style.dart'; import 'package:flutter_dmzj/models/db/download_status.dart'; import 'package:flutter_dmzj/models/db/novel_download_info.dart'; import 'package:flutter_dmzj/services/app_settings_service.dart'; import 'package:flutter_dmzj/app/controller/base_controller.dart'; import 'package:flutter_dmzj/app/log.dart'; import 'package:flutter_dmzj/models/novel/novel_detail_model.dart'; import 'package:flutter_dmzj/requests/novel_request.dart'; import 'package:flutter_dmzj/services/novel_download_service.dart'; import 'package:flutter_dmzj/services/user_service.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:html_unescape/html_unescape.dart'; import 'package:remixicon/remixicon.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; // ignore: depend_on_referenced_packages import 'package:path/path.dart' as p; class NovelReaderController extends BaseController { final int novelId; final String novelTitle; final String novelCover; final List chapters; final FocusNode focusNode = FocusNode(); NovelDetailChapter chapter; NovelReaderController({ required this.novelId, required this.novelTitle, required this.novelCover, required this.chapters, required this.chapter, }) { chapterIndex.value = chapters.indexOf(chapter); } /// 当前章节索引 var chapterIndex = 0.obs; /// 当前页面 var currentIndex = 0.obs; /// 最大页面 var maxPage = 0.obs; /// 阅读进度,百分比 var progress = 0.0.obs; final AppSettingsService settings = AppSettingsService.instance; final NovelRequest request = NovelRequest(); final PageController pageController = PageController(); final ScrollController scrollController = ScrollController(); /// 连接信息监听 StreamSubscription? connectivitySubscription; /// 电量信息监听 StreamSubscription? batterySubscription; /// 连接类型 Rx connectivityType = Rx(ConnectivityResult.other); /// 电量信息 Rx batteryLevel = 0.obs; /// 显示电量 RxBool showBattery = true.obs; /// 文本内容 var content = "".obs; /// 是否是图片 var isPicture = false.obs; /// 是否为本地缓存 var isLocal = false; /// 图片列表 RxList pictures = RxList(); var contentLength = 0; /// 是否显示控制器 var showControls = false.obs; /// 阅读方向 var direction = 0.obs; /// 左手模式 bool get leftHandMode => settings.novelReaderLeftHandMode.value; /// 翻页动画 bool get pageAnimation => settings.novelReaderPageAnimation.value; @override void onInit() { initConnectivity(); initBattery(); direction.value = settings.novelReaderDirection.value; scrollController.addListener(listenVertical); setFull(); loadContent(); super.onInit(); } /// 初始化电池信息 void initBattery() async { try { //没有电池的Mac似乎会闪退,暂时屏蔽Mac //https://github.com/xiaoyaocz/flutter_dmzj/discussions/146 if (Platform.isMacOS) { showBattery.value = false; return; } var battery = Battery(); batterySubscription = battery.onBatteryStateChanged.listen((BatteryState state) async { try { var level = await battery.batteryLevel; batteryLevel.value = level; showBattery.value = true; } catch (e) { showBattery.value = false; } }); batteryLevel.value = await battery.batteryLevel; showBattery.value = true; } catch (e) { showBattery.value = false; } } /// 初始化连接状态 void initConnectivity() async { var connectivity = Connectivity(); connectivitySubscription = connectivity.onConnectivityChanged.listen((ConnectivityResult result) { //提醒 if (connectivityType.value != result && result == ConnectivityResult.mobile) { SmartDialog.showToast("您已切换至数据网络,请注意流量消耗"); } connectivityType.value = result; }); connectivityType.value = await connectivity.checkConnectivity(); } /// 监听竖向模式时滚动百分比 void listenVertical() { if (scrollController.position.maxScrollExtent > 0) { progress.value = scrollController.position.pixels / scrollController.position.maxScrollExtent; } } @override void onClose() { scrollController.removeListener(listenVertical); connectivitySubscription?.cancel(); batterySubscription?.cancel(); exitFull(); uploadHistory(); super.onClose(); } /// 加载内容 Future loadContent() async { try { pageLoadding.value = true; pageError.value = false; content.value = ""; currentIndex.value = 0; isLocal = false; chapter = chapters[chapterIndex.value]; //查询本地是否存在 var localInfo = NovelDownloadService.instance.box .get("${novelId}_${chapter.volumeId}_${chapter.chapterId}"); if (localInfo != null && localInfo.status == DownloadStatus.complete) { return await loadFromLocal(localInfo); } var text = await request.novelContent( volumeId: chapter.volumeId, chapterId: chapter.chapterId, ); contentLength = text.length; var subStr = text.substring(0, text.length < 200 ? text.length : 200); //检查是否是插画 if (subStr.contains(RegExp(''))) { List imgs = []; for (var item in RegExp(r'').allMatches(text)) { var src = item.group(1); if (src != null && src.isNotEmpty) { imgs.add(src); } } isPicture.value = true; pictures.value = imgs; content.value = text; maxPage.value = pictures.length; SmartDialog.showToast("双击插画可放大、保存哦~"); } else { isPicture.value = false; text = HtmlUnescape().convert(text); text = text .replaceAll('\r\n', '\n') .replaceAll("
", "\n") .replaceAll('
', "\n") .replaceAll('\n\n\n', "\n") .replaceAll('\n\n', "\n") .replaceAll('\n', "\n  ") .replaceAll(RegExp(r"  \s+"), "  "); content.value = text; } if (scrollController.hasClients) { scrollController.jumpTo(0); progress.value = 0.0; } preloadContent(); //TODO 阅读记录跳转 //上传记录 uploadHistory(); } catch (e) { pageError.value = true; errorMsg.value = e.toString(); } finally { pageLoadding.value = false; } //SmartDialog.dismiss(status: SmartStatus.loading); } Future loadFromLocal(NovelDownloadInfo local) async { try { isLocal = true; var file = File(p.join(NovelDownloadService.instance.savePath, local.taskId, local.fileName)); var text = await file.readAsString(); //检查是否是插画 if (local.isImage) { List imgs = local.imageFiles .map((e) => p.join(NovelDownloadService.instance.savePath, local.taskId, e)) .toList(); isPicture.value = true; pictures.value = imgs; content.value = text; maxPage.value = pictures.length; SmartDialog.showToast("双击插画可放大、保存哦~"); } else { isPicture.value = false; text = HtmlUnescape().convert(text); text = text .replaceAll('\r\n', '\n') .replaceAll("
", "\n") .replaceAll('
', "\n") .replaceAll('\n\n\n', "\n") .replaceAll('\n\n', "\n") .replaceAll('\n', "\n  ") .replaceAll(RegExp(r"  \s+"), "  "); content.value = text; } if (scrollController.hasClients) { scrollController.jumpTo(0); progress.value = 0.0; } preloadContent(); //TODO 阅读记录跳转 //上传记录 uploadHistory(); } catch (e) { pageError.value = true; errorMsg.value = e.toString(); } finally { pageLoadding.value = false; } } /// 预加载下一话 void preloadContent() async { try { if (chapterIndex.value == chapters.length - 1) { return; } var nextChapter = chapters[chapterIndex.value + 1]; await request.novelContent( volumeId: nextChapter.volumeId, chapterId: nextChapter.chapterId, ); } catch (e) { Log.logPrint(e); } } /// 上传历史记录 void uploadHistory() { var chapter = chapters[chapterIndex.value]; UserService.instance.updateNovelHistory( novelId: novelId, chapterId: chapter.chapterId, //TODO 已读位置计算 index: 0, total: contentLength, novelCover: novelCover, novelName: novelTitle, chapterName: chapter.chapterName, volumeId: chapter.volumeId, volumeName: chapter.volumeName, ); } /// 下一章 void nextChapter() { if (chapterIndex.value == chapters.length - 1) { SmartDialog.showToast("后面没有了"); return; } chapterIndex.value += 1; loadContent(); } /// 上一章 void forwardChapter() { if (chapterIndex.value == 0) { SmartDialog.showToast("前面没有了"); return; } chapterIndex.value -= 1; loadContent(); } /// 下一页 void nextPage() { if (direction.value == ReaderDirection.kUpToDown) { return; } var value = currentIndex.value; var max = maxPage.value; if (value >= max - 1) { nextChapter(); } else { jumpToPage(value + 1, anime: true); } } /// 上一页 void forwardPage() { if (direction.value == ReaderDirection.kUpToDown) { return; } var value = currentIndex.value; if (value == 0) { forwardChapter(); } else { jumpToPage(value - 1, anime: true); } } /// 跳转页数 void jumpToPage(int page, {bool anime = false}) { //竖向 if (direction.value == ReaderDirection.kUpToDown) { final viewportHeight = scrollController.position.viewportDimension; scrollController.jumpTo(viewportHeight * page); } else { anime && pageAnimation ? pageController.animateToPage(page, duration: const Duration(milliseconds: 200), curve: Curves.linear) : pageController.jumpToPage(page); } } /// 显示设置 void showSettings() { setShowControls(); showModalBottomSheet( context: Get.context!, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ), ), constraints: const BoxConstraints( maxWidth: 500, ), builder: (context) => Column( children: [ ListTile( title: const Text("设置"), trailing: IconButton( onPressed: Get.back, icon: const Icon(Icons.close), ), contentPadding: AppStyle.edgeInsetsL12, ), Expanded( child: Obx( () => ListView( padding: AppStyle.edgeInsetsA12, children: [ buildBGItem( context, child: ListTile( title: const Text("阅读方向"), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ buildSelectedButton( onTap: () { setDirection(ReaderDirection.kLeftToRight); }, selected: settings.novelReaderDirection.value == ReaderDirection.kLeftToRight, child: const Icon(Remix.arrow_right_line), ), AppStyle.hGap8, buildSelectedButton( onTap: () { setDirection(ReaderDirection.kRightToLeft); }, selected: settings.novelReaderDirection.value == ReaderDirection.kRightToLeft, child: const Icon(Remix.arrow_left_line), ), AppStyle.hGap8, buildSelectedButton( onTap: () { setDirection(ReaderDirection.kUpToDown); }, selected: settings.novelReaderDirection.value == ReaderDirection.kUpToDown, child: const Icon(Remix.arrow_down_line), ) ], ), ), ), AppStyle.vGap12, buildBGItem( context, child: ListTile( title: const Text("阅读主题"), trailing: Row( mainAxisSize: MainAxisSize.min, children: AppColor.novelThemes.keys .map( (e) => GestureDetector( onTap: () { settings.setNovelReaderTheme(e); }, child: Container( margin: AppStyle.edgeInsetsL8, height: 36, width: 36, decoration: BoxDecoration( color: AppColor.novelThemes[e]!.first, borderRadius: AppStyle.radius24, border: Border.all( color: Colors.grey.withOpacity(.2), ), ), child: Visibility( visible: AppColor.novelThemes.keys .toList() .indexOf(e) == settings.novelReaderTheme.value, child: Icon( Icons.check, color: AppColor.novelThemes[e]!.last, ), ), ), ), ) .toList(), ), ), ), AppStyle.vGap12, buildBGItem( context, child: SwitchListTile( value: settings.novelReaderLeftHandMode.value, onChanged: (e) { settings.setNovelReaderLeftHandMode(e); }, title: const Text("操作反转"), subtitle: const Text("点击左侧下一页,右侧上一页"), ), ), AppStyle.vGap12, buildBGItem( context, child: SwitchListTile( value: settings.novelReaderShowStatus.value, onChanged: (e) { settings.setNovelReaderShowStatus(e); }, title: const Text("显示状态信息"), ), ), AppStyle.vGap12, buildBGItem( context, child: SwitchListTile( value: settings.novelReaderPageAnimation.value, onChanged: (e) { settings.setNovelReaderPageAnimation(e); }, title: const Text("翻页动画"), ), ), AppStyle.vGap12, buildBGItem( context, child: ListTile( title: const Text("字体大小"), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ OutlinedButton( onPressed: () { settings.setNovelReaderFontSize( settings.novelReaderFontSize.value + 1, ); }, child: const Icon( Icons.add, // color: Colors.grey, ), ), AppStyle.hGap12, Text("${settings.novelReaderFontSize.value}"), AppStyle.hGap12, OutlinedButton( onPressed: () { settings.setNovelReaderFontSize( settings.novelReaderFontSize.value - 1, ); }, child: const Icon( Icons.remove, // color: Colors.grey, ), ), ], ), ), ), AppStyle.vGap12, buildBGItem( context, child: ListTile( title: const Text("行距"), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ OutlinedButton( onPressed: () { settings.setNovelReaderLineSpacing( settings.novelReaderLineSpacing.value + 0.1, ); }, child: const Icon( Icons.add, // color: Colors.grey, ), ), AppStyle.hGap12, Text((settings.novelReaderLineSpacing.value) .toStringAsFixed(1)), AppStyle.hGap12, OutlinedButton( onPressed: () { settings.setNovelReaderLineSpacing( settings.novelReaderLineSpacing.value - 0.1, ); }, child: const Icon( Icons.remove, // color: Colors.grey, ), ), ], ), ), ), ], ), ), ), ], ), ); } /// 设置阅读方向 void setDirection(int value) { settings.setNovelReaderDirection(value); direction.value = value; } Widget buildBGItem(BuildContext context, {required Widget child}) { return Container( decoration: BoxDecoration( borderRadius: AppStyle.radius8, color: Theme.of(context).colorScheme.surfaceContainerHighest, ), child: child, ); } Widget buildSelectedButton( {required Widget child, bool selected = false, Function()? onTap}) { return Builder(builder: (context) { return OutlinedButton( style: OutlinedButton.styleFrom( foregroundColor: selected ? Theme.of(context).colorScheme.primary : Colors.grey, side: BorderSide( color: selected ? Theme.of(context).colorScheme.primary : Colors.grey, ), ), onPressed: onTap, child: child, ); }); } /// 显示目录 void showMenu() { setShowControls(); showModalBottomSheet( context: Get.context!, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ), ), constraints: const BoxConstraints( maxWidth: 500, ), builder: (context) => Column( children: [ ListTile( title: Text("目录(${chapters.length})"), trailing: IconButton( onPressed: Get.back, icon: const Icon(Icons.close), ), contentPadding: AppStyle.edgeInsetsL12, ), Divider( height: 1.0, color: Theme.of(context).dividerColor.withOpacity(.2), ), Expanded( child: ScrollablePositionedList.separated( initialScrollIndex: chapterIndex.value, itemCount: chapters.length, separatorBuilder: (_, i) => Divider( indent: 12, endIndent: 12, height: 1.0, color: Theme.of(context).dividerColor.withOpacity(.2), ), itemBuilder: (_, i) { var item = chapters[i]; return ListTile( selected: i == chapterIndex.value, selectedTileColor: Theme.of(context).colorScheme.secondaryContainer, selectedColor: Theme.of(context).colorScheme.onSecondaryContainer, title: Text(item.chapterName), subtitle: Text(item.volumeName), onTap: () { chapterIndex.value = i; loadContent(); Get.back(); }, ); }, ), ), ], ), routeSettings: const RouteSettings(name: "/modalBottomSheet"), ); } /// 设置显示/隐藏控制按钮 void setShowControls() { if (settings.novelReaderFullScreen.value) { if (showControls.value) { setFull(); } else { setFullEdge(); } } Future.delayed(const Duration(milliseconds: 100), () { showControls.value = !showControls.value; }); } /// 进入全屏 void setFull() { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: [], ); } /// 进入全屏edgeToEdge模式 void setFullEdge() { SystemChrome.setEnabledSystemUIMode( SystemUiMode.edgeToEdge, overlays: SystemUiOverlay.values, ); SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, systemNavigationBarColor: Colors.transparent, systemNavigationBarIconBrightness: Brightness.light, )); } /// 退出全屏 void exitFull() { SystemChrome.setEnabledSystemUIMode( SystemUiMode.edgeToEdge, overlays: SystemUiOverlay.values, ); } void keyDown(LogicalKeyboardKey key) { if (key == LogicalKeyboardKey.arrowLeft || key == LogicalKeyboardKey.pageUp) { if (leftHandMode) { nextPage(); } else { forwardPage(); } } else if (key == LogicalKeyboardKey.arrowRight || key == LogicalKeyboardKey.pageDown) { if (leftHandMode) { forwardPage(); } else { nextPage(); } } } }