From 582d23106310244d46d75202596e17ee0e87a8d6 Mon Sep 17 00:00:00 2001 From: nrop19 Date: Sat, 4 Jan 2020 04:29:47 +0800 Subject: [PATCH] v1.0.4 --- .gitignore | 73 ++++++ README.md | 13 +- images/logo.png | Bin 0 -> 5151 bytes lib/activities/book.dart | 331 +++++++++++++++++++++++++ lib/activities/chapter.dart | 323 ++++++++++++++++++++++++ lib/activities/checkData.dart | 135 ++++++++++ lib/activities/home.dart | 258 +++++++++++++++++++ lib/activities/recommend.dart | 83 +++++++ lib/activities/search.dart | 130 ++++++++++ lib/activities/test.dart | 46 ++++ lib/classes/book.dart | 110 ++++++++ lib/classes/data.dart | 151 +++++++++++ lib/classes/http.dart | 47 ++++ lib/main.dart | 157 ++++++++++++ lib/utils.dart | 24 ++ lib/widgets/book.dart | 187 ++++++++++++++ lib/widgets/favorites.dart | 182 ++++++++++++++ lib/widgets/histories.dart | 97 ++++++++ lib/widgets/pullToRefreshHeader.dart | 72 ++++++ lib/widgets/quick.dart | 215 ++++++++++++++++ lib/widgets/sliverExpandableGroup.dart | 91 +++++++ lib/widgets/utils.dart | 30 +++ pubspec.yaml | 91 +++++++ 23 files changed, 2844 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 images/logo.png create mode 100644 lib/activities/book.dart create mode 100644 lib/activities/chapter.dart create mode 100644 lib/activities/checkData.dart create mode 100644 lib/activities/home.dart create mode 100644 lib/activities/recommend.dart create mode 100644 lib/activities/search.dart create mode 100644 lib/activities/test.dart create mode 100644 lib/classes/book.dart create mode 100644 lib/classes/data.dart create mode 100644 lib/classes/http.dart create mode 100644 lib/main.dart create mode 100644 lib/utils.dart create mode 100644 lib/widgets/book.dart create mode 100644 lib/widgets/favorites.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/sliverExpandableGroup.dart create mode 100644 lib/widgets/utils.dart create mode 100644 pubspec.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ddde2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,73 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/README.md b/README.md index 8887dd0..b806848 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ -# weiman_app -微漫 +# weiman v1.0.4 + +### 微漫脱敏后的开源代码 + +#### 不解答任何代码上的问题 + +#### App的问题请到 [Telegram群讨论](https://t.me/boring_programer) + +- 删除了android端文件夹,涉及到apk签名等敏感文件 +- 删除了ios端文件夹 +- 删除了lib/classes/http.dart文件里的网站域名和爬虫逻辑,保护被爬网站的同时防止被爬网站加大防爬难度。 \ No newline at end of file diff --git a/images/logo.png b/images/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 BookState(); +} + +class BookState extends State { + final GlobalKey _refresh = GlobalKey(); + GlobalKey _key = GlobalKey(); + + bool _reverse = false; + bool isFavorite = false; + bool isLoading = true, isSuccess = false; + Book book; + List chapters = []; + + @override + void initState() { + super.initState(); + isFavorite = widget.book.isFavorite(); + SchedulerBinding.instance.addPostFrameCallback((_) { + _refresh.currentState.show(); + }); + } + + Future loadBook() async { + setState(() { + isLoading = true; + isSuccess = false; + }); + try { + book = await UserAgentClient.instance + .getBook(aid: widget.book.aid) + .timeout(Duration(seconds: 5)); + book.history = Data.getHistories()[book.aid]?.history; + chapters + ..clear() + ..addAll(book.chapters); + if (_reverse) chapters = chapters.reversed.toList(); + + /// 更新收藏列表里的漫画数据 + if (isFavorite) Data.addFavorite(book); + + _scrollToRead(); + isSuccess = true; + } catch (e) { + isSuccess = false; + return false; + } + isLoading = false; + print('刷新 $book'); + setState(() {}); + return true; + } + + void _scrollToRead() { + if (book.history != null) { + final history = book.chapters + .firstWhere((chapter) => chapter.cid == book.history.cid); + SchedulerBinding.instance.addPostFrameCallback((_) { + _key.currentState.currentInnerPosition.animateTo( + WidgetChapter.height * chapters.indexOf(history).toDouble(), + duration: Duration(milliseconds: 500), + curve: Curves.linear); + }); + } + } + + _openChapter(Chapter chapter) { + setState(() { + book.history = History(cid: chapter.cid, cname: chapter.cname, time: 0); + openChapter(context, book, chapter); + }); + } + + favoriteBook() { + widget.book.favorite(); + isFavorite = !isFavorite; + setState(() {}); + } + + void _sort() { + setState(() { + _reverse = !_reverse; + chapters = chapters.reversed.toList(); + _scrollToRead(); + }); + } + + List _headerBuilder(BuildContext context, bool innerBoxIsScrolled) { + Color color = isFavorite ? Colors.red : Colors.white; + IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border; + final book = this.book ?? widget.book; + return [ + SliverAppBar( + floating: true, + pinned: true, + snap: false, + title: Text(widget.book.name), + expandedHeight: 200, + actions: [ + IconButton( + onPressed: _sort, + icon: Icon(_reverse + ? FontAwesomeIcons.sortNumericDown + : FontAwesomeIcons.sortNumericDownAlt)), + 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: Image.network( + widget.book.avatar, + ), + ), + ), + Expanded( + child: Container( + padding: EdgeInsets.only(top: 50, right: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '作者:' + (book.author ?? ''), + style: TextStyle(color: Colors.white), + ), + Container( + margin: EdgeInsets.only(top: 10), + ), + Text( + '简介:\n' + (book.description ?? ''), + softWrap: true, + style: TextStyle(color: Colors.white, height: 1.2), + ), + ], + ), + )), + ], + ), + ), + ), + ) + ]; + } + + List chapterWidgets() { + final book = this.book ?? widget.book; + List list = []; + chapters.forEach((chapter) { + final isRead = chapter.cid == book.history?.cid; + list.add(WidgetChapter( + chapter: chapter, + onTap: _openChapter, + read: isRead, + )); + }); + return list; + } + + Widget buildChapter(BuildContext context, int index) { + final book = this.book ?? widget.book; + final chapter = chapters[index]; + final isRead = chapter.cid == book.history?.cid; + return WidgetChapter( + chapter: chapter, + onTap: _openChapter, + read: isRead, + ); + } + + @override + Widget build(BuildContext context) { + Color color = isFavorite ? Colors.red : Colors.white; + IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border; + final book = this.book ?? widget.book; + return Scaffold( + body: PullToRefreshNotification( + key: _refresh, + onRefresh: loadBook, + maxDragOffset: kToolbarHeight * 2, + child: NestedScrollView( + key: _key, + headerSliverBuilder: (_, __) => [], + physics: AlwaysScrollableClampingScrollPhysics(), + body: CustomScrollView( + physics: AlwaysScrollableClampingScrollPhysics(), + slivers: [ + SliverAppBar( + floating: true, + pinned: false, + title: Text(widget.book.name), + expandedHeight: 200, + actions: [ + IconButton( + onPressed: _sort, + icon: Icon(_reverse + ? FontAwesomeIcons.sortNumericDown + : FontAwesomeIcons.sortNumericDownAlt)), + 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: Image.network( + widget.book.avatar, + ), + ), + ), + Expanded( + child: Container( + padding: EdgeInsets.only(top: 50, right: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '作者:' + (book.author ?? ''), + style: TextStyle(color: Colors.white), + ), + Container( + margin: EdgeInsets.only(top: 10), + ), + Text( + '简介:\n' + (book.description ?? ''), + softWrap: true, + style: + TextStyle(color: Colors.white, height: 1.2), + ), + ], + ), + )), + ], + ), + ), + ), + ), + PullToRefreshContainer((info) => SliverPullToRefreshHeader( + info: info, + onTap: () => _refresh.currentState + .show(notificationDragOffset: kToolbarHeight * 2), + )), + NestedScrollViewInnerScrollPositionKeyWidget( + Key('0'), + SliverList( + delegate: SliverChildBuilderDelegate( + buildChapter, + childCount: book.chapters.length, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget build1(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + var pinnedHeaderHeight = + //statusBar height + statusBarHeight + + //pinned SliverAppBar height in header + kToolbarHeight; + + return Scaffold( + body: NestedScrollViewRefreshIndicator( + key: _refresh, + onRefresh: loadBook, + child: NestedScrollView( + key: _key, + pinnedHeaderSliverHeightBuilder: () => pinnedHeaderHeight, + headerSliverBuilder: _headerBuilder, + body: LayoutBuilder( + builder: (_, __) { + if (isLoading) + return Container(); + else if (isSuccess) { + return ListView( + children: ListTile.divideTiles( + context: context, + color: Colors.grey, + tiles: chapterWidgets()) + .toList()); + } + return Container( + constraints: BoxConstraints.expand(), + alignment: Alignment.center, + child: Center( + child: Text( + '读取失败,下拉刷新\n如果多次失败,请检查网络', + textAlign: TextAlign.center, + ), + ), + ); + }, + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + _key.currentState.currentInnerPosition.animateTo(0, + duration: Duration(milliseconds: 100), curve: Curves.linear); + }, + child: Icon(FontAwesomeIcons.angleDoubleUp), + ), + ); + } +} diff --git a/lib/activities/chapter.dart b/lib/activities/chapter.dart new file mode 100644 index 0000000..c2901f0 --- /dev/null +++ b/lib/activities/chapter.dart @@ -0,0 +1,323 @@ +part of '../main.dart'; + +enum LoadState { + Loading, + Finish, + Timeout, +} + +class LoadMoreListSource extends LoadingMoreBase { + @override + Future loadData([bool isloadMoreAction = false]) { + return Future.delayed(Duration(seconds: 1), () { + for (var i = 0; i < 10; i++) { + this.add(0); + } + + return true; + }); + } +} + +class ActivityChapter extends StatefulWidget { + final Book book; + final Chapter chapter; + + ActivityChapter(this.book, this.chapter); + + @override + ChapterState createState() => ChapterState(); +} + +class ChapterState extends State { + final _scaffoldKey = GlobalKey(); + PageController _pageController; + int showIndex = 0; + bool hasNextImage = true; + + @override + void initState() { + super.initState(); + _pageController = PageController( + keepPage: false, + initialPage: widget.book.chapters.indexOf(widget.chapter)); + } + + @override + void dispose() { + _pageController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + 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, + itemBuilder: (ctx, index) { + return ChapterContentView( + actions: [ + IconButton( + icon: Icon(Icons.menu), + onPressed: () { + _scaffoldKey.currentState.openEndDrawer(); + }, + ), + ], + book: widget.book, + chapter: widget.book.chapters[index], + ); + }), +// floatingActionButton: FloatingActionButton( +// child: Text('下一章'), +// onPressed: () { +// if (hasNextChapter) +// return openChapter(widget.book.chapters[chapterIndex + 1]); +// Fluttertoast.showToast(msg: '已经是最后一章了'); +// }, +// ), + ); + } +} + +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(); + _controller = ScrollController(); + updateRead(); + SchedulerBinding.instance.addPostFrameCallback((_) { + _controller.jumpTo(WidgetChapter.height * read); + }); + } + + 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 + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @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(), + ), + ), + ); + } +} + +class ChapterContentView extends StatefulWidget { + final Book book; + final Chapter chapter; + final List actions; + + const ChapterContentView({Key key, this.book, this.chapter, this.actions}) + : super(key: key); + + @override + _ChapterContentView createState() => _ChapterContentView(); +} + +class _ChapterContentView extends State { + final GlobalKey _refresh = GlobalKey(); + final List images = []; + + int chapterIndex = -1; + bool hasNextChapter = false; + + @override + initState() { + super.initState(); + + chapterIndex = widget.book.chapters.indexOf(widget.chapter); + hasNextChapter = widget.book.chapters.last != widget.chapter; + Data.addHistory(widget.book, widget.chapter); + SchedulerBinding.instance.addPostFrameCallback((_) => _refresh?.currentState + ?.show(notificationDragOffset: kToolbarHeight * 2)); + } + + Future fetchImages() async { + print('fetchImages'); + setState(() {}); + images.clear(); + try { + images.addAll(await UserAgentClient.instance + .getImages(aid: widget.book.aid, cid: widget.chapter.cid) + .timeout(const Duration(seconds: 5))); + if (images.length < 5) { + // print('图片 前:' + images.toString()); + var list = await checkImage(images.last); + images.addAll(list); + } + } catch (e) { + print('错误'); + return false; + // throw(e); + } + // print('所有图片:' + images.toString()); + setState(() {}); + return true; + } + + @override + Widget build(BuildContext context) { + return PullToRefreshNotification( + key: _refresh, + onRefresh: fetchImages, + maxDragOffset: kToolbarHeight * 2, + child: CustomScrollView( + physics: AlwaysScrollableClampingScrollPhysics(), + slivers: [ + SliverAppBar( + title: Text(widget.chapter.cname), + pinned: false, + floating: true, + actions: widget.actions, + ), + PullToRefreshContainer((info) => SliverPullToRefreshHeader( + info: info, + onTap: () => _refresh.currentState + .show(notificationDragOffset: kToolbarHeight * 2), + )), + SliverList( + delegate: SliverChildBuilderDelegate( + (ctx, i) => Image.network(images[i]), + childCount: images.length), + ), + ], + ), + ); + } +} + +Future checkImage(String last) async { + final response = new ReceivePort(); + await Isolate.spawn(_checkImage, response.sendPort); + final sendPort = await response.first as SendPort; + //接收消息的ReceivePort + final answer = new ReceivePort(); + //发送数据 + sendPort.send([answer.sendPort, last]); + return answer.first; +} + +void _checkImage(SendPort initialReplyTo) { + UserAgentClient.instance = UserAgentClient('chrome'); + final port = new ReceivePort(); + initialReplyTo.send(port.sendPort); + port.listen((message) async { + // 获取数据并解析 + final send = message[0] as SendPort; + final last = message[1] as String; + // 返回结果 + final uri = Uri.parse(last); + // print({'scheme': uri.scheme, 'host': uri.host, 'path': uri.path}); + final a = uri.scheme + '://' + uri.host; + final b = uri.pathSegments.take(uri.pathSegments.length - 1).join('/'); + // print({'a': a, 'b': b}); + //网址最后的图片文件名 + final file = uri.pathSegments.last.split('.'); + final fileName = file[0]; + // 图片格式 + final fileFormat = file[1]; + final list = []; + int plus = 1; + //print('最后的图片:' + last); + while (true) { + final String file1 = + getFileName(name: fileName, divider: '_', plus: plus), + file2 = getFileName(name: fileName, divider: '_', plus: plus + 1); + var url1 = '$a/$b/$file1.$fileFormat', url2 = '$a/$b/$file2.$fileFormat'; + // print('正在测试:\n' + url1 + '\n' + url2); + final res = await Future.wait([ + UserAgentClient.instance.head(url1), + UserAgentClient.instance.head(url2) + ]); + if (res[0].statusCode != 200) break; + list.add(url1); + if (res[1].statusCode != 200) { + break; + } + list.add(url2); + plus += 2; + } + // print('最后的图片数量: ' + number.toString()); + send.send(list); + }); +} + +String getFileName( + {@required String name, @required String divider, @required int plus}) { + List data = name.split(divider), newName = []; + for (var i = 0; i < data.length; i++) { + try { + int number = int.parse(data[i]) + plus; + newName.add(number.toString()); + } catch (e) { + newName.add(data[i]); + } + } + return newName.join(divider); +} diff --git a/lib/activities/checkData.dart b/lib/activities/checkData.dart new file mode 100644 index 0000000..1d12faa --- /dev/null +++ b/lib/activities/checkData.dart @@ -0,0 +1,135 @@ +part of '../main.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; + 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 = json.decode(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: () { + Fluttertoast.showToast( + msg: '已经复制', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIos: 1, + ); + 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/home.dart b/lib/activities/home.dart new file mode 100644 index 0000000..e647ded --- /dev/null +++ b/lib/activities/home.dart @@ -0,0 +1,258 @@ +part of '../main.dart'; + +class ActivityHome extends StatefulWidget { + final PackageInfo packageInfo; + + const ActivityHome(this.packageInfo, {Key key}) : super(key: key); + + @override + State createState() => HomeState(); +} + +class HomeState extends State { + final _scaffoldKey = GlobalKey(); + final List histories = []; + final List quick = []; + final GlobalKey _quickState = GlobalKey(); + static final weekTime = 7 * 24 * 3600000; + + bool showFavorite = true; + + @override + void initState() { + super.initState(); + analytics.setCurrentScreen(screenName: '/activity_home'); + + /// 提前检查一次藏书的更新情况 + SchedulerBinding.instance.addPostFrameCallback((_) async { + _FavoriteList.getBooks(); + await _FavoriteList.checkNews(); + final updated = _FavoriteList.hasNews.values + .where((int updatedChapters) => updatedChapters > 0) + .length; + if (updated > 0) + Fluttertoast.showToast( + msg: '$updated 本藏书有更新', + gravity: ToastGravity.CENTER, + backgroundColor: Colors.black.withOpacity(0.5), + ); + }); + } + + 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: (_) => ActivityRecommend(), + )); + } + + bool isEdit = false; + + void _draggableModeChanged(bool mode) { + print('mode changed $mode'); + isEdit = mode; + setState(() {}); + } + + @override + Widget build(BuildContext context) { + var media = MediaQuery.of(context); + var width = media.size.width; + width = width * .8; + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: Text('微漫 v' + widget.packageInfo.version), + automaticallyImplyLeading: false, + leading: isEdit + ? IconButton( + icon: Icon(Icons.arrow_back_ios), + onPressed: () { + _quickState.currentState.exit(); + }, + ) + : null, + actions: [ + /// 黑白样式切换 + IconButton( + onPressed: () { + DynamicTheme.of(context).setBrightness( + Theme.of(context).brightness == Brightness.dark + ? Brightness.light + : Brightness.dark); + }, + icon: Icon(Theme.of(context).brightness == Brightness.light + ? FontAwesomeIcons.lightbulb + : FontAwesomeIcons.solidLightbulb), + ), + SizedBox(width: 20), + + /// 收藏列表 + 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), + ), + ], + ), + endDrawer: Drawer( + child: LayoutBuilder( + builder: (_, __) { + if (showFavorite) { + return FavoriteList(); + } else { + return Histories(); + } + }, + ), + ), + body: Container( + alignment: Alignment.center, + 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( + 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(), + ), + ), + ], + ), + Quick( + key: _quickState, + width: width, + draggableModeChanged: _draggableModeChanged, + ), + Container( + margin: EdgeInsets.only(bottom: 10), + child: Text( + '在 level-plus.net 论坛首发', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey[500]), + ), + ), + 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: (_) => ActivityTest())); + }, + child: Text('测试界面'), + ), + ), + Visibility( + visible: isDevMode, + child: FlatButton( + onPressed: () { + Navigator.push(context, + MaterialPageRoute(builder: (_) => ActivityCheckData())); + }, + child: Text('操作 收藏列表数据'), + ), + ), + ], + ), + ), + ); + } +} + +Iterable favoriteTiles(context, Iterable books, + {void Function(Book book) onTap}) { + return books.map((book) => ListTile( + onTap: () { + onTap(book); + }, + title: Text(book.name), + leading: Image.network(book.avatar), + subtitle: Text( + '作者:' + book.author, + style: TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + )); +} diff --git a/lib/activities/recommend.dart b/lib/activities/recommend.dart new file mode 100644 index 0000000..b6dec28 --- /dev/null +++ b/lib/activities/recommend.dart @@ -0,0 +1,83 @@ +part of '../main.dart'; + +class ActivityRecommend extends StatefulWidget { + @override + _ActivityRecommend createState() => _ActivityRecommend(); +} + +class _ActivityRecommend extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('月排行榜'), + ), + body: BookList(), + ); + } +} + +class BookList extends StatefulWidget { + const BookList({Key key}) : super(key: key); + + @override + _BookList createState() => _BookList(); +} + +class _BookList extends State { + final GlobalKey _refresh = GlobalKey(); + final List books = []; + bool loadFail = false; + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.addPostFrameCallback((_) { + _refresh.currentState.show(); + }); + } + + Future loadBooks() async { + loadFail = false; + try { + final books = await UserAgentClient.instance + .getMonthList() + .timeout(Duration(seconds: 5)); + this.books + ..clear() + ..addAll(books); + } catch (e) { + loadFail = true; + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + key: _refresh, + onRefresh: loadBooks, + child: loadFail + ? CustomScrollView( + slivers: [ + SliverFillRemaining( + child: Center(child: Text('读取失败,下拉刷新')), + ) + ], + ) + : ListView( + children: ListTile.divideTiles( + context: context, + tiles: books.map((book) => WidgetBook( + book, + subtitle: book.author, + ))).toList(), + ), + ); + } +} diff --git a/lib/activities/search.dart b/lib/activities/search.dart new file mode 100644 index 0000000..af5cce6 --- /dev/null +++ b/lib/activities/search.dart @@ -0,0 +1,130 @@ +part of '../main.dart'; + +class ActivitySearch extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Search(); + } +} + +class Search extends StatefulWidget { + @override + State createState() { + return SearchState(); + } +} + +enum _SearchState { + None, + Searching, + Done, + Error, +} + +class SearchState extends State { + Future> search; + TextEditingController _controller = TextEditingController(); + CancelableOperation _searcher; + _SearchState _state = _SearchState.None; + final List _books = []; + + void startSearch() { + print('搜索漫画: ' + _controller.text); + if (_searcher != null) _searcher.cancel(); + _books.clear(); + setState(() { + _state = _SearchState.Searching; + }); + _searcher = CancelableOperation.fromFuture( + UserAgentClient.instance.searchBook(_controller.text)) + .then((books) { + setState(() { + print('搜索完成: ' + books.length.toString()); + _books.addAll(books); + _state = _SearchState.Done; + }); + }); + } + + @override + Widget build(BuildContext context) { + search = null; + return Scaffold( + appBar: AppBar( + title: Text('搜索漫画'), + ), + body: Column( + children: [ + Row( + children: [ + Expanded( + child: RawKeyboardListener( + focusNode: FocusNode(), + onKey: (RawKeyEvent event) { + if (event.runtimeType == RawKeyUpEvent && + event.logicalKey.debugName.toLowerCase() == 'enter') { + if (_controller.text.isEmpty) return; + startSearch(); + } + }, + child: TextField( + decoration: InputDecoration( + hintText: '搜索书名', prefixIcon: Icon(Icons.search)), + textAlign: TextAlign.left, + controller: _controller, + autofocus: true, + textInputAction: TextInputAction.search, + onSubmitted: (String name) { + print('onSubmitted'); + startSearch(); + }, + keyboardType: TextInputType.text, + onEditingComplete: () { + print('onEditingComplete'); + startSearch(); + }, + ), + ), + ), + ], + ), + Expanded( + flex: 1, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + switch (_state) { + case _SearchState.Searching: + return Center(child: CircularProgressIndicator()); + case _SearchState.None: + return Center( + child: Icon( + Icons.search, + color: Colors.grey, + )); + default: + if (_books.length == 0) + return Center( + child: Text( + '一本也找不到', + style: TextStyle(color: Colors.blueGrey), + )); + List list = _books + .map((book) => WidgetBook( + book, + subtitle: book.author, + )) + .toList(); + return ListView( + children: + ListTile.divideTiles(context: context, tiles: list) + .toList(), + ); + } + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/activities/test.dart b/lib/activities/test.dart new file mode 100644 index 0000000..b007d63 --- /dev/null +++ b/lib/activities/test.dart @@ -0,0 +1,46 @@ +part of '../main.dart'; + +class ActivityTest extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('asd'), + ), + body: Column( + children: [ + FlatButton( + onPressed: save, + child: Text('保存'), + ), + FlatButton( + onPressed: read, + child: Text('读取'), + ), + FlatButton( + onPressed: clear, + child: Text('清空数据'), + ), + ], + ), + ); + } + + void save() { + Data.addFavorite(Book( + aid: '123', + name: 'name', + avatar: 'avatar', + description: '', + author: '')); + } + + void read() { + var books = Data.getFavorites(); + print(jsonEncode(books)); + } + + void clear() { + Data.clear(); + } +} diff --git a/lib/classes/book.dart b/lib/classes/book.dart new file mode 100644 index 0000000..da74816 --- /dev/null +++ b/lib/classes/book.dart @@ -0,0 +1,110 @@ +part of '../main.dart'; + +class Book { + final String aid; // 书本ID + final String name; // 书本名称 + final String avatar; // 书本封面 + final String author; // 画家 + final String description; // 描述 + final List chapters; + final int chapterCount; + + History history; + + Book({ + @required this.name, + @required this.aid, + @required this.avatar, + this.author, + this.description, + this.chapters: const [], + this.chapterCount: 0, + }); + + @override + String toString() { + return jsonEncode(toJson()); + } + + bool isFavorite() { + var books = Data.getFavorites(); + return books.containsKey(aid); + } + + favorite() { + if (isFavorite()) + Data.removeFavorite(this); + else + Data.addFavorite(this); + } + + Map toJson() { + final Map data = { + 'aid': aid, + 'name': name, + 'avatar': avatar, + 'author': author, + 'chapterCount': chapterCount, + }; + if (history != null) data['history'] = history.toJson(); + return data; + } + + static Book fromJson(Map json) { + final book = Book( + aid: json['aid'], + name: json['name'], + avatar: json['avatar'], + author: json['author'], + description: json['description'], + chapterCount: json['chapterCount'] ?? 0, + ); + if (json.containsKey('history')) + book.history = History.fromJson(json['history']); + return book; + } +} + +class Chapter { + final String cid; // 章节cid + final String cname; // 章节名称 + final String avatar; // 章节封面 + + Chapter({@required this.cid, @required this.cname, @required this.avatar}); + + @override + String toString() { + return jsonEncode({cid: cid, cname: cname, avatar: avatar}); + } +} + +class History { + final String cid; + final String cname; + final int time; + + History({@required this.cid, @required this.cname, @required this.time}); + + @override + String toString() => jsonEncode(toJson()); + + Map toJson() { + return { + 'cid': cid, + 'cname': cname, + 'time': time, + }; + } + + static History fromJson(Map json) { + return History(cid: json['cid'], cname: json['cname'], time: json['time']); + } + + static History fromChapter(Chapter chapter) { + return History( + cid: chapter.cid, + cname: chapter.cname, + time: DateTime.now().millisecondsSinceEpoch, + ); + } +} diff --git a/lib/classes/data.dart b/lib/classes/data.dart new file mode 100644 index 0000000..ee53aa2 --- /dev/null +++ b/lib/classes/data.dart @@ -0,0 +1,151 @@ +part of '../main.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, jsonEncode(value)); + } + } + + static dynamic get(String key) { + return instance.get(key); + } + + 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/http.dart b/lib/classes/http.dart new file mode 100644 index 0000000..2f6a3ba --- /dev/null +++ b/lib/classes/http.dart @@ -0,0 +1,47 @@ +part of '../main.dart'; + +const domain = ''; +final host = Uri.parse(domain).host; + +class UserAgentClient extends http.BaseClient { + final String userAgent; + http.Client _inner; + String lastKey; + int lastKeyTime = 0; + + static UserAgentClient instance; + + UserAgentClient(this.userAgent) { + } + + Future getKey() async { + } + + @override + Future send(http.BaseRequest request) { + } + + Future> getImages( + } + + Future getBook({String aid}) async { + } + + static String _decrypt({String key, String content}) { + } + + Future> searchBook(String name) async { + } + + static void init(String userAgent) { + } + + Future _get(url, {Map headers}) async { + } + + Future> getMonthList() async { + } + + Future> getIndexRandomBooks() async { + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..5b7815f --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,157 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math' as math; + +import 'package:async/async.dart'; +import 'package:draggable_container/draggable_container.dart'; +import 'package:dynamic_theme/dynamic_theme.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; +import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/observer.dart'; +import 'package:flutter/material.dart' hide NestedScrollView; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:html/parser.dart' as html; +import 'package:http/http.dart' as http; +import 'package:http/io_client.dart'; +import 'package:loading_more_list/loading_more_list.dart'; +import 'package:package_info/package_info.dart'; +import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart' + hide CircularProgressIndicator; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; + +part './activities/book.dart'; + +part './activities/chapter.dart'; + +part './activities/checkData.dart'; + +part './activities/home.dart'; + +part './activities/recommend.dart'; + +part './activities/search.dart'; + +part './activities/test.dart'; + +part './classes/book.dart'; + +part './classes/data.dart'; + +part './classes/http.dart'; + +part './widgets/book.dart'; + +part './widgets/favorites.dart'; + +part './widgets/histories.dart'; + +part './widgets/pullToRefreshHeader.dart'; + +part './widgets/quick.dart'; + +part './widgets/sliverExpandableGroup.dart'; + +part './widgets/utils.dart'; + +part 'utils.dart'; + +FirebaseAnalytics analytics; +FirebaseAnalyticsObserver observer; + +const bool isDevMode = !bool.fromEnvironment('dart.vm.product'); + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + try { + analytics = FirebaseAnalytics(); + observer = FirebaseAnalyticsObserver(analytics: analytics); + } catch (e) {} + + await Future.wait([ + Data.init(), + SystemChrome.setPreferredOrientations( + [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]) + ]); + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); + UserAgentClient.init( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'); + runApp(Main(packageInfo: packageInfo)); +// runApp(MaterialApp( +// title: '微漫', +// theme: ThemeData.light(), +// darkTheme: ThemeData.dark(), +// themeMode: ThemeMode.system, +// debugShowCheckedModeBanner: false, +// navigatorObservers: [observer], +// home: ActivityHome(packageInfo), +// )); +} + +class Main extends StatefulWidget { + final PackageInfo packageInfo; + + const Main({Key key, this.packageInfo}) : super(key: key); + + @override + _Main createState() => _Main(); +} + +class _Main extends State
with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DynamicTheme( + defaultBrightness: ThemeMode.system == ThemeMode.light + ? Brightness.light + : Brightness.dark, + data: (brightness) => new ThemeData( + brightness: brightness, + ), + themedWidgetBuilder: (context, theme) { + return new MaterialApp( + title: 'Flutter Demo', + theme: theme, + home: ActivityHome(widget.packageInfo), + ); + }); +// return MaterialApp( +// title: '微漫', +// theme: ThemeData( +// brightness: Brightness.light, +// ), +// darkTheme: ThemeData( +// brightness: Brightness.dark, +// ), +// themeMode: ThemeMode.system, +// debugShowCheckedModeBanner: false, +// navigatorObservers: [observer], +// home: ActivityHome(widget.packageInfo), +// ); + } + + @override + void didChangePlatformBrightness() { + print('改变亮度'); + setState(() {}); + } +} diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..3459f20 --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,24 @@ +part of 'main.dart'; + +final weekTime = Duration.millisecondsPerDay * 7; + +void openBook(BuildContext context, Book book, String heroTag) { + Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: '/activity_book/${book.name}'), + builder: (_) => ActivityBook(book: book, heroTag: heroTag), + ), + ); +} + +void openChapter(BuildContext context, Book book, Chapter chapter) { + Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings( + name: '/activity_chapter/${book.name}/${chapter.cname}'), + builder: (_) => ActivityChapter(book, chapter), + ), + ); +} diff --git a/lib/widgets/book.dart b/lib/widgets/book.dart new file mode 100644 index 0000000..8d89a02 --- /dev/null +++ b/lib/widgets/book.dart @@ -0,0 +1,187 @@ +part of '../main.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) { + var isLiked = book.isFavorite(); + 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: Image.network( + book.avatar, + height: 200, + fit: BoxFit.scaleDown, + )), + trailing: Icon( + isLiked ? Icons.favorite : Icons.favorite_border, + color: isLiked ? Colors.red : Colors.grey, + size: 12, + ), + onTap: () { + if (onTap != null) onTap(book); + openBook(context, book, 'bookAvatar${book.aid}'); + }, + ); + } +} + +class WidgetChapter extends StatelessWidget { + static final double height = 56; + final Chapter chapter; + final Function(Chapter) onTap; + final bool read; + + WidgetChapter({ + Key key, + this.chapter, + this.onTap, + this.read, + }) : 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.body1, + ), + softWrap: true, + maxLines: 2, + ), + leading: Image.network( + chapter.avatar, + fit: BoxFit.fitWidth, + width: 100, + ), + ); + } +} + +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.network( + book.avatar, + 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 UserAgentClient.instance + .getBook(aid: 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.network(widget.book.avatar), + ), + dense: true, + isThreeLine: true, + title: Text(widget.book.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ); + } +} diff --git a/lib/widgets/favorites.dart b/lib/widgets/favorites.dart new file mode 100644 index 0000000..53627ee --- /dev/null +++ b/lib/widgets/favorites.dart @@ -0,0 +1,182 @@ +part of '../main.dart'; + +class FavoriteList extends StatefulWidget { + @override + _FavoriteList createState() => _FavoriteList(); +} + +class _FavoriteList extends State { + static final Map hasNews = {}; + static final List all = [], // 所有收藏 + inWeek = [], // 7天看过的收藏 + other = []; // 其他收藏 + static bool showTip = false; + + static final loadFailTextSpan = TextSpan( + text: '读取失败,下拉刷新', style: TextStyle(color: Colors.redAccent)), + waitToCheck = + TextSpan(text: '等待检查更新', style: TextStyle(color: Colors.grey)), + unCheck = + TextSpan(text: '请下拉列表检查更新', style: TextStyle(color: Colors.grey)), + noUpdate = TextSpan(text: '没有更新', style: TextStyle(color: Colors.grey)); + + static void getBooks() { + all.clear(); + inWeek.clear(); + other.clear(); + all.addAll(Data.getFavorites().values); + if (all.isNotEmpty) { + final now = DateTime.now().millisecondsSinceEpoch; + all.forEach((book) { + if (book.history != null && (now - book.history.time) < weekTime) { + inWeek.add(book); + } else { + other.add(book); + } + }); + } + } + + @override + void initState() { + super.initState(); + getBooks(); + if (all.isNotEmpty) { + if (showTip == false) { + showTip = true; + Fluttertoast.showToast( + msg: '下拉列表可以检查漫画更新', + gravity: ToastGravity.CENTER, + backgroundColor: Colors.black.withOpacity(0.5), + ); + } + } + } + + void _openBook(book) { + openBook(context, book, 'fb ${book.aid}'); + } + + static Future checkNews() async { + hasNews.clear(); + Book currentBook, newBook; + int different; + for (var i = 0; i < all.length; i++) { + currentBook = all[i]; + try { + newBook = await UserAgentClient.instance + .getBook(aid: currentBook.aid) + .timeout(Duration(seconds: 2)); + different = newBook.chapterCount - currentBook.chapterCount; + hasNews[currentBook.aid] = different; + } catch (e) { + hasNews[currentBook.aid] = -1; + } + } + } + + Widget bookBuilder(Book book) { + TextSpan state; + if (hasNews.isEmpty) { + state = unCheck; + } else { + if (hasNews.containsKey(book.aid)) { + if (hasNews[book.aid] > 0) { + state = TextSpan( + text: '有 ${hasNews[book.aid]} 章更新', + style: TextStyle(color: Colors.green)); + } else if (hasNews[book.aid] == -1) { + state = loadFailTextSpan; + } else if (hasNews[book.aid] == 0) { + state = noUpdate; + } + } else { + state = waitToCheck; + } + } + return FBookItem( + book: book, + subtitle: state, + onTap: _openBook, + ); + } + + @override + Widget build(BuildContext context) { + List inWeekUpdated = [], + inWeekUnUpdated = [], + otherUpdated = [], + otherUnUpdated = []; + inWeek.forEach((book) { + if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0) + inWeekUpdated.add(book); + else + inWeekUnUpdated.add(book); + }); + other.forEach((book) { + if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0) + otherUpdated.add(book); + else + otherUnUpdated.add(book); + }); + return Drawer( + child: RefreshIndicator( + onRefresh: () async { + await checkNews(); + setState(() {}); + }, + child: all.isEmpty + ? Center(child: Text('没有收藏')) + : SafeArea( + child: CustomScrollView( + slivers: [ + SliverExpandableGroup( + title: Text('7天内看过并且有更新的藏书(${inWeekUpdated.length})'), + expanded: true, + count: inWeekUpdated.length, + builder: (ctx, i) => bookBuilder(inWeekUpdated[i]), + ), + SliverExpandableGroup( + title: Text('7天内看过的藏书(${inWeekUnUpdated.length})'), + count: inWeekUnUpdated.length, + builder: (ctx, i) => bookBuilder(inWeekUnUpdated[i]), + ), + SliverExpandableGroup( + title: Text('有更新的藏书(${otherUpdated.length})'), + count: otherUpdated.length, + builder: (ctx, i) => bookBuilder(otherUpdated[i]), + ), + SliverExpandableGroup( + title: Text('没有更新的藏书(${otherUnUpdated.length})'), + count: otherUnUpdated.length, + builder: (ctx, i) => bookBuilder(otherUnUpdated[i]), + ), + ], + )), + ), + ); + } +} + +class FBookItem extends StatelessWidget { + final Book book; + final TextSpan subtitle; + final void Function(Book book) onTap; + + const FBookItem({ + Key key, + @required this.book, + @required this.subtitle, + @required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + onTap: () => onTap(book), + leading: Hero(tag: 'fb ${book.aid}', child: Image.network(book.avatar)), + title: Text(book.name, style: Theme.of(context).textTheme.body1), + subtitle: RichText(text: subtitle), + ); + } +} diff --git a/lib/widgets/histories.dart b/lib/widgets/histories.dart new file mode 100644 index 0000000..02b6125 --- /dev/null +++ b/lib/widgets/histories.dart @@ -0,0 +1,97 @@ +part of '../main.dart'; + +class Histories extends StatefulWidget { + @override + _Histories createState() => _Histories(); +} + +class _Histories extends State { + final List inWeek = [], other = []; + + @override + void initState() { + super.initState(); + loadBook(); + } + + void loadBook() { + inWeek.clear(); + other.clear(); + final list = Data.getHistories().values.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) < 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; + list.forEach((book) => Data.removeHistoryFromBook(book)); + setState(() { + loadBook(); + }); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: CustomScrollView( + slivers: [ + SliverExpandableGroup( + title: Text('7天内的浏览历史 (${inWeek.length})'), + expanded: true, + actions: [ + FlatButton( + child: Text('清空'), + onPressed: inWeek.length == 0 ? null : () => clear(true), + ), + ], + count: inWeek.length, + builder: (ctx, i) => WidgetBook( + inWeek[i], + subtitle: inWeek[i].history.cname, + ), + ), + SliverExpandableGroup( + title: Text('更早的浏览历史 (${other.length})'), + actions: [ + FlatButton( + child: Text('清空'), + onPressed: other.length == 0 ? null : () => clear(false), + ), + ], + count: other.length, + builder: (ctx, i) => WidgetBook( + other[i], + subtitle: other[i].history.cname, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/pullToRefreshHeader.dart b/lib/widgets/pullToRefreshHeader.dart new file mode 100644 index 0000000..ea35b82 --- /dev/null +++ b/lib/widgets/pullToRefreshHeader.dart @@ -0,0 +1,72 @@ +part of '../main.dart'; + +class SliverPullToRefreshHeader extends StatelessWidget { + 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; + TextSpan text = TextSpan( + style: Theme.of(context).textTheme.body1.copyWith( + fontSize: fontSize, + ), + children: [ + WidgetSpan( + baseline: TextBaseline.alphabetic, + child: Padding( + child: Image.asset("images/logo.png", height: 20), + padding: EdgeInsets.only(right: 5), + ), + ), + ]); + if (info.mode == RefreshIndicatorMode.error) { + text.children.addAll([ + TextSpan( + text: '读取失败\n当失败次数太多请检查网络情况\n有些很旧的章节会看不到,请见谅\n', + style: TextStyle( + color: Colors.red, + ), + ), + WidgetSpan( + child: RaisedButton.icon( + icon: Icon(Icons.refresh), + onPressed: onTap, + label: Text('再次尝试'))), + ]); + } else if (info.mode == RefreshIndicatorMode.refresh || + info.mode == RefreshIndicatorMode.snap) { + text.children.addAll([ + TextSpan(text: '读取中,请稍候'), + ]); + } else if ([ + RefreshIndicatorMode.drag, + RefreshIndicatorMode.armed, + RefreshIndicatorMode.snap + ].contains(info.mode)) { + text.children.add(TextSpan(text: '重新读取')); + } else { + text.children.add(TextSpan(text: 'Bye~')); + } + return SliverToBoxAdapter( + child: Container( + height: dragOffset, + child: Center( + child: Text.rich( + text, + textAlign: TextAlign.center, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/quick.dart b/lib/widgets/quick.dart new file mode 100644 index 0000000..a21d6b1 --- /dev/null +++ b/lib/widgets/quick.dart @@ -0,0 +1,215 @@ +part of '../main.dart'; + +class QuickBook extends DraggableItem { + static const heroTag = 'quickBookAvatar'; + Widget child; + final BuildContext context; + final Book book; + + QuickBook({@required this.book, @required this.context}) { + child = GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (ctx) => ActivityBook( + book: book, + heroTag: '$heroTag ${book.aid}', + ))); + }, + child: Stack( + children: [ + Hero( + tag: '$heroTag ${book.aid}', + child: Image.network(book.avatar), + ), + 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, + ), + ), + ) + ], + ), + ); + } + + checkUpdate() { + UserAgentClient.instance.getBook(aid: book.aid); + } +} + +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(); + final List id = []; + + void exit() { + _key.currentState.draggableMode = false; + } + + _showSelectBookDialog() async { + final books = Data.getFavorites(); + final list = books.values + .where((book) => !id.contains(book.aid)) + .map((book) => ListTile( + title: Text(book.name), + leading: Image.network(book.avatar), + onTap: () { + Navigator.pop(context, book); + }, + )); + return showDialog( + context: context, + builder: (_) { + return AlertDialog( + title: Text('将收藏的漫画添加到快速导航'), + content: Container( + width: double.maxFinite, + height: 300, + child: list.isNotEmpty + ? ListView( + children: ListTile.divideTiles( + context: context, + tiles: list, + ).toList(), + ) + : Center(child: Text('没有了')), + ), + ); + }); + } + + 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 _showSelectBookDialog(); + print('选择了 $book'); + if (book == null) return; + _key.currentState.insteadOfIndex( + buttonIndex, QuickBook(book: book, context: context), + force: true); + } + }, + ), + ); + } + + int length() { +// print(_key.currentState.items); +// return 0; + return _key.currentState.items.where((item) => item is QuickBook).length; + } + + @override + void initState() { + super.initState(); + + _draggableItems.addAll(Data.quickList().map((book) { + id.add(book.aid); + return QuickBook(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) { + 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), + ), + ), + Container( + width: widget.width, + child: DraggableContainer( + key: _key, + slotMargin: EdgeInsets.only(bottom: 8, left: 7, right: 7), + slotSize: Size(72, 100), + slotDecoration: + BoxDecoration(color: Colors.grey.withOpacity(0.3)), + dragDecoration: BoxDecoration( + boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]), + items: _draggableItems, + onDraggableModeChanged: widget.draggableModeChanged, + onChanged: (List items) { + id.clear(); + items.forEach((item) { + if (item is QuickBook) id.add(item.book.aid); + }); + Data.addQuickAll(id); + final nullIndex = items.indexOf(null); + final buttonIndex = items.indexOf(_addButton); + print('null $nullIndex, button $buttonIndex'); + if (nullIndex > -1 && buttonIndex == -1) { + _key.currentState.insteadOfIndex(nullIndex, _addButton, + triggerEvent: false); + } else if (nullIndex > -1 && + buttonIndex > -1 && + nullIndex < buttonIndex) { + _key.currentState.removeItem(_addButton); + _key.currentState.insteadOfIndex(nullIndex, _addButton, + triggerEvent: false); + } + }, + )), + ], + ); + } +} diff --git a/lib/widgets/sliverExpandableGroup.dart b/lib/widgets/sliverExpandableGroup.dart new file mode 100644 index 0000000..f8c9452 --- /dev/null +++ b/lib/widgets/sliverExpandableGroup.dart @@ -0,0 +1,91 @@ +part of '../main.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; + + 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, + }) : 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), + ), + ); + return SliverStickyHeader( + header: InkWell( + child: Container( + height: widget.height, + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + ), + 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; + }); + }, + ), + 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..fdeda1f --- /dev/null +++ b/lib/widgets/utils.dart @@ -0,0 +1,30 @@ +part of '../main.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, + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..99dc1cc --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,91 @@ +name: weiman +description: 微漫 + +# 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.0.4 + +environment: + sdk: ">=2.3.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + async: any + cupertino_icons: any + http: any + encrypt: any + html: any + shared_preferences: any + fluttertoast: any + draggable_container: any + flutter_sticky_header: any + extended_nested_scroll_view: any + dynamic_theme: any + package_info: any + url_launcher: any + font_awesome_flutter: any + loading_more_list: any + pull_to_refresh_notification: any + + firebase_core: any + firebase_analytics: any + e2e: any + +dev_dependencies: + flutter_test: + sdk: flutter + + +# 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: + - images/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