reader_scene.dart 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'dart:async';
  4. import 'package:shuqi/public.dart';
  5. import 'article_provider.dart';
  6. import 'reader_utils.dart';
  7. import 'reader_config.dart';
  8. import 'reader_page_agent.dart';
  9. import 'reader_menu.dart';
  10. import 'reader_view.dart';
  11. enum PageJumpType { stay, firstPage, lastPage }
  12. class ReaderScene extends StatefulWidget {
  13. final int articleId;
  14. ReaderScene({required this.articleId});
  15. @override
  16. ReaderSceneState createState() => ReaderSceneState();
  17. }
  18. class ReaderSceneState extends State<ReaderScene> with RouteAware {
  19. int pageIndex = 0;
  20. bool isMenuVisiable = false;
  21. PageController pageController = PageController(keepPage: false);
  22. bool isLoading = false;
  23. double topSafeHeight = 0;
  24. Article? preArticle;
  25. Article? currentArticle;
  26. Article? nextArticle;
  27. List<Chapter> chapters = [];
  28. @override
  29. void initState() {
  30. super.initState();
  31. pageController.addListener(onScroll);
  32. setup();
  33. }
  34. @override
  35. void didChangeDependencies() {
  36. super.didChangeDependencies();
  37. routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute<dynamic>);
  38. }
  39. @override
  40. void didPop() {
  41. SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
  42. }
  43. @override
  44. void dispose() {
  45. pageController.dispose();
  46. routeObserver.unsubscribe(this);
  47. super.dispose();
  48. }
  49. void setup() async {
  50. await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
  51. // 不延迟的话,安卓获取到的topSafeHeight是错的。
  52. await Future.delayed(const Duration(milliseconds: 100), () {});
  53. SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
  54. topSafeHeight = Screen.topSafeHeight;
  55. List<dynamic> chaptersResponse = await Request.get(action: 'catalog');
  56. chaptersResponse.forEach((data) {
  57. chapters.add(Chapter.fromJson(data));
  58. });
  59. await resetContent(this.widget.articleId, PageJumpType.stay);
  60. }
  61. resetContent(int articleId, PageJumpType jumpType) async {
  62. currentArticle = await fetchArticle(articleId);
  63. if (currentArticle!.preArticleId > 0) {
  64. preArticle = await fetchArticle(currentArticle!.preArticleId);
  65. } else {
  66. preArticle = null;
  67. }
  68. if (currentArticle!.nextArticleId > 0) {
  69. nextArticle = await fetchArticle(currentArticle!.nextArticleId);
  70. } else {
  71. nextArticle = null;
  72. }
  73. if (jumpType == PageJumpType.firstPage) {
  74. pageIndex = 0;
  75. } else if (jumpType == PageJumpType.lastPage) {
  76. pageIndex = currentArticle!.pageCount - 1;
  77. }
  78. if (jumpType != PageJumpType.stay) {
  79. pageController.jumpToPage((preArticle != null ? preArticle!.pageCount : 0) + pageIndex);
  80. }
  81. setState(() {});
  82. }
  83. onScroll() {
  84. var page = pageController.offset / Screen.width;
  85. var nextArtilePage = currentArticle!.pageCount + (preArticle != null ? preArticle!.pageCount : 0);
  86. if (page >= nextArtilePage) {
  87. print('到达下个章节了');
  88. preArticle = currentArticle;
  89. currentArticle = nextArticle;
  90. nextArticle = null;
  91. pageIndex = 0;
  92. pageController.jumpToPage(preArticle!.pageCount);
  93. fetchNextArticle(currentArticle!.nextArticleId);
  94. setState(() {});
  95. }
  96. if (preArticle != null && page <= preArticle!.pageCount - 1) {
  97. print('到达上个章节了');
  98. nextArticle = currentArticle;
  99. currentArticle = preArticle;
  100. preArticle = null;
  101. pageIndex = currentArticle!.pageCount - 1;
  102. pageController.jumpToPage(currentArticle!.pageCount - 1);
  103. fetchPreviousArticle(currentArticle!.preArticleId);
  104. setState(() {});
  105. }
  106. }
  107. fetchPreviousArticle(int articleId) async {
  108. if (preArticle != null || isLoading || articleId == 0) {
  109. return;
  110. }
  111. isLoading = true;
  112. preArticle = await fetchArticle(articleId);
  113. pageController.jumpToPage(preArticle!.pageCount + pageIndex);
  114. isLoading = false;
  115. setState(() {});
  116. }
  117. fetchNextArticle(int articleId) async {
  118. if (nextArticle != null || isLoading || articleId == 0) {
  119. return;
  120. }
  121. isLoading = true;
  122. nextArticle = await fetchArticle(articleId);
  123. isLoading = false;
  124. setState(() {});
  125. }
  126. Future<Article> fetchArticle(int articleId) async {
  127. var article = await ArticleProvider.fetchArticle(articleId);
  128. var contentHeight = Screen.height - topSafeHeight - ReaderUtils.topOffset - Screen.bottomSafeHeight - ReaderUtils.bottomOffset - 20;
  129. var contentWidth = Screen.width - 15 - 10;
  130. article.pageOffsets = ReaderPageAgent.getPageOffsets(article.content, contentHeight, contentWidth, ReaderConfig.instance.fontSize);
  131. return article;
  132. }
  133. onTap(Offset position) async {
  134. double xRate = position.dx / Screen.width;
  135. if (xRate > 0.33 && xRate < 0.66) {
  136. SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
  137. setState(() {
  138. isMenuVisiable = true;
  139. });
  140. } else if (xRate >= 0.66) {
  141. nextPage();
  142. } else {
  143. previousPage();
  144. }
  145. }
  146. onPageChanged(int index) {
  147. var page = index - (preArticle != null ? preArticle!.pageCount : 0);
  148. if (page < currentArticle!.pageCount && page >= 0) {
  149. setState(() {
  150. pageIndex = page;
  151. });
  152. }
  153. }
  154. previousPage() {
  155. if (pageIndex == 0 && currentArticle!.preArticleId == 0) {
  156. Toast.show('已经是第一页了');
  157. return;
  158. }
  159. pageController.previousPage(duration: Duration(milliseconds: 250), curve: Curves.easeOut);
  160. }
  161. nextPage() {
  162. if (pageIndex >= currentArticle!.pageCount - 1 && currentArticle!.nextArticleId == 0) {
  163. Toast.show('已经是最后一页了');
  164. return;
  165. }
  166. pageController.nextPage(duration: Duration(milliseconds: 250), curve: Curves.easeOut);
  167. }
  168. Widget buildPage(BuildContext context, int index) {
  169. var page = index - (preArticle != null ? preArticle!.pageCount : 0);
  170. var article;
  171. if (page >= this.currentArticle!.pageCount) {
  172. // 到达下一章了
  173. article = nextArticle;
  174. page = 0;
  175. } else if (page < 0) {
  176. // 到达上一章了
  177. article = preArticle;
  178. page = preArticle!.pageCount - 1;
  179. } else {
  180. article = this.currentArticle;
  181. }
  182. return GestureDetector(
  183. onTapUp: (TapUpDetails details) {
  184. onTap(details.globalPosition);
  185. },
  186. child: ReaderView(article: article, page: page, topSafeHeight: topSafeHeight),
  187. );
  188. }
  189. buildPageView() {
  190. if (currentArticle == null) {
  191. return Container();
  192. }
  193. int itemCount = (preArticle != null ? preArticle!.pageCount : 0) + currentArticle!.pageCount + (nextArticle != null ? nextArticle!.pageCount : 0);
  194. return PageView.builder(
  195. physics: BouncingScrollPhysics(),
  196. controller: pageController,
  197. itemCount: itemCount,
  198. itemBuilder: buildPage,
  199. onPageChanged: onPageChanged,
  200. );
  201. }
  202. buildMenu() {
  203. if (!isMenuVisiable) {
  204. return Container();
  205. }
  206. return ReaderMenu(
  207. chapters: chapters,
  208. articleIndex: currentArticle!.index,
  209. onTap: hideMenu,
  210. onPreviousArticle: () {
  211. resetContent(currentArticle!.preArticleId, PageJumpType.firstPage);
  212. },
  213. onNextArticle: () {
  214. resetContent(currentArticle!.nextArticleId, PageJumpType.firstPage);
  215. },
  216. onToggleChapter: (Chapter chapter) {
  217. resetContent(chapter.id, PageJumpType.firstPage);
  218. },
  219. );
  220. }
  221. hideMenu() {
  222. SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
  223. setState(() {
  224. this.isMenuVisiable = false;
  225. });
  226. }
  227. @override
  228. Widget build(BuildContext context) {
  229. if (currentArticle == null) {
  230. return Scaffold();
  231. }
  232. return Scaffold(
  233. body: AnnotatedRegion(
  234. value: SystemUiOverlayStyle.dark,
  235. child: Stack(
  236. children: <Widget>[
  237. Positioned(
  238. left: 0,
  239. top: 0,
  240. right: 0,
  241. bottom: 0,
  242. child:
  243. Image.asset('assets/img/read_bg.png', fit: BoxFit.cover)),
  244. buildPageView(),
  245. buildMenu(),
  246. ],
  247. ),
  248. ),
  249. );
  250. }
  251. }