diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 193e344..b546a87 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -66,19 +66,16 @@ jobs: config: - target: linux host: ubuntu-latest - flutter_version: '3.3.8' + flutter_version: '3.3.9' - target: windows host: windows-latest - flutter_version: '3.3.8' + flutter_version: '3.3.9' - target: macos host: macos-11 - flutter_version: '3.3.8' + flutter_version: '3.3.9' - target: ios host: macos-11 - flutter_version: '3.3.8' - - target: ios - host: macos-11 - flutter_version: '2.10.5' + flutter_version: '3.3.9' - target: android-arm32 host: ubuntu-latest flutter_version: '2.10.5' @@ -90,13 +87,13 @@ jobs: flutter_version: '2.10.5' - target: android-arm32 host: ubuntu-latest - flutter_version: '3.3.8' + flutter_version: '3.3.9' - target: android-arm64 host: ubuntu-latest - flutter_version: '3.3.8' + flutter_version: '3.3.9' - target: android-x86_64 host: ubuntu-latest - flutter_version: '3.3.8' + flutter_version: '3.3.9' runs-on: ${{ matrix.config.host }} @@ -190,13 +187,6 @@ jobs: run: | dart pub global activate ffigen 4.1.3 - - name: Install LLVM and Clang (macos/ios) - if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios' && startsWith(matrix.config.flutter_version, '2') - uses: KyleMayes/install-llvm-action@v1 - with: - version: ${{ env.LLVM_VERSION }} - cached: ${{ steps.cache-llvm.outputs.cache-hit }} - - name: Install LLVM and Clang (Linux/Android) if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' || matrix.config.target == 'linux' ) run: | diff --git a/ci/version.code.txt b/ci/version.code.txt index 58e7c70..30a48ae 100644 --- a/ci/version.code.txt +++ b/ci/version.code.txt @@ -1 +1 @@ -v0.1.12 \ No newline at end of file +v0.1.13 \ No newline at end of file diff --git a/ci/version.info.txt b/ci/version.info.txt index 487503a..fb6ff13 100644 --- a/ci/version.info.txt +++ b/ci/version.info.txt @@ -1,5 +1,8 @@ 计划 - - [x] 下载? / 小说翻页? / 高清画质? + - [x] 下载? / / 高清画质? + +v0.1.13 + - [x] 小说左右翻页 v0.1.11 - [x] 修复登录, 以及登录造成的开启很慢等 diff --git a/lib/configs/novel_reader_type.dart b/lib/configs/novel_reader_type.dart new file mode 100644 index 0000000..b6c115d --- /dev/null +++ b/lib/configs/novel_reader_type.dart @@ -0,0 +1,68 @@ +import 'package:daisy/configs/reader_controller_type.dart'; +import 'package:daisy/ffi.dart'; +import 'package:flutter/material.dart'; + +import '../commons.dart'; + +enum NovelReaderType { + move, + html, +} + +const _propertyName = "NovelReaderType"; +late NovelReaderType _NovelReaderType; + +Future initNovelReaderType() async { + _NovelReaderType = _fromString(await native.loadProperty(k: _propertyName)); +} + +NovelReaderType _fromString(String valueForm) { + for (var value in NovelReaderType.values) { + if (value.toString() == valueForm) { + return value; + } + } + return NovelReaderType.values.first; +} + +NovelReaderType get currentNovelReaderType => _NovelReaderType; + +String novelReaderTypeName(NovelReaderType direction, BuildContext context) { + switch (direction) { + case NovelReaderType.move: + return "平移"; + case NovelReaderType.html: + return "网页"; + } +} + +Future chooseNovelReaderType(BuildContext context) async { + final Map map = {}; + for (var element in NovelReaderType.values) { + map[novelReaderTypeName(element, context)] = element; + } + final newNovelReaderType = await chooseMapDialog( + context, + title: "请选择小说阅读器", + values: map, + ); + if (newNovelReaderType != null) { + await native.saveProperty(k: _propertyName, v: "$newNovelReaderType"); + _NovelReaderType = newNovelReaderType; + } +} + +Widget novelReaderTypeSetting(BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, void Function(void Function()) setState) { + return ListTile( + onTap: () async { + await chooseNovelReaderType(context); + setState(() {}); + }, + title: const Text("小说阅读器类型"), + subtitle: Text(novelReaderTypeName(_NovelReaderType, context)), + ); + }, + ); +} diff --git a/lib/screens/about_screen.dart b/lib/screens/about_screen.dart index f423d0e..c5a283f 100644 --- a/lib/screens/about_screen.dart +++ b/lib/screens/about_screen.dart @@ -5,6 +5,7 @@ import 'package:daisy/configs/themes.dart'; import 'package:daisy/configs/versions.dart'; import 'package:flutter/material.dart'; +import '../configs/novel_reader_type.dart'; import '../cross.dart'; import 'components/badged.dart'; import 'login_screen.dart'; @@ -69,6 +70,8 @@ class _AboutState extends State { const Divider(), androidDisplayModeSetting(), const Divider(), + novelReaderTypeSetting(context), + const Divider(), ], ), ); diff --git a/lib/screens/components/novel_fan_component.dart b/lib/screens/components/novel_fan_component.dart new file mode 100644 index 0000000..5d45bf4 --- /dev/null +++ b/lib/screens/components/novel_fan_component.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; + +class NovelFanComponentController { + _NovelFanComponentState? _state; + + toPrevious() { + _state?._toPrevious(); + } + + toNext() { + _state?._toNext(); + } +} + +class NovelFanComponent extends StatefulWidget { + final Widget? previous; + final Widget? next; + final Widget current; + final void Function()? onNextSetState; + final void Function()? onPreviousSetState; + final NovelFanComponentController? controller; + + const NovelFanComponent({ + this.previous, + this.onPreviousSetState, + this.next, + this.onNextSetState, + this.controller, + required this.current, + Key? key, + }) : super(key: key); + + @override + State createState() => _NovelFanComponentState(); +} + +class _NovelFanComponentState extends State + with SingleTickerProviderStateMixin { + late AnimationController animationController = + AnimationController(vsync: this); + + @override + void initState() { + animationController.addListener(_onAnimat); + widget.controller?._state = this; + super.initState(); + } + + @override + void dispose() { + animationController.removeListener(_onAnimat); + animationController.dispose(); + widget.controller?._state = null; + super.dispose(); + } + + void _onAnimat() { + moved = movedAnimeStar + + (movedAnimeEnd - movedAnimeStar) * animationController.value; + setState(() {}); + } + + int touchState = 0; // 0未触摸 + late Offset prePosition; + late int direction; + late int lastDirection; + late double moved; + + late double movedAnimeStar; + late double movedAnimeEnd; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onPanStart: (detail) { + if (animationController.isAnimating) { + return; + } + touchState = 1; + prePosition = detail.globalPosition; + moved = 0; + }, + onPanUpdate: (detail) { + if (detail.globalPosition.dx != prePosition.dx) { + if (touchState == 1) { + touchState = 2; + direction = detail.globalPosition.dx < prePosition.dx ? -1 : 1; + lastDirection = detail.globalPosition.dx < prePosition.dx ? -1 : 1; + moved += detail.globalPosition.dx - prePosition.dx; + if (direction < 0 && widget.next == null) { + // 直到下次touchDown不响应 + touchState = 0; + } else if (direction > 0 && widget.previous == null) { + // 直到下次touchDown不响应 + touchState = 0; + } + } else if (touchState == 2) { + lastDirection = detail.globalPosition.dx < prePosition.dx ? -1 : 1; + moved += detail.globalPosition.dx - prePosition.dx; + } + } + prePosition = detail.globalPosition; + setState(() {}); + }, + onPanEnd: (detail) async { + if (touchState == 2) { + // 滑动过 + if (direction == lastDirection) { + movedAnimeStar = moved; + movedAnimeEnd = direction * MediaQuery.of(context).size.width; + animationController.duration = const Duration(milliseconds: 240); + await animationController.forward(from: 0); + touchState = 0; + if (direction < 0) { + if (widget.onNextSetState != null) { + widget.onNextSetState!(); + } + } + if (direction > 0) { + if (widget.onPreviousSetState != null) { + widget.onPreviousSetState!(); + } + } + // 滑动 + } else { + // 滑回 + movedAnimeStar = moved; + movedAnimeEnd = 0; + animationController.duration = const Duration(milliseconds: 240); + await animationController.forward(from: 0); + } + } + }, + child: Stack(children: [ + _previous(), + _current(), + _next(), + ]), + ); + } + + void _toPrevious() async { + if (animationController.isAnimating) { + return; + } + if (widget.previous == null) { + return; + } + touchState = 2; + direction = 1; + movedAnimeStar = 0; + movedAnimeEnd = direction * MediaQuery.of(context).size.width; + animationController.duration = const Duration(milliseconds: 140); + await animationController.forward(from: 0); + touchState = 0; + widget.onPreviousSetState!(); + } + + void _toNext() async { + if (animationController.isAnimating) { + return; + } + if (widget.next == null) { + return; + } + touchState = 2; + direction = -1; + movedAnimeStar = 0; + movedAnimeEnd = direction * MediaQuery.of(context).size.width; + animationController.duration = const Duration(milliseconds: 140); + await animationController.forward(from: 0); + touchState = 0; + widget.onNextSetState!(); + } + + Widget _previous() { + if (widget.previous != null) { + return widget.previous!; + } + return Container(); + } + + Widget _current() { + if (widget.previous != null && touchState > 0 && direction > 0) { + return Transform.translate( + offset: Offset( + moved, + 0, + ), + child: widget.current, + ); + } + return widget.current; + } + + Widget _next() { + if (widget.next != null && touchState > 0 && direction < 0) { + return Transform.translate( + offset: Offset( + MediaQuery.of(context).size.width + moved, + 0, + ), + child: widget.next, + ); + } + return Container(); + } +} diff --git a/lib/screens/init_screen.dart b/lib/screens/init_screen.dart index 6491a33..fbe7def 100644 --- a/lib/screens/init_screen.dart +++ b/lib/screens/init_screen.dart @@ -3,6 +3,7 @@ import 'package:daisy/configs/android_version.dart'; import 'package:daisy/configs/login.dart'; import 'package:daisy/configs/novel_background_color.dart'; import 'package:daisy/configs/novel_font_color.dart'; +import 'package:daisy/configs/novel_reader_type.dart'; import 'package:daisy/configs/reader_controller_type.dart'; import 'package:daisy/configs/reader_direction.dart'; import 'package:daisy/configs/reader_slider_position.dart'; @@ -35,6 +36,7 @@ class _InitScreenState extends State { await initReaderDirection(); await initReaderSliderPosition(); await initReaderType(); + await initNovelReaderType(); await initNovelFontSize(); await initNovelFontColor(); await initNovelBackgroundColor(); diff --git a/lib/screens/novel_detail_screen.dart b/lib/screens/novel_detail_screen.dart index eaa3137..87b2d3c 100644 --- a/lib/screens/novel_detail_screen.dart +++ b/lib/screens/novel_detail_screen.dart @@ -1,3 +1,4 @@ +import 'package:daisy/configs/novel_reader_type.dart'; import 'package:flutter/material.dart'; import 'package:uuid/uuid.dart'; import '../commons.dart'; @@ -9,6 +10,7 @@ import 'components/content_error.dart'; import 'components/content_loading.dart'; import 'components/images.dart'; import 'components/subscribed_icon.dart'; +import 'novel_html_reader_screen.dart'; import 'novel_reader_screen.dart'; class NovelDetailScreen extends StatefulWidget { @@ -175,17 +177,34 @@ class _NovelDetailScreenState extends State with RouteAware { if (chapter.chapterId == viewLog.chapterId) { return _continueButton( onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NovelReaderScreen( - novel: _detail, - volumes: _volumes, - chapter: chapter, - volume: volume, - ), - ), - ); + switch (currentNovelReaderType) { + case NovelReaderType.move: + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NovelReaderScreen( + novel: _detail, + volumes: _volumes, + chapter: chapter, + volume: volume, + ), + ), + ); + break; + case NovelReaderType.html: + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NovelHtmlReaderScreen( + novel: _detail, + volumes: _volumes, + chapter: chapter, + volume: volume, + ), + ), + ); + break; + } }, text: "继续阅读 ${viewLog.chapterTitle}"); // - P.${viewLog.pageRank + 1} @@ -196,23 +215,41 @@ class _NovelDetailScreenState extends State with RouteAware { } } if (_volumes.isNotEmpty) { - final volume = _volumes.reduce((o1, o2) => o1.rank < o2.rank ? o1 : o2); + final volume = + _volumes.reduce((o1, o2) => o1.rank < o2.rank ? o1 : o2); if (volume.chapters.isNotEmpty) { final chapter = _volumes[0].chapters.reduce( (o1, o2) => o1.chapterOrder < o2.chapterOrder ? o1 : o2); return _continueButton( onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NovelReaderScreen( - novel: _detail, - volumes: _volumes, - chapter: chapter, - volume: volume, - ), - ), - ); + switch (currentNovelReaderType) { + case NovelReaderType.move: + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NovelReaderScreen( + novel: _detail, + volumes: _volumes, + chapter: chapter, + volume: volume, + ), + ), + ); + break; + case NovelReaderType.html: + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NovelHtmlReaderScreen( + novel: _detail, + volumes: _volumes, + chapter: chapter, + volume: volume, + ), + ), + ); + break; + } }, text: "从头开始 ${chapter.chapterName}"); } @@ -251,19 +288,39 @@ class _NovelDetailScreenState extends State with RouteAware { print(_detail.id); print(volume.id); print(e.chapterId); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NovelReaderScreen( - novel: _detail, - volume: volume, - chapter: e, - volumes: _volumes, - // loadChapter: _loadChapterF(), - // initRank: 0, - ), - ), - ); + + switch (currentNovelReaderType) { + case NovelReaderType.move: + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NovelReaderScreen( + novel: _detail, + volume: volume, + chapter: e, + volumes: _volumes, + // loadChapter: _loadChapterF(), + // initRank: 0, + ), + ), + ); + break; + case NovelReaderType.html: + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NovelHtmlReaderScreen( + novel: _detail, + volume: volume, + chapter: e, + volumes: _volumes, + // loadChapter: _loadChapterF(), + // initRank: 0, + ), + ), + ); + break; + } }, color: Colors.white, child: Text( diff --git a/lib/screens/novel_html_reader_screen.dart b/lib/screens/novel_html_reader_screen.dart new file mode 100644 index 0000000..b5e8411 --- /dev/null +++ b/lib/screens/novel_html_reader_screen.dart @@ -0,0 +1,373 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:daisy/configs/novel_background_color.dart'; +import 'package:daisy/ffi.dart'; +import 'package:daisy/screens/components/content_error.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +import '../configs/novel_font_color.dart'; +import '../configs/novel_font_size.dart'; +import 'components/content_loading.dart'; + +class NovelHtmlReaderScreen extends StatefulWidget { + final NovelDetail novel; + final NovelVolume volume; + final NovelChapter chapter; + final List volumes; + + const NovelHtmlReaderScreen({ + required this.novel, + required this.volume, + required this.chapter, + required this.volumes, + Key? key, + }) : super(key: key); + + @override + State createState() => _NovelHtmlReaderScreenState(); +} + +class _NovelHtmlReaderScreenState extends State { + late Future _contentFuture; + + @override + void initState() { + native.novelViewPage( + novelId: widget.novel.id, + volumeId: widget.volume.id, + volumeTitle: widget.volume.title, + volumeOrder: widget.volume.rank, + chapterId: widget.chapter.chapterId, + chapterTitle: widget.chapter.chapterName, + chapterOrder: widget.chapter.chapterOrder, + progress: 0, + ); + _contentFuture = native.novelContent( + volumeId: widget.volume.id, + chapterId: widget.chapter.chapterId, + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _contentFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasError) { + return Scaffold( + appBar: AppBar( + title: Text(widget.chapter.chapterName), + ), + body: ContentError( + error: snapshot.error, + stackTrace: snapshot.stackTrace, + onRefresh: () async { + setState(() { + _contentFuture = native.novelContent( + volumeId: widget.volume.id, + chapterId: widget.chapter.chapterId, + ); + }); + }, + ), + ); + } + + if (snapshot.connectionState != ConnectionState.done) { + return Scaffold( + appBar: AppBar( + title: Text(widget.chapter.chapterName), + ), + body: const ContentLoading(), + ); + } + + return _buildReader(snapshot.requireData); + }, + ); + } + + bool _inFullScreen = false; + + bool get _fullScreen => _inFullScreen; + + set _fullScreen(bool val) { + _inFullScreen = val; + if (Platform.isIOS || Platform.isAndroid) { + if (val) { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: [], + ); + } else { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + } + } + } + + Widget _buildReader(String text) { + return Scaffold( + body: StatefulBuilder( + builder: ( + BuildContext context, + void Function(void Function()) setState, + ) { + return Stack( + children: [ + GestureDetector( + onTap: () { + setState(() { + _fullScreen = !_fullScreen; + }); + }, + child: Container( + color: getNovelBackgroundColor(context), + child: ListView( + padding: EdgeInsets.only( + left: 10, + right: 10, + top: 15 + (Scaffold.of(context).appBarMaxHeight ?? 0), + bottom: 15 + (Scaffold.of(context).appBarMaxHeight ?? 0), + ), + children: [ + Html( + data: text, + style: { + "body": Style( + fontSize: FontSize.em(novelFontSize), + color: getNovelFontColor(context), + ), + }, + ), + ], + ), + ), + ), + ..._fullScreen + ? [] + : [ + Column( + children: [ + AppBar( + backgroundColor: Colors.black.withOpacity(.5), + title: Text(widget.chapter.chapterName), + actions: [ + IconButton( + onPressed: _onChooseEp, + icon: const Icon(Icons.menu_open), + ), + IconButton( + onPressed: _bottomMenu, + icon: const Icon(Icons.more_horiz), + ) + ], + ), + Expanded(child: Container()), + ], + ), + ], + ], + ); + }, + ), + ); + } + + Future _onChooseEp() async { + showMaterialModalBottomSheet( + context: context, + backgroundColor: const Color(0xAA000000), + builder: (context) { + return SizedBox( + height: MediaQuery.of(context).size.height * (.45), + child: _EpChooser( + widget.novel, + widget.volume, + widget.chapter, + widget.volumes, + onChangeEp, + ), + ); + }, + ); + } + + void _bottomMenu() async { + await showMaterialModalBottomSheet( + context: context, + backgroundColor: const Color(0xAA000000), + builder: (context) { + return SizedBox( + height: MediaQuery.of(context).size.height * (.45), + child: ListView( + children: [ + Row( + children: [ + _bottomIcon( + icon: Icons.text_fields, + title: novelFontSize.toString(), + onPressed: () async { + await modifyNovelFontSize(context); + setState(() => {}); + }, + ), + _bottomIcon( + icon: Icons.format_color_text, + title: "颜色", + onPressed: () async { + await modifyNovelFontColor(context); + setState(() => {}); + }, + ), + _bottomIcon( + icon: Icons.format_shapes, + title: "颜色", + onPressed: () async { + await modifyNovelBackgroundColor(context); + setState(() => {}); + }, + ), + ], + ), + ], + ), + ); + }, + ); + } + + Widget _bottomIcon({ + required IconData icon, + required String title, + required void Function() onPressed, + }) { + return Expanded( + child: Center( + child: Column( + children: [ + IconButton( + iconSize: 55, + icon: Column( + children: [ + Container(height: 3), + Icon( + icon, + size: 25, + color: Colors.white, + ), + Container(height: 3), + Text( + title, + style: const TextStyle(color: Colors.white, fontSize: 10), + maxLines: 1, + textAlign: TextAlign.center, + ), + Container(height: 3), + ], + ), + onPressed: onPressed, + ) + ], + ), + ), + ); + } + + Future onChangeEp(NovelDetail n, NovelVolume v, NovelChapter c) async { + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (BuildContext context) => NovelHtmlReaderScreen( + novel: n, + volume: v, + chapter: c, + volumes: widget.volumes, + ), + )); + } +} + +class _EpChooser extends StatefulWidget { + final NovelDetail novel; + final NovelVolume volume; + final NovelChapter chapter; + final List volumes; + final FutureOr Function(NovelDetail, NovelVolume, NovelChapter) onChangeEp; + + const _EpChooser( + this.novel, + this.volume, + this.chapter, + this.volumes, + this.onChangeEp, + ); + + @override + State createState() => _EpChooserState(); +} + +class _EpChooserState extends State<_EpChooser> { + int position = 0; + List widgets = []; + + @override + void initState() { + for (var c in widget.volumes) { + widgets.add(Container( + margin: const EdgeInsets.only(left: 15, right: 15, top: 15, bottom: 5), + child: Text( + c.title, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + )); + final cd = [...c.chapters]; + cd.sort((o1, o2) => o1.chapterOrder - o2.chapterOrder); + for (var ci in c.chapters) { + if (widget.chapter.chapterId == ci.chapterId) { + position = widgets.length > 2 ? widgets.length - 2 : 0; + } + widgets.add(Container( + margin: const EdgeInsets.only(left: 15, right: 15, top: 5, bottom: 5), + decoration: BoxDecoration( + color: widget.chapter.chapterId == ci.chapterId + ? Colors.grey.withAlpha(100) + : null, + border: Border.all( + color: const Color(0xff484c60), + style: BorderStyle.solid, + width: .5, + ), + ), + child: MaterialButton( + onPressed: () { + Navigator.of(context).pop(); + widget.onChangeEp(widget.novel, c, ci); + }, + textColor: Colors.white, + child: Text(ci.chapterName), + ), + )); + } + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ScrollablePositionedList.builder( + initialScrollIndex: position, + itemCount: widgets.length, + itemBuilder: (BuildContext context, int index) => widgets[index], + ); + } +} diff --git a/lib/screens/novel_reader_screen.dart b/lib/screens/novel_reader_screen.dart index cd9eeff..c83c76a 100644 --- a/lib/screens/novel_reader_screen.dart +++ b/lib/screens/novel_reader_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:daisy/configs/novel_background_color.dart'; import 'package:daisy/ffi.dart'; @@ -13,6 +14,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import '../configs/novel_font_color.dart'; import '../configs/novel_font_size.dart'; import 'components/content_loading.dart'; +import 'components/novel_fan_component.dart'; class NovelReaderScreen extends StatefulWidget { final NovelDetail novel; @@ -34,6 +36,103 @@ class NovelReaderScreen extends StatefulWidget { class _NovelReaderScreenState extends State { late Future _contentFuture; + late String texts = ""; + List chapterTexts = []; + late int fIndex = 0; + + List _reRenderTextIn(String bookText) { + bookText = bookText.replaceAll("
\n", "\n"); + bookText = bookText.replaceAll("
\n", "\n"); + bookText = bookText.replaceAll("
", "\n"); + bookText = bookText.replaceAll("
", "\n"); + bookText = bookText.replaceAll(" ", " "); + bookText = bookText.replaceAll("&", "&"); + bookText = bookText.replaceAll("…", "…"); + bookText = bookText.replaceAll("•", "·"); + bookText = bookText.replaceAll("<", "<"); + bookText = bookText.replaceAll(">", ">"); + bookText = bookText.replaceAll(""", "\""); + bookText = bookText.replaceAll("©", "©"); + bookText = bookText.replaceAll("®", "®"); + bookText = bookText.replaceAll("×", "×"); + bookText = bookText.replaceAll("&pide;", "÷"); + bookText = bookText.replaceAll(" ", " "); + bookText = bookText.replaceAll(" ", " "); + bookText = bookText.replaceAll("“", "“"); + bookText = bookText.replaceAll("”", "”"); + bookText = bookText.replaceAll("—", "—"); + bookText = bookText.replaceAll("·", "·"); + bookText = bookText.replaceAll("‘", "‘"); + bookText = bookText.replaceAll("’", "’"); + + bookText = bookText.trim(); + // 切割文字????s + final _mq = MediaQuery.of(context); + final _width = _mq.size.width + // 左右间距15 + - + 30; + final _height = _mq.size.height + // edge 间距 + // 顶部章节名称间距 + - + 50 + // 底部时间间距 + - + 50; + + List texts = []; + while (true) { + final tryRender = bookText.substring( + 0, + min(1000, bookText.length), + ); + final span = TextSpan( + text: tryRender, + style: TextStyle( + fontSize: 14 * novelFontSize, + height: 1.2, + ), + ); + final max = TextPainter( + text: span, + textDirection: TextDirection.ltr, + ); + max.layout(maxWidth: _width); + int endOffset = max + .getPositionForOffset( + Offset(_width, _height - 14 * novelFontSize * 1.2)) + .offset; + texts.add( + bookText.substring( + 0, + endOffset, + ), + ); + bookText = bookText.substring(endOffset).trim(); + if (bookText.isEmpty) { + break; + } + } + return texts; + } + + resetFont() { + var z = 0; + for (var i = 0; i < fIndex; i++) { + z += chapterTexts[i].length; + } + chapterTexts = _reRenderTextIn(texts); + fIndex = 0; + var y = 0; + for (var i = 0; i < chapterTexts.length; i++) { + if (y >= z) { + fIndex = i; + break; + } + y += chapterTexts[i].length; + } + } @override void initState() { @@ -47,10 +146,16 @@ class _NovelReaderScreenState extends State { chapterOrder: widget.chapter.chapterOrder, progress: 0, ); - _contentFuture = native.novelContent( + _contentFuture = native + .novelContent( volumeId: widget.volume.id, chapterId: widget.chapter.chapterId, - ); + ) + .then((value) { + texts = value; + chapterTexts = _reRenderTextIn(value); + return value; + }); super.initState(); } @@ -69,10 +174,16 @@ class _NovelReaderScreenState extends State { stackTrace: snapshot.stackTrace, onRefresh: () async { setState(() { - _contentFuture = native.novelContent( + _contentFuture = native + .novelContent( volumeId: widget.volume.id, chapterId: widget.chapter.chapterId, - ); + ) + .then((value) { + texts = value; + chapterTexts = _reRenderTextIn(value); + return value; + }); }); }, ), @@ -131,25 +242,7 @@ class _NovelReaderScreenState extends State { }, child: Container( color: getNovelBackgroundColor(context), - child: ListView( - padding: EdgeInsets.only( - left: 10, - right: 10, - top: 15 + (Scaffold.of(context).appBarMaxHeight ?? 0), - bottom: 15 + (Scaffold.of(context).appBarMaxHeight ?? 0), - ), - children: [ - Html( - data: text, - style: { - "body": Style( - fontSize: FontSize.em(novelFontSize), - color: getNovelFontColor(context), - ), - }, - ), - ], - ), + child: move(), //_buildHtmlViewer(text), ), ), ..._fullScreen @@ -217,6 +310,7 @@ class _NovelReaderScreenState extends State { title: novelFontSize.toString(), onPressed: () async { await modifyNovelFontSize(context); + resetFont(); setState(() => {}); }, ), @@ -292,6 +386,94 @@ class _NovelReaderScreenState extends State { ), )); } + + final _nfController = NovelFanComponentController(); + + Widget move() { + return NovelFanComponent( + controller: _nfController, + previous: _movePrevious(), + current: _moveCurrent(), + next: _moveNext(), + onNextSetState: _moveOnNextSetState, + onPreviousSetState: _moveOnPreviousSetState, + ); + } + + void _moveOnPreviousSetState() { + if (fIndex > 0) { + fIndex--; + } + print(fIndex); + setState(() {}); + } + + void _moveOnNextSetState() { + if (fIndex < chapterTexts.length - 1) { + fIndex++; + } + print(fIndex); + setState(() {}); + } + + Widget? _movePrevious() { + if (fIndex != 0) { + return page( + chapterTexts[fIndex - 1], + ); + } + return null; + } + + Widget _moveCurrent() { + return page( + chapterTexts[fIndex], + ); + } + + Widget? _moveNext() { + if (fIndex >= chapterTexts.length - 1) { + return null; + } + return page( + chapterTexts[fIndex + 1], + ); + } + + Widget page(String text) { + final _mq = MediaQuery.of(context); + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + color: getNovelBackgroundColor(context), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + offset: Offset(0.0, 15.0), //阴影xy轴偏移量 + blurRadius: 15.0, //阴影模糊程度 + spreadRadius: 1.0 //阴影扩散程度 + ), + ], + ), + child: Padding( + padding: const EdgeInsets.only( + top: 50 + 36, + bottom: 50, + left: 15, + right: 15, + ), + child: Text( + text, + style: TextStyle( + fontSize: 14 * novelFontSize, + height: 1.2, + color: getNovelFontColor(context), + ), + ), + ), + ); + } } class _EpChooser extends StatefulWidget {