From 282e3ff5299325d5b6ee18bd38a08b0e5fea0527 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Nov 2020 21:18:42 +0000 Subject: [PATCH] =?UTF-8?q?Github=20Action=E8=87=AA=E5=8A=A8=E5=8F=91?= =?UTF-8?q?=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 + README.md | 11 + assets/logo.png | Bin 0 -> 5151 bytes lib/activities/book/book.dart | 274 +++++++++++++ lib/activities/book/tapToSearch.dart | 98 +++++ lib/activities/chapter/activity.dart | 92 +++++ lib/activities/chapter/chapterTab.dart | 261 ++++++++++++ lib/activities/chapter/drawer.dart | 82 ++++ lib/activities/chapter/image.dart | 110 +++++ lib/activities/chapter/viewer.dart | 65 +++ .../chapter/viewerSwitcherWidget.dart | 34 ++ lib/activities/checkDB.dart | 42 ++ lib/activities/checkData.dart | 136 +++++++ lib/activities/dataConvert.dart | 134 ++++++ lib/activities/home.dart | 380 ++++++++++++++++++ lib/activities/hot.dart | 238 +++++++++++ lib/activities/search/search.dart | 103 +++++ lib/activities/search/source.dart | 46 +++ lib/activities/search/tab.dart | 183 +++++++++ lib/activities/setting/hideStatusBar.dart | 35 ++ lib/activities/setting/setting.dart | 192 +++++++++ lib/activities/setting/web.dart | 50 +++ lib/classes/book.dart | 139 +++++++ lib/classes/chapter.dart | 24 ++ lib/classes/chapterContent.dart | 11 + lib/classes/data.dart | 160 ++++++++ lib/classes/history.dart | 34 ++ lib/classes/networkImageSSL.dart | 95 +++++ lib/crawler/http.dart | 80 ++++ lib/db/book.dart | 178 ++++++++ lib/db/book.g.dart | 80 ++++ lib/db/group.dart | 36 ++ lib/db/group.g.dart | 44 ++ lib/db/historyOffset.dart | 17 + lib/db/setting.dart | 79 ++++ lib/main.dart | 134 ++++++ lib/provider/favoriteData.dart | 133 ++++++ lib/provider/theme.dart | 29 ++ lib/utils.dart | 50 +++ lib/widgets/animatedLogo.dart | 49 +++ lib/widgets/book.dart | 192 +++++++++ lib/widgets/bookGroup.dart | 103 +++++ lib/widgets/bookSettingDialog.dart | 100 +++++ lib/widgets/checkConnect/checkConnect.dart | 314 +++++++++++++++ lib/widgets/dbSourceListWidget.dart | 13 + lib/widgets/deleteGroupDialog.dart | 58 +++ lib/widgets/favorites.dart | 240 +++++++++++ lib/widgets/groupFormDialog.dart | 92 +++++ lib/widgets/histories.dart | 149 +++++++ lib/widgets/pullToRefreshHeader.dart | 62 +++ lib/widgets/quick.dart | 219 ++++++++++ lib/widgets/selectFavoriteBooks.dart | 51 +++ lib/widgets/sliverExpandableGroup.dart | 106 +++++ lib/widgets/utils.dart | 45 +++ pubspec.yaml | 122 ++++++ 55 files changed, 5825 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/logo.png create mode 100644 lib/activities/book/book.dart create mode 100644 lib/activities/book/tapToSearch.dart create mode 100644 lib/activities/chapter/activity.dart create mode 100644 lib/activities/chapter/chapterTab.dart create mode 100644 lib/activities/chapter/drawer.dart create mode 100644 lib/activities/chapter/image.dart create mode 100644 lib/activities/chapter/viewer.dart create mode 100644 lib/activities/chapter/viewerSwitcherWidget.dart create mode 100644 lib/activities/checkDB.dart create mode 100644 lib/activities/checkData.dart create mode 100644 lib/activities/dataConvert.dart create mode 100644 lib/activities/home.dart create mode 100644 lib/activities/hot.dart create mode 100644 lib/activities/search/search.dart create mode 100644 lib/activities/search/source.dart create mode 100644 lib/activities/search/tab.dart create mode 100644 lib/activities/setting/hideStatusBar.dart create mode 100644 lib/activities/setting/setting.dart create mode 100644 lib/activities/setting/web.dart create mode 100644 lib/classes/book.dart create mode 100644 lib/classes/chapter.dart create mode 100644 lib/classes/chapterContent.dart create mode 100644 lib/classes/data.dart create mode 100644 lib/classes/history.dart create mode 100644 lib/classes/networkImageSSL.dart create mode 100644 lib/crawler/http.dart create mode 100644 lib/db/book.dart create mode 100644 lib/db/book.g.dart create mode 100644 lib/db/group.dart create mode 100644 lib/db/group.g.dart create mode 100644 lib/db/historyOffset.dart create mode 100644 lib/db/setting.dart create mode 100644 lib/main.dart create mode 100644 lib/provider/favoriteData.dart create mode 100644 lib/provider/theme.dart create mode 100644 lib/utils.dart create mode 100644 lib/widgets/animatedLogo.dart create mode 100644 lib/widgets/book.dart create mode 100644 lib/widgets/bookGroup.dart create mode 100644 lib/widgets/bookSettingDialog.dart create mode 100644 lib/widgets/checkConnect/checkConnect.dart create mode 100644 lib/widgets/dbSourceListWidget.dart create mode 100644 lib/widgets/deleteGroupDialog.dart create mode 100644 lib/widgets/favorites.dart create mode 100644 lib/widgets/groupFormDialog.dart create mode 100644 lib/widgets/histories.dart create mode 100644 lib/widgets/pullToRefreshHeader.dart create mode 100644 lib/widgets/quick.dart create mode 100644 lib/widgets/selectFavoriteBooks.dart create mode 100644 lib/widgets/sliverExpandableGroup.dart create mode 100644 lib/widgets/utils.dart create mode 100644 pubspec.yaml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c06738e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 nrop19 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..211a55d --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# 微漫 v1.1.4 [宣传页面](https://nrop19.github.io/weiman_app) + +### 微漫脱敏后的开源代码 + +#### 不解答任何代码上的问题 + +#### App的问题请到 [Telegram群](https://t.me/boring_programer) 讨论 + +- 删除了android端文件夹,涉及到apk签名等敏感文件 +- 删除了ios端文件夹 +- 删除了lib/crawler/里的其它文件,保护被爬网站的同时防止被爬网站加大防爬难度。 \ No newline at end of file diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7ea19f54eae2348b857dc3454aa3ad84aae3a52b GIT binary patch literal 5151 zcmaJ_c{r5q+n%wLeT&I5#xBcDj2UBR#ukxm*|N-7W5&$L5HFD$WC;l&Mj|O&q^x62 zT12SGQZ%xZC6uj?_xHZ<@B8C>zvnof=f3XixX$xh?(?|+c#`Z+ne%XgxBvhEkEI3L z{$RB@d^y+-zK+ZF<_C*78RJ4egZCpxcoT5|V=UeW2eu6H_Q%=dys?pC{Wv`UfLSTf z!G-K%V~z5~hp2lW#;8+52nT2YK+lju@b(SHk-j^s-WB#;B~q2NPCZy$U(Ss!vx>3>=XA^bxdO8RR|2LscfcoQ^W>d?cM z{tmRU`Ts*hLjH*+k?nE+t@r;VCOJeBa2obF5% zLdy)HW2UKf0tUnAAQ4(H6R0*EgNAFGnw~)XjYZ>q!$WYP#Z}0f#js;zPiHHjWDX_gWDD%J&x*`|q_N{}rooP>jam@&2#l{@Zn+p+oZ@@g8jc zkv}f1kb1x>nlo^H*R<%1G_Vf^bU6gM`sHqiZtB93uB1@o}o}``g-L z+Sd&kZAU##m}cZlunLTw{r(unB-F0bdKIIBhJjsc_)q2)vM8U$ChM+!Fv^o|P zw+-omlI#@TMA6(X&IWN-x7fd1UxSr2iD{PGV(#jT2pDIGD+$Ke4_~UR=B0gm=l!Ut z64OK9n-2(G*xMgVl9EB8+$FT!0cHI9=8sfzN-QlCe6}HC>g{Y?DcQ=9T?j~G+O+|*SxLeMddB~Dv#&u!@{ z&Pu#$Y>%Iw8L6axss|GP@N%Wr6?yV@0)f3t;_`m*86#z?-6BTL4WO_8&cq~YHfxXa zxjg+7K6d}iZr80+qeKm}xL*Ec2xtJl+GMHJv=eyQFPhmZm1&O4@VT0xY7p?jK6IC5 zFw&X+gDHDZOv~c2GO}W4%W=Al9pA&yZR{ypc~X0=m4dRqS2FP=1{*0rappzG~R z2a=omha?^tpYc87BuTuIXeT`` zTgk{UEj}ThU7Oyvi=qSt4}Lo0`(8}e=}Z2bX->NNQnkb%=c5Q`GE_g`g#V%1d_lQr z_jY@4!jI>POw`(C(Qx1w&Z{6fVq)4T z8)2kjfG}>W-9y@spJ53&LP~ylhEYQ3BCU+*%n+MPh(=RQtI!Xyj~^8{=r5(K=5 z3t8+ZUWQApwb(QTpLzVNJ*5Jx1bmwK?CwiYN))dy&9=EeaC*nk=SJ~Hu&Bq!;0 z50(~Bg9MM}nN=6{6lEJRt~e*cw|?cgkktt4MN~0AOX$5Xcf`G%IHNyB)+&CH9T`i6 zg&2$bsd)lCX-%`4lJcyBWoSV)W6ZvA4o%b=;5%8BSSw}4M$c#~@>UUtk%Z@^2k|PiH&0lghlnanU%A<6Q!;gg3(X)_ z9aj#>C5n`k_L2M}G!V^|G&#``)c9~38~LWN{n zqjuM>r&3l8i!Q&RU9uN`bv8|0%pjYpY%qkma{7IS_n$DcE?e!cKRU5AJbzj%MqA}sxP6v^9eY1! zUPNQ)YePT)moH<~aAk@#AnLn6QMRW40QkM-F>CWwq>v)-C|K6;YPS-(x8rh>Bj{_x zUGlMqZEGNx0$x@5u0e7ObAy5=)1C1bq0-sdOJyvd9erri(1(GD``2SDftqPCiL;xc zna4G>J)1#IU$t2_m+517u}im*BF6Ub*s9abHpsG$-C>fBsR5NYBC4*v6u+926ZLb~ zbJp)D-N)Lvyq>2pwH(@*%^l=sOJGtF(F2JnNTZU6wu7o#)`555#_3CYL3nZp@{^e( zKNFM;r7b4`4rC31jBfO=@`2*sY~McE#qSU2U!;D4YXuR8vb*1KHuR+?jVNY(S?E0H zcaE*y)h3m@s1YI;`{K_12Gmb?Df?caOV*i4WTJ;|!E#+>i9e)OyB_*#RRDQ2ww4cb zbyMh8ru7q#wO)vz?dIB@QL`5z6~=WJ*)H5XmH0-;(qdL{$T+l6O2=au*6#34O60aBecgf2otL`liFV7+py}{KnuO~z9w98?y zy~J!baT2yZ+?XEa-NvR+evFR9fgtKK3LVMeTPsuWaK)NAzFRes1DZ(k>~}r1N3zRa_l*hcMvu5^dbC`I(w8v@6RfBeTV%BhE@My9C2#b&6Lt9hz{y9@Wl9?tm!N%zs$0dmY&%~FBnrV6o8HO(iJQc`&> zZ@?x6v!k zNu*~#%IPi}F7DcVGRKjXW~`ekcUBFmk)Kxp2!EIoCtJNa`1b72x*BQ~wH+UEBdN+h zzHbKOk$SWN{YqN{XTJS$E@yU?_93)SsxHkP7Lmbc^bJ^j>w5Y_T~{6U7^J^U&{AN_ z3)c07T&cRIXe7>tE`3xTV>x0utA*(J1h%s<{KakdmLWQg_2X3;)~pBvs)L3y)YPpe zfO=Zm(htlud&iu-t%HTQ)!D~z>M0`O_vDe8S?OP}M|3g_DtDNohU^g*QWQ8SS}zR3 z-R9#W$;$*mZ_}w4N_ka4)}sQ6!r~@`*{69;F3+Aea@O!x{hsKYiA%X=rnl z3eGFK&UU&X_orCAt+du9KvKcd=Ho2ePSER?QDH`4j4aP^oz`O0rEEf`vkBFmDtbS4 z5dt5tf1|ceC&)xFJz#eA-7Vl}U@sE?!xfyHM!r z@5g}E+(&f&9dQZP3TL0&MK?Sen2vvInW`QQkUDg&HGP~tnYi-fbz^3tvR8aPcS%Q!nX(w8_cZ9N=xjRCDV1pF?sMGVX5`~J!UuCq zRrp$N1zTFX2%U^w#uU8!t*m#79esl<=Mnb{_cb+DSCW2Rh{_+K zHsxm~+p^AQ@Y$g}N0LHr#is5_?~0@~`Y|anCFqFv)T{T<8XkKMVIvXI&9|Md0h&8a=M+c7n*=bW_4 z?-WOGq~Ps@ zh2sHGPcu^-rhSg9+}iY41Rni$g4N>naTB)~wYCrSLb|_ys!- zD~w^Q&8gVAj}Xnb%km`YMx942%0&U!zEZ5M-f!8=p{G*5I?FdW`fGJEF33tOym7j~ z)wNCe__lfoG_6coN=TQ61tx1>8w8H4by(Ba(Ly`o)@;H%+S!raPmttXa)7u~P6%Uy=U4r=eFGV _ActivityBook(); +} + +class _ActivityBook extends State { + final GlobalKey _refresh = GlobalKey(); + ScrollController _scrollController; + + bool _reverse = false; + + @override + void initState() { + super.initState(); + widget.book.look = true; + _scrollController = ScrollController(); + print('${widget.book}'); + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + _refresh.currentState + .show(notificationDragOffset: SliverPullToRefreshHeader.height); + }); + } + + @override + dispose() { + _scrollController.dispose(); + super.dispose(); + } + + Future loadBook() async { + try { + final res = await widget.book.load(); + if (mounted && widget.book.needToSave()) { + await widget.book.save(); + // Provider.of(context, listen: false).loadBooksList(true); + } + if (mounted) setState(() {}); + return res; + } catch (e) { + return false; + } + } + + _openChapter(Chapter chapter) async { + await openChapter(context, widget.book, chapter); + setState(() {}); + } + + favoriteBook() async { + final fav = Provider.of(context, listen: false); + if (widget.book.favorite) { + final sure = await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text('确认取消收藏?'), + // content: Text('删除这本藏书后,首页的快速导航也会删除这本藏书'), + actions: [ + FlatButton( + child: Text('确认'), + onPressed: () => Navigator.pop(context, true), + ), + RaisedButton( + child: Text('取消'), + onPressed: () => Navigator.pop(context, false), + ), + ], + )); + if (sure == true) { + fav.deleteBook(widget.book); + } + } else { + await fav.addBook(widget.book); + await showBookSettingDialog(context, widget.book); + if (widget.book.needUpdate == true) { + widget.book.status = BookUpdateStatus.no; + } else { + widget.book.status = BookUpdateStatus.not; + } + } + setState(() {}); + } + + List _sort() { + final List list = List.from(widget.book.chapters); + // print('sort ${list.length}'); + if (_reverse) return list.reversed.toList(); + return list; + } + + IndexedWidgetBuilder buildChapters(List chapters) { + IndexedWidgetBuilder builder = (BuildContext context, int index) { + final chapter = chapters[index]; + Widget child = WidgetChapter( + chapter: chapter, + onTap: _openChapter, + read: chapter.cid == widget.book.history?.cid, + ); + if (index < chapters.length - 1) + child = DecoratedBox( + decoration: border, + child: child, + ); + return child; + }; + return builder; + } + + @override + Widget build(BuildContext context) { + Color color = widget.book.favorite ? Colors.red : Colors.white; + IconData icon = + widget.book.favorite ? Icons.favorite : Icons.favorite_border; + final List chapters = _sort(); + final history = []; + if (widget.book.history != null && widget.book.chapters.length > 0) { + final chapter = widget.book.chapters.firstWhere( + (chapter) => chapter.cid == widget.book.history.cid, + orElse: () => null, + ); + if(chapter != null){ + history.add(ListTile(title: Text('阅读历史'))); + history.add(WidgetChapter( + chapter: chapter, + onTap: _openChapter, + read: true, + )); + history.add(ListTile(title: Text('下一章'))); + final nextIndex = widget.book.chapters.indexOf(chapter) + 1; + if (nextIndex < widget.book.chapterCount) { + history.add(WidgetChapter( + chapter: widget.book.chapters[nextIndex], + onTap: _openChapter, + read: false, + )); + } else { + history.add(ListTile(subtitle: Text('没有了'))); + } + } + history.add(SizedBox(height: 20)); + } + history.add( + ListTile( + title: Row( + children: [ + Text('章节列表'), + SizedBox(width: 10), + TextButton( + onPressed: () { + _reverse = !_reverse; + setState(() {}); + }, + child: Text('倒序'), + ), + ], + ), + ), + ); + + return Scaffold( + body: PullToRefreshNotification( + key: _refresh, + onRefresh: loadBook, + maxDragOffset: kToolbarHeight * 2, + child: CustomScrollView( + controller: _scrollController, + slivers: [ + /// 标题栏 + SliverAppBar( + floating: true, + pinned: true, + title: Text(widget.book.name), + expandedHeight: 200, + actions: [ + IconButton( + onPressed: favoriteBook, icon: Icon(icon, color: color)) + ], + flexibleSpace: FlexibleSpaceBar( + background: SafeArea( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// 漫画封面 + Container( + margin: EdgeInsets.only( + top: 50, left: 20, right: 10, bottom: 20), + height: 160, + child: Hero( + tag: widget.heroTag, + child: ExtendedImage( + width: 100, + image: NetworkImageSSL( + widget.book.http, + widget.book.avatar, + ), + ), + ), + ), + + /// 作者、标签、简介内容 + Expanded( + child: Container( + padding: EdgeInsets.only(top: 50, right: 20), + child: ListView( + children: [ + TapToSearchWidget( + leading: '作者', items: widget.book.authors), + TapToSearchWidget( + leading: '标签', items: widget.book.tags), + Container( + margin: EdgeInsets.only(top: 10), + ), + Text( + widget.book.description ?? '', + softWrap: true, + style: + TextStyle(color: Colors.white, height: 1.2), + ), + ], + ), + )), + ], + ), + ), + ), + ), + + PullToRefreshContainer((info) => SliverPullToRefreshHeader( + info: info, + onTap: () => _refresh.currentState.show( + notificationDragOffset: SliverPullToRefreshHeader.height), + )), + + /// 观看历史 + SliverToBoxAdapter( + child: Column( + children: history, + crossAxisAlignment: CrossAxisAlignment.start, + ), + ), + + /// 章节列表 + SliverList( + delegate: SliverChildBuilderDelegate( + buildChapters(chapters), + childCount: chapters.length, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/activities/book/tapToSearch.dart b/lib/activities/book/tapToSearch.dart new file mode 100644 index 0000000..3b2e1cc --- /dev/null +++ b/lib/activities/book/tapToSearch.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:weiman/activities/search/search.dart'; + +class TapToSearchWidget extends StatelessWidget { + final String leading; + final List items; + + const TapToSearchWidget({ + Key key, + this.leading, + this.items, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton( + child: Text('$leading:'), + onPressed: null, + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all(Colors.white), + overlayColor: + MaterialStateProperty.all(Colors.white.withOpacity(0.3)), + visualDensity: VisualDensity.comfortable, + ), + ), + Expanded( + child: Wrap( + spacing: 10, + crossAxisAlignment: WrapCrossAlignment.center, + children: items.map((e) => _Item(string: e)).toList(), + ), + ), + ], + ); + } +} + +class _Item extends StatelessWidget { + final String string; + + const _Item({Key key, @required this.string}) + : assert(string != null), + super(key: key); + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ActivitySearch( + search: string, + ))); + }, + icon: Icon(Icons.search, size: 14), + label: Text(string), + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all(Colors.white), + overlayColor: + MaterialStateProperty.all(Colors.white.withOpacity(0.3)), + visualDensity: VisualDensity.comfortable, + ), + ); + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ActivitySearch( + search: string, + ))); + }, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: string, + style: TextStyle(decoration: TextDecoration.underline)), + WidgetSpan( + child: Icon( + Icons.search, + color: Colors.white, + size: 14, + )), + ], + ), + style: TextStyle( + color: Colors.white, + textBaseline: TextBaseline.ideographic, + ), + ), + ); + } +} diff --git a/lib/activities/chapter/activity.dart b/lib/activities/chapter/activity.dart new file mode 100644 index 0000000..602cc1c --- /dev/null +++ b/lib/activities/chapter/activity.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; +import 'package:weiman/activities/chapter/chapterTab.dart'; +import 'package:weiman/activities/chapter/drawer.dart'; +import 'package:weiman/classes/chapter.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/db/setting.dart'; +import 'package:weiman/utils.dart'; + +class ActivityChapter extends StatefulWidget { + final Book book; + final Chapter chapter; + + ActivityChapter(this.book, this.chapter); + + @override + _ActivityChapter createState() => _ActivityChapter(); +} + +class _ActivityChapter extends State { + final _scaffoldKey = GlobalKey(); + PageController _pageController; + int showIndex = 0; + bool hasNextImage = true; + + @override + void initState() { + _pageController = PageController( + keepPage: false, + initialPage: widget.book.chapters.indexOf(widget.chapter)); + super.initState(); + saveHistory(widget.chapter); + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + final hide = Provider.of(context, listen: false).getHideOption(); + if (hide == HideOption.always) { + hideStatusBar(); + } + }); + } + + @override + void dispose() { + _pageController?.dispose(); + showStatusBar(); + super.dispose(); + } + + void pageChanged(int page) { + saveHistory(widget.book.chapters[page]); + } + + void saveHistory(Chapter chapter) async { + await widget.book.setHistory(chapter); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (_, data, __) { + return Scaffold( + key: _scaffoldKey, + endDrawer: ChapterDrawer( + book: widget.book, + onTap: (chapter) { + _pageController.jumpToPage(widget.book.chapters.indexOf(chapter)); + }, + ), + body: PageView.builder( + physics: AlwaysScrollableClampingScrollPhysics(), + controller: _pageController, + itemCount: widget.book.chapters.length, + onPageChanged: pageChanged, + itemBuilder: (ctx, index) { + return ChapterTab( + actions: [ + IconButton( + icon: Icon(Icons.menu), + onPressed: () { + _scaffoldKey.currentState.openEndDrawer(); + }, + ), + ], + book: widget.book, + chapter: widget.book.chapters[index], + ); + }, + ), + ); + }); + } +} diff --git a/lib/activities/chapter/chapterTab.dart b/lib/activities/chapter/chapterTab.dart new file mode 100644 index 0000000..a41aaf9 --- /dev/null +++ b/lib/activities/chapter/chapterTab.dart @@ -0,0 +1,261 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:loading_more_list/loading_more_list.dart'; +import 'package:provider/provider.dart'; +import 'package:weiman/activities/chapter/image.dart'; +import 'package:weiman/activities/chapter/viewerSwitcherWidget.dart'; +import 'package:weiman/classes/chapter.dart'; +import 'package:weiman/crawler/http18Comic.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/db/setting.dart'; +import 'package:weiman/utils.dart'; +import 'package:weiman/widgets/animatedLogo.dart'; + +class ChapterSourceList extends LoadingMoreBase { + final Book book; + final Chapter chapter; + final Function onFirstLoaded; + + bool firstLoad = true; + bool hasMore = true; + bool isMultiPage = false; + int page = 1; + + ChapterSourceList({ + this.book, + this.chapter, + this.onFirstLoaded, + }); + + @override + Future loadData([bool isloadMoreAction = false]) async { + final chapterContent = await Http18Comic.instance.getChapterContent( + book, + chapter, + page: page, + ); + print(chapterContent.toString()); + hasMore = chapterContent.hasNextPage; + this.addAll(chapterContent.images); + if (firstLoad) { + firstLoad = false; + isMultiPage = hasMore; + } + page++; + return true; + } + + @override + Future refresh([bool notifyStateChanged = false]) { + firstLoad = true; + hasMore = true; + page = 1; + return super.refresh(notifyStateChanged); + } +} + +class ChapterTab extends StatefulWidget { + final Book book; + final Chapter chapter; + final List actions; + + const ChapterTab({Key key, this.book, this.chapter, this.actions}) + : super(key: key); + + @override + _State createState() => _State(); +} + +class _State extends State { + ChapterSourceList sourceList; + ScrollController scrollController; + + @override + initState() { + scrollController = ScrollController(); + sourceList = ChapterSourceList( + book: widget.book, + chapter: widget.chapter, + ); + widget.book.setHistory(widget.chapter); + super.initState(); + + // 隐藏/显示 状态栏 + final setting = Provider.of(context, listen: false); + final hide = setting.getHideOption(); + if (hide == HideOption.auto) { + scrollController.addListener(() { + final isUp = scrollController.position.userScrollDirection == + ScrollDirection.forward; + if (isUp) + showStatusBar(); + else + hideStatusBar(); + }); + } + } + + @override + dispose() { + widget.book.setHistory(widget.chapter); + scrollController?.dispose(); + super.dispose(); + } + + Widget imageBuilder(ctx, String image, int index) { + index += 1; + bool reDraw = false; + try { + int cid = int.parse(widget.chapter.cid); + reDraw = cid >= 220980; + // print('创建图片 cid $cid, reDraw $reDraw'); + } catch (e) {} + return ImageWidget( + image: image, + index: index, + total: sourceList.length, + reSort: reDraw, + ); + } + + Widget indicatorBuilder(context, IndicatorStatus status) { + print('indicatorBuilder $status'); + bool isSliver = true; + Widget widget; + switch (status) { + case IndicatorStatus.none: + widget = SizedBox(); + break; + case IndicatorStatus.loadingMoreBusying: + widget = Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AnimatedLogoWidget(width: 20, height: 30), + SizedBox(width: 10), + Text("正在读取") + ], + ); + widget = Container( + width: double.infinity, + height: kToolbarHeight, + child: widget, + alignment: Alignment.center, + ); + break; + case IndicatorStatus.fullScreenBusying: + widget = Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedLogoWidget(width: 25, height: 30), + Text('读取中'), + ], + ), + ); + if (isSliver) { + widget = SliverFillRemaining( + child: widget, + ); + } + break; + case IndicatorStatus.error: + case IndicatorStatus.fullScreenError: + widget = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '读取失败\n你可能需要用梯子', + textAlign: TextAlign.center, + ), + RaisedButton( + child: Text('再次重试'), + onPressed: sourceList.errorRefresh, + ) + ], + ); + widget = Container( + width: double.infinity, + height: kToolbarHeight, + child: widget, + alignment: Alignment.center, + ); + if (status == IndicatorStatus.fullScreenError) { + if (isSliver) { + widget = SliverFillRemaining( + child: widget, + ); + } else { + widget = CustomScrollView( + slivers: [ + SliverFillRemaining( + child: widget, + ) + ], + ); + } + } + break; + case IndicatorStatus.noMoreLoad: + widget = SizedBox(); + break; + case IndicatorStatus.empty: + widget = Text( + '没有图片', + ); + widget = Container( + width: double.infinity, + height: kToolbarHeight, + child: widget, + alignment: Alignment.center, + ); + if (isSliver) { + widget = SliverToBoxAdapter( + child: widget, + ); + } else { + widget = CustomScrollView( + slivers: [ + SliverFillRemaining( + child: widget, + ) + ], + ); + } + break; + } + return widget; + } + + @override + Widget build(BuildContext context) { + return CustomScrollView( + controller: scrollController, + slivers: [ + SliverAppBar( + snap: true, + floating: true, + title: Text(widget.chapter.cname), + actions: [ + ViewerSwitcherWidget(), + IconButton( + icon: Icon(Icons.vertical_align_top), + onPressed: () => scrollController.jumpTo(0.0), + ), + ...widget.actions, + ], + ), + LoadingMoreSliverList( + SliverListConfig( + sourceList: sourceList, + itemBuilder: imageBuilder, + addSemanticIndexes: true, + semanticIndexOffset: 10, + autoLoadMore: true, + indicatorBuilder: indicatorBuilder, + ), + ), + ], + ); + } +} diff --git a/lib/activities/chapter/drawer.dart b/lib/activities/chapter/drawer.dart new file mode 100644 index 0000000..dcb4649 --- /dev/null +++ b/lib/activities/chapter/drawer.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:weiman/classes/chapter.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/widgets/book.dart'; + +class ChapterDrawer extends StatefulWidget { + final Book book; + final void Function(Chapter chapter) onTap; + + const ChapterDrawer({ + Key key, + @required this.book, + @required this.onTap, + }) : super(key: key); + + @override + _ChapterDrawer createState() => _ChapterDrawer(); +} + +class _ChapterDrawer extends State { + ScrollController _controller; + int read; + + @override + void initState() { + super.initState(); + updateRead(); + _controller = + ScrollController(initialScrollOffset: WidgetChapter.height * read); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void updateRead() { + final readChapter = widget.book.chapters + .firstWhere((chapter) => widget.book.history?.cid == chapter.cid); + read = widget.book.chapters.indexOf(readChapter); + } + + void scrollToRead() { + setState(() { + updateRead(); + }); + _controller.animateTo( + WidgetChapter.height * read, + duration: Duration(milliseconds: 200), + curve: Curves.linear, + ); + } + + @override + Widget build(BuildContext context) { + return Drawer( + child: SafeArea( + child: ListView( + controller: _controller, + children: ListTile.divideTiles( + context: context, + tiles: widget.book.chapters.map((chapter) { + final isRead = widget.book.history?.cid == chapter.cid; + return WidgetChapter( + chapter: chapter, + onTap: (chapter) { + if (widget.onTap != null) widget.onTap(chapter); + SchedulerBinding.instance.addPostFrameCallback((_) { + scrollToRead(); + }); + }, + read: isRead, + ); + }), + ).toList(), + ), + ), + ); + } +} diff --git a/lib/activities/chapter/image.dart b/lib/activities/chapter/image.dart new file mode 100644 index 0000000..7d63648 --- /dev/null +++ b/lib/activities/chapter/image.dart @@ -0,0 +1,110 @@ +import 'dart:ui' as ui; + +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart' hide Image; +import 'package:provider/provider.dart'; +import 'package:sticky_headers/sticky_headers/widget.dart'; +import 'package:weiman/activities/chapter/viewer.dart'; +import 'package:weiman/classes/networkImageSSL.dart'; +import 'package:weiman/crawler/http18Comic.dart'; +import 'package:weiman/db/setting.dart'; + +class ImageWidget extends StatefulWidget { + final int index; + final int total; + final String image; + final bool reSort; + + const ImageWidget({ + Key key, + this.image, + this.index, + this.total, + this.reSort = false, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + static TextStyle _style = TextStyle(color: Colors.white); + static BoxDecoration _decoration = + BoxDecoration(color: Colors.black.withOpacity(0.4)); + + String get tag { + return 'image_viewer_${widget.index}'; + } + + @override + Widget build(BuildContext context) { + return StickyHeader( + overlapHeaders: true, + header: SafeArea( + top: true, + bottom: false, + child: Row( + children: [ + Container( + padding: EdgeInsets.all(5), + decoration: _decoration, + child: Text( + '${widget.index} / ${widget.total}', + style: _style, + ), + ), + ], + ), + ), + content: ExtendedImage( + image: NetworkImageSSL( + Http18Comic.instance, + widget.image, + reSort: widget.reSort, + ), + loadStateChanged: (ExtendedImageState state) { + Widget widget; + switch (state.extendedImageLoadState) { + case LoadState.loading: + widget = SizedBox( + height: 300, + child: Center( + child: CircularProgressIndicator(), + ), + ); + break; + case LoadState.completed: + widget = GestureDetector( + child: Hero( + child: + ExtendedRawImage(image: state.extendedImageInfo?.image), + tag: tag, + ), + onTap: () => onTap(context), + ); + break; + default: + } + return widget; + }, + ), + ); + } + + onTap(BuildContext context) { + final viewerSwitch = + Provider.of(context, listen: false).getViewerSwitch(); + // print('viewer $viewerSwitch'); + if (!viewerSwitch) return; + Navigator.push( + context, + TransparentMaterialPageRoute( + builder: (_) => ActivityImageViewer( + url: this.widget.image, + heroTag: tag, + reSort: widget.reSort, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/activities/chapter/viewer.dart b/lib/activities/chapter/viewer.dart new file mode 100644 index 0000000..5998bf5 --- /dev/null +++ b/lib/activities/chapter/viewer.dart @@ -0,0 +1,65 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:weiman/classes/networkImageSSL.dart'; +import 'package:weiman/crawler/http18Comic.dart'; + +class ActivityImageViewer extends StatefulWidget { + final String url; + final String heroTag; + final bool reSort; + + const ActivityImageViewer({ + Key key, + this.url, + this.heroTag, + this.reSort = false, + }) : super(key: key); + + @override + _State createState() => _State(); +} + +class _State extends State { + double currentScale = 1.0; + + @override + Widget build(BuildContext context) { + return ExtendedImageSlidePage( + slideAxis: SlideAxis.both, + slideType: SlideType.onlyImage, + child: Material( + color: Colors.transparent, + shadowColor: Colors.transparent, + child: Stack( + fit: StackFit.expand, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: ExtendedImage( + image: NetworkImageSSL( + Http18Comic.instance, + widget.url, + reSort: widget.reSort, + ), + enableSlideOutPage: true, + mode: ExtendedImageMode.gesture, + onDoubleTap: (status) { + currentScale = currentScale == 1 ? 3 : 1; + status.handleDoubleTap(scale: currentScale); + }, + heroBuilderForSlidingPage: (child) { + return Hero( + child: child, + tag: widget.heroTag, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/activities/chapter/viewerSwitcherWidget.dart b/lib/activities/chapter/viewerSwitcherWidget.dart new file mode 100644 index 0000000..26291cf --- /dev/null +++ b/lib/activities/chapter/viewerSwitcherWidget.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:weiman/db/setting.dart'; + +class ViewerSwitcherWidget extends StatefulWidget { + @override + ViewerSwitcherState createState() => ViewerSwitcherState(); +} + +class ViewerSwitcherState extends State { + @override + Widget build(BuildContext context) { + return Consumer(builder: (_, data, __) { + final icon = data.getViewerSwitch() + ? Icons.check_box_outlined + : Icons.check_box_outline_blank; + return + TextButton.icon( + icon: Icon(icon), + label: Text('看图'), + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all(Colors.white), + overlayColor: + MaterialStateProperty.all(Colors.white.withOpacity(0.3)), + visualDensity: VisualDensity.compact, + ), + onPressed: () { + data.setViewerSwitch(!data.getViewerSwitch()); + setState(() {}); + }, + ); + }); + } +} diff --git a/lib/activities/checkDB.dart b/lib/activities/checkDB.dart new file mode 100644 index 0000000..df1a257 --- /dev/null +++ b/lib/activities/checkDB.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:weiman/db/book.dart'; + +class ActivityCheckDB extends StatefulWidget { + @override + _State createState() => _State(); +} + +enum CheckState { + Uncheck, + Pass, + Fail, +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('收藏数据检修'), + ), + body: ListView(children: [ + ListTile( + title: Text('所有藏书章节数量归零'), + onTap: () async { + for (final book in Book.bookBox.values) { + book.chapterCount = 0; + await book.save(); + } + }, + ), + ListTile( + title: Text('清空漫画数据'), + subtitle: Text('有 ${Book.bookBox.length} 本'), + onTap: () async { + await Book.bookBox.clear(); + }, + ), + ]), + ); + } +} diff --git a/lib/activities/checkData.dart b/lib/activities/checkData.dart new file mode 100644 index 0000000..dd4e734 --- /dev/null +++ b/lib/activities/checkData.dart @@ -0,0 +1,136 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:oktoast/oktoast.dart'; + +import 'package:weiman/classes/data.dart'; + +class ActivityCheckData extends StatefulWidget { + @override + _State createState() => _State(); +} + +enum CheckState { + Uncheck, + Pass, + Fail, +} + +final titleTextStyle = TextStyle(fontSize: 14, color: Colors.blue), + passStyle = TextStyle(color: Colors.green), + failStyle = TextStyle(color: Colors.red); + +class _State extends State { + CheckState firstState; + int firstLength = 0; + final TextSpan secondResults = TextSpan(); + TextEditingController _outputController, _inputController; + + @override + void initState() { + super.initState(); + _outputController = TextEditingController(); + _inputController = TextEditingController(); + } + + TextSpan first() { + String text; + switch (firstState) { + case CheckState.Pass: + text = '有数据, 一共 $firstLength 本收藏'; + break; + case CheckState.Fail: + text = '没有收藏数据'; + break; + default: + text = '未检查'; + } + return TextSpan( + text: text, + style: firstState == CheckState.Pass ? passStyle : failStyle); + } + + @override + Widget build(BuildContext context) { + final firstChildren = [ + Text('检查漫画收藏列表'), + RaisedButton( + child: Text('检查'), + color: Colors.blue, + textColor: Colors.white, + onPressed: () { + final has = Data.has(Data.favoriteBooksKey); + if (has) { + final String str = Data.instance.getString(Data.favoriteBooksKey); + final Map map = jsonDecode(str); + firstLength = map.keys.length; + _outputController.text = str; + } + firstState = firstLength > 0 ? CheckState.Pass : CheckState.Fail; + + setState(() {}); + }, + ), + RichText( + text: TextSpan( + text: '结果:', + children: [first()], + style: TextStyle(color: Colors.black)), + ), + ]; + if (firstState == CheckState.Pass) { + firstChildren.add(Text('点击复制')); + firstChildren.add(TextField( + maxLines: 8, + controller: _outputController, + onTap: () { + showToast('已经复制'); + Clipboard.setData(ClipboardData(text: _outputController.text)); + }, + )); + } + return Scaffold( + appBar: AppBar( + title: Text('收藏数据检修'), + ), + body: ListView(children: [ + Card( + child: Padding( + padding: EdgeInsets.all(5), + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: firstChildren, + ), + ), + ), + ), + Card( + child: Padding( + padding: EdgeInsets.all(5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('导入收藏数据'), + TextField( + controller: _inputController, + maxLines: 8, + ), + RaisedButton( + child: Text('导入'), + onPressed: () { + if (_inputController.text.length > 0) { + Data.instance.setString( + Data.favoriteBooksKey, _inputController.text); + } + }, + ), + ], + ), + ), + ), + ]), + ); + } +} diff --git a/lib/activities/dataConvert.dart b/lib/activities/dataConvert.dart new file mode 100644 index 0000000..0d10d87 --- /dev/null +++ b/lib/activities/dataConvert.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:oktoast/oktoast.dart'; + +import 'package:weiman/classes/book.dart'; +import 'package:weiman/classes/data.dart'; +import 'package:weiman/db/book.dart' as newBook; +import 'package:weiman/main.dart'; +import 'home.dart'; + +class ActivityDataConvert extends StatefulWidget { + @override + _State createState() => _State(); +} + +class _State extends State { + List quick; + Map favorites; + bool selectQ = true, selectH = true; + + @override + void initState() { + analytics.setCurrentScreen(screenName: '/activity_data_convert'); + favorites = Data.getFavorites(); + quick = Data.quickList(); + super.initState(); + } + + Future convert() async { + int quickIndex = 0; + int skip = 0; + final awaitList = []; + favorites.keys.forEach((id) { + if (newBook.Book.bookBox.containsKey(id)) return; + final oldBook = favorites[id]; + final isQuick = selectQ && quick.contains(oldBook.aid); + final book = new newBook.Book( + httpId: null, + aid: oldBook.aid, + name: oldBook.name, + avatar: oldBook.avatar, + description: oldBook.description, + authors: [oldBook.author], + chapterCount: oldBook.chapterCount, + quick: isQuick ? quickIndex : null, + needUpdate: true, + favorite: true, + history: null, + ); + if (isQuick) quickIndex++; + awaitList.add(book.save()); + }); + await Future.wait(awaitList); + showToast( + '成功转存 ${awaitList.length} 本小说\n跳过了 $skip 本', + textPadding: EdgeInsets.all(10), + ); + } + + Future clean() async { + await Data.instance.remove(Data.favoriteBooksKey); + await Data.instance.remove(Data.quickKey); + await Data.instance.remove(Data.viewHistoryKey); + } + + void gotoHome() { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => ActivityHome(), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('旧数据转存'), + ), + body: ListView(children: [ + ListTile( + title: Text('从v1.1.2开始,为了实现藏书分组功能,使用了新的数据存储方式' + '\n【旧书】打开后直接搜索同名漫画。' + '\n清空旧数据后这个界面不会再次出现。' + '\n需要将旧的藏书数据转存为新数据吗?' + '\n旧藏书不多的话,我个人建议直接清空,可以防止产生数据干扰')), + ListTile( + title: Text('收藏列表'), + subtitle: Text('一共有 ${favorites.length} 本'), + trailing: Checkbox( + value: true, + onChanged: null, + ), + ), + ListTile( + title: Text('快速导航'), + subtitle: Text('一共有 ${quick.length} 本'), + trailing: Checkbox( + value: selectQ, + onChanged: (value) { + setState(() { + selectQ = value; + }); + }, + ), + ), + ]), + bottomNavigationBar: Row(children: [ + SizedBox(width: 10), + Expanded( + child: OutlineButton( + child: Text('直接清空旧数据'), + onPressed: () async { + await clean(); + gotoHome(); + }, + ), + ), + SizedBox(width: 10), + Expanded( + child: OutlineButton( + child: Text('转存并清空旧数据'), + onPressed: () async { + await convert(); + await clean(); + gotoHome(); + }, + ), + ), + SizedBox(width: 10), + ]), + ); + } +} diff --git a/lib/activities/home.dart b/lib/activities/home.dart new file mode 100644 index 0000000..de39b70 --- /dev/null +++ b/lib/activities/home.dart @@ -0,0 +1,380 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:oktoast/oktoast.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:weiman/activities/dataConvert.dart'; +import 'package:weiman/db/setting.dart'; +import 'package:weiman/provider/theme.dart'; + +import 'package:weiman/activities/checkData.dart'; +import 'package:weiman/activities/hot.dart'; +import 'package:weiman/activities/search/search.dart'; +import 'package:weiman/activities/test2.dart'; +import 'package:weiman/classes/book.dart'; +import 'package:weiman/main.dart'; +import 'package:weiman/provider/favoriteData.dart'; +import 'package:weiman/widgets/checkConnect/checkConnect.dart'; +import 'package:weiman/widgets/favorites.dart'; +import 'package:weiman/widgets/histories.dart'; +import 'package:weiman/widgets/quick.dart'; +import 'checkDB.dart'; +import 'setting/setting.dart'; + +class ActivityHome extends StatefulWidget { + @override + State createState() => HomeState(); +} + +class HomeState extends State { + final _scaffoldKey = GlobalKey(); + final List histories = []; + final List quick = []; + final GlobalKey _quickState = GlobalKey(); + + bool showFavorite = true; + + @override + void initState() { + super.initState(); + analytics.setCurrentScreen(screenName: '/activity_home'); + + /// 提前检查一次藏书的更新情况 + SchedulerBinding.instance.addPostFrameCallback((_) async { + autoSwitchTheme(); + FavoriteData favData = Provider.of(context, listen: false); + await favData.loadBooksList(); + final updated = await favData.checkUpdate(); + if (updated > 0) + showToast( + '$updated 本藏书有更新', + textPadding: EdgeInsets.all(10), + ); + }); + } + + void autoSwitchTheme() async {} + + void gotoSearch() { + Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: '/activity_search'), + builder: (context) => ActivitySearch())); + } + + void gotoRecommend() { + Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: '/activity_recommend'), + builder: (_) => ActivityRank(), + )); + } + + void gotoPatreon() { + launch('https://www.patreon.com/nrop19'); + } + + bool isEdit = false; + + void _draggableModeChanged(bool mode) { + print('mode changed $mode'); + isEdit = mode; + setState(() {}); + } + + Widget themeButton() { + final system = FontAwesomeIcons.cloudSun, + light = FontAwesomeIcons.solidSun, + dark = FontAwesomeIcons.solidMoon; + final theme = Provider.of(context, listen: false); + Widget themeIcon; + switch (theme.themeMode) { + case ThemeMode.light: + themeIcon = Icon(light); + break; + case ThemeMode.dark: + themeIcon = Icon(dark); + break; + default: + themeIcon = Icon(system); + break; + } + return IconButton( + onPressed: () { + switch (theme.themeMode) { + case ThemeMode.light: + theme.changeTheme(ThemeMode.dark); + break; + case ThemeMode.dark: + theme.changeTheme(ThemeMode.system); + break; + default: + theme.changeTheme(ThemeMode.light); + } + Provider.of(context, listen: false) + .setThemeMode(theme.themeMode); + showToastWidget( + Container( + padding: EdgeInsets.all(10), + color: Colors.black.withOpacity(0.7), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(mainAxisSize: MainAxisSize.min, children: [ + Icon( + system, + size: 14, + color: Colors.white, + ), + SizedBox(width: 10), + Text('跟随系统,自动切换明暗模式\n如果系统不支持,默认为明亮模式'), + ]), + SizedBox(height: 10), + Row(mainAxisSize: MainAxisSize.min, children: [ + Icon( + light, + size: 14, + color: Colors.white, + ), + SizedBox(width: 10), + Text('为明亮模式'), + ]), + SizedBox(height: 10), + Row(mainAxisSize: MainAxisSize.min, children: [ + Icon( + dark, + size: 14, + color: Colors.white, + ), + SizedBox(width: 10), + Text('为暗黑模式'), + ]), + ], + ), + ), + dismissOtherToast: true, + duration: Duration(seconds: 4), + ); + }, + icon: themeIcon, + ); + } + + @override + Widget build(BuildContext context) { + final media = MediaQuery.of(context); + final width = (media.size.width * 0.8).roundToDouble(); + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: Text('微漫 v' + packageInfo.version), + automaticallyImplyLeading: false, + leading: isEdit + ? IconButton( + icon: Icon(Icons.arrow_back_ios), + onPressed: () { + _quickState.currentState.exit(); + }, + ) + : null, + actions: [ + /// 黑白样式切换 + themeButton(), + SizedBox(width: 20), + + /// 设置界面 + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: '/activity_setting'), + builder: (_) => ActivitySetting())); + }, + icon: Icon(FontAwesomeIcons.cog), + ), + + /// 收藏列表 + IconButton( + onPressed: () { + showFavorite = true; + _scaffoldKey.currentState.openEndDrawer(); + }, + icon: Icon( + Icons.favorite, + color: Colors.red, + ), + ), + + /// 浏览历史列表 + IconButton( + onPressed: () { + showFavorite = false; + // getHistory(); + _scaffoldKey.currentState.openEndDrawer(); + }, + icon: Icon(Icons.history), + ), + ], + ), + drawerEnableOpenDragGesture: false, + endDrawerEnableOpenDragGesture: false, + endDrawer: Drawer( + child: LayoutBuilder( + builder: (_, constraints) { + if (showFavorite) { + return FavoriteList(); + } else { + return Histories(); + } + }, + ), + ), + body: Center( + child: SingleChildScrollView( + padding: EdgeInsets.only(left: 40, right: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Container( + child: OutlineButton( + onPressed: gotoSearch, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search, + color: Colors.blue, + ), + Text( + '搜索漫画', + style: TextStyle(color: Colors.blue), + ) + ], + ), + borderSide: BorderSide(color: Colors.blue, width: 2), + shape: StadiumBorder(), + ), + ), + Row( + children: [ + Expanded( + flex: 7, + child: OutlineButton( + onPressed: gotoRecommend, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.whatshot, + color: Colors.red, + ), + Text( + '热门漫画', + style: TextStyle(color: Colors.red), + ) + ], + ), + borderSide: BorderSide(color: Colors.red, width: 2), + shape: StadiumBorder(), + ), + ), + ], + ), + Center( + child: Quick( + key: _quickState, + width: width, + draggableModeChanged: _draggableModeChanged, + ), + ), + CheckConnectWidget(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () async { + launch('https://bbs.level-plus.net/'); + }, + child: Text( + '魂+论坛首发', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.blue[200], + decoration: TextDecoration.underline, + ), + ), + ), + SizedBox(width: 20), + GestureDetector( + onTap: () async { + if (await canLaunch('tg://resolve?domain=weiman_app')) + launch('tg://resolve?domain=weiman_app'); + else + launch('https://t.me/weiman_app'); + }, + child: Text( + 'Telegram 广播频道', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.blue[200], + decoration: TextDecoration.underline, + ), + ), + ), + ], + ), + Visibility( + visible: isDevMode, + child: FlatButton( + onPressed: () { + Navigator.push(context, + MaterialPageRoute(builder: (_) => ActivityCheckData())); + }, + child: Text('操作 收藏列表数据'), + ), + ), + Visibility( + visible: isDevMode, + child: FlatButton( + onPressed: () { + Navigator.push(context, + MaterialPageRoute(builder: (_) => ActivityCheckDB())); + }, + child: Text('操作 DB数据'), + ), + ), + Visibility( + visible: isDevMode, + child: FlatButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ActivityDataConvert())); + }, + child: Text('进入旧数据处理功能'), + ), + ), + ], + ), + ), + ), + floatingActionButton: isDevMode + ? FloatingActionButton( + child: Text('测试'), + onPressed: () { + Navigator.push( + context, MaterialPageRoute(builder: (_) => ActivityTest())); + }, + ) + : null, + ); + } +} diff --git a/lib/activities/hot.dart b/lib/activities/hot.dart new file mode 100644 index 0000000..558885e --- /dev/null +++ b/lib/activities/hot.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; +import 'package:loading_more_list/loading_more_list.dart'; +import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; +import 'package:weiman/widgets/animatedLogo.dart'; + +import 'package:weiman/crawler/http.dart'; +import 'package:weiman/crawler/http18Comic.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/widgets/book.dart'; +import 'package:weiman/widgets/pullToRefreshHeader.dart'; + +class ActivityRank extends StatefulWidget { + @override + _ActivityRank createState() => _ActivityRank(); +} + +class _ActivityRank extends State + with SingleTickerProviderStateMixin { + TabController controller; + + @override + void initState() { + controller = TabController(length: 2, vsync: this); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('热门漫画'), + bottom: TabBar(controller: controller, tabs: [ + Tab(text: '韩漫'), + Tab(text: '全部'), + ]), + ), + body: TabBarView(controller: controller, children: [ + HotTab(http: Http18Comic.instance, type: '/hanman'), + HotTab(http: Http18Comic.instance, type: ''), + ]), + ); + } +} + +class SourceList extends LoadingMoreBase { + final String type; + final HttpBook http; + int page = 1; + String firstBookId; + + bool hasMore = true; + + SourceList({this.type, this.http}); + + @override + Future loadData([bool isloadMoreAction = false]) async { + try { + final books = await http.hotBooks(type, page); + if (books.isEmpty) { + hasMore = false; + } else { + if (firstBookId == books[0].aid) { + hasMore = false; + } else { + firstBookId = books[0].aid; + page++; + this.addAll(books); + } + } + return true; + } catch (e) { + return false; + } + } + + @override + Future refresh([bool notifyStateChanged = false]) { + hasMore = true; + page = 1; + return super.refresh(notifyStateChanged); + } +} + +class HotTab extends StatefulWidget { + final String type; + final HttpBook http; + + const HotTab({Key key, this.type, this.http}) : super(key: key); + + @override + _HotTab createState() => _HotTab(); +} + +class _HotTab extends State { + SourceList sourceList; + + @override + void initState() { + sourceList = SourceList(type: widget.type, http: widget.http); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + PullToRefreshContainer( + (info) => SliverPullToRefreshHeader(info: info), + ), + LoadingMoreSliverList(SliverListConfig( + sourceList: sourceList, + indicatorBuilder: indicatorBuilder, + itemBuilder: (_, book, __) => WidgetBook( + book, + subtitle: book.authors?.join('/'), + ), + )), + ], + ); + } + + Widget book(Book book) { + return WidgetBook(book, subtitle: book.authors?.join('/')); + } + + Widget indicatorBuilder(context, IndicatorStatus status) { + print('indicatorBuilder $status'); + bool isSliver = true; + Widget widget; + switch (status) { + case IndicatorStatus.none: + widget = SizedBox(); + break; + case IndicatorStatus.loadingMoreBusying: + widget = Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AnimatedLogoWidget(width: 20, height: 30), + SizedBox(width: 10), + Text("正在读取") + ], + ); + widget = _setbackground(false, widget, 35.0); + break; + case IndicatorStatus.fullScreenBusying: + widget = Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedLogoWidget(width: 25, height: 30), + Text('读取中'), + ], + ), + ); + if (isSliver) { + widget = SliverFillRemaining( + child: widget, + ); + } + break; + case IndicatorStatus.error: + case IndicatorStatus.fullScreenError: + widget = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '读取失败\n你可能需要用梯子', + textAlign: TextAlign.center, + ), + RaisedButton( + child: Text('再次重试'), + onPressed: sourceList.errorRefresh, + ) + ], + ); + final height = status == IndicatorStatus.error ? 35.0 : double.infinity; + widget = _setbackground(false, widget, height); + if (status == IndicatorStatus.fullScreenError) { + if (isSliver) { + widget = SliverFillRemaining( + child: widget, + ); + } else { + widget = CustomScrollView( + slivers: [ + SliverFillRemaining( + child: widget, + ) + ], + ); + } + } + break; + case IndicatorStatus.noMoreLoad: + widget = Text("已经显示全部搜索结果"); + widget = _setbackground(false, widget, 35.0); + break; + case IndicatorStatus.empty: + widget = Text( + '没有内容', + ); + widget = _setbackground(true, widget, double.infinity); + if (isSliver) { + widget = SliverToBoxAdapter( + child: widget, + ); + } else { + widget = CustomScrollView( + slivers: [ + SliverFillRemaining( + child: widget, + ) + ], + ); + } + break; + } + return widget; + } + + Widget _setbackground(bool full, Widget widget, double height) { + widget = Container( + width: double.infinity, + height: kToolbarHeight, + child: widget, + alignment: Alignment.center, + ); + return widget; + } + + Widget getIndicator(BuildContext context) { + return CircularProgressIndicator( + strokeWidth: 2.0, + valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryColor), + ); + } +} diff --git a/lib/activities/search/search.dart b/lib/activities/search/search.dart new file mode 100644 index 0000000..e488968 --- /dev/null +++ b/lib/activities/search/search.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:focus_widget/focus_widget.dart'; + +import 'package:weiman/crawler/http18Comic.dart'; +import 'tab.dart'; + +class ActivitySearch extends StatefulWidget { + final String search; + + const ActivitySearch({Key key, this.search = ''}) : super(key: key); + + @override + State createState() { + return SearchState(); + } +} + +class SearchState extends State + with SingleTickerProviderStateMixin { + TextEditingController _controller; + GlobalKey key = GlobalKey(); + + @override + initState() { + _controller = TextEditingController(text: widget.search); + super.initState(); + } + + @override + dispose() { + _controller.dispose(); + super.dispose(); + } + + void search() { + key.currentState.search = _controller.text; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: RawKeyboardListener( + focusNode: FocusNode(), + onKey: (RawKeyEvent event) { + print('is enter: ${LogicalKeyboardKey.enter == event.logicalKey}'); + if (_controller.text.isEmpty) return; + if (event.runtimeType == RawKeyUpEvent && + LogicalKeyboardKey.enter == event.logicalKey) { + print('回车键搜索'); + search(); + } + }, + child: FocusWidget.builder( + context, + builder: (_, focusNode) => TextField( + focusNode: focusNode, + style: TextStyle(color: Colors.white), + cursorColor: Colors.white, + decoration: InputDecoration( + hintText: '搜索书名', + prefixIcon: IconButton( + onPressed: search, + icon: Icon(Icons.search, color: Colors.white), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white), + ), + border: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white), + ), + ), + textAlign: TextAlign.left, + controller: _controller, + autofocus: widget.search.isEmpty, + textInputAction: TextInputAction.search, + onSubmitted: (String name) { + focusNode.unfocus(); + print('onSubmitted'); + search(); + }, + keyboardType: TextInputType.text, + onEditingComplete: () { + focusNode.unfocus(); + print('onEditingComplete'); + search(); + }, + ), + ), + ), + ), + body: SearchTab( + http: Http18Comic.instance, + search: _controller.text, + key: key, + ), + ); + } +} diff --git a/lib/activities/search/source.dart b/lib/activities/search/source.dart new file mode 100644 index 0000000..9bf5d87 --- /dev/null +++ b/lib/activities/search/source.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:loading_more_list/loading_more_list.dart'; + +import 'package:weiman/db/book.dart'; +import 'package:weiman/crawler/http.dart'; + +class SearchSourceList extends LoadingMoreBase { + final HttpBook http; + String search; + int page = 1; + bool hasMore = true; + String eachPageFirstBookId; + + SearchSourceList({ + @required this.http, + this.search = '', + }); + + @override + Future loadData([bool isloadMoreAction = false]) async { + print('搜书 $search'); + if (search == null || search.isEmpty) return true; + final list = await http.searchBook(search, page); + if (list.isEmpty) { + hasMore = false; + } else if (list[0].aid == eachPageFirstBookId) { + hasMore = false; + } else { + eachPageFirstBookId = list[0].aid; + hasMore = true; + page++; + this.addAll(list); + } + return true; + } + + @override + Future refresh([bool notifyStateChanged = false]) { + page = 1; + hasMore = true; + eachPageFirstBookId = null; + clear(); + print('refresh $page $hasMore'); + return super.refresh(notifyStateChanged); + } +} diff --git a/lib/activities/search/tab.dart b/lib/activities/search/tab.dart new file mode 100644 index 0000000..1c7b617 --- /dev/null +++ b/lib/activities/search/tab.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:loading_more_list/loading_more_list.dart'; +import 'package:weiman/activities/search/source.dart'; +import 'package:weiman/crawler/http.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/widgets/book.dart'; + +class SearchTab extends StatefulWidget { + final HttpBook http; + final String search; + + const SearchTab({ + Key key, + @required this.http, + this.search, + }) : super(key: key); + + @override + SearchTabState createState() => SearchTabState(); +} + +class SearchTabState extends State + with AutomaticKeepAliveClientMixin { + SearchSourceList sourceList; + + @override + void initState() { + sourceList = SearchSourceList(http: widget.http, search: widget.search); + super.initState(); + } + + Widget book(Book book) { + return WidgetBook(book, subtitle: book.authors.join('/')); + } + + Future refresh() async { + return sourceList.refresh(true); + } + + get search => sourceList.search; + + set search(String value) { + print('tab search $value'); + sourceList.search = value; + sourceList.refresh(true); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return LoadingMoreList( + ListConfig( + sourceList: sourceList, + itemBuilder: (_, item, index) => book(item), + autoLoadMore: true, + indicatorBuilder: indicatorBuilder, + ), + ); + } + + Widget indicatorBuilder(context, IndicatorStatus status) { + bool isSliver = false; + Widget widget; + switch (status) { + case IndicatorStatus.none: + widget = SizedBox(); + break; + case IndicatorStatus.loadingMoreBusying: + widget = Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only(right: 5.0), + height: 15.0, + width: 15.0, + child: getIndicator(context), + ), + Text("正在读取") + ], + ); + widget = _setbackground(false, widget, 35.0); + break; + case IndicatorStatus.fullScreenBusying: + widget = widget = _setbackground( + false, + Text( + '正在读取', + ), + 35.0); + if (isSliver) { + widget = SliverFillRemaining( + child: widget, + ); + } + break; + case IndicatorStatus.error: + widget = _setbackground( + false, + Text( + '网络错误\n点击重试', + ), + 35.0); + + widget = GestureDetector( + onTap: () { + sourceList.errorRefresh(); + }, + child: widget, + ); + break; + case IndicatorStatus.fullScreenError: + widget = Text( + '读取失败,如果失败的次数太多可能需要用梯子', + ); + widget = _setbackground(true, widget, double.infinity); + widget = GestureDetector( + onTap: () { + sourceList.errorRefresh(); + }, + child: widget, + ); + if (isSliver) { + widget = SliverFillRemaining( + child: widget, + ); + } else { + widget = CustomScrollView( + slivers: [ + SliverFillRemaining( + child: widget, + ) + ], + ); + } + break; + case IndicatorStatus.noMoreLoad: + widget = Text("已经显示全部搜索结果"); + widget = _setbackground(false, widget, 35.0); + break; + case IndicatorStatus.empty: + widget = Text( + sourceList.search.isEmpty ? '请输入搜索内容' : '搜索不到任何内容', + ); + widget = _setbackground(true, widget, double.infinity); + if (isSliver) { + widget = SliverToBoxAdapter( + child: widget, + ); + } else { + widget = CustomScrollView( + slivers: [ + SliverFillRemaining( + child: widget, + ) + ], + ); + } + break; + } + return widget; + } + + Widget _setbackground(bool full, Widget widget, double height) { + widget = Container( + width: double.infinity, + height: kToolbarHeight, + child: widget, + alignment: Alignment.center, + ); + return widget; + } + + Widget getIndicator(BuildContext context) { + return CircularProgressIndicator( + strokeWidth: 2.0, + valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryColor), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/activities/setting/hideStatusBar.dart b/lib/activities/setting/hideStatusBar.dart new file mode 100644 index 0000000..630e252 --- /dev/null +++ b/lib/activities/setting/hideStatusBar.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:weiman/db/setting.dart'; + +class HideStatusBar extends StatelessWidget { + final options = { + '自动': HideOption.auto, + '全程隐藏': HideOption.always, + '不隐藏': HideOption.none, + }; + final Function(HideOption option) onChanged; + final HideOption option; + + HideStatusBar({Key key, @required this.onChanged, @required this.option}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text('看漫画时隐藏状态栏'), + subtitle: Text('自动:随着图片列表的上下滚动而自动显示或隐藏状态栏\n' + '全程隐藏:进入看图界面就隐藏状态栏,退出就显示状态栏\n' + '不隐藏:就是不隐藏状态栏咯'), + trailing: DropdownButton( + value: option, + items: options.keys + .map((key) => DropdownMenuItem( + child: Text(key), + value: options[key], + )) + .toList(), + onChanged: onChanged, + ), + ); + } +} diff --git a/lib/activities/setting/setting.dart b/lib/activities/setting/setting.dart new file mode 100644 index 0000000..6bcfd5d --- /dev/null +++ b/lib/activities/setting/setting.dart @@ -0,0 +1,192 @@ +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:oktoast/oktoast.dart'; +import 'package:provider/provider.dart'; +import 'package:weiman/activities/setting/hideStatusBar.dart'; +import 'package:weiman/activities/setting/web.dart'; +import 'package:weiman/db/setting.dart'; +import 'package:weiman/main.dart'; + +class ActivitySetting extends StatefulWidget { + @override + _ActivitySetting createState() => _ActivitySetting(); +} + +class _ActivitySetting extends State { + int imagesCount, sizeCount; + bool isClearing = false; + + @override + void initState() { + super.initState(); + imageCaches(); + } + + Future imageCaches() async { + final files = imageCacheDir.listSync(); + imagesCount = files.length; + sizeCount = 0; + files.forEach((file) => sizeCount += file.statSync().size); + if (mounted) setState(() {}); + } + + Future clearDiskCachedImages() async { + await imageCacheDir.delete(recursive: true); + await imageCacheDir.create(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('设置')), + body: Consumer(builder: (_, data, __) { + print('代理 ${data.getProxy()}'); + return ListView( + children: ListTile.divideTiles( + context: context, + tiles: [ + /// 隐藏状态栏设置 + HideStatusBar( + option: data.getHideOption(), + onChanged: (option) => data.setHideOption(option), + ), + + /// 设置代理 + ListTile( + title: Text('设置代理'), + subtitle: Text(data.getProxy() ?? '无'), + onTap: () async { + var proxy = await showDialog( + context: context, + builder: (_) { + final _c = TextEditingController(text: data.getProxy()); + return WillPopScope( + child: AlertDialog( + title: Text('设置网络代理'), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '只支持http代理\nSS,SSR,V2Ray,Trojan(Clash)\n这些梯子App都有提供Http代理功能'), + TextField( + controller: _c, + decoration: InputDecoration( + hintText: '例如Clash提供的127.0.0.1:7890'), + ), + ]), + actions: [ + FlatButton( + child: Text('清空'), + onPressed: () { + _c.clear(); + }, + ), + FlatButton( + child: Text('确定'), + onPressed: () { + Navigator.pop(context, _c.text); + }, + ), + ], + ), + onWillPop: () { + Navigator.pop(context, '-1'); + return Future.value(false); + }, + ); + }); + print('用户输入 $proxy'); + if (proxy == '-1') return; + // 在前 + if (proxy != null) { + proxy = proxy + .trim() + .replaceFirst('http://', '') + .replaceFirst('https://', ''); + } + // 在后 + if (proxy == null || proxy.isEmpty) { + proxy = null; + } + print('设置代理 $proxy'); + await data.setProxy(proxy); + }, + ), + + /// 清空图片缓存 + ListTile( + title: Text('清除所有图片缓存'), + subtitle: isClearing + ? Text('清理中') + : Text.rich( + TextSpan( + children: [ + TextSpan(text: '图片数量:'), + TextSpan( + text: imagesCount == null + ? '读取中' + : '$imagesCount 张'), + TextSpan(text: '\n'), + TextSpan(text: '存储容量:'), + TextSpan( + text: sizeCount == null + ? '读取中' + : '${filesize(sizeCount)}'), + ], + ), + ), + onTap: () async { + if (isClearing == true) return; + final sure = await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text('确认清除所有图片缓存?'), + actions: [ + RaisedButton( + child: Text('确认'), + onPressed: () => Navigator.pop(context, true), + ), + ], + ), + ); + if (sure == true) { + showToast('正在清理图片缓存'); + isClearing = true; + setState(() {}); + await clearDiskCachedImages(); + isClearing = false; + if (mounted) { + setState(() {}); + await imageCaches(); + } + showToast('成功清理图片缓存'); + } + }, + ), + + ListTile( + title: Text('查看最新版'), + subtitle: Text('当前版本为 ${packageInfo.version}'), + onTap: () { + Navigator.push(context, + MaterialPageRoute(builder: (_) => ActivityWeb())); + }, + ), + + /// 清空数据缓存 + /* ListTile( + title: Text('清空漫画数据缓存'), + subtitle: Text('正常情况是不需要清空的'), + onTap: () async { + await HttpBook.dataCache.clearAll(); + showToast('成功清空漫画数据缓存', textPadding: EdgeInsets.all(10)); + }, + ),*/ + ], + ).toList(), + ); + }), + ); + } +} diff --git a/lib/activities/setting/web.dart b/lib/activities/setting/web.dart new file mode 100644 index 0000000..6945dcf --- /dev/null +++ b/lib/activities/setting/web.dart @@ -0,0 +1,50 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:weiman/main.dart'; + +class ActivityWeb extends StatefulWidget { + @override + _State createState() => _State(); +} + +class _State extends State { + LoadState state = LoadState.loading; + + @override + void initState() { + analytics.setCurrentScreen(screenName: '/activity_update_web'); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('最新版本'), + ), + body: Stack( + alignment: Alignment.center, + children: [ + WebView( + initialUrl: 'https://nrop19.github.io/weiman_app', + onWebViewCreated: (controller) { + state = LoadState.loading; + setState(() {}); + }, + onPageFinished: (_) { + state = LoadState.completed; + setState(() {}); + }, + ), + if (state == LoadState.loading) + Container( + color: Colors.grey.withOpacity(0.3), + padding: EdgeInsets.all(20), + child: CircularProgressIndicator(), + ), + ], + ), + ); + } +} diff --git a/lib/classes/book.dart b/lib/classes/book.dart new file mode 100644 index 0000000..d3c2955 --- /dev/null +++ b/lib/classes/book.dart @@ -0,0 +1,139 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import 'package:weiman/crawler/http.dart'; +import 'data.dart'; +class Book { + final String http; + final String aid; // 漫画的数据库ID + final String name; // 书本名称 + final String avatar; // 书本封面 + final String author; // 画家 + final String description; // 描述 + final List chapters; + final int chapterCount; + final int version; + + History history; + + Book({ + @required this.http, + @required this.name, + @required this.aid, + @required this.avatar, + this.author, + this.description, + this.chapters: const [], + this.chapterCount: 0, + this.history, + this.version: 0, + }); + + @override + String toString() { + return jsonEncode(toJson()); + } + + bool isFavorite() { + var books = Data.getFavorites(); + return books.containsKey(aid); + } + + Map toJson() { + print('book toJson'); + final Map data = { + 'http': http, + 'aid': aid, + 'name': name, + 'avatar': avatar, + 'author': author, + 'chapterCount': chapterCount, + 'version': version, + }; + if (history != null) data['history'] = history.toJson(); + return data; + } + + factory Book.fromJson(Map json) { + final book = Book( + http: json['http'], + aid: json['aid'], + name: json['name'], + avatar: json['avatar'], + author: json['author'], + description: json['description'], + chapterCount: json['chapterCount'] ?? 0, + version: json['version'] ?? 0); + if (json.containsKey('history')) + book.history = History.fromJson(json['history']); + return book; + } +} + +class Chapter { + final HttpBook http; + final String cid; // 章节cid + final String cname; // 章节名称 + final String avatar; // 章节封面 + + Chapter({ + @required this.http, + @required this.cid, + @required this.cname, + @required this.avatar, + }); + + @override + String toString() { + final Map data = { + 'cid': cid, + 'cname': cname, + 'avatar': avatar, + }; + return jsonEncode(data); + } +} + +class History { + final String cid; + final String cname; + final int time; + final int image; + + History({ + @required this.cid, + @required this.cname, + @required this.time, + this.image = 0, + }); + + @override + String toString() => jsonEncode(toJson()); + + Map toJson() { + return { + 'cid': cid, + 'cname': cname, + 'time': time, + 'image': image, + }; + } + + static History fromJson(Map json) { + return History( + cid: json['cid'], + cname: json['cname'], + time: json['time'], + image: json['image'] ?? 0, + ); + } + + static History fromChapter(Chapter chapter) { + return History( + cid: chapter.cid, + cname: chapter.cname, + time: DateTime.now().millisecondsSinceEpoch, + ); + } +} diff --git a/lib/classes/chapter.dart b/lib/classes/chapter.dart new file mode 100644 index 0000000..c348bec --- /dev/null +++ b/lib/classes/chapter.dart @@ -0,0 +1,24 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +class Chapter { + final String cid; // 章节cid + final String cname; // 章节名称 + final DateTime time; // 章节更新时间 + + Chapter({ + @required this.cid, + @required this.cname, + this.time, + }); + + @override + String toString() { + final Map data = { + 'cid': cid, + 'cname': cname, + }; + return jsonEncode(data); + } +} diff --git a/lib/classes/chapterContent.dart b/lib/classes/chapterContent.dart new file mode 100644 index 0000000..fa73c08 --- /dev/null +++ b/lib/classes/chapterContent.dart @@ -0,0 +1,11 @@ +class ChapterContent { + final List images; + final bool hasNextPage; + + ChapterContent(this.images, this.hasNextPage); + + @override + String toString() { + return 'ChapterContent images:${images.length} nexPage:$hasNextPage'; + } +} diff --git a/lib/classes/data.dart b/lib/classes/data.dart new file mode 100644 index 0000000..d8ce3ab --- /dev/null +++ b/lib/classes/data.dart @@ -0,0 +1,160 @@ +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'book.dart'; + +class Data { + static SharedPreferences instance; + static final favoriteBooksKey = 'favorite_books'; + static final viewHistoryKey = 'view_history'; + static final quickKey = 'quick_list'; + + static Future init() async { + instance = await SharedPreferences.getInstance(); + } + + static set(String key, T value) { + if (value is String) { + instance.setString(key, value); + } else if (value is int) { + instance.setInt(key, value); + } else if (value is bool) { + instance.setBool(key, value); + } else if (value is List) { + instance.setStringList(key, value); + } else if (value is double) { + instance.setDouble(key, value); + } else if (value is Map) { + instance.setString(key, json.encode(value)); + } + } + + static dynamic get(String key) { + return instance.get(key); + } + + static bool hasData() { + return instance.containsKey(favoriteBooksKey) || + instance.containsKey(viewHistoryKey); + } + + static Map getFavorites() { + if (has(favoriteBooksKey)) { + final String str = instance.getString(favoriteBooksKey); + Map data = jsonDecode(str); + Map res = {}; + data.keys.forEach((key) { + res[key] = Book.fromJson(data[key]); + }); + return res; + } + return {}; + } + + static void addFavorite(Book book) { + var books = getFavorites(); + books[book.aid] = book; + set(favoriteBooksKey, books); + } + + static void removeFavorite(Book book) { + var books = getFavorites(); + if (books.containsKey(book.aid)) { + books.remove(book.aid); + set(favoriteBooksKey, books); + reQuick(); + } + } + + static clear() { + instance.clear(); + } + + static bool has(String key) { + return instance.containsKey(key); + } + + static remove(String key) { + instance.remove(key); + } + + static Map getHistories() { + if (has(viewHistoryKey)) { + var data = + jsonDecode(instance.getString(viewHistoryKey)) as Map; + final Map histories = {}; + data.forEach((key, value) { + histories[key] = Book.fromJson(value); + }); + return histories; + } + return {}; + } + + static addHistory(Book book, Chapter chapter) { + book.history = History( + cid: chapter.cid, + cname: chapter.cname, + time: DateTime.now().millisecondsSinceEpoch); + final books = getHistories(); + books[book.aid] = book; + set(viewHistoryKey, books); + // print('保存历史\n' + books.toString()); + } + + static removeHistory(bool Function(Book book) isDelete) { + var books = getHistories(); + books.keys + .where((key) => isDelete(books[key])) + .toList() + .forEach(books.remove); + set(viewHistoryKey, books); + } + + static removeHistoryFromBook(Book book) { + final books = getHistories(); + books.remove(book.aid); + set(viewHistoryKey, books); + } + + /// 快速导航 id 列表,内部方法 + static List quickIdList() { + if (instance.containsKey(quickKey)) { + return instance.getStringList(quickKey); + } + return []; + } + + /// 快速导航列表 + static List quickList() { + final books = getFavorites(); + final ids = books.keys; + final List quickIds = quickIdList(); + print('快捷 $quickIds'); + return quickIds + .where((id) => ids.contains(id)) + .map((id) => books[id]) + .toList(); + } + + /// 增加快速导航 + static addQuick(Book book) { + final list = quickIdList(); + list.add(book.aid); + instance.setStringList(quickKey, list.toSet().toList()); + } + + static addQuickAll(List id) { + print('保存qid $id'); + instance.setStringList(quickKey, id.toSet().toList()); + } + + /// 重新整理Quick的id列表 + static reQuick() { + final books = getFavorites(); + final quickIds = quickIdList(); + instance.setStringList( + quickKey, quickIds.where(books.keys.contains).toSet().toList()); + } +} diff --git a/lib/classes/history.dart b/lib/classes/history.dart new file mode 100644 index 0000000..aeb126c --- /dev/null +++ b/lib/classes/history.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'package:weiman/classes/chapter.dart'; + +class History extends Chapter { + DateTime time; // 历史时间 + + History({ + @required cid, + @required cname, + @required this.time, + }) : super(cid: cid, cname: cname); + + Map toJson() { + return {'cid': cid, 'cname': cname, 'time': time}; + } + + factory History.fromJson(Map map) { + if (map == null) return null; + return History( + cid: map['cid'], + cname: map['cname'], + time: map['time'], + ); + } + + factory History.fromChapter(Chapter chapter) { + return History( + cid: chapter.cid, + cname: chapter.cname, + time: DateTime.now(), + ); + } +} diff --git a/lib/classes/networkImageSSL.dart b/lib/classes/networkImageSSL.dart new file mode 100644 index 0000000..ce91bd2 --- /dev/null +++ b/lib/classes/networkImageSSL.dart @@ -0,0 +1,95 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:weiman/crawler/http.dart'; + +/// The dart:io implementation of [image_provider.NetworkImage]. +class NetworkImageSSL extends ImageProvider + implements NetworkImage { + /// Creates an object that fetches the image at the given URL. + /// + /// The arguments [url] and [scale] must not be null. + const NetworkImageSSL( + this.http, + this.url, { + this.scale = 1.0, + this.headers, + this.timeout = 8, + this.reSort = false, + }) : assert(url != null), + assert(scale != null); + + final HttpBook http; + + final int timeout; + @override + final String url; + + @override + final double scale; + + @override + final Map headers; + + final bool reSort; + + static void init(ByteData data) {} + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) { + // Ownership of this controller is handed off to [_loadAsync]; it is that + // method's responsibility to close the controller's stream when the image + // has been loaded or an error is thrown. + final StreamController chunkEvents = + StreamController(); + + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, chunkEvents, decode), + chunkEvents: chunkEvents.stream, + scale: key.scale, + informationCollector: () { + return [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty('Image key', key), + ]; + }, + ); + } + + Future _loadAsync( + NetworkImageSSL key, + StreamController chunkEvents, + DecoderCallback decode, + ) async { + try { + assert(key == this); + final Uint8List bytes = await http.getImage(url, reSort: reSort); + if (bytes.lengthInBytes == 0) + throw Exception('NetworkImage is an empty file: $url'); + return decode(bytes); + } finally { + chunkEvents.close(); + } + } + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) return false; + final NetworkImageSSL typedOther = other; + return url == typedOther.url && scale == typedOther.scale; + } + + @override + int get hashCode => hashValues(url, scale); + + @override + String toString() => '$runtimeType("$url", scale: $scale)'; +} diff --git a/lib/crawler/http.dart b/lib/crawler/http.dart new file mode 100644 index 0000000..9909009 --- /dev/null +++ b/lib/crawler/http.dart @@ -0,0 +1,80 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:weiman/classes/chapter.dart'; +import 'package:weiman/classes/chapterContent.dart'; +import 'package:weiman/db/book.dart'; + +import 'http18Comic.dart'; + +final headers = { + 'user-agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36', + 'accept': + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,zh-HK;q=0.7', + 'cache-control': 'no-cache', + 'pragma': 'no-cache', +}; + +class MyHttpClient { + static Map clients = {}; + + static init(String proxy, int timeout) { + Http18Comic.instance = Http18Comic( + baseUrls.values.first, + name: baseUrls.keys.first, + headers: headers, + timeout: timeout, + ); + + clients[Http18Comic.instance.id] = Http18Comic.instance; + + setGlobalProxy(proxy); + } +} + +abstract class HttpBook { + final String id; + final String name; + + final Dio dio; + + HttpBook(this.id, this.name, this.dio); + + Future> searchBook(String name, [int page]); + + Future getBook(String aid); + + Future> getChapterImages(Book book, Chapter chapter); + + Future getChapterContent(Book book, Chapter chapter); + + Future> getImage(String url, {bool reSort = false}); + + Future> hotBooks([String type = '', int page]); +} + +class MyProxyHttpOverride extends HttpOverrides { + final String proxy; + + MyProxyHttpOverride(this.proxy); + + @override + HttpClient createHttpClient(SecurityContext context) { + return super.createHttpClient(context) + ..findProxy = (uri) { + return 'PROXY $proxy;'; + } + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + } +} + +void setGlobalProxy(String proxy) { + print('setGlobalProxy $proxy'); + if (proxy != null) + HttpOverrides.global = MyProxyHttpOverride(proxy); + else + HttpOverrides.global = null; +} diff --git a/lib/db/book.dart b/lib/db/book.dart new file mode 100644 index 0000000..cff905f --- /dev/null +++ b/lib/db/book.dart @@ -0,0 +1,178 @@ +import 'package:hive/hive.dart'; + +import 'package:weiman/classes/chapter.dart'; +import 'package:weiman/classes/history.dart'; +import 'package:weiman/crawler/http.dart'; +import 'package:weiman/db/group.dart'; + +part 'book.g.dart'; + +const BookName = 'book'; +enum BookUpdateStatus { + not, // 不检查更新 + no, // 没有更新 + had, // 有更新 + fail, // 检查更新失败 + wait, // 检查更新的队列中 + loading, // 正在检查更新 + old, // 旧藏书,不检查更新 +} + +@HiveType(typeId: 1) +class Book extends HiveObject { + static Box bookBox; + + @HiveField(0) + String aid; + + @HiveField(1) + String name; + + @HiveField(2) + String avatar; + + @HiveField(3) + List authors; + + @HiveField(4) + String description; + + @HiveField(5) + int chapterCount; + + // [新章节数量]减[旧章节数量]得到的差值 + int newChapterCount; + + BookUpdateStatus status; + + List chapters; + + List tags; + + @HiveField(6) + bool favorite; + + @HiveField(7) + bool needUpdate; + + @HiveField(8) + bool hasUpdate; + + @HiveField(9) + DateTime updatedAt; + + // 首页快速导航 + @HiveField(10) + int quick; + + @HiveField(11) + Map _history; + + @HiveField(12) + int groupId; + + @HiveField(13) + String httpId; + + bool look = false; + + Group get group => + groupId == null ? null : Group.groupBox.get(groupId, defaultValue: null); + + HttpBook get http => MyHttpClient.clients[httpId]; + + History get history => History.fromJson(_history); + + Future setFavorite(bool value) { + favorite = value; + return save(); + } + + Future setHistory(Chapter value) { + if (value == null) { + _history = null; + } else { + _history = History.fromChapter(value).toJson(); + } + return save(); + } + + Book({ + this.httpId, + this.aid, + this.name, + this.groupId, + this.avatar, + this.authors, + this.description, + this.chapterCount, + this.favorite = false, + this.needUpdate = false, + this.quick, + this.chapters = const [], + this.tags = const [], + Map history, + }) : _history = history; + + @override + String toString() { + return 'Book:${toJson()}'; + } + + toJson() { + return { + 'key': key, + 'aid': aid, + 'name': name, + 'httpId': httpId, + 'groupId': groupId, + 'favorite': favorite, + 'history': _history, + 'status': status, + 'chapterCount': chapterCount, + }; + } + + bool needToSave() { + return favorite == true || _history != null || quick != null; + } + + @override + Future save() { + if (needToSave()) { + return bookBox.put(aid, this); + } + return bookBox.delete(aid); + } + + Future load() async { + if (httpId == null) return false; + final newBook = await this.http.getBook(aid); + print('load newBook:${newBook.httpId}'); + chapters = newBook.chapters; + chapterCount = newBook.chapterCount; + authors = newBook.authors; + description = newBook.description; + httpId = newBook.httpId; + tags = newBook.tags; + print('book httpId $httpId'); + return true; + } + + Future> loadChapter(Chapter chapter) async { + if (httpId == null) return null; + return this.http.getChapterImages(this, chapter); + } + + Future update() async { + try { + final newBook = await this.http.getBook(aid); + print('$name 旧$chapterCount 新${newBook.chapterCount}'); + newChapterCount = newBook.chapterCount - chapterCount; + status = newChapterCount > 0 ? BookUpdateStatus.had : BookUpdateStatus.no; + } catch (e) { + status = BookUpdateStatus.fail; + } + print('book update $status'); + } +} diff --git a/lib/db/book.g.dart b/lib/db/book.g.dart new file mode 100644 index 0000000..dc7946e --- /dev/null +++ b/lib/db/book.g.dart @@ -0,0 +1,80 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'book.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class BookAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + Book read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Book( + httpId: fields[13] as String, + aid: fields[0] as String, + name: fields[1] as String, + groupId: fields[12] as int, + avatar: fields[2] as String, + authors: (fields[3] as List)?.cast(), + description: fields[4] as String, + chapterCount: fields[5] as int, + favorite: fields[6] as bool, + needUpdate: fields[7] as bool, + quick: fields[10] as int, + ) + ..hasUpdate = fields[8] as bool + ..updatedAt = fields[9] as DateTime + .._history = (fields[11] as Map)?.cast(); + } + + @override + void write(BinaryWriter writer, Book obj) { + writer + ..writeByte(14) + ..writeByte(0) + ..write(obj.aid) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.avatar) + ..writeByte(3) + ..write(obj.authors) + ..writeByte(4) + ..write(obj.description) + ..writeByte(5) + ..write(obj.chapterCount) + ..writeByte(6) + ..write(obj.favorite) + ..writeByte(7) + ..write(obj.needUpdate) + ..writeByte(8) + ..write(obj.hasUpdate) + ..writeByte(9) + ..write(obj.updatedAt) + ..writeByte(10) + ..write(obj.quick) + ..writeByte(11) + ..write(obj._history) + ..writeByte(12) + ..write(obj.groupId) + ..writeByte(13) + ..write(obj.httpId); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BookAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/db/group.dart b/lib/db/group.dart new file mode 100644 index 0000000..6178aee --- /dev/null +++ b/lib/db/group.dart @@ -0,0 +1,36 @@ +import 'package:hive/hive.dart'; + +import 'book.dart'; + +part 'group.g.dart'; + +const GroupName = 'group'; + +@HiveType(typeId: 0) +class Group extends HiveObject { + static Box groupBox; + static Box bookBox; + + @HiveField(0) + String name; + + @HiveField(1) + bool expended; + + Group(this.name, [this.expended = false]); + + List get books => bookBox.values + .where((book) => book.favorite && book.groupId == this.key) + .toList(); + + @override + String toString() { + return 'Group:${{'key': key, 'name': name, 'books': books.length}}'; + } + + @override + Future save() { + if (!isInBox) return groupBox.add(this); + return super.save(); + } +} diff --git a/lib/db/group.g.dart b/lib/db/group.g.dart new file mode 100644 index 0000000..88ddb7d --- /dev/null +++ b/lib/db/group.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'group.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class GroupAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + Group read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Group( + fields[0] as String, + fields[1] as bool, + ); + } + + @override + void write(BinaryWriter writer, Group obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.name) + ..writeByte(1) + ..write(obj.expended); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GroupAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/db/historyOffset.dart b/lib/db/historyOffset.dart new file mode 100644 index 0000000..3e4529c --- /dev/null +++ b/lib/db/historyOffset.dart @@ -0,0 +1,17 @@ +import 'package:hive/hive.dart'; + +const HistoryOffsetName = 'history'; + +class HistoryOffset { + static Box box; + + static double get(String cid) { + print('get $cid'); + return box.get(cid) ?? 0.0; + } + + static Future save(String cid, double offset) { + print('save $cid $offset'); + return box.put(cid, offset); + } +} diff --git a/lib/db/setting.dart b/lib/db/setting.dart new file mode 100644 index 0000000..bac9da1 --- /dev/null +++ b/lib/db/setting.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:weiman/crawler/http.dart'; +import 'package:weiman/crawler/http18Comic.dart'; + +enum HideOption { + none, + auto, + always, +} + +class Setting with ChangeNotifier { + static final String name = 'setting'; + static Box settingBox; + Http18Comic http; + + Setting() { + MyHttpClient.init(getProxy(), 10000); + } + + HideOption getHideOption() { + final index = + settingBox.get('hideOption', defaultValue: HideOption.auto.index); + return HideOption.values[index]; + } + + Future setHideOption(HideOption option) async { + await settingBox.put('hideOption', option.index); + notifyListeners(); + } + + String getProxy() { + print('getProxy'); + return settingBox.get('proxy', defaultValue: null); + } + + Future setProxy(String proxy) async { + print('db/setting.setProxy $proxy'); + await settingBox.put('proxy', proxy); + MyHttpClient.init(proxy, 10000); + notifyListeners(); + } + + ThemeMode getThemeMode() { + final int index = settingBox.get('theme', defaultValue: -1); + if (index == -1) return ThemeMode.system; + return ThemeMode.values[index]; + } + + Future setThemeMode(ThemeMode mode) { + return settingBox.put('theme', mode.index); + } + + void refresh() { + notifyListeners(); + } + + Http18Comic getHttp() { + final String name = + settingBox.get('http', defaultValue: baseUrls.keys.first); + final http = Http18Comic(baseUrls[name], name: name, headers: headers); + setProxy(getProxy()); + return http; + } + + Future setHttp(HttpBook http) async { + await settingBox.put('http', http.name); + notifyListeners(); + } + + bool getViewerSwitch() { + return settingBox.get('viewerSwitch', defaultValue: true); + } + + Future setViewerSwitch(bool value) async { + await settingBox.put('viewerSwitch', value); + notifyListeners(); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..705b50e --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,134 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/observer.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:oktoast/oktoast.dart'; +import 'package:package_info/package_info.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:weiman/activities/dataConvert.dart'; +import 'package:weiman/activities/home.dart'; +import 'package:weiman/classes/data.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/db/group.dart'; +import 'package:weiman/db/historyOffset.dart'; +import 'package:weiman/db/setting.dart'; +import 'package:weiman/provider/favoriteData.dart'; +import 'package:weiman/provider/theme.dart'; + +FirebaseAnalytics analytics; +FirebaseAnalyticsObserver observer; + +const bool isDevMode = !bool.fromEnvironment('dart.vm.product'); + +int version; +BoxDecoration border; + +Directory imageCacheDir; +String imageCacheDirPath; +PackageInfo packageInfo; + +void main() async { + print("开发模式 $isDevMode"); + FlutterError.onError = (FlutterErrorDetails details) {}; + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + + getTemporaryDirectory().then((dir) { + imageCacheDir = Directory(path.join(dir.path, 'images')); + imageCacheDirPath = imageCacheDir.path; + if (imageCacheDir.existsSync() == false) imageCacheDir.createSync(); + print('图片缓存目录 $imageCacheDirPath'); + }); + + try { + analytics = FirebaseAnalytics(); + observer = FirebaseAnalyticsObserver(analytics: analytics); + } catch (e) {} + + await Future.wait([ + Hive.initFlutter(), + Data.init(), + SystemChrome.setPreferredOrientations( + [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]) + ]); + Hive.registerAdapter(GroupAdapter()); + Hive.registerAdapter(BookAdapter()); + await Future.wait([ + Hive.openBox(GroupName).then((value) => Group.groupBox = value), + Hive.openBox(BookName) + .then((value) => Book.bookBox = Group.bookBox = value), + Hive.openBox(HistoryOffsetName).then((value) => HistoryOffset.box = value), + Hive.openBox(Setting.name).then((value) => Setting.settingBox = value), + ]); + packageInfo = await PackageInfo.fromPlatform(); + version = int.parse(packageInfo.buildNumber); + runApp(Main()); +} + +class Main extends StatefulWidget { + @override + _Main createState() => _Main(); +} + +class _Main extends State
with WidgetsBindingObserver { + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void didChangePlatformBrightness() { + super.didChangePlatformBrightness(); + Provider.of(context, listen: false).update(context); + } + + @override + Widget build(BuildContext context) { + border = BoxDecoration( + border: Border( + bottom: Divider.createBorderSide(context, color: Colors.grey))); + return OKToast( + child: MultiProvider( + providers: [ + ChangeNotifierProvider( + lazy: false, + create: (_) => Setting(), + ), + ChangeNotifierProvider( + lazy: false, + create: (_) => FavoriteData(), + ), + ChangeNotifierProvider( + lazy: true, + create: (_) => ThemeProvider(_), + ), + ], + child: Consumer( + builder: (_, theme, __) => MaterialApp( + title: '微漫 v${packageInfo.version}', + themeMode: theme.themeMode, + theme: ThemeData.light(), + darkTheme: ThemeData( + brightness: Brightness.dark, + accentColor: Colors.redAccent, + ), + home: Data.hasData() ? ActivityDataConvert() : ActivityHome(), + // home: ActivityHome(), + debugShowCheckedModeBanner: isDevMode, + navigatorObservers: [observer], + ), + ), + ), + ); + } +} diff --git a/lib/provider/favoriteData.dart b/lib/provider/favoriteData.dart new file mode 100644 index 0000000..8f3b0fc --- /dev/null +++ b/lib/provider/favoriteData.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/db/group.dart'; + +class FavoriteData extends ChangeNotifier { + final List all = [], others = []; + final Map> groups = {}; + + FavoriteData() { + loadBooksList(); + } + + Future loadBooksList([notify = false]) async { + final groupList = Group.groupBox.values.toList(); + final groupMap = {for (final group in groupList) group.key: group}; + groups.clear(); + groupList.forEach((group) { + groups[group] = []; + }); + + all.clear(); + others.clear(); + + // if(isDevMode){ + // final temp = [ + // Book( + // aid: '180454', + // name: '朋友,女朋友', + // avatar: + // 'https://cdn-msp.18comic.org/media/albums/206567.jpg', + // chapterCount: 0, + // httpId: '18', + // needUpdate: false, + // authors: [], + // ), + // Book( + // aid: '206567', + // name: '抑欲人妻', + // avatar: + // 'https://cdn-msp.18comic.org/media/albums/206567.jpg', + // chapterCount: 0, + // httpId: '18', + // needUpdate: true, + // authors: [], + // ), + // Book( + // aid: '147335', + // name: '亲爱的大叔', + // avatar: + // 'https://cdn-msp.msp-comic.xyz/media/albums/147335.jpg', + // chapterCount: 0, + // httpId: '18', + // needUpdate: true, + // authors: [], + // ), + // ]; + // all.addAll(temp); + // others.addAll(temp); + // } + + Book.bookBox.values.forEach((book) { + if (book.favorite != true) return; + all.add(book); + if (groupMap.containsKey(book.groupId)) { + //有分组的藏书 + groups[groupMap[book.groupId]].add(book); + } else { + //没有分组的藏书 + others.add(book); + } + }); + + print({'all': all.length, 'other': others.length}); + + if (notify) notifyListeners(); + } + + Future checkUpdate() async { + final groupList = [others, ...groups.values]; + for (final array in groupList) { + for (final book in array) { + if (book.httpId == null) { + book.status = BookUpdateStatus.old; + } else if (book.needUpdate != true) { + book.status = BookUpdateStatus.not; + } else { + book.status = BookUpdateStatus.wait; + } + notifyListeners(); + if (book.status != BookUpdateStatus.wait) continue; + book.status = BookUpdateStatus.loading; + notifyListeners(); + await book.update(); + if (book.status == BookUpdateStatus.had) sort(array, book); + notifyListeners(); + } + } + return all.where((book) => book.status == BookUpdateStatus.had).length; + } + + /// 显示在前排 + void sort(List array, Book book) { + print('sort ${book.name}'); + array.remove(book); + array.insert(0, book); + } + + Future deleteBook(Book book) async { + book.favorite = false; + await book.save(); + // print('删书 ${book.name} 成功'); + loadBooksList(true); + } + + Future deleteGroup(Group group, [bool deleteBooks = false]) async { + if (deleteBooks && groups.containsKey(group)) { + await Future.wait(groups[group].map((book) => book.setFavorite(false))); + } + await Group.groupBox.delete(group.key); + await loadBooksList(true); + } + + Future addGroup(Group group) async { + group.save(); + await loadBooksList(true); + } + + Future addBook(Book book) async { + book.favorite = true; + await book.save(); + loadBooksList(true); + } +} diff --git a/lib/provider/theme.dart b/lib/provider/theme.dart new file mode 100644 index 0000000..adc4007 --- /dev/null +++ b/lib/provider/theme.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:weiman/db/setting.dart'; + +class ThemeProvider extends ChangeNotifier { + ThemeMode themeMode = ThemeMode.system; // 主题模式 + + ThemeProvider(BuildContext context) { + themeMode = Provider.of(context, listen: false).getThemeMode(); + } + + void changeTheme(ThemeMode mode) { + print('改变主题 $mode'); + themeMode = mode; + notifyListeners(); + } + + void update(BuildContext context) { + final bright = MediaQuery.platformBrightnessOf(context); + switch (bright) { + case Brightness.light: + changeTheme(ThemeMode.light); + break; + case Brightness.dark: + changeTheme(ThemeMode.dark); + } + print('update $bright'); + } +} diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..036f65b --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:weiman/activities/book/book.dart'; +import 'package:weiman/activities/chapter/activity.dart'; +import 'package:weiman/activities/search/search.dart'; +import 'package:weiman/classes/chapter.dart'; +import 'package:weiman/db/book.dart'; + +final weekTime = Duration.millisecondsPerDay * 7; + +void openSearch(BuildContext context, String word) {} + +Future openBook(BuildContext context, Book book, String heroTag) { + print('openBook $book'); + if (book.http == null) { + return Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: '/activity_search/${book.name}'), + builder: (_) => ActivitySearch(search: book.name), + ), + ); + } + return Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: '/activity_book/${book.name}'), + builder: (_) => ActivityBook(book: book, heroTag: heroTag), + ), + ); +} + +Future openChapter(BuildContext context, Book book, Chapter chapter) { + return Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings( + name: '/activity_chapter/${book.name}/${chapter.cname}'), + builder: (_) => ActivityChapter(book, chapter), + ), + ); +} + +void showStatusBar() { + SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); +} + +void hideStatusBar() { + SystemChrome.setEnabledSystemUIOverlays([]); +} diff --git a/lib/widgets/animatedLogo.dart b/lib/widgets/animatedLogo.dart new file mode 100644 index 0000000..e4a09c7 --- /dev/null +++ b/lib/widgets/animatedLogo.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:sa_anicoto/sa_anicoto.dart'; + +class AnimatedLogoWidget extends StatefulWidget { + final double width, height; + + const AnimatedLogoWidget({ + Key key, + @required this.width, + @required this.height, + }) : super(key: key); + + @override + _AnimatedLogoWidget createState() => _AnimatedLogoWidget(); +} + +class _AnimatedLogoWidget extends State + with AnimationMixin { + Animation size; // Declare animation variable + + @override + void initState() { + size = Tween(begin: 0, end: widget.height - 20).animate(controller); + controller.mirror( + duration: Duration(seconds: 1)); // Start the animation playback + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: widget.width, + height: widget.height, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned( + top: size.value, + child: Image.asset( + 'assets/logo.png', + width: 20, + height: 20, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/book.dart b/lib/widgets/book.dart new file mode 100644 index 0000000..58ee111 --- /dev/null +++ b/lib/widgets/book.dart @@ -0,0 +1,192 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:weiman/classes/chapter.dart'; +import 'package:weiman/classes/networkImageSSL.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/utils.dart'; + +class WidgetBook extends StatelessWidget { + final Book book; + final String subtitle; + final Function(Book) onTap; + + const WidgetBook( + this.book, { + Key key, + @required this.subtitle, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final isLiked = book.favorite; + return ListTile( + title: Text( + book.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + dense: true, + leading: Hero( + tag: 'bookAvatar${book.aid}', + child: ExtendedImage(image: NetworkImageSSL(book.http, book.avatar)), + ), + trailing: Icon( + isLiked ? Icons.favorite : Icons.favorite_border, + color: isLiked ? Colors.red : Colors.grey, + size: 12, + ), + onTap: () { + if (onTap != null) return onTap(book); + openBook(context, book, 'bookAvatar${book.aid}'); + }, + ); + } +} + +final dateFormat = DateFormat('yyyy-MM-dd'); + +class WidgetChapter extends StatelessWidget { + static final double height = kToolbarHeight; + final Chapter chapter; + final Function(Chapter) onTap; + final bool read; + + WidgetChapter({ + Key key, + this.chapter, + this.onTap, + this.read = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final children = [TextSpan(text: chapter.cname)]; + if (read) { + children.insert( + 0, + TextSpan( + text: '[已看]', + style: TextStyle(color: Colors.orange), + )); + } + return ListTile( + onTap: () { + if (onTap != null) onTap(chapter); + }, + title: RichText( + text: TextSpan( + children: children, + style: Theme.of(context).textTheme.bodyText2, + ), + softWrap: true, + maxLines: 2, + ), + subtitle: chapter.time == null + ? null + : Text('更新时间 ${dateFormat.format(chapter.time)}'), + ); + } +} + +class WidgetHistory extends StatelessWidget { + final Book book; + final Function(Book book) onTap; + + WidgetHistory(this.book, this.onTap); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: ListTile( + onTap: () { + if (onTap != null) onTap(book); + }, + title: Text(book.name), + leading: Image( + image: ExtendedNetworkImageProvider(book.avatar, cache: true), + fit: BoxFit.fitHeight, + ), + subtitle: Text(book.history.cname), + ), + ); + } +} + +class WidgetBookCheckNew extends StatefulWidget { + final Book book; + + const WidgetBookCheckNew({Key key, this.book}) : super(key: key); + + @override + _WidgetBookCheckNew createState() => _WidgetBookCheckNew(); +} + +class _WidgetBookCheckNew extends State { + bool loading = true, hasError = false; + int news; + + @override + void initState() { + super.initState(); + load(); + } + + void load() async { +// loading = true; +// try { +// final book = await Http18Comic.instance +// .getBook(widget.book.aid) +// .timeout(Duration(seconds: 2)); +// news = book.chapterCount - widget.book.chapterCount; +// hasError = false; +// } catch (e) { +// hasError = true; +// } +// loading = false; +// setState(() {}); + } + + @override + Widget build(BuildContext context) { + final children = []; + if (widget.book.history != null) + children.add(Text( + widget.book.history.cname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + )); + + if (loading) + children.add(Text('检查更新中')); + else if (hasError) + children.add(Text('网络错误')); + else if (news > 0) + children.add(Text('有 $news 章更新')); + else + children.add(Text('没有更新')); + return ListTile( + onTap: () => + openBook(context, widget.book, 'checkBook${widget.book.aid}'), + leading: Hero( + tag: 'checkBook${widget.book.aid}', + child: Image( + image: + ExtendedNetworkImageProvider(widget.book.avatar, cache: true)), + ), + dense: true, + isThreeLine: true, + title: Text(widget.book.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ); + } +} diff --git a/lib/widgets/bookGroup.dart b/lib/widgets/bookGroup.dart new file mode 100644 index 0000000..a120a35 --- /dev/null +++ b/lib/widgets/bookGroup.dart @@ -0,0 +1,103 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:weiman/db/group.dart'; + +class BookGroupHeader extends StatefulWidget { + final Group group; + final int count; + final List actions; + final Color divideColor; + final double height; + final IndexedWidgetBuilder builder; + final List slideActions; + + const BookGroupHeader({ + Key key, + @required this.group, + @required this.count, + @required this.builder, + this.actions = const [], + this.divideColor = Colors.grey, + this.height = kToolbarHeight, + this.slideActions, + }) : assert(group != null), + assert(builder != null), + super(key: key); + + @override + _State createState() => _State(); +} + +class _State extends State { + bool expended; + + @override + void initState() { + expended = widget.group.expended ?? false; + super.initState(); + } + + @override + Widget build(BuildContext context) { + Decoration _decoration = BoxDecoration( + border: Border( + bottom: Divider.createBorderSide(context, color: widget.divideColor), + ), + ); + Widget header = InkWell( + child: Container( + height: widget.height, + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + ), + child: Row(children: [ + Transform.rotate( + angle: expended ? 0 : math.pi, + child: Icon( + Icons.arrow_drop_down, + color: Colors.grey, + ), + ), + Expanded(child: Text('${widget.group.name}(${widget.count})')), + ...widget.actions, + ]), + ), + onTap: () { + expended = !expended; + widget.group + ..expended = expended + ..save(); + setState(() {}); + }, + ); + if (widget.slideActions != null && widget.slideActions.length > 0) { + header = Slidable( + child: header, + actionPane: SlidableDrawerActionPane(), + secondaryActions: widget.slideActions, + ); + } + return SliverStickyHeader( + header: header, + sliver: expended + ? SliverList( + delegate: SliverChildBuilderDelegate( + (ctx, i) { + if (i < widget.count - 1) { + return DecoratedBox( + decoration: _decoration, + child: widget.builder(context, i), + ); + } + return widget.builder(context, i); + }, + childCount: widget.count, + )) + : null, + ); + } +} diff --git a/lib/widgets/bookSettingDialog.dart b/lib/widgets/bookSettingDialog.dart new file mode 100644 index 0000000..3612d9c --- /dev/null +++ b/lib/widgets/bookSettingDialog.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/db/group.dart'; +import 'package:weiman/provider/favoriteData.dart'; +import 'package:weiman/widgets/groupFormDialog.dart'; + +Future showBookSettingDialog(BuildContext context, Book book) { + return showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text('藏书《${book.name}》的设置'), + scrollable: true, + content: WidgetSetting(book: book), + ), + ); +} + +class WidgetSetting extends StatefulWidget { + final Book book; + + const WidgetSetting({Key key, this.book}) : super(key: key); + + @override + _WidgetSetting createState() => _WidgetSetting(); +} + +class _WidgetSetting extends State { + static final updateMenus = {true: '自动', false: '不检查'}; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: ListTile.divideTiles(context: context, tiles: [ + ListTile( + title: Text('检查更新'), + trailing: DropdownButton( + value: widget.book.needUpdate, + items: updateMenus.keys + .map((key) => + DropdownMenuItem(value: key, child: Text(updateMenus[key]))) + .toList(), + onChanged: changeUpdate, + ), + ), + ListTile( + title: Text('分组'), + trailing: DropdownButton( + hint: Text('没有分组'), + value: widget.book.group, + items: [ + DropdownMenuItem( + child: Text('新建'), + value: null, + ), + ...Group.groupBox.values + .map((e) => DropdownMenuItem(value: e, child: Text(e.name))) + .toList(), + ], + onChanged: changeGroup, + ), + ), + ]).toList(), + ); + } + + changeUpdate(bool needUpdate) async { + widget.book.needUpdate = needUpdate; + await widget.book.save(); + setState(() {}); + } + + changeGroup(Group group) async { + if (group == null) { + group = await showGroupFormDialog(context); + } + widget.book.groupId = group == null ? widget.book.groupId : group.key; + await widget.book.save(); + setState(() {}); + } + + changeFavorite() async { + await widget.book.setFavorite(!widget.book.favorite); + setState(() {}); + } + + removeHistory() async { + if (widget.book.history != null) await widget.book.setHistory(null); + setState(() {}); + } + + @override + void setState(fn) { + final fav = Provider.of(context, listen: false); + fav.loadBooksList(true); + super.setState(fn); + } +} diff --git a/lib/widgets/checkConnect/checkConnect.dart b/lib/widgets/checkConnect/checkConnect.dart new file mode 100644 index 0000000..6329a47 --- /dev/null +++ b/lib/widgets/checkConnect/checkConnect.dart @@ -0,0 +1,314 @@ +import 'package:dio/dio.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:html/parser.dart'; +import 'package:provider/provider.dart'; +import 'package:weiman/crawler/http.dart'; +import 'package:weiman/crawler/http18Comic.dart'; +import 'package:weiman/db/setting.dart'; + +class CheckConnectWidget extends StatefulWidget { + @override + _CheckConnectWidget createState() => _CheckConnectWidget(); +} + +class _CheckConnectWidget extends State { + LoadState state = LoadState.loading; + final List<_Check> https = []; + String lastProxy; + + @override + void initState() { + final setting = Provider.of(context, listen: false); + lastProxy = setting.getProxy(); + createHttps(); + super.initState(); + setting.addListener(() { + final proxy = setting.getProxy(); + if (lastProxy != proxy) { + lastProxy = proxy; + createHttps(); + } + }); + } + + void createHttps() { + print('重建http池 proxy:$lastProxy'); + https.clear(); + https.addAll( + baseUrls.keys.map( + (key) => _Check( + name: key, + url: baseUrls[key], + proxy: lastProxy, + ), + ), + ); + check(); + } + + void check() async { + setState(() { + state = LoadState.loading; + }); + https.forEach((http) => http.load()); + await Future.wait(https.map((http) => http.load())); + final bool hasCompleted = + https.where((http) => http.state == LoadState.completed).isNotEmpty; + state = hasCompleted ? LoadState.completed : LoadState.failed; + if (hasCompleted) { + final sort = https.toList()..sort((a, b) => a.time.compareTo(b.time)); + Http18Comic.instance = sort.first.http; + } + setState(() {}); + } + + void _showDialog(String title) async { + await showDialog( + context: context, + builder: (_) => Dialog( + title: title, + https: https, + retry: check, + ), + ); + } + + @override + Widget build(BuildContext context) { + Widget row; + switch (state) { + case LoadState.loading: + row = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + SizedBox(width: 10), + Text('正在尝试连接漫画网站'), + ], + ); + break; + case LoadState.failed: + row = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: Icon(Icons.error, color: Colors.red), + ), + SizedBox(width: 10), + Text('连接不上漫画网站,点击查看错误'), + ], + ); + break; + default: + row = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: Icon(Icons.check_circle, color: Colors.green), + ), + SizedBox(width: 10), + Text('成功连接到漫画网站,点击查看结果'), + ], + ); + } + return Padding( + padding: EdgeInsets.only(top: 10, bottom: 15), + child: GestureDetector( + child: row, + onTap: () => _showDialog('测试结果,选择源'), + ), + ); + } +} + +class Dialog extends StatefulWidget { + final String title; + final List<_Check> https; + final Function retry; + + const Dialog({Key key, this.title, this.https, this.retry}) : super(key: key); + + @override + State createState() => _Dialog(); +} + +class _Dialog extends State { + @override + Widget build(BuildContext context) { + final proxy = widget.https[0].proxy; + return AlertDialog( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.title), + if (proxy != null) + Text('正在使用代理:$proxy', style: TextStyle(fontSize: 14)), + ], + ), + content: Container( + width: 300, + height: 300, + child: ListView( + physics: ClampingScrollPhysics(), + shrinkWrap: true, + children: ListTile.divideTiles( + context: context, + tiles: widget.https.map( + (e) => e.build(onTap: () => setState(() {})), + )).toList(), + ), + ), + actions: [ + FlatButton( + child: Text('再次测试'), + onPressed: () { + widget.retry(); + setState(() {}); + }, + ), + ], + ); + } +} + +class _Check { + final String name; + final String proxy; + Http18Comic http; + Future future; + Duration time; + String error; + LoadState state; + + _Check({ + String url, + @required this.name, + @required this.proxy, + }) { + http = Http18Comic( + url, + name: name, + headers: headers, + proxy: proxy, + ); + } + + Future load() { + future = this._load(); + return future; + } + + Future _load() async { + state = LoadState.loading; + final now = DateTime.now(); + try { + final Response res = await http.dio.get('/'); + final $ = parse(res.data); + final $title = $.querySelector('title'); + if (res.data.contains('Restricted') || + $title == null || + $title.text.indexOf('禁漫天堂') == -1) { + throw DioError( + request: res.request, + response: res, + error: '你使用的IP被漫画网站禁止访问,请更换网络IP\n不要使用日本IP。', + ); + } + state = LoadState.completed; + } catch (e) { + print(e); + if (e.runtimeType == DioError) { + final DioError error = e as DioError; + switch (error.type) { + case DioErrorType.CONNECT_TIMEOUT: + case DioErrorType.RECEIVE_TIMEOUT: + case DioErrorType.SEND_TIMEOUT: + this.error = '连接超时'; + break; + default: + this.error = error.error.toString(); + } + if (error.response?.data != null) { + this.error += '\n接收到的内容:\n' + error.response.data; + } + } else { + this.error = e.toString(); + } + state = LoadState.failed; + print('$name 结果 $state'); + } + time = DateTime.now().difference(now); + } + + Widget build({Function onTap}) { + return FutureBuilder( + future: future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + final Widget title = Text(name); + switch (snapshot.connectionState) { + case ConnectionState.active: + case ConnectionState.waiting: + return ListTile( + title: title, + subtitle: Row(children: [ + SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + SizedBox(width: 5), + Text('读取中'), + ]), + ); + break; + case ConnectionState.done: + if (state == LoadState.failed) { + return ListTile( + title: title, + subtitle: Text('连接失败,点击查看原因'), + onTap: () { + showDialog( + context: context, + builder: (_) { + return AlertDialog( + title: Text('$name 错误内容'), + content: Text(error), + ); + }); + }, + ); + } + final _time = time.inMilliseconds; + final timeString = _time > 1000 + ? '${(time.inMilliseconds / 1000).toStringAsFixed(2)} 秒' + : '${time.inMilliseconds} 毫秒'; + return CheckboxListTile( + title: title, + subtitle: Text('连接成功\n耗时:$timeString'), + isThreeLine: true, + value: Http18Comic.instance?.name == name, + onChanged: (name) { + Http18Comic.instance = http; + MyHttpClient.clients[http.id] = http; + onTap(); + }, + ); + break; + default: + return ListTile(title: title, subtitle: Text('还没有开始网络请求')); + } + }, + ); + } +} diff --git a/lib/widgets/dbSourceListWidget.dart b/lib/widgets/dbSourceListWidget.dart new file mode 100644 index 0000000..e9025fe --- /dev/null +++ b/lib/widgets/dbSourceListWidget.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class DBSourceListWidget extends StatefulWidget { + @override + _DBSourceListWidget createState() => _DBSourceListWidget(); +} + +class _DBSourceListWidget extends State { + @override + Widget build(BuildContext context) { + return ListView(children: []); + } +} diff --git a/lib/widgets/deleteGroupDialog.dart b/lib/widgets/deleteGroupDialog.dart new file mode 100644 index 0000000..1e77a37 --- /dev/null +++ b/lib/widgets/deleteGroupDialog.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:weiman/db/group.dart'; +import 'package:weiman/provider/favoriteData.dart'; + +Future showDeleteGroupDialog(BuildContext context, Group group) { + return showDialog( + context: context, + builder: (_) => DeleteGroupWidget(group: group), + ); +} + +class DeleteGroupWidget extends StatefulWidget { + final Group group; + + const DeleteGroupWidget({Key key, this.group}) : super(key: key); + + @override + _DeleteGroupWidget createState() => _DeleteGroupWidget(); +} + +class _DeleteGroupWidget extends State { + bool deleteBooks = false; + + @override + Widget build(BuildContext context) { + final length = widget.group.books.length; + return AlertDialog( + title: Text('删除分组 ${widget.group.name}'), + scrollable: true, + content: Column( + children: ListTile.divideTiles(context: context, tiles: [ + if (length > 0) + ListTile( + title: Text('删除藏书'), + subtitle: Text('有 $length 本藏书'), + trailing: Checkbox( + value: deleteBooks, + onChanged: (v) => setState(() => deleteBooks = v), + ), + ) + ]).toList(), + ), + actions: [ + FlatButton( + child: Text('确认'), + onPressed: () async { + await Provider.of(context, listen: false) + .deleteGroup(widget.group, deleteBooks); + Navigator.pop(context); + }, + ), + RaisedButton( + child: Text('取消'), onPressed: () => Navigator.pop(context)), + ], + ); + } +} diff --git a/lib/widgets/favorites.dart b/lib/widgets/favorites.dart new file mode 100644 index 0000000..29f4b44 --- /dev/null +++ b/lib/widgets/favorites.dart @@ -0,0 +1,240 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:oktoast/oktoast.dart'; +import 'package:provider/provider.dart'; +import 'package:weiman/classes/networkImageSSL.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/db/group.dart'; +import 'package:weiman/provider/favoriteData.dart'; +import 'package:weiman/utils.dart'; +import 'package:weiman/widgets/bookGroup.dart'; +import 'package:weiman/widgets/bookSettingDialog.dart'; +import 'package:weiman/widgets/deleteGroupDialog.dart'; +import 'package:weiman/widgets/groupFormDialog.dart'; +import 'package:weiman/widgets/sliverExpandableGroup.dart'; +import 'package:weiman/widgets/utils.dart'; + +class FavoriteList extends StatefulWidget { + @override + _FavoriteList createState() => _FavoriteList(); +} + +class _FavoriteList extends State { + static bool showTip = true; + + @override + initState() { + super.initState(); + if (showTip) { + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + showToast( + '下拉收藏列表检查更新\n分组和藏书左滑显示更多操作', + textPadding: EdgeInsets.all(10), + duration: Duration(seconds: 4), + ); + showTip = false; + }); + } + } + + Widget bookBuilder(Book book) { + return FBookItem( + book: book, + onDelete: deleteBook, + ); + } + + deleteBook(Book book) async { + final sure = await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text('删除藏书 ${book.name} ?'), + actions: [ + FlatButton( + child: Text('确认'), + onPressed: () { + Navigator.pop(context, true); + }), + ], + ), + ); + print('删书 $sure'); + if (sure != true) return; + + await Provider.of(context, listen: false).deleteBook(book); + } + + Future deleteGroup(Group group) async { + await showDeleteGroupDialog(context, group); + setState(() {}); + } + + Future groupRename(Group group) async { + await showGroupFormDialog(context, group); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (_, favorite, __) { + if (favorite.all.isEmpty && favorite.groups.keys.isEmpty) + return Center(child: Text('没有收藏')); + return ClipRect( + child: RefreshIndicator( + onRefresh: favorite.checkUpdate, + child: SafeArea( + child: CustomScrollView( + slivers: [ + ...favorite.groups.keys.map((group) { + final list = favorite.groups[group]; + return BookGroupHeader( + group: group, + count: list.length, + builder: (ctx, i) => bookBuilder(favorite.groups[group][i]), + slideActions: [ + IconSlideAction( + caption: '重命名', + color: Colors.blue, + icon: Icons.edit, + onTap: () => groupRename(group), + ), + IconSlideAction( + caption: '删除', + color: Colors.red, + icon: Icons.delete, + onTap: () => deleteGroup(group), + ), + ], + ); + }), + SliverExpandableGroup( + title: Text('没有分组的藏书(${favorite.others.length})'), + expanded: false, + count: favorite.others.length, + builder: (ctx, i) => bookBuilder(favorite.others[i]), + ), + ], + ), + ), + ), + ); + }); + } +} + +final bookStatusWidgets = { + BookUpdateStatus.loading: + TextSpan(text: '正在读取网络数据', style: TextStyle(color: Colors.blue)), + BookUpdateStatus.not: + TextSpan(text: '该藏书设置为不更新', style: TextStyle(color: Colors.grey)), + BookUpdateStatus.no: + TextSpan(text: '该藏书没有新章节', style: TextStyle(color: Colors.grey)), + BookUpdateStatus.wait: + TextSpan(text: '处于更新队列,等待更新', style: TextStyle(color: Colors.grey)), + BookUpdateStatus.old: + TextSpan(text: '旧藏书不检查更新', style: TextStyle(color: Colors.redAccent)), + BookUpdateStatus.fail: + TextSpan(text: '网络问题,检查更新失败', style: TextStyle(color: Colors.redAccent)), +}; + +class FBookItem extends StatefulWidget { + final Book book; + final void Function(Book book) onDelete; + + const FBookItem({ + Key key, + @required this.book, + @required this.onDelete, + }) : super(key: key); + + @override + _FBookItem createState() => _FBookItem(); +} + +class _FBookItem extends State { + @override + Widget build(BuildContext context) { + TextSpan subtitle = + bookStatusWidgets[widget.book.status ?? BookUpdateStatus.no]; + if (widget.book.status == BookUpdateStatus.had) { + final _subtitle = '有 ${widget.book.newChapterCount} 章更新'; + subtitle = TextSpan( + text: _subtitle, + style: TextStyle( + color: widget.book.look ? Colors.grey : Colors.green, + ), + ); + } + return Slidable( + actionPane: SlidableDrawerActionPane(), + closeOnScroll: true, + actionExtentRatio: 0.25, + secondaryActions: [ + IconSlideAction( + caption: '设置', + color: Colors.blue, + icon: Icons.settings, + onTap: () async { + final before = widget.book.needUpdate; + await showBookSettingDialog(context, widget.book); + if (before != widget.book.needUpdate) { + widget.book.status = widget.book.needUpdate + ? BookUpdateStatus.no + : BookUpdateStatus.not; + } + if (mounted) setState(() {}); + }, + ), + if (widget.book.status == BookUpdateStatus.had && + widget.book.look == false) + IconSlideAction( + caption: '已读', + color: Colors.greenAccent, + iconWidget: SizedBox( + width: 24, + height: 24, + child: Icon( + FontAwesomeIcons.bellSlash, + size: 20, + color: Colors.white, + ), + ), + foregroundColor: Colors.white, + onTap: () async { + widget.book.chapterCount += widget.book.newChapterCount; + widget.book.look = true; + await widget.book.save(); + setState(() {}); + }, + ), + IconSlideAction( + caption: '删除', + color: Colors.red, + icon: Icons.delete, + onTap: () => widget.onDelete(widget.book), + ), + ], + child: ListTile( + onTap: () async { + await openBook(context, widget.book, 'fb ${widget.book.aid}'); + setState(() {}); + }, + // onLongPress: () => onDelete(book), + leading: Hero( + tag: 'fb ${widget.book.aid}', + child: widget.book.http == null + ? oldBookAvatar(text: '旧书', width: 50.0, height: 80.0) + : ExtendedImage( + image: NetworkImageSSL(widget.book.http, widget.book.avatar), + width: 50.0, + height: 80.0), + ), + title: Text(widget.book.name), + subtitle: RichText(text: subtitle), + ), + ); + } +} diff --git a/lib/widgets/groupFormDialog.dart b/lib/widgets/groupFormDialog.dart new file mode 100644 index 0000000..eac52ef --- /dev/null +++ b/lib/widgets/groupFormDialog.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +import 'package:weiman/db/group.dart'; + +Future showGroupFormDialog(BuildContext context, [Group group]) { + return showDialog( + context: context, + builder: (_) { + return GroupFormDialog(group: group); + }, + ); +} + +class GroupFormDialog extends StatefulWidget { + final Group group; + + const GroupFormDialog({Key key, this.group}) : super(key: key); + + @override + _GroupFormDialog createState() => _GroupFormDialog(); +} + +class _GroupFormDialog extends State { + final _form = GlobalKey(); + TextEditingController _nameController; + Group group; + + @override + void initState() { + group = widget.group; + _nameController = TextEditingController(text: widget.group?.name ?? ''); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(widget.group == null ? '创建分组' : '分组重命名'), + content: Form( + key: _form, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + autofocus: true, + controller: _nameController, + decoration: InputDecoration.collapsed( + hintText: group == null ? '输入分组名称' : '原名 ${group.name}', + ), + validator: (value) { + value = value.trim(); + if (value.isEmpty) { + return '分组名称不能为空'; + } + final sameName = + Group.groupBox.values.firstWhere((Group group) { + return group.name == value && group.key != this.group?.key; + }, orElse: () => null); + if (sameName != null) { + return '已经存在同名的分组'; + } + return null; + }, + ) + ], + ), + ), + actions: [ + FlatButton( + child: Text('确认'), + onPressed: () async { + if (group == null) { + group = Group(_nameController.text); + } else { + group.name = _nameController.text; + } + await group.save(); + Navigator.pop(context, group); + }, + ), + RaisedButton( + child: Text('取消'), + textColor: Colors.white, + color: Colors.blue, + onPressed: () { + Navigator.pop(context, group); + }, + ), + ], + ); + } +} diff --git a/lib/widgets/histories.dart b/lib/widgets/histories.dart new file mode 100644 index 0000000..e506901 --- /dev/null +++ b/lib/widgets/histories.dart @@ -0,0 +1,149 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:oktoast/oktoast.dart'; + +import 'package:weiman/classes/networkImageSSL.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/utils.dart'; +import 'package:weiman/widgets/sliverExpandableGroup.dart'; +import 'package:weiman/widgets/utils.dart'; + +class Histories extends StatefulWidget { + @override + _Histories createState() => _Histories(); +} + +class _Histories extends State { + static bool _showTips = true; + final List inWeek = [], other = []; + + @override + void initState() { + super.initState(); + loadBook(); + if (_showTips) + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + _showTips = false; + showToast( + '阅读记录和时间分组\n往左滑显示更多操作', + textPadding: EdgeInsets.all(10), + duration: Duration(seconds: 4), + ); + }); + } + + void loadBook() { + inWeek.clear(); + other.clear(); + final list = + Book.bookBox.values.where((book) => book.history != null).toList(); + final now = DateTime.now().millisecondsSinceEpoch; + list.sort((a, b) => b.history.time.compareTo(a.history.time)); + list.forEach((book) { + if ((now - book.history.time.millisecondsSinceEpoch) < weekTime) { + inWeek.add(book); + } else { + other.add(book); + } + }); + } + + void clear(bool inWeek) async { + final title = '确认清空 ' + (inWeek ? '7天内的' : '更早的') + '浏览记录 ?'; + final res = await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text(title), + actions: [ + FlatButton( + textColor: Colors.grey, + child: Text('取消'), + onPressed: () => Navigator.pop(context, false), + ), + FlatButton( + child: Text('确认'), + onPressed: () => Navigator.pop(context, true), + ), + ], + )); + print('清理历史 $inWeek $res'); + if (res == false) return; + List list = inWeek ? this.inWeek : this.other; + await Future.wait(list.map((book) => book.setHistory(null))); + setState(() { + loadBook(); + }); + } + + Widget book(List array, int index) { + final Book book = array[index]; + return Slidable( + child: ListTile( + leading: book.http == null + ? oldBookAvatar(text: '旧\n书', width: 50.0, height: 80.0) + : ExtendedImage( + image: NetworkImageSSL(book.http, book.avatar), + width: 50.0, + height: 80.0), + title: Text(book.name), + subtitle: Text(book.history.cname), + onTap: () => openBook(context, book, 'fb ${book.aid}'), + ), + actionPane: SlidableDrawerActionPane(), + secondaryActions: [ + IconSlideAction( + caption: '删除', + color: Colors.red, + icon: Icons.delete, + onTap: () async { + await book.setHistory(null); + setState(() { + array.remove(book); + }); + }, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: ClipRect( + child: CustomScrollView( + slivers: [ + SliverExpandableGroup( + title: Text('7天内的浏览历史 (${inWeek.length})'), + expanded: true, + count: inWeek.length, + builder: (ctx, i) => book(inWeek, i), + slideActions: [ + IconSlideAction( + caption: '清空', + color: Colors.red, + icon: Icons.delete, + onTap: () => clear(true), + ), + ], + ), + SliverExpandableGroup( + title: Text('更早的浏览历史 (${other.length})'), + count: other.length, + builder: (ctx, i) => book(other, i), + slideActions: [ + IconSlideAction( + caption: '清空', + color: Colors.red, + icon: Icons.delete, + onTap: () => clear(false), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/pullToRefreshHeader.dart b/lib/widgets/pullToRefreshHeader.dart new file mode 100644 index 0000000..4941464 --- /dev/null +++ b/lib/widgets/pullToRefreshHeader.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; +import 'package:weiman/widgets/animatedLogo.dart'; + +class SliverPullToRefreshHeader extends StatelessWidget { + static final double height = kToolbarHeight * 2; + final PullToRefreshScrollNotificationInfo info; + final void Function() onTap; + final double fontSize; + + const SliverPullToRefreshHeader({ + Key key, + @required this.info, + this.onTap, + this.fontSize = 16, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (info == null) return SliverToBoxAdapter(child: SizedBox()); + double dragOffset = info?.dragOffset ?? 0.0; + Widget widget; + if (info.mode == RefreshIndicatorMode.error) { + widget = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('读取网络数据失败\n你可能需要梯子'), + RaisedButton.icon( + icon: Icon(Icons.refresh), + onPressed: onTap, + label: Text('再次尝试'), + ), + ], + ); + } else if (info.mode == RefreshIndicatorMode.refresh || + info.mode == RefreshIndicatorMode.snap) { + widget = Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedLogoWidget(width: 20, height: 30), + SizedBox(width: 5), + Text('读取中,请稍候'), + ], + ); + } else if ([ + RefreshIndicatorMode.drag, + RefreshIndicatorMode.armed, + RefreshIndicatorMode.snap + ].contains(info.mode)) { + widget = Text('下拉刷新'); + } else { + widget = SizedBox(); + } + return SliverToBoxAdapter( + child: Container( + height: dragOffset, + alignment: Alignment.center, + child: widget, + ), + ); + } +} diff --git a/lib/widgets/quick.dart b/lib/widgets/quick.dart new file mode 100644 index 0000000..034437a --- /dev/null +++ b/lib/widgets/quick.dart @@ -0,0 +1,219 @@ +import 'package:draggable_container/draggable_container.dart'; +import 'package:flutter/material.dart'; + +import 'package:weiman/classes/networkImageSSL.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/utils.dart'; +import 'selectFavoriteBooks.dart'; +import 'utils.dart'; + +class QuickBook extends DraggableItem { + static const heroTag = 'quickBookAvatar'; + Widget child; + final BuildContext context; + final Book book; + final double width, height; + + QuickBook(this.width, this.height, + {@required this.book, @required this.context}) { + child = GestureDetector( + onTap: () { + openBook(context, book, '$heroTag ${book.aid}'); + }, + child: Stack( + children: [ + book.http == null + ? oldBookAvatar(width: width, height: height) + : SizedBox( + width: width, + height: height, + child: Hero( + tag: '$heroTag ${book.aid}', + child: Image( + image: NetworkImageSSL(book.http, book.avatar), + fit: BoxFit.cover, + ), + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + padding: EdgeInsets.only(left: 2, right: 2, top: 2, bottom: 2), + color: Colors.black.withOpacity(0.5), + child: Text( + book.name, + softWrap: true, + maxLines: 1, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white, fontSize: 10), + overflow: TextOverflow.ellipsis, + ), + ), + ) + ], + ), + ); + } +} + +class Quick extends StatefulWidget { + final double width, height; + final Function(bool mode) draggableModeChanged; + + const Quick( + {Key key, this.width, this.height, @required this.draggableModeChanged}) + : super(key: key); + + @override + QuickState createState() => QuickState(); +} + +class QuickState extends State { + final int count = 8; + final List _draggableItems = []; + DraggableItem _addButton; + GlobalKey _key = + GlobalKey(); + double width = 0, height = 0; + + void exit() { + _key.currentState.draggableMode = false; + } + + QuickState() { + _addButton = DraggableItem( + deletable: false, + fixed: true, + child: FlatButton( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add, + color: Colors.grey, + ), + Text( + '添加', + style: TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ) + ], + ), + onPressed: () async { + final items = _key.currentState.items; + final buttonIndex = items.indexOf(_addButton); + print('add $buttonIndex'); + if (buttonIndex > -1) { + final book = await showFavoriteBooksDialog(context); + print('选择了 $book'); + if (book == null) return; + book + ..quick = buttonIndex + ..save(); + _key.currentState.insteadOfIndex(buttonIndex, + QuickBook(width, height, book: book, context: context), + force: true); + } + }, + ), + ); + } + + int length() { + return _key.currentState.items.where((item) => item is QuickBook).length; + } + + @override + void initState() { + super.initState(); + + width = widget.width / 4 - 10; + height = (width / 0.7).roundToDouble(); + final list = []; + Book.bookBox.values.forEach((book) { + if (book.quick != null && list.length < count) { + list.add(book); + } else { + book.quick = null; + book.save(); + } + }); + print('quick book length ${list.length}'); + list.sort((a, b) => a.quick.compareTo(b.quick)); + _draggableItems.addAll(list.map((book) { + return QuickBook(width, height, book: book, context: context); + })); + if (_draggableItems.length < count) _draggableItems.add(_addButton); + for (var i = count - _draggableItems.length; i > 0; i--) { + _draggableItems.add(null); + } + } + + @override + Widget build(BuildContext context) { + print('quick build'); + return Column( + children: [ + Container( + margin: EdgeInsets.only(top: 8, bottom: 4, left: 8), + width: widget.width, + child: Text( + '快速导航(长按编辑)', + textAlign: TextAlign.left, + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + ), + DraggableContainer( + key: _key, + slotMargin: EdgeInsets.only(bottom: 8, left: 6, right: 6), + slotSize: Size(width, height), + slotDecoration: BoxDecoration(color: Colors.grey.withOpacity(0.3)), + dragDecoration: BoxDecoration( + boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]), + items: _draggableItems, + onDraggableModeChanged: widget.draggableModeChanged, + onBeforeDelete: (index, item) async { + if (item is QuickBook) { + print('on before delete ${item.book.name}'); + item.book.quick = null; + item.book.save(); + } + return true; + }, + onChanged: (List items) { + final nullIndex = items.indexOf(null); + final buttonIndex = items.indexOf(_addButton); + print('null $nullIndex, button $buttonIndex'); + if (nullIndex > -1 && buttonIndex == -1) { + print('显示添加按钮 1'); + _key.currentState.insteadOfIndex( + nullIndex, _addButton, + triggerEvent: false, force: true); + print('显示添加按钮 2'); + setState(() {}); + } else if (nullIndex > -1 && + buttonIndex > -1 && + nullIndex < buttonIndex) { + _key.currentState.removeItem(_addButton); + _key.currentState + .insteadOfIndex(nullIndex, _addButton, triggerEvent: false); + } + var quick = 0; + items.forEach((item) { + if (item is QuickBook) { + item.book + ..quick = quick + ..save(); + quick++; + } + }); + }, + ), + ], + ); + } +} diff --git a/lib/widgets/selectFavoriteBooks.dart b/lib/widgets/selectFavoriteBooks.dart new file mode 100644 index 0000000..ad66c93 --- /dev/null +++ b/lib/widgets/selectFavoriteBooks.dart @@ -0,0 +1,51 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:weiman/classes/networkImageSSL.dart'; +import 'package:weiman/db/book.dart'; +import 'package:weiman/provider/favoriteData.dart'; + +Future showFavoriteBooksDialog(BuildContext context) { + return showDialog( + context: context, + builder: (_) => FavoriteBooksDialog(title: '将藏书添加到快速导航'), + ); +} + +class FavoriteBooksDialog extends StatelessWidget { + final String title; + + const FavoriteBooksDialog({ + Key key, + @required this.title, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final fav = Provider.of(context, listen: false); + return AlertDialog( + title: Text(title), + scrollable: true, + content: Column( + children: ListTile.divideTiles( + context: context, + tiles: fav.all + .where((book) => book.quick == null) + .map( + (book) => ListTile( + title: Text(book.name), + leading: ExtendedImage( + image: NetworkImageSSL(book.http, book.avatar), + fit: BoxFit.cover, + width: 40, + ), + onTap: () => Navigator.pop(context, book), + ), + ) + .toList(), + ).toList(), + ), + ); + } +} diff --git a/lib/widgets/sliverExpandableGroup.dart b/lib/widgets/sliverExpandableGroup.dart new file mode 100644 index 0000000..2e8d688 --- /dev/null +++ b/lib/widgets/sliverExpandableGroup.dart @@ -0,0 +1,106 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +class SliverExpandableBuilder { + final int count; + final WidgetBuilder builder; + + const SliverExpandableBuilder(this.count, this.builder); +} + +class SliverExpandableGroup extends StatefulWidget { + final Widget title; + final bool expanded; + final List actions; + final Color divideColor; + final double height; + final int count; + final IndexedWidgetBuilder builder; + final List slideActions; + + const SliverExpandableGroup({ + Key key, + @required this.title, + @required this.count, + @required this.builder, + this.expanded = false, + this.actions = const [], + this.divideColor = Colors.grey, + this.height = kToolbarHeight, + this.slideActions, + }) : assert(title != null), + assert(builder != null), + super(key: key); + + @override + _SliverExpandableGroup createState() => _SliverExpandableGroup(); +} + +class _SliverExpandableGroup extends State { + bool _expanded; + + @override + initState() { + super.initState(); + _expanded = widget.expanded; + } + + @override + Widget build(BuildContext context) { + Decoration _decoration = BoxDecoration( + border: Border( + bottom: Divider.createBorderSide(context, color: widget.divideColor), + ), + ); + Widget header = InkWell( + child: Container( + height: widget.height, + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + ), + child: Row(children: [ + Transform.rotate( + angle: _expanded ? 0 : math.pi, + child: Icon( + Icons.arrow_drop_down, + color: Colors.grey, + ), + ), + Expanded(child: widget.title), + ...widget.actions, + ]), + ), + onTap: () { + setState(() { + _expanded = !_expanded; + }); + }, + ); + if (widget.slideActions != null && widget.slideActions.length > 0) { + header = Slidable( + child: header, + actionPane: SlidableDrawerActionPane(), + secondaryActions: widget.slideActions, + ); + } + return SliverStickyHeader( + header: header, + sliver: _expanded + ? SliverList( + delegate: SliverChildBuilderDelegate((ctx, i) { + if (i < widget.count - 1) { + return DecoratedBox( + decoration: _decoration, + child: widget.builder(context, i), + ); + } + return widget.builder(context, i); + }, childCount: widget.count)) + : null, + ); + } +} diff --git a/lib/widgets/utils.dart b/lib/widgets/utils.dart new file mode 100644 index 0000000..3edcf46 --- /dev/null +++ b/lib/widgets/utils.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class TextDivider extends StatelessWidget { + final String text; + final double leftPadding, padding; + final List actions; + + const TextDivider({ + Key key, + @required this.text, + this.padding = 5, + this.leftPadding = 15, + this.actions = const [], + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: + EdgeInsets.only(left: leftPadding, top: padding, bottom: padding), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded(child: Text(text, style: TextStyle(color: Colors.grey))), + ...actions, + ], + ), + ); + } +} + +Widget oldBookAvatar({ + String text = '旧\n藏\n书', + width = double.infinity, + height = double.infinity, +}) { + return Container( + width: width, + height: height, + alignment: Alignment.center, + color: Colors.greenAccent, + child: Text(text), + ); +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..7c5440d --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,122 @@ +name: weiman +description: 微漫App + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.1.4+2007 + +environment: + sdk: ">=2.9.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + dio: any + dio_http_cache: any + image: any + intl: any + async: any + http: any + encrypt: any + html: any + hive: any + sa_anicoto: any + hive_flutter: any + shared_preferences: any + random_string: any + filesize: any + oktoast: any + path_provider: any + draggable_container: any + sticky_headers: any + flutter_sticky_header: any + extended_nested_scroll_view: any + package_info: any + url_launcher: any + font_awesome_flutter: any + webview_flutter: any + loadmore: any + pull_to_refresh_notification: any + http_client_helper: any + extended_image: any + screenshot: any + focus_widget: any + provider: any + loading_more_list: any + flutter_slidable: any + + firebase_core: any + firebase_analytics: any + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^0.1.3 + +dev_dependencies: + flutter_test: + sdk: flutter + + hive_generator: any + build_runner: any + +#dependency_overrides: +# analyzer: '0.39.14' + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + assets: + - assets/logo.png + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages