├── LICENSE ├── README.md ├── assets └── logo.png ├── lib ├── activities │ ├── book │ │ ├── book.dart │ │ └── tapToSearch.dart │ ├── chapter │ │ ├── activity.dart │ │ ├── chapterTab.dart │ │ ├── drawer.dart │ │ ├── image.dart │ │ ├── viewer.dart │ │ └── viewerSwitcherWidget.dart │ ├── checkDB.dart │ ├── checkData.dart │ ├── dataConvert.dart │ ├── home.dart │ ├── hot.dart │ ├── search │ │ ├── search.dart │ │ ├── source.dart │ │ └── tab.dart │ └── setting │ │ ├── hideStatusBar.dart │ │ ├── setting.dart │ │ └── web.dart ├── classes │ ├── book.dart │ ├── chapter.dart │ ├── chapterContent.dart │ ├── data.dart │ ├── history.dart │ └── networkImageSSL.dart ├── crawler │ └── http.dart ├── db │ ├── book.dart │ ├── book.g.dart │ ├── group.dart │ ├── group.g.dart │ ├── historyOffset.dart │ └── setting.dart ├── main.dart ├── provider │ ├── favoriteData.dart │ └── theme.dart ├── utils.dart └── widgets │ ├── animatedLogo.dart │ ├── book.dart │ ├── bookGroup.dart │ ├── bookSettingDialog.dart │ ├── checkConnect │ └── checkConnect.dart │ ├── dbSourceListWidget.dart │ ├── deleteGroupDialog.dart │ ├── favorites.dart │ ├── groupFormDialog.dart │ ├── histories.dart │ ├── pullToRefreshHeader.dart │ ├── quick.dart │ ├── selectFavoriteBooks.dart │ ├── sliverExpandableGroup.dart │ └── utils.dart └── pubspec.yaml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 nrop19 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 微漫 v1.1.4 [宣传页面](https://nrop19.github.io/weiman_app) 2 | 3 | ### 微漫脱敏后的开源代码 4 | 5 | #### 不解答任何代码上的问题 6 | 7 | #### App的问题请到 [Telegram群](https://t.me/boring_programer) 讨论 8 | 9 | - 删除了android端文件夹,涉及到apk签名等敏感文件 10 | - 删除了ios端文件夹 11 | - 删除了lib/crawler/里的其它文件,保护被爬网站的同时防止被爬网站加大防爬难度。 -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrop19/weiman_app/282e3ff5299325d5b6ee18bd38a08b0e5fea0527/assets/logo.png -------------------------------------------------------------------------------- /lib/activities/book/book.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/scheduler.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; 6 | import 'package:weiman/activities/book/tapToSearch.dart'; 7 | import 'package:weiman/classes/chapter.dart'; 8 | import 'package:weiman/classes/networkImageSSL.dart'; 9 | import 'package:weiman/db/book.dart'; 10 | import 'package:weiman/main.dart'; 11 | import 'package:weiman/provider/favoriteData.dart'; 12 | import 'package:weiman/utils.dart'; 13 | import 'package:weiman/widgets/book.dart'; 14 | import 'package:weiman/widgets/bookSettingDialog.dart'; 15 | import 'package:weiman/widgets/pullToRefreshHeader.dart'; 16 | 17 | class ActivityBook extends StatefulWidget { 18 | final Book book; 19 | final String heroTag; 20 | 21 | ActivityBook({@required this.book, @required this.heroTag}); 22 | 23 | @override 24 | _ActivityBook createState() => _ActivityBook(); 25 | } 26 | 27 | class _ActivityBook extends State { 28 | final GlobalKey _refresh = GlobalKey(); 29 | ScrollController _scrollController; 30 | 31 | bool _reverse = false; 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | widget.book.look = true; 37 | _scrollController = ScrollController(); 38 | print('${widget.book}'); 39 | SchedulerBinding.instance.addPostFrameCallback((timeStamp) { 40 | _refresh.currentState 41 | .show(notificationDragOffset: SliverPullToRefreshHeader.height); 42 | }); 43 | } 44 | 45 | @override 46 | dispose() { 47 | _scrollController.dispose(); 48 | super.dispose(); 49 | } 50 | 51 | Future loadBook() async { 52 | try { 53 | final res = await widget.book.load(); 54 | if (mounted && widget.book.needToSave()) { 55 | await widget.book.save(); 56 | // Provider.of(context, listen: false).loadBooksList(true); 57 | } 58 | if (mounted) setState(() {}); 59 | return res; 60 | } catch (e) { 61 | return false; 62 | } 63 | } 64 | 65 | _openChapter(Chapter chapter) async { 66 | await openChapter(context, widget.book, chapter); 67 | setState(() {}); 68 | } 69 | 70 | favoriteBook() async { 71 | final fav = Provider.of(context, listen: false); 72 | if (widget.book.favorite) { 73 | final sure = await showDialog( 74 | context: context, 75 | builder: (_) => AlertDialog( 76 | title: Text('确认取消收藏?'), 77 | // content: Text('删除这本藏书后,首页的快速导航也会删除这本藏书'), 78 | actions: [ 79 | FlatButton( 80 | child: Text('确认'), 81 | onPressed: () => Navigator.pop(context, true), 82 | ), 83 | RaisedButton( 84 | child: Text('取消'), 85 | onPressed: () => Navigator.pop(context, false), 86 | ), 87 | ], 88 | )); 89 | if (sure == true) { 90 | fav.deleteBook(widget.book); 91 | } 92 | } else { 93 | await fav.addBook(widget.book); 94 | await showBookSettingDialog(context, widget.book); 95 | if (widget.book.needUpdate == true) { 96 | widget.book.status = BookUpdateStatus.no; 97 | } else { 98 | widget.book.status = BookUpdateStatus.not; 99 | } 100 | } 101 | setState(() {}); 102 | } 103 | 104 | List _sort() { 105 | final List list = List.from(widget.book.chapters); 106 | // print('sort ${list.length}'); 107 | if (_reverse) return list.reversed.toList(); 108 | return list; 109 | } 110 | 111 | IndexedWidgetBuilder buildChapters(List chapters) { 112 | IndexedWidgetBuilder builder = (BuildContext context, int index) { 113 | final chapter = chapters[index]; 114 | Widget child = WidgetChapter( 115 | chapter: chapter, 116 | onTap: _openChapter, 117 | read: chapter.cid == widget.book.history?.cid, 118 | ); 119 | if (index < chapters.length - 1) 120 | child = DecoratedBox( 121 | decoration: border, 122 | child: child, 123 | ); 124 | return child; 125 | }; 126 | return builder; 127 | } 128 | 129 | @override 130 | Widget build(BuildContext context) { 131 | Color color = widget.book.favorite ? Colors.red : Colors.white; 132 | IconData icon = 133 | widget.book.favorite ? Icons.favorite : Icons.favorite_border; 134 | final List chapters = _sort(); 135 | final history = []; 136 | if (widget.book.history != null && widget.book.chapters.length > 0) { 137 | final chapter = widget.book.chapters.firstWhere( 138 | (chapter) => chapter.cid == widget.book.history.cid, 139 | orElse: () => null, 140 | ); 141 | if(chapter != null){ 142 | history.add(ListTile(title: Text('阅读历史'))); 143 | history.add(WidgetChapter( 144 | chapter: chapter, 145 | onTap: _openChapter, 146 | read: true, 147 | )); 148 | history.add(ListTile(title: Text('下一章'))); 149 | final nextIndex = widget.book.chapters.indexOf(chapter) + 1; 150 | if (nextIndex < widget.book.chapterCount) { 151 | history.add(WidgetChapter( 152 | chapter: widget.book.chapters[nextIndex], 153 | onTap: _openChapter, 154 | read: false, 155 | )); 156 | } else { 157 | history.add(ListTile(subtitle: Text('没有了'))); 158 | } 159 | } 160 | history.add(SizedBox(height: 20)); 161 | } 162 | history.add( 163 | ListTile( 164 | title: Row( 165 | children: [ 166 | Text('章节列表'), 167 | SizedBox(width: 10), 168 | TextButton( 169 | onPressed: () { 170 | _reverse = !_reverse; 171 | setState(() {}); 172 | }, 173 | child: Text('倒序'), 174 | ), 175 | ], 176 | ), 177 | ), 178 | ); 179 | 180 | return Scaffold( 181 | body: PullToRefreshNotification( 182 | key: _refresh, 183 | onRefresh: loadBook, 184 | maxDragOffset: kToolbarHeight * 2, 185 | child: CustomScrollView( 186 | controller: _scrollController, 187 | slivers: [ 188 | /// 标题栏 189 | SliverAppBar( 190 | floating: true, 191 | pinned: true, 192 | title: Text(widget.book.name), 193 | expandedHeight: 200, 194 | actions: [ 195 | IconButton( 196 | onPressed: favoriteBook, icon: Icon(icon, color: color)) 197 | ], 198 | flexibleSpace: FlexibleSpaceBar( 199 | background: SafeArea( 200 | child: Row( 201 | crossAxisAlignment: CrossAxisAlignment.start, 202 | children: [ 203 | /// 漫画封面 204 | Container( 205 | margin: EdgeInsets.only( 206 | top: 50, left: 20, right: 10, bottom: 20), 207 | height: 160, 208 | child: Hero( 209 | tag: widget.heroTag, 210 | child: ExtendedImage( 211 | width: 100, 212 | image: NetworkImageSSL( 213 | widget.book.http, 214 | widget.book.avatar, 215 | ), 216 | ), 217 | ), 218 | ), 219 | 220 | /// 作者、标签、简介内容 221 | Expanded( 222 | child: Container( 223 | padding: EdgeInsets.only(top: 50, right: 20), 224 | child: ListView( 225 | children: [ 226 | TapToSearchWidget( 227 | leading: '作者', items: widget.book.authors), 228 | TapToSearchWidget( 229 | leading: '标签', items: widget.book.tags), 230 | Container( 231 | margin: EdgeInsets.only(top: 10), 232 | ), 233 | Text( 234 | widget.book.description ?? '', 235 | softWrap: true, 236 | style: 237 | TextStyle(color: Colors.white, height: 1.2), 238 | ), 239 | ], 240 | ), 241 | )), 242 | ], 243 | ), 244 | ), 245 | ), 246 | ), 247 | 248 | PullToRefreshContainer((info) => SliverPullToRefreshHeader( 249 | info: info, 250 | onTap: () => _refresh.currentState.show( 251 | notificationDragOffset: SliverPullToRefreshHeader.height), 252 | )), 253 | 254 | /// 观看历史 255 | SliverToBoxAdapter( 256 | child: Column( 257 | children: history, 258 | crossAxisAlignment: CrossAxisAlignment.start, 259 | ), 260 | ), 261 | 262 | /// 章节列表 263 | SliverList( 264 | delegate: SliverChildBuilderDelegate( 265 | buildChapters(chapters), 266 | childCount: chapters.length, 267 | ), 268 | ), 269 | ], 270 | ), 271 | ), 272 | ); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /lib/activities/book/tapToSearch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:weiman/activities/search/search.dart'; 3 | 4 | class TapToSearchWidget extends StatelessWidget { 5 | final String leading; 6 | final List items; 7 | 8 | const TapToSearchWidget({ 9 | Key key, 10 | this.leading, 11 | this.items, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Row( 17 | crossAxisAlignment: CrossAxisAlignment.start, 18 | children: [ 19 | TextButton( 20 | child: Text('$leading:'), 21 | onPressed: null, 22 | style: ButtonStyle( 23 | foregroundColor: MaterialStateProperty.all(Colors.white), 24 | overlayColor: 25 | MaterialStateProperty.all(Colors.white.withOpacity(0.3)), 26 | visualDensity: VisualDensity.comfortable, 27 | ), 28 | ), 29 | Expanded( 30 | child: Wrap( 31 | spacing: 10, 32 | crossAxisAlignment: WrapCrossAlignment.center, 33 | children: items.map((e) => _Item(string: e)).toList(), 34 | ), 35 | ), 36 | ], 37 | ); 38 | } 39 | } 40 | 41 | class _Item extends StatelessWidget { 42 | final String string; 43 | 44 | const _Item({Key key, @required this.string}) 45 | : assert(string != null), 46 | super(key: key); 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return TextButton.icon( 51 | onPressed: () { 52 | Navigator.push( 53 | context, 54 | MaterialPageRoute( 55 | builder: (_) => ActivitySearch( 56 | search: string, 57 | ))); 58 | }, 59 | icon: Icon(Icons.search, size: 14), 60 | label: Text(string), 61 | style: ButtonStyle( 62 | foregroundColor: MaterialStateProperty.all(Colors.white), 63 | overlayColor: 64 | MaterialStateProperty.all(Colors.white.withOpacity(0.3)), 65 | visualDensity: VisualDensity.comfortable, 66 | ), 67 | ); 68 | return GestureDetector( 69 | onTap: () { 70 | Navigator.push( 71 | context, 72 | MaterialPageRoute( 73 | builder: (_) => ActivitySearch( 74 | search: string, 75 | ))); 76 | }, 77 | child: Text.rich( 78 | TextSpan( 79 | children: [ 80 | TextSpan( 81 | text: string, 82 | style: TextStyle(decoration: TextDecoration.underline)), 83 | WidgetSpan( 84 | child: Icon( 85 | Icons.search, 86 | color: Colors.white, 87 | size: 14, 88 | )), 89 | ], 90 | ), 91 | style: TextStyle( 92 | color: Colors.white, 93 | textBaseline: TextBaseline.ideographic, 94 | ), 95 | ), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/activities/chapter/activity.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; 5 | import 'package:weiman/activities/chapter/chapterTab.dart'; 6 | import 'package:weiman/activities/chapter/drawer.dart'; 7 | import 'package:weiman/classes/chapter.dart'; 8 | import 'package:weiman/db/book.dart'; 9 | import 'package:weiman/db/setting.dart'; 10 | import 'package:weiman/utils.dart'; 11 | 12 | class ActivityChapter extends StatefulWidget { 13 | final Book book; 14 | final Chapter chapter; 15 | 16 | ActivityChapter(this.book, this.chapter); 17 | 18 | @override 19 | _ActivityChapter createState() => _ActivityChapter(); 20 | } 21 | 22 | class _ActivityChapter extends State { 23 | final _scaffoldKey = GlobalKey(); 24 | PageController _pageController; 25 | int showIndex = 0; 26 | bool hasNextImage = true; 27 | 28 | @override 29 | void initState() { 30 | _pageController = PageController( 31 | keepPage: false, 32 | initialPage: widget.book.chapters.indexOf(widget.chapter)); 33 | super.initState(); 34 | saveHistory(widget.chapter); 35 | SchedulerBinding.instance.addPostFrameCallback((timeStamp) { 36 | final hide = Provider.of(context, listen: false).getHideOption(); 37 | if (hide == HideOption.always) { 38 | hideStatusBar(); 39 | } 40 | }); 41 | } 42 | 43 | @override 44 | void dispose() { 45 | _pageController?.dispose(); 46 | showStatusBar(); 47 | super.dispose(); 48 | } 49 | 50 | void pageChanged(int page) { 51 | saveHistory(widget.book.chapters[page]); 52 | } 53 | 54 | void saveHistory(Chapter chapter) async { 55 | await widget.book.setHistory(chapter); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return Consumer(builder: (_, data, __) { 61 | return Scaffold( 62 | key: _scaffoldKey, 63 | endDrawer: ChapterDrawer( 64 | book: widget.book, 65 | onTap: (chapter) { 66 | _pageController.jumpToPage(widget.book.chapters.indexOf(chapter)); 67 | }, 68 | ), 69 | body: PageView.builder( 70 | physics: AlwaysScrollableClampingScrollPhysics(), 71 | controller: _pageController, 72 | itemCount: widget.book.chapters.length, 73 | onPageChanged: pageChanged, 74 | itemBuilder: (ctx, index) { 75 | return ChapterTab( 76 | actions: [ 77 | IconButton( 78 | icon: Icon(Icons.menu), 79 | onPressed: () { 80 | _scaffoldKey.currentState.openEndDrawer(); 81 | }, 82 | ), 83 | ], 84 | book: widget.book, 85 | chapter: widget.book.chapters[index], 86 | ); 87 | }, 88 | ), 89 | ); 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/activities/chapter/chapterTab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | import 'package:loading_more_list/loading_more_list.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:weiman/activities/chapter/image.dart'; 6 | import 'package:weiman/activities/chapter/viewerSwitcherWidget.dart'; 7 | import 'package:weiman/classes/chapter.dart'; 8 | import 'package:weiman/crawler/http18Comic.dart'; 9 | import 'package:weiman/db/book.dart'; 10 | import 'package:weiman/db/setting.dart'; 11 | import 'package:weiman/utils.dart'; 12 | import 'package:weiman/widgets/animatedLogo.dart'; 13 | 14 | class ChapterSourceList extends LoadingMoreBase { 15 | final Book book; 16 | final Chapter chapter; 17 | final Function onFirstLoaded; 18 | 19 | bool firstLoad = true; 20 | bool hasMore = true; 21 | bool isMultiPage = false; 22 | int page = 1; 23 | 24 | ChapterSourceList({ 25 | this.book, 26 | this.chapter, 27 | this.onFirstLoaded, 28 | }); 29 | 30 | @override 31 | Future loadData([bool isloadMoreAction = false]) async { 32 | final chapterContent = await Http18Comic.instance.getChapterContent( 33 | book, 34 | chapter, 35 | page: page, 36 | ); 37 | print(chapterContent.toString()); 38 | hasMore = chapterContent.hasNextPage; 39 | this.addAll(chapterContent.images); 40 | if (firstLoad) { 41 | firstLoad = false; 42 | isMultiPage = hasMore; 43 | } 44 | page++; 45 | return true; 46 | } 47 | 48 | @override 49 | Future refresh([bool notifyStateChanged = false]) { 50 | firstLoad = true; 51 | hasMore = true; 52 | page = 1; 53 | return super.refresh(notifyStateChanged); 54 | } 55 | } 56 | 57 | class ChapterTab extends StatefulWidget { 58 | final Book book; 59 | final Chapter chapter; 60 | final List actions; 61 | 62 | const ChapterTab({Key key, this.book, this.chapter, this.actions}) 63 | : super(key: key); 64 | 65 | @override 66 | _State createState() => _State(); 67 | } 68 | 69 | class _State extends State { 70 | ChapterSourceList sourceList; 71 | ScrollController scrollController; 72 | 73 | @override 74 | initState() { 75 | scrollController = ScrollController(); 76 | sourceList = ChapterSourceList( 77 | book: widget.book, 78 | chapter: widget.chapter, 79 | ); 80 | widget.book.setHistory(widget.chapter); 81 | super.initState(); 82 | 83 | // 隐藏/显示 状态栏 84 | final setting = Provider.of(context, listen: false); 85 | final hide = setting.getHideOption(); 86 | if (hide == HideOption.auto) { 87 | scrollController.addListener(() { 88 | final isUp = scrollController.position.userScrollDirection == 89 | ScrollDirection.forward; 90 | if (isUp) 91 | showStatusBar(); 92 | else 93 | hideStatusBar(); 94 | }); 95 | } 96 | } 97 | 98 | @override 99 | dispose() { 100 | widget.book.setHistory(widget.chapter); 101 | scrollController?.dispose(); 102 | super.dispose(); 103 | } 104 | 105 | Widget imageBuilder(ctx, String image, int index) { 106 | index += 1; 107 | bool reDraw = false; 108 | try { 109 | int cid = int.parse(widget.chapter.cid); 110 | reDraw = cid >= 220980; 111 | // print('创建图片 cid $cid, reDraw $reDraw'); 112 | } catch (e) {} 113 | return ImageWidget( 114 | image: image, 115 | index: index, 116 | total: sourceList.length, 117 | reSort: reDraw, 118 | ); 119 | } 120 | 121 | Widget indicatorBuilder(context, IndicatorStatus status) { 122 | print('indicatorBuilder $status'); 123 | bool isSliver = true; 124 | Widget widget; 125 | switch (status) { 126 | case IndicatorStatus.none: 127 | widget = SizedBox(); 128 | break; 129 | case IndicatorStatus.loadingMoreBusying: 130 | widget = Row( 131 | mainAxisAlignment: MainAxisAlignment.center, 132 | crossAxisAlignment: CrossAxisAlignment.center, 133 | children: [ 134 | AnimatedLogoWidget(width: 20, height: 30), 135 | SizedBox(width: 10), 136 | Text("正在读取") 137 | ], 138 | ); 139 | widget = Container( 140 | width: double.infinity, 141 | height: kToolbarHeight, 142 | child: widget, 143 | alignment: Alignment.center, 144 | ); 145 | break; 146 | case IndicatorStatus.fullScreenBusying: 147 | widget = Center( 148 | child: Row( 149 | mainAxisSize: MainAxisSize.min, 150 | children: [ 151 | AnimatedLogoWidget(width: 25, height: 30), 152 | Text('读取中'), 153 | ], 154 | ), 155 | ); 156 | if (isSliver) { 157 | widget = SliverFillRemaining( 158 | child: widget, 159 | ); 160 | } 161 | break; 162 | case IndicatorStatus.error: 163 | case IndicatorStatus.fullScreenError: 164 | widget = Column( 165 | mainAxisSize: MainAxisSize.min, 166 | children: [ 167 | Text( 168 | '读取失败\n你可能需要用梯子', 169 | textAlign: TextAlign.center, 170 | ), 171 | RaisedButton( 172 | child: Text('再次重试'), 173 | onPressed: sourceList.errorRefresh, 174 | ) 175 | ], 176 | ); 177 | widget = Container( 178 | width: double.infinity, 179 | height: kToolbarHeight, 180 | child: widget, 181 | alignment: Alignment.center, 182 | ); 183 | if (status == IndicatorStatus.fullScreenError) { 184 | if (isSliver) { 185 | widget = SliverFillRemaining( 186 | child: widget, 187 | ); 188 | } else { 189 | widget = CustomScrollView( 190 | slivers: [ 191 | SliverFillRemaining( 192 | child: widget, 193 | ) 194 | ], 195 | ); 196 | } 197 | } 198 | break; 199 | case IndicatorStatus.noMoreLoad: 200 | widget = SizedBox(); 201 | break; 202 | case IndicatorStatus.empty: 203 | widget = Text( 204 | '没有图片', 205 | ); 206 | widget = Container( 207 | width: double.infinity, 208 | height: kToolbarHeight, 209 | child: widget, 210 | alignment: Alignment.center, 211 | ); 212 | if (isSliver) { 213 | widget = SliverToBoxAdapter( 214 | child: widget, 215 | ); 216 | } else { 217 | widget = CustomScrollView( 218 | slivers: [ 219 | SliverFillRemaining( 220 | child: widget, 221 | ) 222 | ], 223 | ); 224 | } 225 | break; 226 | } 227 | return widget; 228 | } 229 | 230 | @override 231 | Widget build(BuildContext context) { 232 | return CustomScrollView( 233 | controller: scrollController, 234 | slivers: [ 235 | SliverAppBar( 236 | snap: true, 237 | floating: true, 238 | title: Text(widget.chapter.cname), 239 | actions: [ 240 | ViewerSwitcherWidget(), 241 | IconButton( 242 | icon: Icon(Icons.vertical_align_top), 243 | onPressed: () => scrollController.jumpTo(0.0), 244 | ), 245 | ...widget.actions, 246 | ], 247 | ), 248 | LoadingMoreSliverList( 249 | SliverListConfig( 250 | sourceList: sourceList, 251 | itemBuilder: imageBuilder, 252 | addSemanticIndexes: true, 253 | semanticIndexOffset: 10, 254 | autoLoadMore: true, 255 | indicatorBuilder: indicatorBuilder, 256 | ), 257 | ), 258 | ], 259 | ); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /lib/activities/chapter/drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:weiman/classes/chapter.dart'; 4 | import 'package:weiman/db/book.dart'; 5 | import 'package:weiman/widgets/book.dart'; 6 | 7 | class ChapterDrawer extends StatefulWidget { 8 | final Book book; 9 | final void Function(Chapter chapter) onTap; 10 | 11 | const ChapterDrawer({ 12 | Key key, 13 | @required this.book, 14 | @required this.onTap, 15 | }) : super(key: key); 16 | 17 | @override 18 | _ChapterDrawer createState() => _ChapterDrawer(); 19 | } 20 | 21 | class _ChapterDrawer extends State { 22 | ScrollController _controller; 23 | int read; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | updateRead(); 29 | _controller = 30 | ScrollController(initialScrollOffset: WidgetChapter.height * read); 31 | } 32 | 33 | @override 34 | void dispose() { 35 | _controller.dispose(); 36 | super.dispose(); 37 | } 38 | 39 | void updateRead() { 40 | final readChapter = widget.book.chapters 41 | .firstWhere((chapter) => widget.book.history?.cid == chapter.cid); 42 | read = widget.book.chapters.indexOf(readChapter); 43 | } 44 | 45 | void scrollToRead() { 46 | setState(() { 47 | updateRead(); 48 | }); 49 | _controller.animateTo( 50 | WidgetChapter.height * read, 51 | duration: Duration(milliseconds: 200), 52 | curve: Curves.linear, 53 | ); 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return Drawer( 59 | child: SafeArea( 60 | child: ListView( 61 | controller: _controller, 62 | children: ListTile.divideTiles( 63 | context: context, 64 | tiles: widget.book.chapters.map((chapter) { 65 | final isRead = widget.book.history?.cid == chapter.cid; 66 | return WidgetChapter( 67 | chapter: chapter, 68 | onTap: (chapter) { 69 | if (widget.onTap != null) widget.onTap(chapter); 70 | SchedulerBinding.instance.addPostFrameCallback((_) { 71 | scrollToRead(); 72 | }); 73 | }, 74 | read: isRead, 75 | ); 76 | }), 77 | ).toList(), 78 | ), 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/activities/chapter/image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:extended_image/extended_image.dart'; 4 | import 'package:flutter/material.dart' hide Image; 5 | import 'package:provider/provider.dart'; 6 | import 'package:sticky_headers/sticky_headers/widget.dart'; 7 | import 'package:weiman/activities/chapter/viewer.dart'; 8 | import 'package:weiman/classes/networkImageSSL.dart'; 9 | import 'package:weiman/crawler/http18Comic.dart'; 10 | import 'package:weiman/db/setting.dart'; 11 | 12 | class ImageWidget extends StatefulWidget { 13 | final int index; 14 | final int total; 15 | final String image; 16 | final bool reSort; 17 | 18 | const ImageWidget({ 19 | Key key, 20 | this.image, 21 | this.index, 22 | this.total, 23 | this.reSort = false, 24 | }) : super(key: key); 25 | 26 | @override 27 | State createState() => _State(); 28 | } 29 | 30 | class _State extends State { 31 | static TextStyle _style = TextStyle(color: Colors.white); 32 | static BoxDecoration _decoration = 33 | BoxDecoration(color: Colors.black.withOpacity(0.4)); 34 | 35 | String get tag { 36 | return 'image_viewer_${widget.index}'; 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return StickyHeader( 42 | overlapHeaders: true, 43 | header: SafeArea( 44 | top: true, 45 | bottom: false, 46 | child: Row( 47 | children: [ 48 | Container( 49 | padding: EdgeInsets.all(5), 50 | decoration: _decoration, 51 | child: Text( 52 | '${widget.index} / ${widget.total}', 53 | style: _style, 54 | ), 55 | ), 56 | ], 57 | ), 58 | ), 59 | content: ExtendedImage( 60 | image: NetworkImageSSL( 61 | Http18Comic.instance, 62 | widget.image, 63 | reSort: widget.reSort, 64 | ), 65 | loadStateChanged: (ExtendedImageState state) { 66 | Widget widget; 67 | switch (state.extendedImageLoadState) { 68 | case LoadState.loading: 69 | widget = SizedBox( 70 | height: 300, 71 | child: Center( 72 | child: CircularProgressIndicator(), 73 | ), 74 | ); 75 | break; 76 | case LoadState.completed: 77 | widget = GestureDetector( 78 | child: Hero( 79 | child: 80 | ExtendedRawImage(image: state.extendedImageInfo?.image), 81 | tag: tag, 82 | ), 83 | onTap: () => onTap(context), 84 | ); 85 | break; 86 | default: 87 | } 88 | return widget; 89 | }, 90 | ), 91 | ); 92 | } 93 | 94 | onTap(BuildContext context) { 95 | final viewerSwitch = 96 | Provider.of(context, listen: false).getViewerSwitch(); 97 | // print('viewer $viewerSwitch'); 98 | if (!viewerSwitch) return; 99 | Navigator.push( 100 | context, 101 | TransparentMaterialPageRoute( 102 | builder: (_) => ActivityImageViewer( 103 | url: this.widget.image, 104 | heroTag: tag, 105 | reSort: widget.reSort, 106 | ), 107 | ), 108 | ); 109 | } 110 | } -------------------------------------------------------------------------------- /lib/activities/chapter/viewer.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:weiman/classes/networkImageSSL.dart'; 4 | import 'package:weiman/crawler/http18Comic.dart'; 5 | 6 | class ActivityImageViewer extends StatefulWidget { 7 | final String url; 8 | final String heroTag; 9 | final bool reSort; 10 | 11 | const ActivityImageViewer({ 12 | Key key, 13 | this.url, 14 | this.heroTag, 15 | this.reSort = false, 16 | }) : super(key: key); 17 | 18 | @override 19 | _State createState() => _State(); 20 | } 21 | 22 | class _State extends State { 23 | double currentScale = 1.0; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return ExtendedImageSlidePage( 28 | slideAxis: SlideAxis.both, 29 | slideType: SlideType.onlyImage, 30 | child: Material( 31 | color: Colors.transparent, 32 | shadowColor: Colors.transparent, 33 | child: Stack( 34 | fit: StackFit.expand, 35 | children: [ 36 | GestureDetector( 37 | onTap: () { 38 | Navigator.pop(context); 39 | }, 40 | child: ExtendedImage( 41 | image: NetworkImageSSL( 42 | Http18Comic.instance, 43 | widget.url, 44 | reSort: widget.reSort, 45 | ), 46 | enableSlideOutPage: true, 47 | mode: ExtendedImageMode.gesture, 48 | onDoubleTap: (status) { 49 | currentScale = currentScale == 1 ? 3 : 1; 50 | status.handleDoubleTap(scale: currentScale); 51 | }, 52 | heroBuilderForSlidingPage: (child) { 53 | return Hero( 54 | child: child, 55 | tag: widget.heroTag, 56 | ); 57 | }, 58 | ), 59 | ), 60 | ], 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/activities/chapter/viewerSwitcherWidget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:weiman/db/setting.dart'; 4 | 5 | class ViewerSwitcherWidget extends StatefulWidget { 6 | @override 7 | ViewerSwitcherState createState() => ViewerSwitcherState(); 8 | } 9 | 10 | class ViewerSwitcherState extends State { 11 | @override 12 | Widget build(BuildContext context) { 13 | return Consumer(builder: (_, data, __) { 14 | final icon = data.getViewerSwitch() 15 | ? Icons.check_box_outlined 16 | : Icons.check_box_outline_blank; 17 | return 18 | TextButton.icon( 19 | icon: Icon(icon), 20 | label: Text('看图'), 21 | style: ButtonStyle( 22 | foregroundColor: MaterialStateProperty.all(Colors.white), 23 | overlayColor: 24 | MaterialStateProperty.all(Colors.white.withOpacity(0.3)), 25 | visualDensity: VisualDensity.compact, 26 | ), 27 | onPressed: () { 28 | data.setViewerSwitch(!data.getViewerSwitch()); 29 | setState(() {}); 30 | }, 31 | ); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/activities/checkDB.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:weiman/db/book.dart'; 3 | 4 | class ActivityCheckDB extends StatefulWidget { 5 | @override 6 | _State createState() => _State(); 7 | } 8 | 9 | enum CheckState { 10 | Uncheck, 11 | Pass, 12 | Fail, 13 | } 14 | 15 | class _State extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | appBar: AppBar( 20 | title: Text('收藏数据检修'), 21 | ), 22 | body: ListView(children: [ 23 | ListTile( 24 | title: Text('所有藏书章节数量归零'), 25 | onTap: () async { 26 | for (final book in Book.bookBox.values) { 27 | book.chapterCount = 0; 28 | await book.save(); 29 | } 30 | }, 31 | ), 32 | ListTile( 33 | title: Text('清空漫画数据'), 34 | subtitle: Text('有 ${Book.bookBox.length} 本'), 35 | onTap: () async { 36 | await Book.bookBox.clear(); 37 | }, 38 | ), 39 | ]), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/activities/checkData.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:oktoast/oktoast.dart'; 6 | 7 | import 'package:weiman/classes/data.dart'; 8 | 9 | class ActivityCheckData extends StatefulWidget { 10 | @override 11 | _State createState() => _State(); 12 | } 13 | 14 | enum CheckState { 15 | Uncheck, 16 | Pass, 17 | Fail, 18 | } 19 | 20 | final titleTextStyle = TextStyle(fontSize: 14, color: Colors.blue), 21 | passStyle = TextStyle(color: Colors.green), 22 | failStyle = TextStyle(color: Colors.red); 23 | 24 | class _State extends State { 25 | CheckState firstState; 26 | int firstLength = 0; 27 | final TextSpan secondResults = TextSpan(); 28 | TextEditingController _outputController, _inputController; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _outputController = TextEditingController(); 34 | _inputController = TextEditingController(); 35 | } 36 | 37 | TextSpan first() { 38 | String text; 39 | switch (firstState) { 40 | case CheckState.Pass: 41 | text = '有数据, 一共 $firstLength 本收藏'; 42 | break; 43 | case CheckState.Fail: 44 | text = '没有收藏数据'; 45 | break; 46 | default: 47 | text = '未检查'; 48 | } 49 | return TextSpan( 50 | text: text, 51 | style: firstState == CheckState.Pass ? passStyle : failStyle); 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | final firstChildren = [ 57 | Text('检查漫画收藏列表'), 58 | RaisedButton( 59 | child: Text('检查'), 60 | color: Colors.blue, 61 | textColor: Colors.white, 62 | onPressed: () { 63 | final has = Data.has(Data.favoriteBooksKey); 64 | if (has) { 65 | final String str = Data.instance.getString(Data.favoriteBooksKey); 66 | final Map map = jsonDecode(str); 67 | firstLength = map.keys.length; 68 | _outputController.text = str; 69 | } 70 | firstState = firstLength > 0 ? CheckState.Pass : CheckState.Fail; 71 | 72 | setState(() {}); 73 | }, 74 | ), 75 | RichText( 76 | text: TextSpan( 77 | text: '结果:', 78 | children: [first()], 79 | style: TextStyle(color: Colors.black)), 80 | ), 81 | ]; 82 | if (firstState == CheckState.Pass) { 83 | firstChildren.add(Text('点击复制')); 84 | firstChildren.add(TextField( 85 | maxLines: 8, 86 | controller: _outputController, 87 | onTap: () { 88 | showToast('已经复制'); 89 | Clipboard.setData(ClipboardData(text: _outputController.text)); 90 | }, 91 | )); 92 | } 93 | return Scaffold( 94 | appBar: AppBar( 95 | title: Text('收藏数据检修'), 96 | ), 97 | body: ListView(children: [ 98 | Card( 99 | child: Padding( 100 | padding: EdgeInsets.all(5), 101 | child: Container( 102 | child: Column( 103 | crossAxisAlignment: CrossAxisAlignment.start, 104 | children: firstChildren, 105 | ), 106 | ), 107 | ), 108 | ), 109 | Card( 110 | child: Padding( 111 | padding: EdgeInsets.all(5), 112 | child: Column( 113 | crossAxisAlignment: CrossAxisAlignment.start, 114 | children: [ 115 | Text('导入收藏数据'), 116 | TextField( 117 | controller: _inputController, 118 | maxLines: 8, 119 | ), 120 | RaisedButton( 121 | child: Text('导入'), 122 | onPressed: () { 123 | if (_inputController.text.length > 0) { 124 | Data.instance.setString( 125 | Data.favoriteBooksKey, _inputController.text); 126 | } 127 | }, 128 | ), 129 | ], 130 | ), 131 | ), 132 | ), 133 | ]), 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/activities/dataConvert.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:oktoast/oktoast.dart'; 3 | 4 | import 'package:weiman/classes/book.dart'; 5 | import 'package:weiman/classes/data.dart'; 6 | import 'package:weiman/db/book.dart' as newBook; 7 | import 'package:weiman/main.dart'; 8 | import 'home.dart'; 9 | 10 | class ActivityDataConvert extends StatefulWidget { 11 | @override 12 | _State createState() => _State(); 13 | } 14 | 15 | class _State extends State { 16 | List quick; 17 | Map favorites; 18 | bool selectQ = true, selectH = true; 19 | 20 | @override 21 | void initState() { 22 | analytics.setCurrentScreen(screenName: '/activity_data_convert'); 23 | favorites = Data.getFavorites(); 24 | quick = Data.quickList(); 25 | super.initState(); 26 | } 27 | 28 | Future convert() async { 29 | int quickIndex = 0; 30 | int skip = 0; 31 | final awaitList = []; 32 | favorites.keys.forEach((id) { 33 | if (newBook.Book.bookBox.containsKey(id)) return; 34 | final oldBook = favorites[id]; 35 | final isQuick = selectQ && quick.contains(oldBook.aid); 36 | final book = new newBook.Book( 37 | httpId: null, 38 | aid: oldBook.aid, 39 | name: oldBook.name, 40 | avatar: oldBook.avatar, 41 | description: oldBook.description, 42 | authors: [oldBook.author], 43 | chapterCount: oldBook.chapterCount, 44 | quick: isQuick ? quickIndex : null, 45 | needUpdate: true, 46 | favorite: true, 47 | history: null, 48 | ); 49 | if (isQuick) quickIndex++; 50 | awaitList.add(book.save()); 51 | }); 52 | await Future.wait(awaitList); 53 | showToast( 54 | '成功转存 ${awaitList.length} 本小说\n跳过了 $skip 本', 55 | textPadding: EdgeInsets.all(10), 56 | ); 57 | } 58 | 59 | Future clean() async { 60 | await Data.instance.remove(Data.favoriteBooksKey); 61 | await Data.instance.remove(Data.quickKey); 62 | await Data.instance.remove(Data.viewHistoryKey); 63 | } 64 | 65 | void gotoHome() { 66 | Navigator.pushReplacement( 67 | context, 68 | MaterialPageRoute( 69 | builder: (_) => ActivityHome(), 70 | ), 71 | ); 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return Scaffold( 77 | appBar: AppBar( 78 | title: Text('旧数据转存'), 79 | ), 80 | body: ListView(children: [ 81 | ListTile( 82 | title: Text('从v1.1.2开始,为了实现藏书分组功能,使用了新的数据存储方式' 83 | '\n【旧书】打开后直接搜索同名漫画。' 84 | '\n清空旧数据后这个界面不会再次出现。' 85 | '\n需要将旧的藏书数据转存为新数据吗?' 86 | '\n旧藏书不多的话,我个人建议直接清空,可以防止产生数据干扰')), 87 | ListTile( 88 | title: Text('收藏列表'), 89 | subtitle: Text('一共有 ${favorites.length} 本'), 90 | trailing: Checkbox( 91 | value: true, 92 | onChanged: null, 93 | ), 94 | ), 95 | ListTile( 96 | title: Text('快速导航'), 97 | subtitle: Text('一共有 ${quick.length} 本'), 98 | trailing: Checkbox( 99 | value: selectQ, 100 | onChanged: (value) { 101 | setState(() { 102 | selectQ = value; 103 | }); 104 | }, 105 | ), 106 | ), 107 | ]), 108 | bottomNavigationBar: Row(children: [ 109 | SizedBox(width: 10), 110 | Expanded( 111 | child: OutlineButton( 112 | child: Text('直接清空旧数据'), 113 | onPressed: () async { 114 | await clean(); 115 | gotoHome(); 116 | }, 117 | ), 118 | ), 119 | SizedBox(width: 10), 120 | Expanded( 121 | child: OutlineButton( 122 | child: Text('转存并清空旧数据'), 123 | onPressed: () async { 124 | await convert(); 125 | await clean(); 126 | gotoHome(); 127 | }, 128 | ), 129 | ), 130 | SizedBox(width: 10), 131 | ]), 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/activities/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 4 | import 'package:oktoast/oktoast.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:url_launcher/url_launcher.dart'; 7 | import 'package:weiman/activities/dataConvert.dart'; 8 | import 'package:weiman/db/setting.dart'; 9 | import 'package:weiman/provider/theme.dart'; 10 | 11 | import 'package:weiman/activities/checkData.dart'; 12 | import 'package:weiman/activities/hot.dart'; 13 | import 'package:weiman/activities/search/search.dart'; 14 | import 'package:weiman/activities/test2.dart'; 15 | import 'package:weiman/classes/book.dart'; 16 | import 'package:weiman/main.dart'; 17 | import 'package:weiman/provider/favoriteData.dart'; 18 | import 'package:weiman/widgets/checkConnect/checkConnect.dart'; 19 | import 'package:weiman/widgets/favorites.dart'; 20 | import 'package:weiman/widgets/histories.dart'; 21 | import 'package:weiman/widgets/quick.dart'; 22 | import 'checkDB.dart'; 23 | import 'setting/setting.dart'; 24 | 25 | class ActivityHome extends StatefulWidget { 26 | @override 27 | State createState() => HomeState(); 28 | } 29 | 30 | class HomeState extends State { 31 | final _scaffoldKey = GlobalKey(); 32 | final List histories = []; 33 | final List quick = []; 34 | final GlobalKey _quickState = GlobalKey(); 35 | 36 | bool showFavorite = true; 37 | 38 | @override 39 | void initState() { 40 | super.initState(); 41 | analytics.setCurrentScreen(screenName: '/activity_home'); 42 | 43 | /// 提前检查一次藏书的更新情况 44 | SchedulerBinding.instance.addPostFrameCallback((_) async { 45 | autoSwitchTheme(); 46 | FavoriteData favData = Provider.of(context, listen: false); 47 | await favData.loadBooksList(); 48 | final updated = await favData.checkUpdate(); 49 | if (updated > 0) 50 | showToast( 51 | '$updated 本藏书有更新', 52 | textPadding: EdgeInsets.all(10), 53 | ); 54 | }); 55 | } 56 | 57 | void autoSwitchTheme() async {} 58 | 59 | void gotoSearch() { 60 | Navigator.push( 61 | context, 62 | MaterialPageRoute( 63 | settings: RouteSettings(name: '/activity_search'), 64 | builder: (context) => ActivitySearch())); 65 | } 66 | 67 | void gotoRecommend() { 68 | Navigator.push( 69 | context, 70 | MaterialPageRoute( 71 | settings: RouteSettings(name: '/activity_recommend'), 72 | builder: (_) => ActivityRank(), 73 | )); 74 | } 75 | 76 | void gotoPatreon() { 77 | launch('https://www.patreon.com/nrop19'); 78 | } 79 | 80 | bool isEdit = false; 81 | 82 | void _draggableModeChanged(bool mode) { 83 | print('mode changed $mode'); 84 | isEdit = mode; 85 | setState(() {}); 86 | } 87 | 88 | Widget themeButton() { 89 | final system = FontAwesomeIcons.cloudSun, 90 | light = FontAwesomeIcons.solidSun, 91 | dark = FontAwesomeIcons.solidMoon; 92 | final theme = Provider.of(context, listen: false); 93 | Widget themeIcon; 94 | switch (theme.themeMode) { 95 | case ThemeMode.light: 96 | themeIcon = Icon(light); 97 | break; 98 | case ThemeMode.dark: 99 | themeIcon = Icon(dark); 100 | break; 101 | default: 102 | themeIcon = Icon(system); 103 | break; 104 | } 105 | return IconButton( 106 | onPressed: () { 107 | switch (theme.themeMode) { 108 | case ThemeMode.light: 109 | theme.changeTheme(ThemeMode.dark); 110 | break; 111 | case ThemeMode.dark: 112 | theme.changeTheme(ThemeMode.system); 113 | break; 114 | default: 115 | theme.changeTheme(ThemeMode.light); 116 | } 117 | Provider.of(context, listen: false) 118 | .setThemeMode(theme.themeMode); 119 | showToastWidget( 120 | Container( 121 | padding: EdgeInsets.all(10), 122 | color: Colors.black.withOpacity(0.7), 123 | child: Column( 124 | mainAxisSize: MainAxisSize.min, 125 | crossAxisAlignment: CrossAxisAlignment.start, 126 | children: [ 127 | Row(mainAxisSize: MainAxisSize.min, children: [ 128 | Icon( 129 | system, 130 | size: 14, 131 | color: Colors.white, 132 | ), 133 | SizedBox(width: 10), 134 | Text('跟随系统,自动切换明暗模式\n如果系统不支持,默认为明亮模式'), 135 | ]), 136 | SizedBox(height: 10), 137 | Row(mainAxisSize: MainAxisSize.min, children: [ 138 | Icon( 139 | light, 140 | size: 14, 141 | color: Colors.white, 142 | ), 143 | SizedBox(width: 10), 144 | Text('为明亮模式'), 145 | ]), 146 | SizedBox(height: 10), 147 | Row(mainAxisSize: MainAxisSize.min, children: [ 148 | Icon( 149 | dark, 150 | size: 14, 151 | color: Colors.white, 152 | ), 153 | SizedBox(width: 10), 154 | Text('为暗黑模式'), 155 | ]), 156 | ], 157 | ), 158 | ), 159 | dismissOtherToast: true, 160 | duration: Duration(seconds: 4), 161 | ); 162 | }, 163 | icon: themeIcon, 164 | ); 165 | } 166 | 167 | @override 168 | Widget build(BuildContext context) { 169 | final media = MediaQuery.of(context); 170 | final width = (media.size.width * 0.8).roundToDouble(); 171 | return Scaffold( 172 | key: _scaffoldKey, 173 | appBar: AppBar( 174 | title: Text('微漫 v' + packageInfo.version), 175 | automaticallyImplyLeading: false, 176 | leading: isEdit 177 | ? IconButton( 178 | icon: Icon(Icons.arrow_back_ios), 179 | onPressed: () { 180 | _quickState.currentState.exit(); 181 | }, 182 | ) 183 | : null, 184 | actions: [ 185 | /// 黑白样式切换 186 | themeButton(), 187 | SizedBox(width: 20), 188 | 189 | /// 设置界面 190 | IconButton( 191 | onPressed: () { 192 | Navigator.push( 193 | context, 194 | MaterialPageRoute( 195 | settings: RouteSettings(name: '/activity_setting'), 196 | builder: (_) => ActivitySetting())); 197 | }, 198 | icon: Icon(FontAwesomeIcons.cog), 199 | ), 200 | 201 | /// 收藏列表 202 | IconButton( 203 | onPressed: () { 204 | showFavorite = true; 205 | _scaffoldKey.currentState.openEndDrawer(); 206 | }, 207 | icon: Icon( 208 | Icons.favorite, 209 | color: Colors.red, 210 | ), 211 | ), 212 | 213 | /// 浏览历史列表 214 | IconButton( 215 | onPressed: () { 216 | showFavorite = false; 217 | // getHistory(); 218 | _scaffoldKey.currentState.openEndDrawer(); 219 | }, 220 | icon: Icon(Icons.history), 221 | ), 222 | ], 223 | ), 224 | drawerEnableOpenDragGesture: false, 225 | endDrawerEnableOpenDragGesture: false, 226 | endDrawer: Drawer( 227 | child: LayoutBuilder( 228 | builder: (_, constraints) { 229 | if (showFavorite) { 230 | return FavoriteList(); 231 | } else { 232 | return Histories(); 233 | } 234 | }, 235 | ), 236 | ), 237 | body: Center( 238 | child: SingleChildScrollView( 239 | padding: EdgeInsets.only(left: 40, right: 40), 240 | child: Column( 241 | mainAxisAlignment: MainAxisAlignment.center, 242 | crossAxisAlignment: CrossAxisAlignment.center, 243 | mainAxisSize: MainAxisSize.max, 244 | children: [ 245 | Container( 246 | child: OutlineButton( 247 | onPressed: gotoSearch, 248 | child: Row( 249 | mainAxisAlignment: MainAxisAlignment.center, 250 | children: [ 251 | Icon( 252 | Icons.search, 253 | color: Colors.blue, 254 | ), 255 | Text( 256 | '搜索漫画', 257 | style: TextStyle(color: Colors.blue), 258 | ) 259 | ], 260 | ), 261 | borderSide: BorderSide(color: Colors.blue, width: 2), 262 | shape: StadiumBorder(), 263 | ), 264 | ), 265 | Row( 266 | children: [ 267 | Expanded( 268 | flex: 7, 269 | child: OutlineButton( 270 | onPressed: gotoRecommend, 271 | child: Row( 272 | mainAxisAlignment: MainAxisAlignment.center, 273 | children: [ 274 | Icon( 275 | Icons.whatshot, 276 | color: Colors.red, 277 | ), 278 | Text( 279 | '热门漫画', 280 | style: TextStyle(color: Colors.red), 281 | ) 282 | ], 283 | ), 284 | borderSide: BorderSide(color: Colors.red, width: 2), 285 | shape: StadiumBorder(), 286 | ), 287 | ), 288 | ], 289 | ), 290 | Center( 291 | child: Quick( 292 | key: _quickState, 293 | width: width, 294 | draggableModeChanged: _draggableModeChanged, 295 | ), 296 | ), 297 | CheckConnectWidget(), 298 | Row( 299 | mainAxisAlignment: MainAxisAlignment.center, 300 | children: [ 301 | GestureDetector( 302 | onTap: () async { 303 | launch('https://bbs.level-plus.net/'); 304 | }, 305 | child: Text( 306 | '魂+论坛首发', 307 | textAlign: TextAlign.center, 308 | style: TextStyle( 309 | color: Colors.blue[200], 310 | decoration: TextDecoration.underline, 311 | ), 312 | ), 313 | ), 314 | SizedBox(width: 20), 315 | GestureDetector( 316 | onTap: () async { 317 | if (await canLaunch('tg://resolve?domain=weiman_app')) 318 | launch('tg://resolve?domain=weiman_app'); 319 | else 320 | launch('https://t.me/weiman_app'); 321 | }, 322 | child: Text( 323 | 'Telegram 广播频道', 324 | textAlign: TextAlign.center, 325 | style: TextStyle( 326 | color: Colors.blue[200], 327 | decoration: TextDecoration.underline, 328 | ), 329 | ), 330 | ), 331 | ], 332 | ), 333 | Visibility( 334 | visible: isDevMode, 335 | child: FlatButton( 336 | onPressed: () { 337 | Navigator.push(context, 338 | MaterialPageRoute(builder: (_) => ActivityCheckData())); 339 | }, 340 | child: Text('操作 收藏列表数据'), 341 | ), 342 | ), 343 | Visibility( 344 | visible: isDevMode, 345 | child: FlatButton( 346 | onPressed: () { 347 | Navigator.push(context, 348 | MaterialPageRoute(builder: (_) => ActivityCheckDB())); 349 | }, 350 | child: Text('操作 DB数据'), 351 | ), 352 | ), 353 | Visibility( 354 | visible: isDevMode, 355 | child: FlatButton( 356 | onPressed: () { 357 | Navigator.push( 358 | context, 359 | MaterialPageRoute( 360 | builder: (_) => ActivityDataConvert())); 361 | }, 362 | child: Text('进入旧数据处理功能'), 363 | ), 364 | ), 365 | ], 366 | ), 367 | ), 368 | ), 369 | floatingActionButton: isDevMode 370 | ? FloatingActionButton( 371 | child: Text('测试'), 372 | onPressed: () { 373 | Navigator.push( 374 | context, MaterialPageRoute(builder: (_) => ActivityTest())); 375 | }, 376 | ) 377 | : null, 378 | ); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /lib/activities/hot.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:loading_more_list/loading_more_list.dart'; 3 | import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; 4 | import 'package:weiman/widgets/animatedLogo.dart'; 5 | 6 | import 'package:weiman/crawler/http.dart'; 7 | import 'package:weiman/crawler/http18Comic.dart'; 8 | import 'package:weiman/db/book.dart'; 9 | import 'package:weiman/widgets/book.dart'; 10 | import 'package:weiman/widgets/pullToRefreshHeader.dart'; 11 | 12 | class ActivityRank extends StatefulWidget { 13 | @override 14 | _ActivityRank createState() => _ActivityRank(); 15 | } 16 | 17 | class _ActivityRank extends State 18 | with SingleTickerProviderStateMixin { 19 | TabController controller; 20 | 21 | @override 22 | void initState() { 23 | controller = TabController(length: 2, vsync: this); 24 | super.initState(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Scaffold( 30 | appBar: AppBar( 31 | title: Text('热门漫画'), 32 | bottom: TabBar(controller: controller, tabs: [ 33 | Tab(text: '韩漫'), 34 | Tab(text: '全部'), 35 | ]), 36 | ), 37 | body: TabBarView(controller: controller, children: [ 38 | HotTab(http: Http18Comic.instance, type: '/hanman'), 39 | HotTab(http: Http18Comic.instance, type: ''), 40 | ]), 41 | ); 42 | } 43 | } 44 | 45 | class SourceList extends LoadingMoreBase { 46 | final String type; 47 | final HttpBook http; 48 | int page = 1; 49 | String firstBookId; 50 | 51 | bool hasMore = true; 52 | 53 | SourceList({this.type, this.http}); 54 | 55 | @override 56 | Future loadData([bool isloadMoreAction = false]) async { 57 | try { 58 | final books = await http.hotBooks(type, page); 59 | if (books.isEmpty) { 60 | hasMore = false; 61 | } else { 62 | if (firstBookId == books[0].aid) { 63 | hasMore = false; 64 | } else { 65 | firstBookId = books[0].aid; 66 | page++; 67 | this.addAll(books); 68 | } 69 | } 70 | return true; 71 | } catch (e) { 72 | return false; 73 | } 74 | } 75 | 76 | @override 77 | Future refresh([bool notifyStateChanged = false]) { 78 | hasMore = true; 79 | page = 1; 80 | return super.refresh(notifyStateChanged); 81 | } 82 | } 83 | 84 | class HotTab extends StatefulWidget { 85 | final String type; 86 | final HttpBook http; 87 | 88 | const HotTab({Key key, this.type, this.http}) : super(key: key); 89 | 90 | @override 91 | _HotTab createState() => _HotTab(); 92 | } 93 | 94 | class _HotTab extends State { 95 | SourceList sourceList; 96 | 97 | @override 98 | void initState() { 99 | sourceList = SourceList(type: widget.type, http: widget.http); 100 | super.initState(); 101 | } 102 | 103 | @override 104 | Widget build(BuildContext context) { 105 | return CustomScrollView( 106 | slivers: [ 107 | PullToRefreshContainer( 108 | (info) => SliverPullToRefreshHeader(info: info), 109 | ), 110 | LoadingMoreSliverList(SliverListConfig( 111 | sourceList: sourceList, 112 | indicatorBuilder: indicatorBuilder, 113 | itemBuilder: (_, book, __) => WidgetBook( 114 | book, 115 | subtitle: book.authors?.join('/'), 116 | ), 117 | )), 118 | ], 119 | ); 120 | } 121 | 122 | Widget book(Book book) { 123 | return WidgetBook(book, subtitle: book.authors?.join('/')); 124 | } 125 | 126 | Widget indicatorBuilder(context, IndicatorStatus status) { 127 | print('indicatorBuilder $status'); 128 | bool isSliver = true; 129 | Widget widget; 130 | switch (status) { 131 | case IndicatorStatus.none: 132 | widget = SizedBox(); 133 | break; 134 | case IndicatorStatus.loadingMoreBusying: 135 | widget = Row( 136 | mainAxisAlignment: MainAxisAlignment.center, 137 | crossAxisAlignment: CrossAxisAlignment.center, 138 | children: [ 139 | AnimatedLogoWidget(width: 20, height: 30), 140 | SizedBox(width: 10), 141 | Text("正在读取") 142 | ], 143 | ); 144 | widget = _setbackground(false, widget, 35.0); 145 | break; 146 | case IndicatorStatus.fullScreenBusying: 147 | widget = Center( 148 | child: Row( 149 | mainAxisSize: MainAxisSize.min, 150 | children: [ 151 | AnimatedLogoWidget(width: 25, height: 30), 152 | Text('读取中'), 153 | ], 154 | ), 155 | ); 156 | if (isSliver) { 157 | widget = SliverFillRemaining( 158 | child: widget, 159 | ); 160 | } 161 | break; 162 | case IndicatorStatus.error: 163 | case IndicatorStatus.fullScreenError: 164 | widget = Column( 165 | mainAxisSize: MainAxisSize.min, 166 | children: [ 167 | Text( 168 | '读取失败\n你可能需要用梯子', 169 | textAlign: TextAlign.center, 170 | ), 171 | RaisedButton( 172 | child: Text('再次重试'), 173 | onPressed: sourceList.errorRefresh, 174 | ) 175 | ], 176 | ); 177 | final height = status == IndicatorStatus.error ? 35.0 : double.infinity; 178 | widget = _setbackground(false, widget, height); 179 | if (status == IndicatorStatus.fullScreenError) { 180 | if (isSliver) { 181 | widget = SliverFillRemaining( 182 | child: widget, 183 | ); 184 | } else { 185 | widget = CustomScrollView( 186 | slivers: [ 187 | SliverFillRemaining( 188 | child: widget, 189 | ) 190 | ], 191 | ); 192 | } 193 | } 194 | break; 195 | case IndicatorStatus.noMoreLoad: 196 | widget = Text("已经显示全部搜索结果"); 197 | widget = _setbackground(false, widget, 35.0); 198 | break; 199 | case IndicatorStatus.empty: 200 | widget = Text( 201 | '没有内容', 202 | ); 203 | widget = _setbackground(true, widget, double.infinity); 204 | if (isSliver) { 205 | widget = SliverToBoxAdapter( 206 | child: widget, 207 | ); 208 | } else { 209 | widget = CustomScrollView( 210 | slivers: [ 211 | SliverFillRemaining( 212 | child: widget, 213 | ) 214 | ], 215 | ); 216 | } 217 | break; 218 | } 219 | return widget; 220 | } 221 | 222 | Widget _setbackground(bool full, Widget widget, double height) { 223 | widget = Container( 224 | width: double.infinity, 225 | height: kToolbarHeight, 226 | child: widget, 227 | alignment: Alignment.center, 228 | ); 229 | return widget; 230 | } 231 | 232 | Widget getIndicator(BuildContext context) { 233 | return CircularProgressIndicator( 234 | strokeWidth: 2.0, 235 | valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryColor), 236 | ); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /lib/activities/search/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:focus_widget/focus_widget.dart'; 4 | 5 | import 'package:weiman/crawler/http18Comic.dart'; 6 | import 'tab.dart'; 7 | 8 | class ActivitySearch extends StatefulWidget { 9 | final String search; 10 | 11 | const ActivitySearch({Key key, this.search = ''}) : super(key: key); 12 | 13 | @override 14 | State createState() { 15 | return SearchState(); 16 | } 17 | } 18 | 19 | class SearchState extends State 20 | with SingleTickerProviderStateMixin { 21 | TextEditingController _controller; 22 | GlobalKey key = GlobalKey(); 23 | 24 | @override 25 | initState() { 26 | _controller = TextEditingController(text: widget.search); 27 | super.initState(); 28 | } 29 | 30 | @override 31 | dispose() { 32 | _controller.dispose(); 33 | super.dispose(); 34 | } 35 | 36 | void search() { 37 | key.currentState.search = _controller.text; 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return Scaffold( 43 | appBar: AppBar( 44 | title: RawKeyboardListener( 45 | focusNode: FocusNode(), 46 | onKey: (RawKeyEvent event) { 47 | print('is enter: ${LogicalKeyboardKey.enter == event.logicalKey}'); 48 | if (_controller.text.isEmpty) return; 49 | if (event.runtimeType == RawKeyUpEvent && 50 | LogicalKeyboardKey.enter == event.logicalKey) { 51 | print('回车键搜索'); 52 | search(); 53 | } 54 | }, 55 | child: FocusWidget.builder( 56 | context, 57 | builder: (_, focusNode) => TextField( 58 | focusNode: focusNode, 59 | style: TextStyle(color: Colors.white), 60 | cursorColor: Colors.white, 61 | decoration: InputDecoration( 62 | hintText: '搜索书名', 63 | prefixIcon: IconButton( 64 | onPressed: search, 65 | icon: Icon(Icons.search, color: Colors.white), 66 | ), 67 | enabledBorder: UnderlineInputBorder( 68 | borderSide: BorderSide(color: Colors.white), 69 | ), 70 | focusedBorder: UnderlineInputBorder( 71 | borderSide: BorderSide(color: Colors.white), 72 | ), 73 | border: UnderlineInputBorder( 74 | borderSide: BorderSide(color: Colors.white), 75 | ), 76 | ), 77 | textAlign: TextAlign.left, 78 | controller: _controller, 79 | autofocus: widget.search.isEmpty, 80 | textInputAction: TextInputAction.search, 81 | onSubmitted: (String name) { 82 | focusNode.unfocus(); 83 | print('onSubmitted'); 84 | search(); 85 | }, 86 | keyboardType: TextInputType.text, 87 | onEditingComplete: () { 88 | focusNode.unfocus(); 89 | print('onEditingComplete'); 90 | search(); 91 | }, 92 | ), 93 | ), 94 | ), 95 | ), 96 | body: SearchTab( 97 | http: Http18Comic.instance, 98 | search: _controller.text, 99 | key: key, 100 | ), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/activities/search/source.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:loading_more_list/loading_more_list.dart'; 3 | 4 | import 'package:weiman/db/book.dart'; 5 | import 'package:weiman/crawler/http.dart'; 6 | 7 | class SearchSourceList extends LoadingMoreBase { 8 | final HttpBook http; 9 | String search; 10 | int page = 1; 11 | bool hasMore = true; 12 | String eachPageFirstBookId; 13 | 14 | SearchSourceList({ 15 | @required this.http, 16 | this.search = '', 17 | }); 18 | 19 | @override 20 | Future loadData([bool isloadMoreAction = false]) async { 21 | print('搜书 $search'); 22 | if (search == null || search.isEmpty) return true; 23 | final list = await http.searchBook(search, page); 24 | if (list.isEmpty) { 25 | hasMore = false; 26 | } else if (list[0].aid == eachPageFirstBookId) { 27 | hasMore = false; 28 | } else { 29 | eachPageFirstBookId = list[0].aid; 30 | hasMore = true; 31 | page++; 32 | this.addAll(list); 33 | } 34 | return true; 35 | } 36 | 37 | @override 38 | Future refresh([bool notifyStateChanged = false]) { 39 | page = 1; 40 | hasMore = true; 41 | eachPageFirstBookId = null; 42 | clear(); 43 | print('refresh $page $hasMore'); 44 | return super.refresh(notifyStateChanged); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/activities/search/tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:loading_more_list/loading_more_list.dart'; 3 | import 'package:weiman/activities/search/source.dart'; 4 | import 'package:weiman/crawler/http.dart'; 5 | import 'package:weiman/db/book.dart'; 6 | import 'package:weiman/widgets/book.dart'; 7 | 8 | class SearchTab extends StatefulWidget { 9 | final HttpBook http; 10 | final String search; 11 | 12 | const SearchTab({ 13 | Key key, 14 | @required this.http, 15 | this.search, 16 | }) : super(key: key); 17 | 18 | @override 19 | SearchTabState createState() => SearchTabState(); 20 | } 21 | 22 | class SearchTabState extends State 23 | with AutomaticKeepAliveClientMixin { 24 | SearchSourceList sourceList; 25 | 26 | @override 27 | void initState() { 28 | sourceList = SearchSourceList(http: widget.http, search: widget.search); 29 | super.initState(); 30 | } 31 | 32 | Widget book(Book book) { 33 | return WidgetBook(book, subtitle: book.authors.join('/')); 34 | } 35 | 36 | Future refresh() async { 37 | return sourceList.refresh(true); 38 | } 39 | 40 | get search => sourceList.search; 41 | 42 | set search(String value) { 43 | print('tab search $value'); 44 | sourceList.search = value; 45 | sourceList.refresh(true); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | super.build(context); 51 | return LoadingMoreList( 52 | ListConfig( 53 | sourceList: sourceList, 54 | itemBuilder: (_, item, index) => book(item), 55 | autoLoadMore: true, 56 | indicatorBuilder: indicatorBuilder, 57 | ), 58 | ); 59 | } 60 | 61 | Widget indicatorBuilder(context, IndicatorStatus status) { 62 | bool isSliver = false; 63 | Widget widget; 64 | switch (status) { 65 | case IndicatorStatus.none: 66 | widget = SizedBox(); 67 | break; 68 | case IndicatorStatus.loadingMoreBusying: 69 | widget = Row( 70 | mainAxisAlignment: MainAxisAlignment.center, 71 | crossAxisAlignment: CrossAxisAlignment.center, 72 | children: [ 73 | Container( 74 | margin: EdgeInsets.only(right: 5.0), 75 | height: 15.0, 76 | width: 15.0, 77 | child: getIndicator(context), 78 | ), 79 | Text("正在读取") 80 | ], 81 | ); 82 | widget = _setbackground(false, widget, 35.0); 83 | break; 84 | case IndicatorStatus.fullScreenBusying: 85 | widget = widget = _setbackground( 86 | false, 87 | Text( 88 | '正在读取', 89 | ), 90 | 35.0); 91 | if (isSliver) { 92 | widget = SliverFillRemaining( 93 | child: widget, 94 | ); 95 | } 96 | break; 97 | case IndicatorStatus.error: 98 | widget = _setbackground( 99 | false, 100 | Text( 101 | '网络错误\n点击重试', 102 | ), 103 | 35.0); 104 | 105 | widget = GestureDetector( 106 | onTap: () { 107 | sourceList.errorRefresh(); 108 | }, 109 | child: widget, 110 | ); 111 | break; 112 | case IndicatorStatus.fullScreenError: 113 | widget = Text( 114 | '读取失败,如果失败的次数太多可能需要用梯子', 115 | ); 116 | widget = _setbackground(true, widget, double.infinity); 117 | widget = GestureDetector( 118 | onTap: () { 119 | sourceList.errorRefresh(); 120 | }, 121 | child: widget, 122 | ); 123 | if (isSliver) { 124 | widget = SliverFillRemaining( 125 | child: widget, 126 | ); 127 | } else { 128 | widget = CustomScrollView( 129 | slivers: [ 130 | SliverFillRemaining( 131 | child: widget, 132 | ) 133 | ], 134 | ); 135 | } 136 | break; 137 | case IndicatorStatus.noMoreLoad: 138 | widget = Text("已经显示全部搜索结果"); 139 | widget = _setbackground(false, widget, 35.0); 140 | break; 141 | case IndicatorStatus.empty: 142 | widget = Text( 143 | sourceList.search.isEmpty ? '请输入搜索内容' : '搜索不到任何内容', 144 | ); 145 | widget = _setbackground(true, widget, double.infinity); 146 | if (isSliver) { 147 | widget = SliverToBoxAdapter( 148 | child: widget, 149 | ); 150 | } else { 151 | widget = CustomScrollView( 152 | slivers: [ 153 | SliverFillRemaining( 154 | child: widget, 155 | ) 156 | ], 157 | ); 158 | } 159 | break; 160 | } 161 | return widget; 162 | } 163 | 164 | Widget _setbackground(bool full, Widget widget, double height) { 165 | widget = Container( 166 | width: double.infinity, 167 | height: kToolbarHeight, 168 | child: widget, 169 | alignment: Alignment.center, 170 | ); 171 | return widget; 172 | } 173 | 174 | Widget getIndicator(BuildContext context) { 175 | return CircularProgressIndicator( 176 | strokeWidth: 2.0, 177 | valueColor: AlwaysStoppedAnimation(Theme.of(context).primaryColor), 178 | ); 179 | } 180 | 181 | @override 182 | bool get wantKeepAlive => true; 183 | } 184 | -------------------------------------------------------------------------------- /lib/activities/setting/hideStatusBar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:weiman/db/setting.dart'; 3 | 4 | class HideStatusBar extends StatelessWidget { 5 | final options = { 6 | '自动': HideOption.auto, 7 | '全程隐藏': HideOption.always, 8 | '不隐藏': HideOption.none, 9 | }; 10 | final Function(HideOption option) onChanged; 11 | final HideOption option; 12 | 13 | HideStatusBar({Key key, @required this.onChanged, @required this.option}) 14 | : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return ListTile( 19 | title: Text('看漫画时隐藏状态栏'), 20 | subtitle: Text('自动:随着图片列表的上下滚动而自动显示或隐藏状态栏\n' 21 | '全程隐藏:进入看图界面就隐藏状态栏,退出就显示状态栏\n' 22 | '不隐藏:就是不隐藏状态栏咯'), 23 | trailing: DropdownButton( 24 | value: option, 25 | items: options.keys 26 | .map((key) => DropdownMenuItem( 27 | child: Text(key), 28 | value: options[key], 29 | )) 30 | .toList(), 31 | onChanged: onChanged, 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/activities/setting/setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:filesize/filesize.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:oktoast/oktoast.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:weiman/activities/setting/hideStatusBar.dart'; 6 | import 'package:weiman/activities/setting/web.dart'; 7 | import 'package:weiman/db/setting.dart'; 8 | import 'package:weiman/main.dart'; 9 | 10 | class ActivitySetting extends StatefulWidget { 11 | @override 12 | _ActivitySetting createState() => _ActivitySetting(); 13 | } 14 | 15 | class _ActivitySetting extends State { 16 | int imagesCount, sizeCount; 17 | bool isClearing = false; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | imageCaches(); 23 | } 24 | 25 | Future imageCaches() async { 26 | final files = imageCacheDir.listSync(); 27 | imagesCount = files.length; 28 | sizeCount = 0; 29 | files.forEach((file) => sizeCount += file.statSync().size); 30 | if (mounted) setState(() {}); 31 | } 32 | 33 | Future clearDiskCachedImages() async { 34 | await imageCacheDir.delete(recursive: true); 35 | await imageCacheDir.create(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return Scaffold( 41 | appBar: AppBar(title: Text('设置')), 42 | body: Consumer(builder: (_, data, __) { 43 | print('代理 ${data.getProxy()}'); 44 | return ListView( 45 | children: ListTile.divideTiles( 46 | context: context, 47 | tiles: [ 48 | /// 隐藏状态栏设置 49 | HideStatusBar( 50 | option: data.getHideOption(), 51 | onChanged: (option) => data.setHideOption(option), 52 | ), 53 | 54 | /// 设置代理 55 | ListTile( 56 | title: Text('设置代理'), 57 | subtitle: Text(data.getProxy() ?? '无'), 58 | onTap: () async { 59 | var proxy = await showDialog( 60 | context: context, 61 | builder: (_) { 62 | final _c = TextEditingController(text: data.getProxy()); 63 | return WillPopScope( 64 | child: AlertDialog( 65 | title: Text('设置网络代理'), 66 | content: Column( 67 | crossAxisAlignment: CrossAxisAlignment.start, 68 | mainAxisSize: MainAxisSize.min, 69 | children: [ 70 | Text( 71 | '只支持http代理\nSS,SSR,V2Ray,Trojan(Clash)\n这些梯子App都有提供Http代理功能'), 72 | TextField( 73 | controller: _c, 74 | decoration: InputDecoration( 75 | hintText: '例如Clash提供的127.0.0.1:7890'), 76 | ), 77 | ]), 78 | actions: [ 79 | FlatButton( 80 | child: Text('清空'), 81 | onPressed: () { 82 | _c.clear(); 83 | }, 84 | ), 85 | FlatButton( 86 | child: Text('确定'), 87 | onPressed: () { 88 | Navigator.pop(context, _c.text); 89 | }, 90 | ), 91 | ], 92 | ), 93 | onWillPop: () { 94 | Navigator.pop(context, '-1'); 95 | return Future.value(false); 96 | }, 97 | ); 98 | }); 99 | print('用户输入 $proxy'); 100 | if (proxy == '-1') return; 101 | // 在前 102 | if (proxy != null) { 103 | proxy = proxy 104 | .trim() 105 | .replaceFirst('http://', '') 106 | .replaceFirst('https://', ''); 107 | } 108 | // 在后 109 | if (proxy == null || proxy.isEmpty) { 110 | proxy = null; 111 | } 112 | print('设置代理 $proxy'); 113 | await data.setProxy(proxy); 114 | }, 115 | ), 116 | 117 | /// 清空图片缓存 118 | ListTile( 119 | title: Text('清除所有图片缓存'), 120 | subtitle: isClearing 121 | ? Text('清理中') 122 | : Text.rich( 123 | TextSpan( 124 | children: [ 125 | TextSpan(text: '图片数量:'), 126 | TextSpan( 127 | text: imagesCount == null 128 | ? '读取中' 129 | : '$imagesCount 张'), 130 | TextSpan(text: '\n'), 131 | TextSpan(text: '存储容量:'), 132 | TextSpan( 133 | text: sizeCount == null 134 | ? '读取中' 135 | : '${filesize(sizeCount)}'), 136 | ], 137 | ), 138 | ), 139 | onTap: () async { 140 | if (isClearing == true) return; 141 | final sure = await showDialog( 142 | context: context, 143 | builder: (_) => AlertDialog( 144 | title: Text('确认清除所有图片缓存?'), 145 | actions: [ 146 | RaisedButton( 147 | child: Text('确认'), 148 | onPressed: () => Navigator.pop(context, true), 149 | ), 150 | ], 151 | ), 152 | ); 153 | if (sure == true) { 154 | showToast('正在清理图片缓存'); 155 | isClearing = true; 156 | setState(() {}); 157 | await clearDiskCachedImages(); 158 | isClearing = false; 159 | if (mounted) { 160 | setState(() {}); 161 | await imageCaches(); 162 | } 163 | showToast('成功清理图片缓存'); 164 | } 165 | }, 166 | ), 167 | 168 | ListTile( 169 | title: Text('查看最新版'), 170 | subtitle: Text('当前版本为 ${packageInfo.version}'), 171 | onTap: () { 172 | Navigator.push(context, 173 | MaterialPageRoute(builder: (_) => ActivityWeb())); 174 | }, 175 | ), 176 | 177 | /// 清空数据缓存 178 | /* ListTile( 179 | title: Text('清空漫画数据缓存'), 180 | subtitle: Text('正常情况是不需要清空的'), 181 | onTap: () async { 182 | await HttpBook.dataCache.clearAll(); 183 | showToast('成功清空漫画数据缓存', textPadding: EdgeInsets.all(10)); 184 | }, 185 | ),*/ 186 | ], 187 | ).toList(), 188 | ); 189 | }), 190 | ); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /lib/activities/setting/web.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:webview_flutter/webview_flutter.dart'; 4 | import 'package:weiman/main.dart'; 5 | 6 | class ActivityWeb extends StatefulWidget { 7 | @override 8 | _State createState() => _State(); 9 | } 10 | 11 | class _State extends State { 12 | LoadState state = LoadState.loading; 13 | 14 | @override 15 | void initState() { 16 | analytics.setCurrentScreen(screenName: '/activity_update_web'); 17 | super.initState(); 18 | } 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | appBar: AppBar( 24 | title: Text('最新版本'), 25 | ), 26 | body: Stack( 27 | alignment: Alignment.center, 28 | children: [ 29 | WebView( 30 | initialUrl: 'https://nrop19.github.io/weiman_app', 31 | onWebViewCreated: (controller) { 32 | state = LoadState.loading; 33 | setState(() {}); 34 | }, 35 | onPageFinished: (_) { 36 | state = LoadState.completed; 37 | setState(() {}); 38 | }, 39 | ), 40 | if (state == LoadState.loading) 41 | Container( 42 | color: Colors.grey.withOpacity(0.3), 43 | padding: EdgeInsets.all(20), 44 | child: CircularProgressIndicator(), 45 | ), 46 | ], 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/classes/book.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:weiman/crawler/http.dart'; 6 | import 'data.dart'; 7 | class Book { 8 | final String http; 9 | final String aid; // 漫画的数据库ID 10 | final String name; // 书本名称 11 | final String avatar; // 书本封面 12 | final String author; // 画家 13 | final String description; // 描述 14 | final List chapters; 15 | final int chapterCount; 16 | final int version; 17 | 18 | History history; 19 | 20 | Book({ 21 | @required this.http, 22 | @required this.name, 23 | @required this.aid, 24 | @required this.avatar, 25 | this.author, 26 | this.description, 27 | this.chapters: const [], 28 | this.chapterCount: 0, 29 | this.history, 30 | this.version: 0, 31 | }); 32 | 33 | @override 34 | String toString() { 35 | return jsonEncode(toJson()); 36 | } 37 | 38 | bool isFavorite() { 39 | var books = Data.getFavorites(); 40 | return books.containsKey(aid); 41 | } 42 | 43 | Map toJson() { 44 | print('book toJson'); 45 | final Map data = { 46 | 'http': http, 47 | 'aid': aid, 48 | 'name': name, 49 | 'avatar': avatar, 50 | 'author': author, 51 | 'chapterCount': chapterCount, 52 | 'version': version, 53 | }; 54 | if (history != null) data['history'] = history.toJson(); 55 | return data; 56 | } 57 | 58 | factory Book.fromJson(Map json) { 59 | final book = Book( 60 | http: json['http'], 61 | aid: json['aid'], 62 | name: json['name'], 63 | avatar: json['avatar'], 64 | author: json['author'], 65 | description: json['description'], 66 | chapterCount: json['chapterCount'] ?? 0, 67 | version: json['version'] ?? 0); 68 | if (json.containsKey('history')) 69 | book.history = History.fromJson(json['history']); 70 | return book; 71 | } 72 | } 73 | 74 | class Chapter { 75 | final HttpBook http; 76 | final String cid; // 章节cid 77 | final String cname; // 章节名称 78 | final String avatar; // 章节封面 79 | 80 | Chapter({ 81 | @required this.http, 82 | @required this.cid, 83 | @required this.cname, 84 | @required this.avatar, 85 | }); 86 | 87 | @override 88 | String toString() { 89 | final Map data = { 90 | 'cid': cid, 91 | 'cname': cname, 92 | 'avatar': avatar, 93 | }; 94 | return jsonEncode(data); 95 | } 96 | } 97 | 98 | class History { 99 | final String cid; 100 | final String cname; 101 | final int time; 102 | final int image; 103 | 104 | History({ 105 | @required this.cid, 106 | @required this.cname, 107 | @required this.time, 108 | this.image = 0, 109 | }); 110 | 111 | @override 112 | String toString() => jsonEncode(toJson()); 113 | 114 | Map toJson() { 115 | return { 116 | 'cid': cid, 117 | 'cname': cname, 118 | 'time': time, 119 | 'image': image, 120 | }; 121 | } 122 | 123 | static History fromJson(Map json) { 124 | return History( 125 | cid: json['cid'], 126 | cname: json['cname'], 127 | time: json['time'], 128 | image: json['image'] ?? 0, 129 | ); 130 | } 131 | 132 | static History fromChapter(Chapter chapter) { 133 | return History( 134 | cid: chapter.cid, 135 | cname: chapter.cname, 136 | time: DateTime.now().millisecondsSinceEpoch, 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/classes/chapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class Chapter { 6 | final String cid; // 章节cid 7 | final String cname; // 章节名称 8 | final DateTime time; // 章节更新时间 9 | 10 | Chapter({ 11 | @required this.cid, 12 | @required this.cname, 13 | this.time, 14 | }); 15 | 16 | @override 17 | String toString() { 18 | final Map data = { 19 | 'cid': cid, 20 | 'cname': cname, 21 | }; 22 | return jsonEncode(data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/classes/chapterContent.dart: -------------------------------------------------------------------------------- 1 | class ChapterContent { 2 | final List images; 3 | final bool hasNextPage; 4 | 5 | ChapterContent(this.images, this.hasNextPage); 6 | 7 | @override 8 | String toString() { 9 | return 'ChapterContent images:${images.length} nexPage:$hasNextPage'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/classes/data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | import 'book.dart'; 6 | 7 | class Data { 8 | static SharedPreferences instance; 9 | static final favoriteBooksKey = 'favorite_books'; 10 | static final viewHistoryKey = 'view_history'; 11 | static final quickKey = 'quick_list'; 12 | 13 | static Future init() async { 14 | instance = await SharedPreferences.getInstance(); 15 | } 16 | 17 | static set(String key, T value) { 18 | if (value is String) { 19 | instance.setString(key, value); 20 | } else if (value is int) { 21 | instance.setInt(key, value); 22 | } else if (value is bool) { 23 | instance.setBool(key, value); 24 | } else if (value is List) { 25 | instance.setStringList(key, value); 26 | } else if (value is double) { 27 | instance.setDouble(key, value); 28 | } else if (value is Map) { 29 | instance.setString(key, json.encode(value)); 30 | } 31 | } 32 | 33 | static dynamic get(String key) { 34 | return instance.get(key); 35 | } 36 | 37 | static bool hasData() { 38 | return instance.containsKey(favoriteBooksKey) || 39 | instance.containsKey(viewHistoryKey); 40 | } 41 | 42 | static Map getFavorites() { 43 | if (has(favoriteBooksKey)) { 44 | final String str = instance.getString(favoriteBooksKey); 45 | Map data = jsonDecode(str); 46 | Map res = {}; 47 | data.keys.forEach((key) { 48 | res[key] = Book.fromJson(data[key]); 49 | }); 50 | return res; 51 | } 52 | return {}; 53 | } 54 | 55 | static void addFavorite(Book book) { 56 | var books = getFavorites(); 57 | books[book.aid] = book; 58 | set(favoriteBooksKey, books); 59 | } 60 | 61 | static void removeFavorite(Book book) { 62 | var books = getFavorites(); 63 | if (books.containsKey(book.aid)) { 64 | books.remove(book.aid); 65 | set(favoriteBooksKey, books); 66 | reQuick(); 67 | } 68 | } 69 | 70 | static clear() { 71 | instance.clear(); 72 | } 73 | 74 | static bool has(String key) { 75 | return instance.containsKey(key); 76 | } 77 | 78 | static remove(String key) { 79 | instance.remove(key); 80 | } 81 | 82 | static Map getHistories() { 83 | if (has(viewHistoryKey)) { 84 | var data = 85 | jsonDecode(instance.getString(viewHistoryKey)) as Map; 86 | final Map histories = {}; 87 | data.forEach((key, value) { 88 | histories[key] = Book.fromJson(value); 89 | }); 90 | return histories; 91 | } 92 | return {}; 93 | } 94 | 95 | static addHistory(Book book, Chapter chapter) { 96 | book.history = History( 97 | cid: chapter.cid, 98 | cname: chapter.cname, 99 | time: DateTime.now().millisecondsSinceEpoch); 100 | final books = getHistories(); 101 | books[book.aid] = book; 102 | set(viewHistoryKey, books); 103 | // print('保存历史\n' + books.toString()); 104 | } 105 | 106 | static removeHistory(bool Function(Book book) isDelete) { 107 | var books = getHistories(); 108 | books.keys 109 | .where((key) => isDelete(books[key])) 110 | .toList() 111 | .forEach(books.remove); 112 | set(viewHistoryKey, books); 113 | } 114 | 115 | static removeHistoryFromBook(Book book) { 116 | final books = getHistories(); 117 | books.remove(book.aid); 118 | set(viewHistoryKey, books); 119 | } 120 | 121 | /// 快速导航 id 列表,内部方法 122 | static List quickIdList() { 123 | if (instance.containsKey(quickKey)) { 124 | return instance.getStringList(quickKey); 125 | } 126 | return []; 127 | } 128 | 129 | /// 快速导航列表 130 | static List quickList() { 131 | final books = getFavorites(); 132 | final ids = books.keys; 133 | final List quickIds = quickIdList(); 134 | print('快捷 $quickIds'); 135 | return quickIds 136 | .where((id) => ids.contains(id)) 137 | .map((id) => books[id]) 138 | .toList(); 139 | } 140 | 141 | /// 增加快速导航 142 | static addQuick(Book book) { 143 | final list = quickIdList(); 144 | list.add(book.aid); 145 | instance.setStringList(quickKey, list.toSet().toList()); 146 | } 147 | 148 | static addQuickAll(List id) { 149 | print('保存qid $id'); 150 | instance.setStringList(quickKey, id.toSet().toList()); 151 | } 152 | 153 | /// 重新整理Quick的id列表 154 | static reQuick() { 155 | final books = getFavorites(); 156 | final quickIds = quickIdList(); 157 | instance.setStringList( 158 | quickKey, quickIds.where(books.keys.contains).toSet().toList()); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/classes/history.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:weiman/classes/chapter.dart'; 4 | 5 | class History extends Chapter { 6 | DateTime time; // 历史时间 7 | 8 | History({ 9 | @required cid, 10 | @required cname, 11 | @required this.time, 12 | }) : super(cid: cid, cname: cname); 13 | 14 | Map toJson() { 15 | return {'cid': cid, 'cname': cname, 'time': time}; 16 | } 17 | 18 | factory History.fromJson(Map map) { 19 | if (map == null) return null; 20 | return History( 21 | cid: map['cid'], 22 | cname: map['cname'], 23 | time: map['time'], 24 | ); 25 | } 26 | 27 | factory History.fromChapter(Chapter chapter) { 28 | return History( 29 | cid: chapter.cid, 30 | cname: chapter.cname, 31 | time: DateTime.now(), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/classes/networkImageSSL.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | import 'dart:ui'; 4 | 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:weiman/crawler/http.dart'; 8 | 9 | /// The dart:io implementation of [image_provider.NetworkImage]. 10 | class NetworkImageSSL extends ImageProvider 11 | implements NetworkImage { 12 | /// Creates an object that fetches the image at the given URL. 13 | /// 14 | /// The arguments [url] and [scale] must not be null. 15 | const NetworkImageSSL( 16 | this.http, 17 | this.url, { 18 | this.scale = 1.0, 19 | this.headers, 20 | this.timeout = 8, 21 | this.reSort = false, 22 | }) : assert(url != null), 23 | assert(scale != null); 24 | 25 | final HttpBook http; 26 | 27 | final int timeout; 28 | @override 29 | final String url; 30 | 31 | @override 32 | final double scale; 33 | 34 | @override 35 | final Map headers; 36 | 37 | final bool reSort; 38 | 39 | static void init(ByteData data) {} 40 | 41 | @override 42 | Future obtainKey(ImageConfiguration configuration) { 43 | return SynchronousFuture(this); 44 | } 45 | 46 | @override 47 | ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) { 48 | // Ownership of this controller is handed off to [_loadAsync]; it is that 49 | // method's responsibility to close the controller's stream when the image 50 | // has been loaded or an error is thrown. 51 | final StreamController chunkEvents = 52 | StreamController(); 53 | 54 | return MultiFrameImageStreamCompleter( 55 | codec: _loadAsync(key, chunkEvents, decode), 56 | chunkEvents: chunkEvents.stream, 57 | scale: key.scale, 58 | informationCollector: () { 59 | return [ 60 | DiagnosticsProperty('Image provider', this), 61 | DiagnosticsProperty('Image key', key), 62 | ]; 63 | }, 64 | ); 65 | } 66 | 67 | Future _loadAsync( 68 | NetworkImageSSL key, 69 | StreamController chunkEvents, 70 | DecoderCallback decode, 71 | ) async { 72 | try { 73 | assert(key == this); 74 | final Uint8List bytes = await http.getImage(url, reSort: reSort); 75 | if (bytes.lengthInBytes == 0) 76 | throw Exception('NetworkImage is an empty file: $url'); 77 | return decode(bytes); 78 | } finally { 79 | chunkEvents.close(); 80 | } 81 | } 82 | 83 | @override 84 | bool operator ==(dynamic other) { 85 | if (other.runtimeType != runtimeType) return false; 86 | final NetworkImageSSL typedOther = other; 87 | return url == typedOther.url && scale == typedOther.scale; 88 | } 89 | 90 | @override 91 | int get hashCode => hashValues(url, scale); 92 | 93 | @override 94 | String toString() => '$runtimeType("$url", scale: $scale)'; 95 | } 96 | -------------------------------------------------------------------------------- /lib/crawler/http.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:weiman/classes/chapter.dart'; 5 | import 'package:weiman/classes/chapterContent.dart'; 6 | import 'package:weiman/db/book.dart'; 7 | 8 | import 'http18Comic.dart'; 9 | 10 | final headers = { 11 | 'user-agent': 12 | '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', 13 | 'accept': 14 | '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', 15 | 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,zh-HK;q=0.7', 16 | 'cache-control': 'no-cache', 17 | 'pragma': 'no-cache', 18 | }; 19 | 20 | class MyHttpClient { 21 | static Map clients = {}; 22 | 23 | static init(String proxy, int timeout) { 24 | Http18Comic.instance = Http18Comic( 25 | baseUrls.values.first, 26 | name: baseUrls.keys.first, 27 | headers: headers, 28 | timeout: timeout, 29 | ); 30 | 31 | clients[Http18Comic.instance.id] = Http18Comic.instance; 32 | 33 | setGlobalProxy(proxy); 34 | } 35 | } 36 | 37 | abstract class HttpBook { 38 | final String id; 39 | final String name; 40 | 41 | final Dio dio; 42 | 43 | HttpBook(this.id, this.name, this.dio); 44 | 45 | Future> searchBook(String name, [int page]); 46 | 47 | Future getBook(String aid); 48 | 49 | Future> getChapterImages(Book book, Chapter chapter); 50 | 51 | Future getChapterContent(Book book, Chapter chapter); 52 | 53 | Future> getImage(String url, {bool reSort = false}); 54 | 55 | Future> hotBooks([String type = '', int page]); 56 | } 57 | 58 | class MyProxyHttpOverride extends HttpOverrides { 59 | final String proxy; 60 | 61 | MyProxyHttpOverride(this.proxy); 62 | 63 | @override 64 | HttpClient createHttpClient(SecurityContext context) { 65 | return super.createHttpClient(context) 66 | ..findProxy = (uri) { 67 | return 'PROXY $proxy;'; 68 | } 69 | ..badCertificateCallback = 70 | (X509Certificate cert, String host, int port) => true; 71 | } 72 | } 73 | 74 | void setGlobalProxy(String proxy) { 75 | print('setGlobalProxy $proxy'); 76 | if (proxy != null) 77 | HttpOverrides.global = MyProxyHttpOverride(proxy); 78 | else 79 | HttpOverrides.global = null; 80 | } 81 | -------------------------------------------------------------------------------- /lib/db/book.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | 3 | import 'package:weiman/classes/chapter.dart'; 4 | import 'package:weiman/classes/history.dart'; 5 | import 'package:weiman/crawler/http.dart'; 6 | import 'package:weiman/db/group.dart'; 7 | 8 | part 'book.g.dart'; 9 | 10 | const BookName = 'book'; 11 | enum BookUpdateStatus { 12 | not, // 不检查更新 13 | no, // 没有更新 14 | had, // 有更新 15 | fail, // 检查更新失败 16 | wait, // 检查更新的队列中 17 | loading, // 正在检查更新 18 | old, // 旧藏书,不检查更新 19 | } 20 | 21 | @HiveType(typeId: 1) 22 | class Book extends HiveObject { 23 | static Box bookBox; 24 | 25 | @HiveField(0) 26 | String aid; 27 | 28 | @HiveField(1) 29 | String name; 30 | 31 | @HiveField(2) 32 | String avatar; 33 | 34 | @HiveField(3) 35 | List authors; 36 | 37 | @HiveField(4) 38 | String description; 39 | 40 | @HiveField(5) 41 | int chapterCount; 42 | 43 | // [新章节数量]减[旧章节数量]得到的差值 44 | int newChapterCount; 45 | 46 | BookUpdateStatus status; 47 | 48 | List chapters; 49 | 50 | List tags; 51 | 52 | @HiveField(6) 53 | bool favorite; 54 | 55 | @HiveField(7) 56 | bool needUpdate; 57 | 58 | @HiveField(8) 59 | bool hasUpdate; 60 | 61 | @HiveField(9) 62 | DateTime updatedAt; 63 | 64 | // 首页快速导航 65 | @HiveField(10) 66 | int quick; 67 | 68 | @HiveField(11) 69 | Map _history; 70 | 71 | @HiveField(12) 72 | int groupId; 73 | 74 | @HiveField(13) 75 | String httpId; 76 | 77 | bool look = false; 78 | 79 | Group get group => 80 | groupId == null ? null : Group.groupBox.get(groupId, defaultValue: null); 81 | 82 | HttpBook get http => MyHttpClient.clients[httpId]; 83 | 84 | History get history => History.fromJson(_history); 85 | 86 | Future setFavorite(bool value) { 87 | favorite = value; 88 | return save(); 89 | } 90 | 91 | Future setHistory(Chapter value) { 92 | if (value == null) { 93 | _history = null; 94 | } else { 95 | _history = History.fromChapter(value).toJson(); 96 | } 97 | return save(); 98 | } 99 | 100 | Book({ 101 | this.httpId, 102 | this.aid, 103 | this.name, 104 | this.groupId, 105 | this.avatar, 106 | this.authors, 107 | this.description, 108 | this.chapterCount, 109 | this.favorite = false, 110 | this.needUpdate = false, 111 | this.quick, 112 | this.chapters = const [], 113 | this.tags = const [], 114 | Map history, 115 | }) : _history = history; 116 | 117 | @override 118 | String toString() { 119 | return 'Book:${toJson()}'; 120 | } 121 | 122 | toJson() { 123 | return { 124 | 'key': key, 125 | 'aid': aid, 126 | 'name': name, 127 | 'httpId': httpId, 128 | 'groupId': groupId, 129 | 'favorite': favorite, 130 | 'history': _history, 131 | 'status': status, 132 | 'chapterCount': chapterCount, 133 | }; 134 | } 135 | 136 | bool needToSave() { 137 | return favorite == true || _history != null || quick != null; 138 | } 139 | 140 | @override 141 | Future save() { 142 | if (needToSave()) { 143 | return bookBox.put(aid, this); 144 | } 145 | return bookBox.delete(aid); 146 | } 147 | 148 | Future load() async { 149 | if (httpId == null) return false; 150 | final newBook = await this.http.getBook(aid); 151 | print('load newBook:${newBook.httpId}'); 152 | chapters = newBook.chapters; 153 | chapterCount = newBook.chapterCount; 154 | authors = newBook.authors; 155 | description = newBook.description; 156 | httpId = newBook.httpId; 157 | tags = newBook.tags; 158 | print('book httpId $httpId'); 159 | return true; 160 | } 161 | 162 | Future> loadChapter(Chapter chapter) async { 163 | if (httpId == null) return null; 164 | return this.http.getChapterImages(this, chapter); 165 | } 166 | 167 | Future update() async { 168 | try { 169 | final newBook = await this.http.getBook(aid); 170 | print('$name 旧$chapterCount 新${newBook.chapterCount}'); 171 | newChapterCount = newBook.chapterCount - chapterCount; 172 | status = newChapterCount > 0 ? BookUpdateStatus.had : BookUpdateStatus.no; 173 | } catch (e) { 174 | status = BookUpdateStatus.fail; 175 | } 176 | print('book update $status'); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/db/book.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'book.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class BookAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 1; 12 | 13 | @override 14 | Book read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return Book( 20 | httpId: fields[13] as String, 21 | aid: fields[0] as String, 22 | name: fields[1] as String, 23 | groupId: fields[12] as int, 24 | avatar: fields[2] as String, 25 | authors: (fields[3] as List)?.cast(), 26 | description: fields[4] as String, 27 | chapterCount: fields[5] as int, 28 | favorite: fields[6] as bool, 29 | needUpdate: fields[7] as bool, 30 | quick: fields[10] as int, 31 | ) 32 | ..hasUpdate = fields[8] as bool 33 | ..updatedAt = fields[9] as DateTime 34 | .._history = (fields[11] as Map)?.cast(); 35 | } 36 | 37 | @override 38 | void write(BinaryWriter writer, Book obj) { 39 | writer 40 | ..writeByte(14) 41 | ..writeByte(0) 42 | ..write(obj.aid) 43 | ..writeByte(1) 44 | ..write(obj.name) 45 | ..writeByte(2) 46 | ..write(obj.avatar) 47 | ..writeByte(3) 48 | ..write(obj.authors) 49 | ..writeByte(4) 50 | ..write(obj.description) 51 | ..writeByte(5) 52 | ..write(obj.chapterCount) 53 | ..writeByte(6) 54 | ..write(obj.favorite) 55 | ..writeByte(7) 56 | ..write(obj.needUpdate) 57 | ..writeByte(8) 58 | ..write(obj.hasUpdate) 59 | ..writeByte(9) 60 | ..write(obj.updatedAt) 61 | ..writeByte(10) 62 | ..write(obj.quick) 63 | ..writeByte(11) 64 | ..write(obj._history) 65 | ..writeByte(12) 66 | ..write(obj.groupId) 67 | ..writeByte(13) 68 | ..write(obj.httpId); 69 | } 70 | 71 | @override 72 | int get hashCode => typeId.hashCode; 73 | 74 | @override 75 | bool operator ==(Object other) => 76 | identical(this, other) || 77 | other is BookAdapter && 78 | runtimeType == other.runtimeType && 79 | typeId == other.typeId; 80 | } 81 | -------------------------------------------------------------------------------- /lib/db/group.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | 3 | import 'book.dart'; 4 | 5 | part 'group.g.dart'; 6 | 7 | const GroupName = 'group'; 8 | 9 | @HiveType(typeId: 0) 10 | class Group extends HiveObject { 11 | static Box groupBox; 12 | static Box bookBox; 13 | 14 | @HiveField(0) 15 | String name; 16 | 17 | @HiveField(1) 18 | bool expended; 19 | 20 | Group(this.name, [this.expended = false]); 21 | 22 | List get books => bookBox.values 23 | .where((book) => book.favorite && book.groupId == this.key) 24 | .toList(); 25 | 26 | @override 27 | String toString() { 28 | return 'Group:${{'key': key, 'name': name, 'books': books.length}}'; 29 | } 30 | 31 | @override 32 | Future save() { 33 | if (!isInBox) return groupBox.add(this); 34 | return super.save(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/db/group.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'group.dart'; 4 | 5 | // ************************************************************************** 6 | // TypeAdapterGenerator 7 | // ************************************************************************** 8 | 9 | class GroupAdapter extends TypeAdapter { 10 | @override 11 | final int typeId = 0; 12 | 13 | @override 14 | Group read(BinaryReader reader) { 15 | final numOfFields = reader.readByte(); 16 | final fields = { 17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 18 | }; 19 | return Group( 20 | fields[0] as String, 21 | fields[1] as bool, 22 | ); 23 | } 24 | 25 | @override 26 | void write(BinaryWriter writer, Group obj) { 27 | writer 28 | ..writeByte(2) 29 | ..writeByte(0) 30 | ..write(obj.name) 31 | ..writeByte(1) 32 | ..write(obj.expended); 33 | } 34 | 35 | @override 36 | int get hashCode => typeId.hashCode; 37 | 38 | @override 39 | bool operator ==(Object other) => 40 | identical(this, other) || 41 | other is GroupAdapter && 42 | runtimeType == other.runtimeType && 43 | typeId == other.typeId; 44 | } 45 | -------------------------------------------------------------------------------- /lib/db/historyOffset.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | 3 | const HistoryOffsetName = 'history'; 4 | 5 | class HistoryOffset { 6 | static Box box; 7 | 8 | static double get(String cid) { 9 | print('get $cid'); 10 | return box.get(cid) ?? 0.0; 11 | } 12 | 13 | static Future save(String cid, double offset) { 14 | print('save $cid $offset'); 15 | return box.put(cid, offset); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/db/setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hive/hive.dart'; 3 | import 'package:weiman/crawler/http.dart'; 4 | import 'package:weiman/crawler/http18Comic.dart'; 5 | 6 | enum HideOption { 7 | none, 8 | auto, 9 | always, 10 | } 11 | 12 | class Setting with ChangeNotifier { 13 | static final String name = 'setting'; 14 | static Box settingBox; 15 | Http18Comic http; 16 | 17 | Setting() { 18 | MyHttpClient.init(getProxy(), 10000); 19 | } 20 | 21 | HideOption getHideOption() { 22 | final index = 23 | settingBox.get('hideOption', defaultValue: HideOption.auto.index); 24 | return HideOption.values[index]; 25 | } 26 | 27 | Future setHideOption(HideOption option) async { 28 | await settingBox.put('hideOption', option.index); 29 | notifyListeners(); 30 | } 31 | 32 | String getProxy() { 33 | print('getProxy'); 34 | return settingBox.get('proxy', defaultValue: null); 35 | } 36 | 37 | Future setProxy(String proxy) async { 38 | print('db/setting.setProxy $proxy'); 39 | await settingBox.put('proxy', proxy); 40 | MyHttpClient.init(proxy, 10000); 41 | notifyListeners(); 42 | } 43 | 44 | ThemeMode getThemeMode() { 45 | final int index = settingBox.get('theme', defaultValue: -1); 46 | if (index == -1) return ThemeMode.system; 47 | return ThemeMode.values[index]; 48 | } 49 | 50 | Future setThemeMode(ThemeMode mode) { 51 | return settingBox.put('theme', mode.index); 52 | } 53 | 54 | void refresh() { 55 | notifyListeners(); 56 | } 57 | 58 | Http18Comic getHttp() { 59 | final String name = 60 | settingBox.get('http', defaultValue: baseUrls.keys.first); 61 | final http = Http18Comic(baseUrls[name], name: name, headers: headers); 62 | setProxy(getProxy()); 63 | return http; 64 | } 65 | 66 | Future setHttp(HttpBook http) async { 67 | await settingBox.put('http', http.name); 68 | notifyListeners(); 69 | } 70 | 71 | bool getViewerSwitch() { 72 | return settingBox.get('viewerSwitch', defaultValue: true); 73 | } 74 | 75 | Future setViewerSwitch(bool value) async { 76 | await settingBox.put('viewerSwitch', value); 77 | notifyListeners(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:firebase_analytics/firebase_analytics.dart'; 5 | import 'package:firebase_analytics/observer.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/foundation.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/services.dart'; 10 | import 'package:hive/hive.dart'; 11 | import 'package:hive_flutter/hive_flutter.dart'; 12 | import 'package:oktoast/oktoast.dart'; 13 | import 'package:package_info/package_info.dart'; 14 | import 'package:path/path.dart' as path; 15 | import 'package:path_provider/path_provider.dart'; 16 | import 'package:provider/provider.dart'; 17 | import 'package:weiman/activities/dataConvert.dart'; 18 | import 'package:weiman/activities/home.dart'; 19 | import 'package:weiman/classes/data.dart'; 20 | import 'package:weiman/db/book.dart'; 21 | import 'package:weiman/db/group.dart'; 22 | import 'package:weiman/db/historyOffset.dart'; 23 | import 'package:weiman/db/setting.dart'; 24 | import 'package:weiman/provider/favoriteData.dart'; 25 | import 'package:weiman/provider/theme.dart'; 26 | 27 | FirebaseAnalytics analytics; 28 | FirebaseAnalyticsObserver observer; 29 | 30 | const bool isDevMode = !bool.fromEnvironment('dart.vm.product'); 31 | 32 | int version; 33 | BoxDecoration border; 34 | 35 | Directory imageCacheDir; 36 | String imageCacheDirPath; 37 | PackageInfo packageInfo; 38 | 39 | void main() async { 40 | print("开发模式 $isDevMode"); 41 | FlutterError.onError = (FlutterErrorDetails details) {}; 42 | WidgetsFlutterBinding.ensureInitialized(); 43 | await Firebase.initializeApp(); 44 | 45 | getTemporaryDirectory().then((dir) { 46 | imageCacheDir = Directory(path.join(dir.path, 'images')); 47 | imageCacheDirPath = imageCacheDir.path; 48 | if (imageCacheDir.existsSync() == false) imageCacheDir.createSync(); 49 | print('图片缓存目录 $imageCacheDirPath'); 50 | }); 51 | 52 | try { 53 | analytics = FirebaseAnalytics(); 54 | observer = FirebaseAnalyticsObserver(analytics: analytics); 55 | } catch (e) {} 56 | 57 | await Future.wait([ 58 | Hive.initFlutter(), 59 | Data.init(), 60 | SystemChrome.setPreferredOrientations( 61 | [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]) 62 | ]); 63 | Hive.registerAdapter(GroupAdapter()); 64 | Hive.registerAdapter(BookAdapter()); 65 | await Future.wait([ 66 | Hive.openBox(GroupName).then((value) => Group.groupBox = value), 67 | Hive.openBox(BookName) 68 | .then((value) => Book.bookBox = Group.bookBox = value), 69 | Hive.openBox(HistoryOffsetName).then((value) => HistoryOffset.box = value), 70 | Hive.openBox(Setting.name).then((value) => Setting.settingBox = value), 71 | ]); 72 | packageInfo = await PackageInfo.fromPlatform(); 73 | version = int.parse(packageInfo.buildNumber); 74 | runApp(Main()); 75 | } 76 | 77 | class Main extends StatefulWidget { 78 | @override 79 | _Main createState() => _Main(); 80 | } 81 | 82 | class _Main extends State
with WidgetsBindingObserver { 83 | @override 84 | void initState() { 85 | WidgetsBinding.instance.addObserver(this); 86 | super.initState(); 87 | } 88 | 89 | @override 90 | void didChangePlatformBrightness() { 91 | super.didChangePlatformBrightness(); 92 | Provider.of(context, listen: false).update(context); 93 | } 94 | 95 | @override 96 | Widget build(BuildContext context) { 97 | border = BoxDecoration( 98 | border: Border( 99 | bottom: Divider.createBorderSide(context, color: Colors.grey))); 100 | return OKToast( 101 | child: MultiProvider( 102 | providers: [ 103 | ChangeNotifierProvider( 104 | lazy: false, 105 | create: (_) => Setting(), 106 | ), 107 | ChangeNotifierProvider( 108 | lazy: false, 109 | create: (_) => FavoriteData(), 110 | ), 111 | ChangeNotifierProvider( 112 | lazy: true, 113 | create: (_) => ThemeProvider(_), 114 | ), 115 | ], 116 | child: Consumer( 117 | builder: (_, theme, __) => MaterialApp( 118 | title: '微漫 v${packageInfo.version}', 119 | themeMode: theme.themeMode, 120 | theme: ThemeData.light(), 121 | darkTheme: ThemeData( 122 | brightness: Brightness.dark, 123 | accentColor: Colors.redAccent, 124 | ), 125 | home: Data.hasData() ? ActivityDataConvert() : ActivityHome(), 126 | // home: ActivityHome(), 127 | debugShowCheckedModeBanner: isDevMode, 128 | navigatorObservers: [observer], 129 | ), 130 | ), 131 | ), 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/provider/favoriteData.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:weiman/db/book.dart'; 3 | import 'package:weiman/db/group.dart'; 4 | 5 | class FavoriteData extends ChangeNotifier { 6 | final List all = [], others = []; 7 | final Map> groups = {}; 8 | 9 | FavoriteData() { 10 | loadBooksList(); 11 | } 12 | 13 | Future loadBooksList([notify = false]) async { 14 | final groupList = Group.groupBox.values.toList(); 15 | final groupMap = {for (final group in groupList) group.key: group}; 16 | groups.clear(); 17 | groupList.forEach((group) { 18 | groups[group] = []; 19 | }); 20 | 21 | all.clear(); 22 | others.clear(); 23 | 24 | // if(isDevMode){ 25 | // final temp = [ 26 | // Book( 27 | // aid: '180454', 28 | // name: '朋友,女朋友', 29 | // avatar: 30 | // 'https://cdn-msp.18comic.org/media/albums/206567.jpg', 31 | // chapterCount: 0, 32 | // httpId: '18', 33 | // needUpdate: false, 34 | // authors: [], 35 | // ), 36 | // Book( 37 | // aid: '206567', 38 | // name: '抑欲人妻', 39 | // avatar: 40 | // 'https://cdn-msp.18comic.org/media/albums/206567.jpg', 41 | // chapterCount: 0, 42 | // httpId: '18', 43 | // needUpdate: true, 44 | // authors: [], 45 | // ), 46 | // Book( 47 | // aid: '147335', 48 | // name: '亲爱的大叔', 49 | // avatar: 50 | // 'https://cdn-msp.msp-comic.xyz/media/albums/147335.jpg', 51 | // chapterCount: 0, 52 | // httpId: '18', 53 | // needUpdate: true, 54 | // authors: [], 55 | // ), 56 | // ]; 57 | // all.addAll(temp); 58 | // others.addAll(temp); 59 | // } 60 | 61 | Book.bookBox.values.forEach((book) { 62 | if (book.favorite != true) return; 63 | all.add(book); 64 | if (groupMap.containsKey(book.groupId)) { 65 | //有分组的藏书 66 | groups[groupMap[book.groupId]].add(book); 67 | } else { 68 | //没有分组的藏书 69 | others.add(book); 70 | } 71 | }); 72 | 73 | print({'all': all.length, 'other': others.length}); 74 | 75 | if (notify) notifyListeners(); 76 | } 77 | 78 | Future checkUpdate() async { 79 | final groupList = [others, ...groups.values]; 80 | for (final array in groupList) { 81 | for (final book in array) { 82 | if (book.httpId == null) { 83 | book.status = BookUpdateStatus.old; 84 | } else if (book.needUpdate != true) { 85 | book.status = BookUpdateStatus.not; 86 | } else { 87 | book.status = BookUpdateStatus.wait; 88 | } 89 | notifyListeners(); 90 | if (book.status != BookUpdateStatus.wait) continue; 91 | book.status = BookUpdateStatus.loading; 92 | notifyListeners(); 93 | await book.update(); 94 | if (book.status == BookUpdateStatus.had) sort(array, book); 95 | notifyListeners(); 96 | } 97 | } 98 | return all.where((book) => book.status == BookUpdateStatus.had).length; 99 | } 100 | 101 | /// 显示在前排 102 | void sort(List array, Book book) { 103 | print('sort ${book.name}'); 104 | array.remove(book); 105 | array.insert(0, book); 106 | } 107 | 108 | Future deleteBook(Book book) async { 109 | book.favorite = false; 110 | await book.save(); 111 | // print('删书 ${book.name} 成功'); 112 | loadBooksList(true); 113 | } 114 | 115 | Future deleteGroup(Group group, [bool deleteBooks = false]) async { 116 | if (deleteBooks && groups.containsKey(group)) { 117 | await Future.wait(groups[group].map((book) => book.setFavorite(false))); 118 | } 119 | await Group.groupBox.delete(group.key); 120 | await loadBooksList(true); 121 | } 122 | 123 | Future addGroup(Group group) async { 124 | group.save(); 125 | await loadBooksList(true); 126 | } 127 | 128 | Future addBook(Book book) async { 129 | book.favorite = true; 130 | await book.save(); 131 | loadBooksList(true); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/provider/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:weiman/db/setting.dart'; 4 | 5 | class ThemeProvider extends ChangeNotifier { 6 | ThemeMode themeMode = ThemeMode.system; // 主题模式 7 | 8 | ThemeProvider(BuildContext context) { 9 | themeMode = Provider.of(context, listen: false).getThemeMode(); 10 | } 11 | 12 | void changeTheme(ThemeMode mode) { 13 | print('改变主题 $mode'); 14 | themeMode = mode; 15 | notifyListeners(); 16 | } 17 | 18 | void update(BuildContext context) { 19 | final bright = MediaQuery.platformBrightnessOf(context); 20 | switch (bright) { 21 | case Brightness.light: 22 | changeTheme(ThemeMode.light); 23 | break; 24 | case Brightness.dark: 25 | changeTheme(ThemeMode.dark); 26 | } 27 | print('update $bright'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:weiman/activities/book/book.dart'; 4 | import 'package:weiman/activities/chapter/activity.dart'; 5 | import 'package:weiman/activities/search/search.dart'; 6 | import 'package:weiman/classes/chapter.dart'; 7 | import 'package:weiman/db/book.dart'; 8 | 9 | final weekTime = Duration.millisecondsPerDay * 7; 10 | 11 | void openSearch(BuildContext context, String word) {} 12 | 13 | Future openBook(BuildContext context, Book book, String heroTag) { 14 | print('openBook $book'); 15 | if (book.http == null) { 16 | return Navigator.push( 17 | context, 18 | MaterialPageRoute( 19 | settings: RouteSettings(name: '/activity_search/${book.name}'), 20 | builder: (_) => ActivitySearch(search: book.name), 21 | ), 22 | ); 23 | } 24 | return Navigator.push( 25 | context, 26 | MaterialPageRoute( 27 | settings: RouteSettings(name: '/activity_book/${book.name}'), 28 | builder: (_) => ActivityBook(book: book, heroTag: heroTag), 29 | ), 30 | ); 31 | } 32 | 33 | Future openChapter(BuildContext context, Book book, Chapter chapter) { 34 | return Navigator.push( 35 | context, 36 | MaterialPageRoute( 37 | settings: RouteSettings( 38 | name: '/activity_chapter/${book.name}/${chapter.cname}'), 39 | builder: (_) => ActivityChapter(book, chapter), 40 | ), 41 | ); 42 | } 43 | 44 | void showStatusBar() { 45 | SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); 46 | } 47 | 48 | void hideStatusBar() { 49 | SystemChrome.setEnabledSystemUIOverlays([]); 50 | } 51 | -------------------------------------------------------------------------------- /lib/widgets/animatedLogo.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:sa_anicoto/sa_anicoto.dart'; 3 | 4 | class AnimatedLogoWidget extends StatefulWidget { 5 | final double width, height; 6 | 7 | const AnimatedLogoWidget({ 8 | Key key, 9 | @required this.width, 10 | @required this.height, 11 | }) : super(key: key); 12 | 13 | @override 14 | _AnimatedLogoWidget createState() => _AnimatedLogoWidget(); 15 | } 16 | 17 | class _AnimatedLogoWidget extends State 18 | with AnimationMixin { 19 | Animation size; // Declare animation variable 20 | 21 | @override 22 | void initState() { 23 | size = Tween(begin: 0, end: widget.height - 20).animate(controller); 24 | controller.mirror( 25 | duration: Duration(seconds: 1)); // Start the animation playback 26 | super.initState(); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Container( 32 | width: widget.width, 33 | height: widget.height, 34 | child: Stack( 35 | alignment: Alignment.center, 36 | children: [ 37 | Positioned( 38 | top: size.value, 39 | child: Image.asset( 40 | 'assets/logo.png', 41 | width: 20, 42 | height: 20, 43 | ), 44 | ), 45 | ], 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/widgets/book.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:weiman/classes/chapter.dart'; 5 | import 'package:weiman/classes/networkImageSSL.dart'; 6 | import 'package:weiman/db/book.dart'; 7 | import 'package:weiman/utils.dart'; 8 | 9 | class WidgetBook extends StatelessWidget { 10 | final Book book; 11 | final String subtitle; 12 | final Function(Book) onTap; 13 | 14 | const WidgetBook( 15 | this.book, { 16 | Key key, 17 | @required this.subtitle, 18 | this.onTap, 19 | }) : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final isLiked = book.favorite; 24 | return ListTile( 25 | title: Text( 26 | book.name, 27 | maxLines: 1, 28 | overflow: TextOverflow.ellipsis, 29 | ), 30 | subtitle: Text( 31 | subtitle, 32 | maxLines: 1, 33 | overflow: TextOverflow.ellipsis, 34 | ), 35 | dense: true, 36 | leading: Hero( 37 | tag: 'bookAvatar${book.aid}', 38 | child: ExtendedImage(image: NetworkImageSSL(book.http, book.avatar)), 39 | ), 40 | trailing: Icon( 41 | isLiked ? Icons.favorite : Icons.favorite_border, 42 | color: isLiked ? Colors.red : Colors.grey, 43 | size: 12, 44 | ), 45 | onTap: () { 46 | if (onTap != null) return onTap(book); 47 | openBook(context, book, 'bookAvatar${book.aid}'); 48 | }, 49 | ); 50 | } 51 | } 52 | 53 | final dateFormat = DateFormat('yyyy-MM-dd'); 54 | 55 | class WidgetChapter extends StatelessWidget { 56 | static final double height = kToolbarHeight; 57 | final Chapter chapter; 58 | final Function(Chapter) onTap; 59 | final bool read; 60 | 61 | WidgetChapter({ 62 | Key key, 63 | this.chapter, 64 | this.onTap, 65 | this.read = false, 66 | }) : super(key: key); 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | final children = [TextSpan(text: chapter.cname)]; 71 | if (read) { 72 | children.insert( 73 | 0, 74 | TextSpan( 75 | text: '[已看]', 76 | style: TextStyle(color: Colors.orange), 77 | )); 78 | } 79 | return ListTile( 80 | onTap: () { 81 | if (onTap != null) onTap(chapter); 82 | }, 83 | title: RichText( 84 | text: TextSpan( 85 | children: children, 86 | style: Theme.of(context).textTheme.bodyText2, 87 | ), 88 | softWrap: true, 89 | maxLines: 2, 90 | ), 91 | subtitle: chapter.time == null 92 | ? null 93 | : Text('更新时间 ${dateFormat.format(chapter.time)}'), 94 | ); 95 | } 96 | } 97 | 98 | class WidgetHistory extends StatelessWidget { 99 | final Book book; 100 | final Function(Book book) onTap; 101 | 102 | WidgetHistory(this.book, this.onTap); 103 | 104 | @override 105 | Widget build(BuildContext context) { 106 | return SliverToBoxAdapter( 107 | child: ListTile( 108 | onTap: () { 109 | if (onTap != null) onTap(book); 110 | }, 111 | title: Text(book.name), 112 | leading: Image( 113 | image: ExtendedNetworkImageProvider(book.avatar, cache: true), 114 | fit: BoxFit.fitHeight, 115 | ), 116 | subtitle: Text(book.history.cname), 117 | ), 118 | ); 119 | } 120 | } 121 | 122 | class WidgetBookCheckNew extends StatefulWidget { 123 | final Book book; 124 | 125 | const WidgetBookCheckNew({Key key, this.book}) : super(key: key); 126 | 127 | @override 128 | _WidgetBookCheckNew createState() => _WidgetBookCheckNew(); 129 | } 130 | 131 | class _WidgetBookCheckNew extends State { 132 | bool loading = true, hasError = false; 133 | int news; 134 | 135 | @override 136 | void initState() { 137 | super.initState(); 138 | load(); 139 | } 140 | 141 | void load() async { 142 | // loading = true; 143 | // try { 144 | // final book = await Http18Comic.instance 145 | // .getBook(widget.book.aid) 146 | // .timeout(Duration(seconds: 2)); 147 | // news = book.chapterCount - widget.book.chapterCount; 148 | // hasError = false; 149 | // } catch (e) { 150 | // hasError = true; 151 | // } 152 | // loading = false; 153 | // setState(() {}); 154 | } 155 | 156 | @override 157 | Widget build(BuildContext context) { 158 | final children = []; 159 | if (widget.book.history != null) 160 | children.add(Text( 161 | widget.book.history.cname, 162 | maxLines: 1, 163 | overflow: TextOverflow.ellipsis, 164 | )); 165 | 166 | if (loading) 167 | children.add(Text('检查更新中')); 168 | else if (hasError) 169 | children.add(Text('网络错误')); 170 | else if (news > 0) 171 | children.add(Text('有 $news 章更新')); 172 | else 173 | children.add(Text('没有更新')); 174 | return ListTile( 175 | onTap: () => 176 | openBook(context, widget.book, 'checkBook${widget.book.aid}'), 177 | leading: Hero( 178 | tag: 'checkBook${widget.book.aid}', 179 | child: Image( 180 | image: 181 | ExtendedNetworkImageProvider(widget.book.avatar, cache: true)), 182 | ), 183 | dense: true, 184 | isThreeLine: true, 185 | title: Text(widget.book.name), 186 | subtitle: Column( 187 | crossAxisAlignment: CrossAxisAlignment.start, 188 | children: children, 189 | ), 190 | ); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /lib/widgets/bookGroup.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_slidable/flutter_slidable.dart'; 5 | import 'package:flutter_sticky_header/flutter_sticky_header.dart'; 6 | import 'package:weiman/db/group.dart'; 7 | 8 | class BookGroupHeader extends StatefulWidget { 9 | final Group group; 10 | final int count; 11 | final List actions; 12 | final Color divideColor; 13 | final double height; 14 | final IndexedWidgetBuilder builder; 15 | final List slideActions; 16 | 17 | const BookGroupHeader({ 18 | Key key, 19 | @required this.group, 20 | @required this.count, 21 | @required this.builder, 22 | this.actions = const [], 23 | this.divideColor = Colors.grey, 24 | this.height = kToolbarHeight, 25 | this.slideActions, 26 | }) : assert(group != null), 27 | assert(builder != null), 28 | super(key: key); 29 | 30 | @override 31 | _State createState() => _State(); 32 | } 33 | 34 | class _State extends State { 35 | bool expended; 36 | 37 | @override 38 | void initState() { 39 | expended = widget.group.expended ?? false; 40 | super.initState(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | Decoration _decoration = BoxDecoration( 46 | border: Border( 47 | bottom: Divider.createBorderSide(context, color: widget.divideColor), 48 | ), 49 | ); 50 | Widget header = InkWell( 51 | child: Container( 52 | height: widget.height, 53 | alignment: Alignment.centerLeft, 54 | decoration: BoxDecoration( 55 | color: Theme.of(context).backgroundColor, 56 | ), 57 | child: Row(children: [ 58 | Transform.rotate( 59 | angle: expended ? 0 : math.pi, 60 | child: Icon( 61 | Icons.arrow_drop_down, 62 | color: Colors.grey, 63 | ), 64 | ), 65 | Expanded(child: Text('${widget.group.name}(${widget.count})')), 66 | ...widget.actions, 67 | ]), 68 | ), 69 | onTap: () { 70 | expended = !expended; 71 | widget.group 72 | ..expended = expended 73 | ..save(); 74 | setState(() {}); 75 | }, 76 | ); 77 | if (widget.slideActions != null && widget.slideActions.length > 0) { 78 | header = Slidable( 79 | child: header, 80 | actionPane: SlidableDrawerActionPane(), 81 | secondaryActions: widget.slideActions, 82 | ); 83 | } 84 | return SliverStickyHeader( 85 | header: header, 86 | sliver: expended 87 | ? SliverList( 88 | delegate: SliverChildBuilderDelegate( 89 | (ctx, i) { 90 | if (i < widget.count - 1) { 91 | return DecoratedBox( 92 | decoration: _decoration, 93 | child: widget.builder(context, i), 94 | ); 95 | } 96 | return widget.builder(context, i); 97 | }, 98 | childCount: widget.count, 99 | )) 100 | : null, 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/widgets/bookSettingDialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:weiman/db/book.dart'; 4 | import 'package:weiman/db/group.dart'; 5 | import 'package:weiman/provider/favoriteData.dart'; 6 | import 'package:weiman/widgets/groupFormDialog.dart'; 7 | 8 | Future showBookSettingDialog(BuildContext context, Book book) { 9 | return showDialog( 10 | context: context, 11 | builder: (_) => AlertDialog( 12 | title: Text('藏书《${book.name}》的设置'), 13 | scrollable: true, 14 | content: WidgetSetting(book: book), 15 | ), 16 | ); 17 | } 18 | 19 | class WidgetSetting extends StatefulWidget { 20 | final Book book; 21 | 22 | const WidgetSetting({Key key, this.book}) : super(key: key); 23 | 24 | @override 25 | _WidgetSetting createState() => _WidgetSetting(); 26 | } 27 | 28 | class _WidgetSetting extends State { 29 | static final updateMenus = {true: '自动', false: '不检查'}; 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return Column( 34 | mainAxisSize: MainAxisSize.min, 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | children: ListTile.divideTiles(context: context, tiles: [ 37 | ListTile( 38 | title: Text('检查更新'), 39 | trailing: DropdownButton( 40 | value: widget.book.needUpdate, 41 | items: updateMenus.keys 42 | .map((key) => 43 | DropdownMenuItem(value: key, child: Text(updateMenus[key]))) 44 | .toList(), 45 | onChanged: changeUpdate, 46 | ), 47 | ), 48 | ListTile( 49 | title: Text('分组'), 50 | trailing: DropdownButton( 51 | hint: Text('没有分组'), 52 | value: widget.book.group, 53 | items: [ 54 | DropdownMenuItem( 55 | child: Text('新建'), 56 | value: null, 57 | ), 58 | ...Group.groupBox.values 59 | .map((e) => DropdownMenuItem(value: e, child: Text(e.name))) 60 | .toList(), 61 | ], 62 | onChanged: changeGroup, 63 | ), 64 | ), 65 | ]).toList(), 66 | ); 67 | } 68 | 69 | changeUpdate(bool needUpdate) async { 70 | widget.book.needUpdate = needUpdate; 71 | await widget.book.save(); 72 | setState(() {}); 73 | } 74 | 75 | changeGroup(Group group) async { 76 | if (group == null) { 77 | group = await showGroupFormDialog(context); 78 | } 79 | widget.book.groupId = group == null ? widget.book.groupId : group.key; 80 | await widget.book.save(); 81 | setState(() {}); 82 | } 83 | 84 | changeFavorite() async { 85 | await widget.book.setFavorite(!widget.book.favorite); 86 | setState(() {}); 87 | } 88 | 89 | removeHistory() async { 90 | if (widget.book.history != null) await widget.book.setHistory(null); 91 | setState(() {}); 92 | } 93 | 94 | @override 95 | void setState(fn) { 96 | final fav = Provider.of(context, listen: false); 97 | fav.loadBooksList(true); 98 | super.setState(fn); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/widgets/checkConnect/checkConnect.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:extended_image/extended_image.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:html/parser.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:weiman/crawler/http.dart'; 7 | import 'package:weiman/crawler/http18Comic.dart'; 8 | import 'package:weiman/db/setting.dart'; 9 | 10 | class CheckConnectWidget extends StatefulWidget { 11 | @override 12 | _CheckConnectWidget createState() => _CheckConnectWidget(); 13 | } 14 | 15 | class _CheckConnectWidget extends State { 16 | LoadState state = LoadState.loading; 17 | final List<_Check> https = []; 18 | String lastProxy; 19 | 20 | @override 21 | void initState() { 22 | final setting = Provider.of(context, listen: false); 23 | lastProxy = setting.getProxy(); 24 | createHttps(); 25 | super.initState(); 26 | setting.addListener(() { 27 | final proxy = setting.getProxy(); 28 | if (lastProxy != proxy) { 29 | lastProxy = proxy; 30 | createHttps(); 31 | } 32 | }); 33 | } 34 | 35 | void createHttps() { 36 | print('重建http池 proxy:$lastProxy'); 37 | https.clear(); 38 | https.addAll( 39 | baseUrls.keys.map( 40 | (key) => _Check( 41 | name: key, 42 | url: baseUrls[key], 43 | proxy: lastProxy, 44 | ), 45 | ), 46 | ); 47 | check(); 48 | } 49 | 50 | void check() async { 51 | setState(() { 52 | state = LoadState.loading; 53 | }); 54 | https.forEach((http) => http.load()); 55 | await Future.wait(https.map((http) => http.load())); 56 | final bool hasCompleted = 57 | https.where((http) => http.state == LoadState.completed).isNotEmpty; 58 | state = hasCompleted ? LoadState.completed : LoadState.failed; 59 | if (hasCompleted) { 60 | final sort = https.toList()..sort((a, b) => a.time.compareTo(b.time)); 61 | Http18Comic.instance = sort.first.http; 62 | } 63 | setState(() {}); 64 | } 65 | 66 | void _showDialog(String title) async { 67 | await showDialog( 68 | context: context, 69 | builder: (_) => Dialog( 70 | title: title, 71 | https: https, 72 | retry: check, 73 | ), 74 | ); 75 | } 76 | 77 | @override 78 | Widget build(BuildContext context) { 79 | Widget row; 80 | switch (state) { 81 | case LoadState.loading: 82 | row = Row( 83 | mainAxisAlignment: MainAxisAlignment.center, 84 | children: [ 85 | SizedBox( 86 | width: 20, 87 | height: 20, 88 | child: CircularProgressIndicator(strokeWidth: 2), 89 | ), 90 | SizedBox(width: 10), 91 | Text('正在尝试连接漫画网站'), 92 | ], 93 | ); 94 | break; 95 | case LoadState.failed: 96 | row = Row( 97 | mainAxisAlignment: MainAxisAlignment.center, 98 | children: [ 99 | SizedBox( 100 | width: 20, 101 | height: 20, 102 | child: Icon(Icons.error, color: Colors.red), 103 | ), 104 | SizedBox(width: 10), 105 | Text('连接不上漫画网站,点击查看错误'), 106 | ], 107 | ); 108 | break; 109 | default: 110 | row = Row( 111 | mainAxisAlignment: MainAxisAlignment.center, 112 | children: [ 113 | SizedBox( 114 | width: 20, 115 | height: 20, 116 | child: Icon(Icons.check_circle, color: Colors.green), 117 | ), 118 | SizedBox(width: 10), 119 | Text('成功连接到漫画网站,点击查看结果'), 120 | ], 121 | ); 122 | } 123 | return Padding( 124 | padding: EdgeInsets.only(top: 10, bottom: 15), 125 | child: GestureDetector( 126 | child: row, 127 | onTap: () => _showDialog('测试结果,选择源'), 128 | ), 129 | ); 130 | } 131 | } 132 | 133 | class Dialog extends StatefulWidget { 134 | final String title; 135 | final List<_Check> https; 136 | final Function retry; 137 | 138 | const Dialog({Key key, this.title, this.https, this.retry}) : super(key: key); 139 | 140 | @override 141 | State createState() => _Dialog(); 142 | } 143 | 144 | class _Dialog extends State { 145 | @override 146 | Widget build(BuildContext context) { 147 | final proxy = widget.https[0].proxy; 148 | return AlertDialog( 149 | title: Column( 150 | crossAxisAlignment: CrossAxisAlignment.start, 151 | children: [ 152 | Text(widget.title), 153 | if (proxy != null) 154 | Text('正在使用代理:$proxy', style: TextStyle(fontSize: 14)), 155 | ], 156 | ), 157 | content: Container( 158 | width: 300, 159 | height: 300, 160 | child: ListView( 161 | physics: ClampingScrollPhysics(), 162 | shrinkWrap: true, 163 | children: ListTile.divideTiles( 164 | context: context, 165 | tiles: widget.https.map( 166 | (e) => e.build(onTap: () => setState(() {})), 167 | )).toList(), 168 | ), 169 | ), 170 | actions: [ 171 | FlatButton( 172 | child: Text('再次测试'), 173 | onPressed: () { 174 | widget.retry(); 175 | setState(() {}); 176 | }, 177 | ), 178 | ], 179 | ); 180 | } 181 | } 182 | 183 | class _Check { 184 | final String name; 185 | final String proxy; 186 | Http18Comic http; 187 | Future future; 188 | Duration time; 189 | String error; 190 | LoadState state; 191 | 192 | _Check({ 193 | String url, 194 | @required this.name, 195 | @required this.proxy, 196 | }) { 197 | http = Http18Comic( 198 | url, 199 | name: name, 200 | headers: headers, 201 | proxy: proxy, 202 | ); 203 | } 204 | 205 | Future load() { 206 | future = this._load(); 207 | return future; 208 | } 209 | 210 | Future _load() async { 211 | state = LoadState.loading; 212 | final now = DateTime.now(); 213 | try { 214 | final Response res = await http.dio.get('/'); 215 | final $ = parse(res.data); 216 | final $title = $.querySelector('title'); 217 | if (res.data.contains('Restricted') || 218 | $title == null || 219 | $title.text.indexOf('禁漫天堂') == -1) { 220 | throw DioError( 221 | request: res.request, 222 | response: res, 223 | error: '你使用的IP被漫画网站禁止访问,请更换网络IP\n不要使用日本IP。', 224 | ); 225 | } 226 | state = LoadState.completed; 227 | } catch (e) { 228 | print(e); 229 | if (e.runtimeType == DioError) { 230 | final DioError error = e as DioError; 231 | switch (error.type) { 232 | case DioErrorType.CONNECT_TIMEOUT: 233 | case DioErrorType.RECEIVE_TIMEOUT: 234 | case DioErrorType.SEND_TIMEOUT: 235 | this.error = '连接超时'; 236 | break; 237 | default: 238 | this.error = error.error.toString(); 239 | } 240 | if (error.response?.data != null) { 241 | this.error += '\n接收到的内容:\n' + error.response.data; 242 | } 243 | } else { 244 | this.error = e.toString(); 245 | } 246 | state = LoadState.failed; 247 | print('$name 结果 $state'); 248 | } 249 | time = DateTime.now().difference(now); 250 | } 251 | 252 | Widget build({Function onTap}) { 253 | return FutureBuilder( 254 | future: future, 255 | builder: (BuildContext context, AsyncSnapshot snapshot) { 256 | final Widget title = Text(name); 257 | switch (snapshot.connectionState) { 258 | case ConnectionState.active: 259 | case ConnectionState.waiting: 260 | return ListTile( 261 | title: title, 262 | subtitle: Row(children: [ 263 | SizedBox( 264 | width: 14, 265 | height: 14, 266 | child: CircularProgressIndicator( 267 | strokeWidth: 2, 268 | ), 269 | ), 270 | SizedBox(width: 5), 271 | Text('读取中'), 272 | ]), 273 | ); 274 | break; 275 | case ConnectionState.done: 276 | if (state == LoadState.failed) { 277 | return ListTile( 278 | title: title, 279 | subtitle: Text('连接失败,点击查看原因'), 280 | onTap: () { 281 | showDialog( 282 | context: context, 283 | builder: (_) { 284 | return AlertDialog( 285 | title: Text('$name 错误内容'), 286 | content: Text(error), 287 | ); 288 | }); 289 | }, 290 | ); 291 | } 292 | final _time = time.inMilliseconds; 293 | final timeString = _time > 1000 294 | ? '${(time.inMilliseconds / 1000).toStringAsFixed(2)} 秒' 295 | : '${time.inMilliseconds} 毫秒'; 296 | return CheckboxListTile( 297 | title: title, 298 | subtitle: Text('连接成功\n耗时:$timeString'), 299 | isThreeLine: true, 300 | value: Http18Comic.instance?.name == name, 301 | onChanged: (name) { 302 | Http18Comic.instance = http; 303 | MyHttpClient.clients[http.id] = http; 304 | onTap(); 305 | }, 306 | ); 307 | break; 308 | default: 309 | return ListTile(title: title, subtitle: Text('还没有开始网络请求')); 310 | } 311 | }, 312 | ); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /lib/widgets/dbSourceListWidget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DBSourceListWidget extends StatefulWidget { 4 | @override 5 | _DBSourceListWidget createState() => _DBSourceListWidget(); 6 | } 7 | 8 | class _DBSourceListWidget extends State { 9 | @override 10 | Widget build(BuildContext context) { 11 | return ListView(children: []); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/widgets/deleteGroupDialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:weiman/db/group.dart'; 4 | import 'package:weiman/provider/favoriteData.dart'; 5 | 6 | Future showDeleteGroupDialog(BuildContext context, Group group) { 7 | return showDialog( 8 | context: context, 9 | builder: (_) => DeleteGroupWidget(group: group), 10 | ); 11 | } 12 | 13 | class DeleteGroupWidget extends StatefulWidget { 14 | final Group group; 15 | 16 | const DeleteGroupWidget({Key key, this.group}) : super(key: key); 17 | 18 | @override 19 | _DeleteGroupWidget createState() => _DeleteGroupWidget(); 20 | } 21 | 22 | class _DeleteGroupWidget extends State { 23 | bool deleteBooks = false; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final length = widget.group.books.length; 28 | return AlertDialog( 29 | title: Text('删除分组 ${widget.group.name}'), 30 | scrollable: true, 31 | content: Column( 32 | children: ListTile.divideTiles(context: context, tiles: [ 33 | if (length > 0) 34 | ListTile( 35 | title: Text('删除藏书'), 36 | subtitle: Text('有 $length 本藏书'), 37 | trailing: Checkbox( 38 | value: deleteBooks, 39 | onChanged: (v) => setState(() => deleteBooks = v), 40 | ), 41 | ) 42 | ]).toList(), 43 | ), 44 | actions: [ 45 | FlatButton( 46 | child: Text('确认'), 47 | onPressed: () async { 48 | await Provider.of(context, listen: false) 49 | .deleteGroup(widget.group, deleteBooks); 50 | Navigator.pop(context); 51 | }, 52 | ), 53 | RaisedButton( 54 | child: Text('取消'), onPressed: () => Navigator.pop(context)), 55 | ], 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/widgets/favorites.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/scheduler.dart'; 4 | import 'package:flutter_slidable/flutter_slidable.dart'; 5 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 6 | import 'package:oktoast/oktoast.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:weiman/classes/networkImageSSL.dart'; 9 | import 'package:weiman/db/book.dart'; 10 | import 'package:weiman/db/group.dart'; 11 | import 'package:weiman/provider/favoriteData.dart'; 12 | import 'package:weiman/utils.dart'; 13 | import 'package:weiman/widgets/bookGroup.dart'; 14 | import 'package:weiman/widgets/bookSettingDialog.dart'; 15 | import 'package:weiman/widgets/deleteGroupDialog.dart'; 16 | import 'package:weiman/widgets/groupFormDialog.dart'; 17 | import 'package:weiman/widgets/sliverExpandableGroup.dart'; 18 | import 'package:weiman/widgets/utils.dart'; 19 | 20 | class FavoriteList extends StatefulWidget { 21 | @override 22 | _FavoriteList createState() => _FavoriteList(); 23 | } 24 | 25 | class _FavoriteList extends State { 26 | static bool showTip = true; 27 | 28 | @override 29 | initState() { 30 | super.initState(); 31 | if (showTip) { 32 | SchedulerBinding.instance.addPostFrameCallback((timeStamp) { 33 | showToast( 34 | '下拉收藏列表检查更新\n分组和藏书左滑显示更多操作', 35 | textPadding: EdgeInsets.all(10), 36 | duration: Duration(seconds: 4), 37 | ); 38 | showTip = false; 39 | }); 40 | } 41 | } 42 | 43 | Widget bookBuilder(Book book) { 44 | return FBookItem( 45 | book: book, 46 | onDelete: deleteBook, 47 | ); 48 | } 49 | 50 | deleteBook(Book book) async { 51 | final sure = await showDialog( 52 | context: context, 53 | builder: (_) => AlertDialog( 54 | title: Text('删除藏书 ${book.name} ?'), 55 | actions: [ 56 | FlatButton( 57 | child: Text('确认'), 58 | onPressed: () { 59 | Navigator.pop(context, true); 60 | }), 61 | ], 62 | ), 63 | ); 64 | print('删书 $sure'); 65 | if (sure != true) return; 66 | 67 | await Provider.of(context, listen: false).deleteBook(book); 68 | } 69 | 70 | Future deleteGroup(Group group) async { 71 | await showDeleteGroupDialog(context, group); 72 | setState(() {}); 73 | } 74 | 75 | Future groupRename(Group group) async { 76 | await showGroupFormDialog(context, group); 77 | setState(() {}); 78 | } 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | return Consumer(builder: (_, favorite, __) { 83 | if (favorite.all.isEmpty && favorite.groups.keys.isEmpty) 84 | return Center(child: Text('没有收藏')); 85 | return ClipRect( 86 | child: RefreshIndicator( 87 | onRefresh: favorite.checkUpdate, 88 | child: SafeArea( 89 | child: CustomScrollView( 90 | slivers: [ 91 | ...favorite.groups.keys.map((group) { 92 | final list = favorite.groups[group]; 93 | return BookGroupHeader( 94 | group: group, 95 | count: list.length, 96 | builder: (ctx, i) => bookBuilder(favorite.groups[group][i]), 97 | slideActions: [ 98 | IconSlideAction( 99 | caption: '重命名', 100 | color: Colors.blue, 101 | icon: Icons.edit, 102 | onTap: () => groupRename(group), 103 | ), 104 | IconSlideAction( 105 | caption: '删除', 106 | color: Colors.red, 107 | icon: Icons.delete, 108 | onTap: () => deleteGroup(group), 109 | ), 110 | ], 111 | ); 112 | }), 113 | SliverExpandableGroup( 114 | title: Text('没有分组的藏书(${favorite.others.length})'), 115 | expanded: false, 116 | count: favorite.others.length, 117 | builder: (ctx, i) => bookBuilder(favorite.others[i]), 118 | ), 119 | ], 120 | ), 121 | ), 122 | ), 123 | ); 124 | }); 125 | } 126 | } 127 | 128 | final bookStatusWidgets = { 129 | BookUpdateStatus.loading: 130 | TextSpan(text: '正在读取网络数据', style: TextStyle(color: Colors.blue)), 131 | BookUpdateStatus.not: 132 | TextSpan(text: '该藏书设置为不更新', style: TextStyle(color: Colors.grey)), 133 | BookUpdateStatus.no: 134 | TextSpan(text: '该藏书没有新章节', style: TextStyle(color: Colors.grey)), 135 | BookUpdateStatus.wait: 136 | TextSpan(text: '处于更新队列,等待更新', style: TextStyle(color: Colors.grey)), 137 | BookUpdateStatus.old: 138 | TextSpan(text: '旧藏书不检查更新', style: TextStyle(color: Colors.redAccent)), 139 | BookUpdateStatus.fail: 140 | TextSpan(text: '网络问题,检查更新失败', style: TextStyle(color: Colors.redAccent)), 141 | }; 142 | 143 | class FBookItem extends StatefulWidget { 144 | final Book book; 145 | final void Function(Book book) onDelete; 146 | 147 | const FBookItem({ 148 | Key key, 149 | @required this.book, 150 | @required this.onDelete, 151 | }) : super(key: key); 152 | 153 | @override 154 | _FBookItem createState() => _FBookItem(); 155 | } 156 | 157 | class _FBookItem extends State { 158 | @override 159 | Widget build(BuildContext context) { 160 | TextSpan subtitle = 161 | bookStatusWidgets[widget.book.status ?? BookUpdateStatus.no]; 162 | if (widget.book.status == BookUpdateStatus.had) { 163 | final _subtitle = '有 ${widget.book.newChapterCount} 章更新'; 164 | subtitle = TextSpan( 165 | text: _subtitle, 166 | style: TextStyle( 167 | color: widget.book.look ? Colors.grey : Colors.green, 168 | ), 169 | ); 170 | } 171 | return Slidable( 172 | actionPane: SlidableDrawerActionPane(), 173 | closeOnScroll: true, 174 | actionExtentRatio: 0.25, 175 | secondaryActions: [ 176 | IconSlideAction( 177 | caption: '设置', 178 | color: Colors.blue, 179 | icon: Icons.settings, 180 | onTap: () async { 181 | final before = widget.book.needUpdate; 182 | await showBookSettingDialog(context, widget.book); 183 | if (before != widget.book.needUpdate) { 184 | widget.book.status = widget.book.needUpdate 185 | ? BookUpdateStatus.no 186 | : BookUpdateStatus.not; 187 | } 188 | if (mounted) setState(() {}); 189 | }, 190 | ), 191 | if (widget.book.status == BookUpdateStatus.had && 192 | widget.book.look == false) 193 | IconSlideAction( 194 | caption: '已读', 195 | color: Colors.greenAccent, 196 | iconWidget: SizedBox( 197 | width: 24, 198 | height: 24, 199 | child: Icon( 200 | FontAwesomeIcons.bellSlash, 201 | size: 20, 202 | color: Colors.white, 203 | ), 204 | ), 205 | foregroundColor: Colors.white, 206 | onTap: () async { 207 | widget.book.chapterCount += widget.book.newChapterCount; 208 | widget.book.look = true; 209 | await widget.book.save(); 210 | setState(() {}); 211 | }, 212 | ), 213 | IconSlideAction( 214 | caption: '删除', 215 | color: Colors.red, 216 | icon: Icons.delete, 217 | onTap: () => widget.onDelete(widget.book), 218 | ), 219 | ], 220 | child: ListTile( 221 | onTap: () async { 222 | await openBook(context, widget.book, 'fb ${widget.book.aid}'); 223 | setState(() {}); 224 | }, 225 | // onLongPress: () => onDelete(book), 226 | leading: Hero( 227 | tag: 'fb ${widget.book.aid}', 228 | child: widget.book.http == null 229 | ? oldBookAvatar(text: '旧书', width: 50.0, height: 80.0) 230 | : ExtendedImage( 231 | image: NetworkImageSSL(widget.book.http, widget.book.avatar), 232 | width: 50.0, 233 | height: 80.0), 234 | ), 235 | title: Text(widget.book.name), 236 | subtitle: RichText(text: subtitle), 237 | ), 238 | ); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /lib/widgets/groupFormDialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:weiman/db/group.dart'; 4 | 5 | Future showGroupFormDialog(BuildContext context, [Group group]) { 6 | return showDialog( 7 | context: context, 8 | builder: (_) { 9 | return GroupFormDialog(group: group); 10 | }, 11 | ); 12 | } 13 | 14 | class GroupFormDialog extends StatefulWidget { 15 | final Group group; 16 | 17 | const GroupFormDialog({Key key, this.group}) : super(key: key); 18 | 19 | @override 20 | _GroupFormDialog createState() => _GroupFormDialog(); 21 | } 22 | 23 | class _GroupFormDialog extends State { 24 | final _form = GlobalKey(); 25 | TextEditingController _nameController; 26 | Group group; 27 | 28 | @override 29 | void initState() { 30 | group = widget.group; 31 | _nameController = TextEditingController(text: widget.group?.name ?? ''); 32 | super.initState(); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return AlertDialog( 38 | title: Text(widget.group == null ? '创建分组' : '分组重命名'), 39 | content: Form( 40 | key: _form, 41 | child: Column( 42 | mainAxisSize: MainAxisSize.min, 43 | children: [ 44 | TextFormField( 45 | autofocus: true, 46 | controller: _nameController, 47 | decoration: InputDecoration.collapsed( 48 | hintText: group == null ? '输入分组名称' : '原名 ${group.name}', 49 | ), 50 | validator: (value) { 51 | value = value.trim(); 52 | if (value.isEmpty) { 53 | return '分组名称不能为空'; 54 | } 55 | final sameName = 56 | Group.groupBox.values.firstWhere((Group group) { 57 | return group.name == value && group.key != this.group?.key; 58 | }, orElse: () => null); 59 | if (sameName != null) { 60 | return '已经存在同名的分组'; 61 | } 62 | return null; 63 | }, 64 | ) 65 | ], 66 | ), 67 | ), 68 | actions: [ 69 | FlatButton( 70 | child: Text('确认'), 71 | onPressed: () async { 72 | if (group == null) { 73 | group = Group(_nameController.text); 74 | } else { 75 | group.name = _nameController.text; 76 | } 77 | await group.save(); 78 | Navigator.pop(context, group); 79 | }, 80 | ), 81 | RaisedButton( 82 | child: Text('取消'), 83 | textColor: Colors.white, 84 | color: Colors.blue, 85 | onPressed: () { 86 | Navigator.pop(context, group); 87 | }, 88 | ), 89 | ], 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/widgets/histories.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/scheduler.dart'; 4 | import 'package:flutter_slidable/flutter_slidable.dart'; 5 | import 'package:oktoast/oktoast.dart'; 6 | 7 | import 'package:weiman/classes/networkImageSSL.dart'; 8 | import 'package:weiman/db/book.dart'; 9 | import 'package:weiman/utils.dart'; 10 | import 'package:weiman/widgets/sliverExpandableGroup.dart'; 11 | import 'package:weiman/widgets/utils.dart'; 12 | 13 | class Histories extends StatefulWidget { 14 | @override 15 | _Histories createState() => _Histories(); 16 | } 17 | 18 | class _Histories extends State { 19 | static bool _showTips = true; 20 | final List inWeek = [], other = []; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | loadBook(); 26 | if (_showTips) 27 | SchedulerBinding.instance.addPostFrameCallback((timeStamp) { 28 | _showTips = false; 29 | showToast( 30 | '阅读记录和时间分组\n往左滑显示更多操作', 31 | textPadding: EdgeInsets.all(10), 32 | duration: Duration(seconds: 4), 33 | ); 34 | }); 35 | } 36 | 37 | void loadBook() { 38 | inWeek.clear(); 39 | other.clear(); 40 | final list = 41 | Book.bookBox.values.where((book) => book.history != null).toList(); 42 | final now = DateTime.now().millisecondsSinceEpoch; 43 | list.sort((a, b) => b.history.time.compareTo(a.history.time)); 44 | list.forEach((book) { 45 | if ((now - book.history.time.millisecondsSinceEpoch) < weekTime) { 46 | inWeek.add(book); 47 | } else { 48 | other.add(book); 49 | } 50 | }); 51 | } 52 | 53 | void clear(bool inWeek) async { 54 | final title = '确认清空 ' + (inWeek ? '7天内的' : '更早的') + '浏览记录 ?'; 55 | final res = await showDialog( 56 | context: context, 57 | builder: (_) => AlertDialog( 58 | title: Text(title), 59 | actions: [ 60 | FlatButton( 61 | textColor: Colors.grey, 62 | child: Text('取消'), 63 | onPressed: () => Navigator.pop(context, false), 64 | ), 65 | FlatButton( 66 | child: Text('确认'), 67 | onPressed: () => Navigator.pop(context, true), 68 | ), 69 | ], 70 | )); 71 | print('清理历史 $inWeek $res'); 72 | if (res == false) return; 73 | List list = inWeek ? this.inWeek : this.other; 74 | await Future.wait(list.map((book) => book.setHistory(null))); 75 | setState(() { 76 | loadBook(); 77 | }); 78 | } 79 | 80 | Widget book(List array, int index) { 81 | final Book book = array[index]; 82 | return Slidable( 83 | child: ListTile( 84 | leading: book.http == null 85 | ? oldBookAvatar(text: '旧\n书', width: 50.0, height: 80.0) 86 | : ExtendedImage( 87 | image: NetworkImageSSL(book.http, book.avatar), 88 | width: 50.0, 89 | height: 80.0), 90 | title: Text(book.name), 91 | subtitle: Text(book.history.cname), 92 | onTap: () => openBook(context, book, 'fb ${book.aid}'), 93 | ), 94 | actionPane: SlidableDrawerActionPane(), 95 | secondaryActions: [ 96 | IconSlideAction( 97 | caption: '删除', 98 | color: Colors.red, 99 | icon: Icons.delete, 100 | onTap: () async { 101 | await book.setHistory(null); 102 | setState(() { 103 | array.remove(book); 104 | }); 105 | }, 106 | ), 107 | ], 108 | ); 109 | } 110 | 111 | @override 112 | Widget build(BuildContext context) { 113 | return SafeArea( 114 | child: ClipRect( 115 | child: CustomScrollView( 116 | slivers: [ 117 | SliverExpandableGroup( 118 | title: Text('7天内的浏览历史 (${inWeek.length})'), 119 | expanded: true, 120 | count: inWeek.length, 121 | builder: (ctx, i) => book(inWeek, i), 122 | slideActions: [ 123 | IconSlideAction( 124 | caption: '清空', 125 | color: Colors.red, 126 | icon: Icons.delete, 127 | onTap: () => clear(true), 128 | ), 129 | ], 130 | ), 131 | SliverExpandableGroup( 132 | title: Text('更早的浏览历史 (${other.length})'), 133 | count: other.length, 134 | builder: (ctx, i) => book(other, i), 135 | slideActions: [ 136 | IconSlideAction( 137 | caption: '清空', 138 | color: Colors.red, 139 | icon: Icons.delete, 140 | onTap: () => clear(false), 141 | ), 142 | ], 143 | ), 144 | ], 145 | ), 146 | ), 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/widgets/pullToRefreshHeader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; 3 | import 'package:weiman/widgets/animatedLogo.dart'; 4 | 5 | class SliverPullToRefreshHeader extends StatelessWidget { 6 | static final double height = kToolbarHeight * 2; 7 | final PullToRefreshScrollNotificationInfo info; 8 | final void Function() onTap; 9 | final double fontSize; 10 | 11 | const SliverPullToRefreshHeader({ 12 | Key key, 13 | @required this.info, 14 | this.onTap, 15 | this.fontSize = 16, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | if (info == null) return SliverToBoxAdapter(child: SizedBox()); 21 | double dragOffset = info?.dragOffset ?? 0.0; 22 | Widget widget; 23 | if (info.mode == RefreshIndicatorMode.error) { 24 | widget = Column( 25 | mainAxisSize: MainAxisSize.min, 26 | children: [ 27 | Text('读取网络数据失败\n你可能需要梯子'), 28 | RaisedButton.icon( 29 | icon: Icon(Icons.refresh), 30 | onPressed: onTap, 31 | label: Text('再次尝试'), 32 | ), 33 | ], 34 | ); 35 | } else if (info.mode == RefreshIndicatorMode.refresh || 36 | info.mode == RefreshIndicatorMode.snap) { 37 | widget = Row( 38 | mainAxisSize: MainAxisSize.min, 39 | children: [ 40 | AnimatedLogoWidget(width: 20, height: 30), 41 | SizedBox(width: 5), 42 | Text('读取中,请稍候'), 43 | ], 44 | ); 45 | } else if ([ 46 | RefreshIndicatorMode.drag, 47 | RefreshIndicatorMode.armed, 48 | RefreshIndicatorMode.snap 49 | ].contains(info.mode)) { 50 | widget = Text('下拉刷新'); 51 | } else { 52 | widget = SizedBox(); 53 | } 54 | return SliverToBoxAdapter( 55 | child: Container( 56 | height: dragOffset, 57 | alignment: Alignment.center, 58 | child: widget, 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/widgets/quick.dart: -------------------------------------------------------------------------------- 1 | import 'package:draggable_container/draggable_container.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'package:weiman/classes/networkImageSSL.dart'; 5 | import 'package:weiman/db/book.dart'; 6 | import 'package:weiman/utils.dart'; 7 | import 'selectFavoriteBooks.dart'; 8 | import 'utils.dart'; 9 | 10 | class QuickBook extends DraggableItem { 11 | static const heroTag = 'quickBookAvatar'; 12 | Widget child; 13 | final BuildContext context; 14 | final Book book; 15 | final double width, height; 16 | 17 | QuickBook(this.width, this.height, 18 | {@required this.book, @required this.context}) { 19 | child = GestureDetector( 20 | onTap: () { 21 | openBook(context, book, '$heroTag ${book.aid}'); 22 | }, 23 | child: Stack( 24 | children: [ 25 | book.http == null 26 | ? oldBookAvatar(width: width, height: height) 27 | : SizedBox( 28 | width: width, 29 | height: height, 30 | child: Hero( 31 | tag: '$heroTag ${book.aid}', 32 | child: Image( 33 | image: NetworkImageSSL(book.http, book.avatar), 34 | fit: BoxFit.cover, 35 | ), 36 | ), 37 | ), 38 | Positioned( 39 | left: 0, 40 | right: 0, 41 | bottom: 0, 42 | child: Container( 43 | padding: EdgeInsets.only(left: 2, right: 2, top: 2, bottom: 2), 44 | color: Colors.black.withOpacity(0.5), 45 | child: Text( 46 | book.name, 47 | softWrap: true, 48 | maxLines: 1, 49 | textAlign: TextAlign.center, 50 | style: TextStyle(color: Colors.white, fontSize: 10), 51 | overflow: TextOverflow.ellipsis, 52 | ), 53 | ), 54 | ) 55 | ], 56 | ), 57 | ); 58 | } 59 | } 60 | 61 | class Quick extends StatefulWidget { 62 | final double width, height; 63 | final Function(bool mode) draggableModeChanged; 64 | 65 | const Quick( 66 | {Key key, this.width, this.height, @required this.draggableModeChanged}) 67 | : super(key: key); 68 | 69 | @override 70 | QuickState createState() => QuickState(); 71 | } 72 | 73 | class QuickState extends State { 74 | final int count = 8; 75 | final List _draggableItems = []; 76 | DraggableItem _addButton; 77 | GlobalKey _key = 78 | GlobalKey(); 79 | double width = 0, height = 0; 80 | 81 | void exit() { 82 | _key.currentState.draggableMode = false; 83 | } 84 | 85 | QuickState() { 86 | _addButton = DraggableItem( 87 | deletable: false, 88 | fixed: true, 89 | child: FlatButton( 90 | child: Column( 91 | mainAxisAlignment: MainAxisAlignment.center, 92 | children: [ 93 | Icon( 94 | Icons.add, 95 | color: Colors.grey, 96 | ), 97 | Text( 98 | '添加', 99 | style: TextStyle( 100 | fontSize: 10, 101 | color: Colors.grey, 102 | ), 103 | ) 104 | ], 105 | ), 106 | onPressed: () async { 107 | final items = _key.currentState.items; 108 | final buttonIndex = items.indexOf(_addButton); 109 | print('add $buttonIndex'); 110 | if (buttonIndex > -1) { 111 | final book = await showFavoriteBooksDialog(context); 112 | print('选择了 $book'); 113 | if (book == null) return; 114 | book 115 | ..quick = buttonIndex 116 | ..save(); 117 | _key.currentState.insteadOfIndex(buttonIndex, 118 | QuickBook(width, height, book: book, context: context), 119 | force: true); 120 | } 121 | }, 122 | ), 123 | ); 124 | } 125 | 126 | int length() { 127 | return _key.currentState.items.where((item) => item is QuickBook).length; 128 | } 129 | 130 | @override 131 | void initState() { 132 | super.initState(); 133 | 134 | width = widget.width / 4 - 10; 135 | height = (width / 0.7).roundToDouble(); 136 | final list = []; 137 | Book.bookBox.values.forEach((book) { 138 | if (book.quick != null && list.length < count) { 139 | list.add(book); 140 | } else { 141 | book.quick = null; 142 | book.save(); 143 | } 144 | }); 145 | print('quick book length ${list.length}'); 146 | list.sort((a, b) => a.quick.compareTo(b.quick)); 147 | _draggableItems.addAll(list.map((book) { 148 | return QuickBook(width, height, book: book, context: context); 149 | })); 150 | if (_draggableItems.length < count) _draggableItems.add(_addButton); 151 | for (var i = count - _draggableItems.length; i > 0; i--) { 152 | _draggableItems.add(null); 153 | } 154 | } 155 | 156 | @override 157 | Widget build(BuildContext context) { 158 | print('quick build'); 159 | return Column( 160 | children: [ 161 | Container( 162 | margin: EdgeInsets.only(top: 8, bottom: 4, left: 8), 163 | width: widget.width, 164 | child: Text( 165 | '快速导航(长按编辑)', 166 | textAlign: TextAlign.left, 167 | style: TextStyle(color: Colors.grey, fontSize: 12), 168 | ), 169 | ), 170 | DraggableContainer( 171 | key: _key, 172 | slotMargin: EdgeInsets.only(bottom: 8, left: 6, right: 6), 173 | slotSize: Size(width, height), 174 | slotDecoration: BoxDecoration(color: Colors.grey.withOpacity(0.3)), 175 | dragDecoration: BoxDecoration( 176 | boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]), 177 | items: _draggableItems, 178 | onDraggableModeChanged: widget.draggableModeChanged, 179 | onBeforeDelete: (index, item) async { 180 | if (item is QuickBook) { 181 | print('on before delete ${item.book.name}'); 182 | item.book.quick = null; 183 | item.book.save(); 184 | } 185 | return true; 186 | }, 187 | onChanged: (List items) { 188 | final nullIndex = items.indexOf(null); 189 | final buttonIndex = items.indexOf(_addButton); 190 | print('null $nullIndex, button $buttonIndex'); 191 | if (nullIndex > -1 && buttonIndex == -1) { 192 | print('显示添加按钮 1'); 193 | _key.currentState.insteadOfIndex( 194 | nullIndex, _addButton, 195 | triggerEvent: false, force: true); 196 | print('显示添加按钮 2'); 197 | setState(() {}); 198 | } else if (nullIndex > -1 && 199 | buttonIndex > -1 && 200 | nullIndex < buttonIndex) { 201 | _key.currentState.removeItem(_addButton); 202 | _key.currentState 203 | .insteadOfIndex(nullIndex, _addButton, triggerEvent: false); 204 | } 205 | var quick = 0; 206 | items.forEach((item) { 207 | if (item is QuickBook) { 208 | item.book 209 | ..quick = quick 210 | ..save(); 211 | quick++; 212 | } 213 | }); 214 | }, 215 | ), 216 | ], 217 | ); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /lib/widgets/selectFavoriteBooks.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | import 'package:weiman/classes/networkImageSSL.dart'; 6 | import 'package:weiman/db/book.dart'; 7 | import 'package:weiman/provider/favoriteData.dart'; 8 | 9 | Future showFavoriteBooksDialog(BuildContext context) { 10 | return showDialog( 11 | context: context, 12 | builder: (_) => FavoriteBooksDialog(title: '将藏书添加到快速导航'), 13 | ); 14 | } 15 | 16 | class FavoriteBooksDialog extends StatelessWidget { 17 | final String title; 18 | 19 | const FavoriteBooksDialog({ 20 | Key key, 21 | @required this.title, 22 | }) : super(key: key); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | final fav = Provider.of(context, listen: false); 27 | return AlertDialog( 28 | title: Text(title), 29 | scrollable: true, 30 | content: Column( 31 | children: ListTile.divideTiles( 32 | context: context, 33 | tiles: fav.all 34 | .where((book) => book.quick == null) 35 | .map( 36 | (book) => ListTile( 37 | title: Text(book.name), 38 | leading: ExtendedImage( 39 | image: NetworkImageSSL(book.http, book.avatar), 40 | fit: BoxFit.cover, 41 | width: 40, 42 | ), 43 | onTap: () => Navigator.pop(context, book), 44 | ), 45 | ) 46 | .toList(), 47 | ).toList(), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/widgets/sliverExpandableGroup.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_slidable/flutter_slidable.dart'; 5 | import 'package:flutter_sticky_header/flutter_sticky_header.dart'; 6 | 7 | class SliverExpandableBuilder { 8 | final int count; 9 | final WidgetBuilder builder; 10 | 11 | const SliverExpandableBuilder(this.count, this.builder); 12 | } 13 | 14 | class SliverExpandableGroup extends StatefulWidget { 15 | final Widget title; 16 | final bool expanded; 17 | final List actions; 18 | final Color divideColor; 19 | final double height; 20 | final int count; 21 | final IndexedWidgetBuilder builder; 22 | final List slideActions; 23 | 24 | const SliverExpandableGroup({ 25 | Key key, 26 | @required this.title, 27 | @required this.count, 28 | @required this.builder, 29 | this.expanded = false, 30 | this.actions = const [], 31 | this.divideColor = Colors.grey, 32 | this.height = kToolbarHeight, 33 | this.slideActions, 34 | }) : assert(title != null), 35 | assert(builder != null), 36 | super(key: key); 37 | 38 | @override 39 | _SliverExpandableGroup createState() => _SliverExpandableGroup(); 40 | } 41 | 42 | class _SliverExpandableGroup extends State { 43 | bool _expanded; 44 | 45 | @override 46 | initState() { 47 | super.initState(); 48 | _expanded = widget.expanded; 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | Decoration _decoration = BoxDecoration( 54 | border: Border( 55 | bottom: Divider.createBorderSide(context, color: widget.divideColor), 56 | ), 57 | ); 58 | Widget header = InkWell( 59 | child: Container( 60 | height: widget.height, 61 | alignment: Alignment.centerLeft, 62 | decoration: BoxDecoration( 63 | color: Theme.of(context).backgroundColor, 64 | ), 65 | child: Row(children: [ 66 | Transform.rotate( 67 | angle: _expanded ? 0 : math.pi, 68 | child: Icon( 69 | Icons.arrow_drop_down, 70 | color: Colors.grey, 71 | ), 72 | ), 73 | Expanded(child: widget.title), 74 | ...widget.actions, 75 | ]), 76 | ), 77 | onTap: () { 78 | setState(() { 79 | _expanded = !_expanded; 80 | }); 81 | }, 82 | ); 83 | if (widget.slideActions != null && widget.slideActions.length > 0) { 84 | header = Slidable( 85 | child: header, 86 | actionPane: SlidableDrawerActionPane(), 87 | secondaryActions: widget.slideActions, 88 | ); 89 | } 90 | return SliverStickyHeader( 91 | header: header, 92 | sliver: _expanded 93 | ? SliverList( 94 | delegate: SliverChildBuilderDelegate((ctx, i) { 95 | if (i < widget.count - 1) { 96 | return DecoratedBox( 97 | decoration: _decoration, 98 | child: widget.builder(context, i), 99 | ); 100 | } 101 | return widget.builder(context, i); 102 | }, childCount: widget.count)) 103 | : null, 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/widgets/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | 4 | class TextDivider extends StatelessWidget { 5 | final String text; 6 | final double leftPadding, padding; 7 | final List actions; 8 | 9 | const TextDivider({ 10 | Key key, 11 | @required this.text, 12 | this.padding = 5, 13 | this.leftPadding = 15, 14 | this.actions = const [], 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Container( 20 | padding: 21 | EdgeInsets.only(left: leftPadding, top: padding, bottom: padding), 22 | child: Row( 23 | mainAxisAlignment: MainAxisAlignment.end, 24 | children: [ 25 | Expanded(child: Text(text, style: TextStyle(color: Colors.grey))), 26 | ...actions, 27 | ], 28 | ), 29 | ); 30 | } 31 | } 32 | 33 | Widget oldBookAvatar({ 34 | String text = '旧\n藏\n书', 35 | width = double.infinity, 36 | height = double.infinity, 37 | }) { 38 | return Container( 39 | width: width, 40 | height: height, 41 | alignment: Alignment.center, 42 | color: Colors.greenAccent, 43 | child: Text(text), 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: weiman 2 | description: 微漫App 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.1.4+2007 19 | 20 | environment: 21 | sdk: ">=2.9.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | dio: any 28 | dio_http_cache: any 29 | image: any 30 | intl: any 31 | async: any 32 | http: any 33 | encrypt: any 34 | html: any 35 | hive: any 36 | sa_anicoto: any 37 | hive_flutter: any 38 | shared_preferences: any 39 | random_string: any 40 | filesize: any 41 | oktoast: any 42 | path_provider: any 43 | draggable_container: any 44 | sticky_headers: any 45 | flutter_sticky_header: any 46 | extended_nested_scroll_view: any 47 | package_info: any 48 | url_launcher: any 49 | font_awesome_flutter: any 50 | webview_flutter: any 51 | loadmore: any 52 | pull_to_refresh_notification: any 53 | http_client_helper: any 54 | extended_image: any 55 | screenshot: any 56 | focus_widget: any 57 | provider: any 58 | loading_more_list: any 59 | flutter_slidable: any 60 | 61 | firebase_core: any 62 | firebase_analytics: any 63 | 64 | 65 | # The following adds the Cupertino Icons font to your application. 66 | # Use with the CupertinoIcons class for iOS style icons. 67 | cupertino_icons: ^0.1.3 68 | 69 | dev_dependencies: 70 | flutter_test: 71 | sdk: flutter 72 | 73 | hive_generator: any 74 | build_runner: any 75 | 76 | #dependency_overrides: 77 | # analyzer: '0.39.14' 78 | 79 | # For information on the generic Dart part of this file, see the 80 | # following page: https://dart.dev/tools/pub/pubspec 81 | 82 | # The following section is specific to Flutter. 83 | flutter: 84 | 85 | # The following line ensures that the Material Icons font is 86 | # included with your application, so that you can use the icons in 87 | # the material Icons class. 88 | uses-material-design: true 89 | 90 | assets: 91 | - assets/logo.png 92 | 93 | # To add assets to your application, add an assets section, like this: 94 | # assets: 95 | # - images/a_dot_burr.jpeg 96 | # - images/a_dot_ham.jpeg 97 | 98 | # An image asset can refer to one or more resolution-specific "variants", see 99 | # https://flutter.dev/assets-and-images/#resolution-aware. 100 | 101 | # For details regarding adding assets from package dependencies, see 102 | # https://flutter.dev/assets-and-images/#from-packages 103 | 104 | # To add custom fonts to your application, add a fonts section here, 105 | # in this "flutter" section. Each entry in this list should have a 106 | # "family" key with the font family name, and a "fonts" key with a 107 | # list giving the asset and other descriptors for the font. For 108 | # example: 109 | # fonts: 110 | # - family: Schyler 111 | # fonts: 112 | # - asset: fonts/Schyler-Regular.ttf 113 | # - asset: fonts/Schyler-Italic.ttf 114 | # style: italic 115 | # - family: Trajan Pro 116 | # fonts: 117 | # - asset: fonts/TrajanPro.ttf 118 | # - asset: fonts/TrajanPro_Bold.ttf 119 | # weight: 700 120 | # 121 | # For details regarding fonts from package dependencies, 122 | # see https://flutter.dev/custom-fonts/#from-packages 123 | --------------------------------------------------------------------------------