1
0
mirror of https://github.com/nrop19/weiman_app.git synced 2025-08-02 06:52:36 +08:00

Github Action自动发布

This commit is contained in:
github-actions 2020-11-07 21:18:42 +00:00
commit 282e3ff529
55 changed files with 5825 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 nrop19
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# 微漫 v1.1.4 [宣传页面](https://nrop19.github.io/weiman_app)
### 微漫脱敏后的开源代码
#### 不解答任何代码上的问题
#### App的问题请到 [Telegram群](https://t.me/boring_programer) 讨论
- 删除了android端文件夹涉及到apk签名等敏感文件
- 删除了ios端文件夹
- 删除了lib/crawler/里的其它文件,保护被爬网站的同时防止被爬网站加大防爬难度。

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,274 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
import 'package:weiman/activities/book/tapToSearch.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/main.dart';
import 'package:weiman/provider/favoriteData.dart';
import 'package:weiman/utils.dart';
import 'package:weiman/widgets/book.dart';
import 'package:weiman/widgets/bookSettingDialog.dart';
import 'package:weiman/widgets/pullToRefreshHeader.dart';
class ActivityBook extends StatefulWidget {
final Book book;
final String heroTag;
ActivityBook({@required this.book, @required this.heroTag});
@override
_ActivityBook createState() => _ActivityBook();
}
class _ActivityBook extends State<ActivityBook> {
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
ScrollController _scrollController;
bool _reverse = false;
@override
void initState() {
super.initState();
widget.book.look = true;
_scrollController = ScrollController();
print('${widget.book}');
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
_refresh.currentState
.show(notificationDragOffset: SliverPullToRefreshHeader.height);
});
}
@override
dispose() {
_scrollController.dispose();
super.dispose();
}
Future<bool> loadBook() async {
try {
final res = await widget.book.load();
if (mounted && widget.book.needToSave()) {
await widget.book.save();
// Provider.of<FavoriteData>(context, listen: false).loadBooksList(true);
}
if (mounted) setState(() {});
return res;
} catch (e) {
return false;
}
}
_openChapter(Chapter chapter) async {
await openChapter(context, widget.book, chapter);
setState(() {});
}
favoriteBook() async {
final fav = Provider.of<FavoriteData>(context, listen: false);
if (widget.book.favorite) {
final sure = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('确认取消收藏?'),
// content: Text('删除这本藏书后,首页的快速导航也会删除这本藏书'),
actions: [
FlatButton(
child: Text('确认'),
onPressed: () => Navigator.pop(context, true),
),
RaisedButton(
child: Text('取消'),
onPressed: () => Navigator.pop(context, false),
),
],
));
if (sure == true) {
fav.deleteBook(widget.book);
}
} else {
await fav.addBook(widget.book);
await showBookSettingDialog(context, widget.book);
if (widget.book.needUpdate == true) {
widget.book.status = BookUpdateStatus.no;
} else {
widget.book.status = BookUpdateStatus.not;
}
}
setState(() {});
}
List<Chapter> _sort() {
final List<Chapter> list = List.from(widget.book.chapters);
// print('sort ${list.length}');
if (_reverse) return list.reversed.toList();
return list;
}
IndexedWidgetBuilder buildChapters(List<Chapter> chapters) {
IndexedWidgetBuilder builder = (BuildContext context, int index) {
final chapter = chapters[index];
Widget child = WidgetChapter(
chapter: chapter,
onTap: _openChapter,
read: chapter.cid == widget.book.history?.cid,
);
if (index < chapters.length - 1)
child = DecoratedBox(
decoration: border,
child: child,
);
return child;
};
return builder;
}
@override
Widget build(BuildContext context) {
Color color = widget.book.favorite ? Colors.red : Colors.white;
IconData icon =
widget.book.favorite ? Icons.favorite : Icons.favorite_border;
final List<Chapter> chapters = _sort();
final history = <Widget>[];
if (widget.book.history != null && widget.book.chapters.length > 0) {
final chapter = widget.book.chapters.firstWhere(
(chapter) => chapter.cid == widget.book.history.cid,
orElse: () => null,
);
if(chapter != null){
history.add(ListTile(title: Text('阅读历史')));
history.add(WidgetChapter(
chapter: chapter,
onTap: _openChapter,
read: true,
));
history.add(ListTile(title: Text('下一章')));
final nextIndex = widget.book.chapters.indexOf(chapter) + 1;
if (nextIndex < widget.book.chapterCount) {
history.add(WidgetChapter(
chapter: widget.book.chapters[nextIndex],
onTap: _openChapter,
read: false,
));
} else {
history.add(ListTile(subtitle: Text('没有了')));
}
}
history.add(SizedBox(height: 20));
}
history.add(
ListTile(
title: Row(
children: [
Text('章节列表'),
SizedBox(width: 10),
TextButton(
onPressed: () {
_reverse = !_reverse;
setState(() {});
},
child: Text('倒序'),
),
],
),
),
);
return Scaffold(
body: PullToRefreshNotification(
key: _refresh,
onRefresh: loadBook,
maxDragOffset: kToolbarHeight * 2,
child: CustomScrollView(
controller: _scrollController,
slivers: [
///
SliverAppBar(
floating: true,
pinned: true,
title: Text(widget.book.name),
expandedHeight: 200,
actions: <Widget>[
IconButton(
onPressed: favoriteBook, icon: Icon(icon, color: color))
],
flexibleSpace: FlexibleSpaceBar(
background: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
///
Container(
margin: EdgeInsets.only(
top: 50, left: 20, right: 10, bottom: 20),
height: 160,
child: Hero(
tag: widget.heroTag,
child: ExtendedImage(
width: 100,
image: NetworkImageSSL(
widget.book.http,
widget.book.avatar,
),
),
),
),
///
Expanded(
child: Container(
padding: EdgeInsets.only(top: 50, right: 20),
child: ListView(
children: <Widget>[
TapToSearchWidget(
leading: '作者', items: widget.book.authors),
TapToSearchWidget(
leading: '标签', items: widget.book.tags),
Container(
margin: EdgeInsets.only(top: 10),
),
Text(
widget.book.description ?? '',
softWrap: true,
style:
TextStyle(color: Colors.white, height: 1.2),
),
],
),
)),
],
),
),
),
),
PullToRefreshContainer((info) => SliverPullToRefreshHeader(
info: info,
onTap: () => _refresh.currentState.show(
notificationDragOffset: SliverPullToRefreshHeader.height),
)),
///
SliverToBoxAdapter(
child: Column(
children: history,
crossAxisAlignment: CrossAxisAlignment.start,
),
),
///
SliverList(
delegate: SliverChildBuilderDelegate(
buildChapters(chapters),
childCount: chapters.length,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:weiman/activities/search/search.dart';
class TapToSearchWidget extends StatelessWidget {
final String leading;
final List<String> items;
const TapToSearchWidget({
Key key,
this.leading,
this.items,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton(
child: Text('$leading'),
onPressed: null,
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
overlayColor:
MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.3)),
visualDensity: VisualDensity.comfortable,
),
),
Expanded(
child: Wrap(
spacing: 10,
crossAxisAlignment: WrapCrossAlignment.center,
children: items.map((e) => _Item(string: e)).toList(),
),
),
],
);
}
}
class _Item extends StatelessWidget {
final String string;
const _Item({Key key, @required this.string})
: assert(string != null),
super(key: key);
@override
Widget build(BuildContext context) {
return TextButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ActivitySearch(
search: string,
)));
},
icon: Icon(Icons.search, size: 14),
label: Text(string),
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
overlayColor:
MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.3)),
visualDensity: VisualDensity.comfortable,
),
);
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ActivitySearch(
search: string,
)));
},
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: string,
style: TextStyle(decoration: TextDecoration.underline)),
WidgetSpan(
child: Icon(
Icons.search,
color: Colors.white,
size: 14,
)),
],
),
style: TextStyle(
color: Colors.white,
textBaseline: TextBaseline.ideographic,
),
),
);
}
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
import 'package:weiman/activities/chapter/chapterTab.dart';
import 'package:weiman/activities/chapter/drawer.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/setting.dart';
import 'package:weiman/utils.dart';
class ActivityChapter extends StatefulWidget {
final Book book;
final Chapter chapter;
ActivityChapter(this.book, this.chapter);
@override
_ActivityChapter createState() => _ActivityChapter();
}
class _ActivityChapter extends State<ActivityChapter> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
PageController _pageController;
int showIndex = 0;
bool hasNextImage = true;
@override
void initState() {
_pageController = PageController(
keepPage: false,
initialPage: widget.book.chapters.indexOf(widget.chapter));
super.initState();
saveHistory(widget.chapter);
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
final hide = Provider.of<Setting>(context, listen: false).getHideOption();
if (hide == HideOption.always) {
hideStatusBar();
}
});
}
@override
void dispose() {
_pageController?.dispose();
showStatusBar();
super.dispose();
}
void pageChanged(int page) {
saveHistory(widget.book.chapters[page]);
}
void saveHistory(Chapter chapter) async {
await widget.book.setHistory(chapter);
}
@override
Widget build(BuildContext context) {
return Consumer<Setting>(builder: (_, data, __) {
return Scaffold(
key: _scaffoldKey,
endDrawer: ChapterDrawer(
book: widget.book,
onTap: (chapter) {
_pageController.jumpToPage(widget.book.chapters.indexOf(chapter));
},
),
body: PageView.builder(
physics: AlwaysScrollableClampingScrollPhysics(),
controller: _pageController,
itemCount: widget.book.chapters.length,
onPageChanged: pageChanged,
itemBuilder: (ctx, index) {
return ChapterTab(
actions: [
IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openEndDrawer();
},
),
],
book: widget.book,
chapter: widget.book.chapters[index],
);
},
),
);
});
}
}

View File

@ -0,0 +1,261 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:provider/provider.dart';
import 'package:weiman/activities/chapter/image.dart';
import 'package:weiman/activities/chapter/viewerSwitcherWidget.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/crawler/http18Comic.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/setting.dart';
import 'package:weiman/utils.dart';
import 'package:weiman/widgets/animatedLogo.dart';
class ChapterSourceList extends LoadingMoreBase<String> {
final Book book;
final Chapter chapter;
final Function onFirstLoaded;
bool firstLoad = true;
bool hasMore = true;
bool isMultiPage = false;
int page = 1;
ChapterSourceList({
this.book,
this.chapter,
this.onFirstLoaded,
});
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
final chapterContent = await Http18Comic.instance.getChapterContent(
book,
chapter,
page: page,
);
print(chapterContent.toString());
hasMore = chapterContent.hasNextPage;
this.addAll(chapterContent.images);
if (firstLoad) {
firstLoad = false;
isMultiPage = hasMore;
}
page++;
return true;
}
@override
Future<bool> refresh([bool notifyStateChanged = false]) {
firstLoad = true;
hasMore = true;
page = 1;
return super.refresh(notifyStateChanged);
}
}
class ChapterTab extends StatefulWidget {
final Book book;
final Chapter chapter;
final List<Widget> actions;
const ChapterTab({Key key, this.book, this.chapter, this.actions})
: super(key: key);
@override
_State createState() => _State();
}
class _State extends State<ChapterTab> {
ChapterSourceList sourceList;
ScrollController scrollController;
@override
initState() {
scrollController = ScrollController();
sourceList = ChapterSourceList(
book: widget.book,
chapter: widget.chapter,
);
widget.book.setHistory(widget.chapter);
super.initState();
// /
final setting = Provider.of<Setting>(context, listen: false);
final hide = setting.getHideOption();
if (hide == HideOption.auto) {
scrollController.addListener(() {
final isUp = scrollController.position.userScrollDirection ==
ScrollDirection.forward;
if (isUp)
showStatusBar();
else
hideStatusBar();
});
}
}
@override
dispose() {
widget.book.setHistory(widget.chapter);
scrollController?.dispose();
super.dispose();
}
Widget imageBuilder(ctx, String image, int index) {
index += 1;
bool reDraw = false;
try {
int cid = int.parse(widget.chapter.cid);
reDraw = cid >= 220980;
// print('创建图片 cid $cid reDraw $reDraw');
} catch (e) {}
return ImageWidget(
image: image,
index: index,
total: sourceList.length,
reSort: reDraw,
);
}
Widget indicatorBuilder(context, IndicatorStatus status) {
print('indicatorBuilder $status');
bool isSliver = true;
Widget widget;
switch (status) {
case IndicatorStatus.none:
widget = SizedBox();
break;
case IndicatorStatus.loadingMoreBusying:
widget = Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
AnimatedLogoWidget(width: 20, height: 30),
SizedBox(width: 10),
Text("正在读取")
],
);
widget = Container(
width: double.infinity,
height: kToolbarHeight,
child: widget,
alignment: Alignment.center,
);
break;
case IndicatorStatus.fullScreenBusying:
widget = Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedLogoWidget(width: 25, height: 30),
Text('读取中'),
],
),
);
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
}
break;
case IndicatorStatus.error:
case IndicatorStatus.fullScreenError:
widget = Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'读取失败\n你可能需要用梯子',
textAlign: TextAlign.center,
),
RaisedButton(
child: Text('再次重试'),
onPressed: sourceList.errorRefresh,
)
],
);
widget = Container(
width: double.infinity,
height: kToolbarHeight,
child: widget,
alignment: Alignment.center,
);
if (status == IndicatorStatus.fullScreenError) {
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
}
break;
case IndicatorStatus.noMoreLoad:
widget = SizedBox();
break;
case IndicatorStatus.empty:
widget = Text(
'没有图片',
);
widget = Container(
width: double.infinity,
height: kToolbarHeight,
child: widget,
alignment: Alignment.center,
);
if (isSliver) {
widget = SliverToBoxAdapter(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
break;
}
return widget;
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
controller: scrollController,
slivers: [
SliverAppBar(
snap: true,
floating: true,
title: Text(widget.chapter.cname),
actions: [
ViewerSwitcherWidget(),
IconButton(
icon: Icon(Icons.vertical_align_top),
onPressed: () => scrollController.jumpTo(0.0),
),
...widget.actions,
],
),
LoadingMoreSliverList(
SliverListConfig(
sourceList: sourceList,
itemBuilder: imageBuilder,
addSemanticIndexes: true,
semanticIndexOffset: 10,
autoLoadMore: true,
indicatorBuilder: indicatorBuilder,
),
),
],
);
}
}

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/widgets/book.dart';
class ChapterDrawer extends StatefulWidget {
final Book book;
final void Function(Chapter chapter) onTap;
const ChapterDrawer({
Key key,
@required this.book,
@required this.onTap,
}) : super(key: key);
@override
_ChapterDrawer createState() => _ChapterDrawer();
}
class _ChapterDrawer extends State<ChapterDrawer> {
ScrollController _controller;
int read;
@override
void initState() {
super.initState();
updateRead();
_controller =
ScrollController(initialScrollOffset: WidgetChapter.height * read);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void updateRead() {
final readChapter = widget.book.chapters
.firstWhere((chapter) => widget.book.history?.cid == chapter.cid);
read = widget.book.chapters.indexOf(readChapter);
}
void scrollToRead() {
setState(() {
updateRead();
});
_controller.animateTo(
WidgetChapter.height * read,
duration: Duration(milliseconds: 200),
curve: Curves.linear,
);
}
@override
Widget build(BuildContext context) {
return Drawer(
child: SafeArea(
child: ListView(
controller: _controller,
children: ListTile.divideTiles(
context: context,
tiles: widget.book.chapters.map((chapter) {
final isRead = widget.book.history?.cid == chapter.cid;
return WidgetChapter(
chapter: chapter,
onTap: (chapter) {
if (widget.onTap != null) widget.onTap(chapter);
SchedulerBinding.instance.addPostFrameCallback((_) {
scrollToRead();
});
},
read: isRead,
);
}),
).toList(),
),
),
);
}
}

View File

@ -0,0 +1,110 @@
import 'dart:ui' as ui;
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart' hide Image;
import 'package:provider/provider.dart';
import 'package:sticky_headers/sticky_headers/widget.dart';
import 'package:weiman/activities/chapter/viewer.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/crawler/http18Comic.dart';
import 'package:weiman/db/setting.dart';
class ImageWidget extends StatefulWidget {
final int index;
final int total;
final String image;
final bool reSort;
const ImageWidget({
Key key,
this.image,
this.index,
this.total,
this.reSort = false,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<ImageWidget> {
static TextStyle _style = TextStyle(color: Colors.white);
static BoxDecoration _decoration =
BoxDecoration(color: Colors.black.withOpacity(0.4));
String get tag {
return 'image_viewer_${widget.index}';
}
@override
Widget build(BuildContext context) {
return StickyHeader(
overlapHeaders: true,
header: SafeArea(
top: true,
bottom: false,
child: Row(
children: [
Container(
padding: EdgeInsets.all(5),
decoration: _decoration,
child: Text(
'${widget.index} / ${widget.total}',
style: _style,
),
),
],
),
),
content: ExtendedImage(
image: NetworkImageSSL(
Http18Comic.instance,
widget.image,
reSort: widget.reSort,
),
loadStateChanged: (ExtendedImageState state) {
Widget widget;
switch (state.extendedImageLoadState) {
case LoadState.loading:
widget = SizedBox(
height: 300,
child: Center(
child: CircularProgressIndicator(),
),
);
break;
case LoadState.completed:
widget = GestureDetector(
child: Hero(
child:
ExtendedRawImage(image: state.extendedImageInfo?.image),
tag: tag,
),
onTap: () => onTap(context),
);
break;
default:
}
return widget;
},
),
);
}
onTap(BuildContext context) {
final viewerSwitch =
Provider.of<Setting>(context, listen: false).getViewerSwitch();
// print('viewer $viewerSwitch');
if (!viewerSwitch) return;
Navigator.push(
context,
TransparentMaterialPageRoute(
builder: (_) => ActivityImageViewer(
url: this.widget.image,
heroTag: tag,
reSort: widget.reSort,
),
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/crawler/http18Comic.dart';
class ActivityImageViewer extends StatefulWidget {
final String url;
final String heroTag;
final bool reSort;
const ActivityImageViewer({
Key key,
this.url,
this.heroTag,
this.reSort = false,
}) : super(key: key);
@override
_State createState() => _State();
}
class _State extends State<ActivityImageViewer> {
double currentScale = 1.0;
@override
Widget build(BuildContext context) {
return ExtendedImageSlidePage(
slideAxis: SlideAxis.both,
slideType: SlideType.onlyImage,
child: Material(
color: Colors.transparent,
shadowColor: Colors.transparent,
child: Stack(
fit: StackFit.expand,
children: [
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: ExtendedImage(
image: NetworkImageSSL(
Http18Comic.instance,
widget.url,
reSort: widget.reSort,
),
enableSlideOutPage: true,
mode: ExtendedImageMode.gesture,
onDoubleTap: (status) {
currentScale = currentScale == 1 ? 3 : 1;
status.handleDoubleTap(scale: currentScale);
},
heroBuilderForSlidingPage: (child) {
return Hero(
child: child,
tag: widget.heroTag,
);
},
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weiman/db/setting.dart';
class ViewerSwitcherWidget extends StatefulWidget {
@override
ViewerSwitcherState createState() => ViewerSwitcherState();
}
class ViewerSwitcherState extends State<ViewerSwitcherWidget> {
@override
Widget build(BuildContext context) {
return Consumer<Setting>(builder: (_, data, __) {
final icon = data.getViewerSwitch()
? Icons.check_box_outlined
: Icons.check_box_outline_blank;
return
TextButton.icon(
icon: Icon(icon),
label: Text('看图'),
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
overlayColor:
MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.3)),
visualDensity: VisualDensity.compact,
),
onPressed: () {
data.setViewerSwitch(!data.getViewerSwitch());
setState(() {});
},
);
});
}
}

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:weiman/db/book.dart';
class ActivityCheckDB extends StatefulWidget {
@override
_State createState() => _State();
}
enum CheckState {
Uncheck,
Pass,
Fail,
}
class _State extends State<ActivityCheckDB> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('收藏数据检修'),
),
body: ListView(children: [
ListTile(
title: Text('所有藏书章节数量归零'),
onTap: () async {
for (final book in Book.bookBox.values) {
book.chapterCount = 0;
await book.save();
}
},
),
ListTile(
title: Text('清空漫画数据'),
subtitle: Text('${Book.bookBox.length}'),
onTap: () async {
await Book.bookBox.clear();
},
),
]),
);
}
}

View File

@ -0,0 +1,136 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:oktoast/oktoast.dart';
import 'package:weiman/classes/data.dart';
class ActivityCheckData extends StatefulWidget {
@override
_State createState() => _State();
}
enum CheckState {
Uncheck,
Pass,
Fail,
}
final titleTextStyle = TextStyle(fontSize: 14, color: Colors.blue),
passStyle = TextStyle(color: Colors.green),
failStyle = TextStyle(color: Colors.red);
class _State extends State<ActivityCheckData> {
CheckState firstState;
int firstLength = 0;
final TextSpan secondResults = TextSpan();
TextEditingController _outputController, _inputController;
@override
void initState() {
super.initState();
_outputController = TextEditingController();
_inputController = TextEditingController();
}
TextSpan first() {
String text;
switch (firstState) {
case CheckState.Pass:
text = '有数据, 一共 $firstLength 本收藏';
break;
case CheckState.Fail:
text = '没有收藏数据';
break;
default:
text = '未检查';
}
return TextSpan(
text: text,
style: firstState == CheckState.Pass ? passStyle : failStyle);
}
@override
Widget build(BuildContext context) {
final firstChildren = [
Text('检查漫画收藏列表'),
RaisedButton(
child: Text('检查'),
color: Colors.blue,
textColor: Colors.white,
onPressed: () {
final has = Data.has(Data.favoriteBooksKey);
if (has) {
final String str = Data.instance.getString(Data.favoriteBooksKey);
final Map<String, Object> map = jsonDecode(str);
firstLength = map.keys.length;
_outputController.text = str;
}
firstState = firstLength > 0 ? CheckState.Pass : CheckState.Fail;
setState(() {});
},
),
RichText(
text: TextSpan(
text: '结果:',
children: [first()],
style: TextStyle(color: Colors.black)),
),
];
if (firstState == CheckState.Pass) {
firstChildren.add(Text('点击复制'));
firstChildren.add(TextField(
maxLines: 8,
controller: _outputController,
onTap: () {
showToast('已经复制');
Clipboard.setData(ClipboardData(text: _outputController.text));
},
));
}
return Scaffold(
appBar: AppBar(
title: Text('收藏数据检修'),
),
body: ListView(children: [
Card(
child: Padding(
padding: EdgeInsets.all(5),
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: firstChildren,
),
),
),
),
Card(
child: Padding(
padding: EdgeInsets.all(5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('导入收藏数据'),
TextField(
controller: _inputController,
maxLines: 8,
),
RaisedButton(
child: Text('导入'),
onPressed: () {
if (_inputController.text.length > 0) {
Data.instance.setString(
Data.favoriteBooksKey, _inputController.text);
}
},
),
],
),
),
),
]),
);
}
}

View File

@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:oktoast/oktoast.dart';
import 'package:weiman/classes/book.dart';
import 'package:weiman/classes/data.dart';
import 'package:weiman/db/book.dart' as newBook;
import 'package:weiman/main.dart';
import 'home.dart';
class ActivityDataConvert extends StatefulWidget {
@override
_State createState() => _State();
}
class _State extends State<ActivityDataConvert> {
List<Book> quick;
Map<String, Book> favorites;
bool selectQ = true, selectH = true;
@override
void initState() {
analytics.setCurrentScreen(screenName: '/activity_data_convert');
favorites = Data.getFavorites();
quick = Data.quickList();
super.initState();
}
Future convert() async {
int quickIndex = 0;
int skip = 0;
final awaitList = <Future>[];
favorites.keys.forEach((id) {
if (newBook.Book.bookBox.containsKey(id)) return;
final oldBook = favorites[id];
final isQuick = selectQ && quick.contains(oldBook.aid);
final book = new newBook.Book(
httpId: null,
aid: oldBook.aid,
name: oldBook.name,
avatar: oldBook.avatar,
description: oldBook.description,
authors: [oldBook.author],
chapterCount: oldBook.chapterCount,
quick: isQuick ? quickIndex : null,
needUpdate: true,
favorite: true,
history: null,
);
if (isQuick) quickIndex++;
awaitList.add(book.save());
});
await Future.wait(awaitList);
showToast(
'成功转存 ${awaitList.length} 本小说\n跳过了 $skip',
textPadding: EdgeInsets.all(10),
);
}
Future clean() async {
await Data.instance.remove(Data.favoriteBooksKey);
await Data.instance.remove(Data.quickKey);
await Data.instance.remove(Data.viewHistoryKey);
}
void gotoHome() {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => ActivityHome(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('旧数据转存'),
),
body: ListView(children: [
ListTile(
title: Text('从v1.1.2开始,为了实现藏书分组功能,使用了新的数据存储方式'
'\n【旧书】打开后直接搜索同名漫画。'
'\n清空旧数据后这个界面不会再次出现。'
'\n需要将旧的藏书数据转存为新数据吗?'
'\n旧藏书不多的话,我个人建议直接清空,可以防止产生数据干扰')),
ListTile(
title: Text('收藏列表'),
subtitle: Text('一共有 ${favorites.length}'),
trailing: Checkbox(
value: true,
onChanged: null,
),
),
ListTile(
title: Text('快速导航'),
subtitle: Text('一共有 ${quick.length}'),
trailing: Checkbox(
value: selectQ,
onChanged: (value) {
setState(() {
selectQ = value;
});
},
),
),
]),
bottomNavigationBar: Row(children: [
SizedBox(width: 10),
Expanded(
child: OutlineButton(
child: Text('直接清空旧数据'),
onPressed: () async {
await clean();
gotoHome();
},
),
),
SizedBox(width: 10),
Expanded(
child: OutlineButton(
child: Text('转存并清空旧数据'),
onPressed: () async {
await convert();
await clean();
gotoHome();
},
),
),
SizedBox(width: 10),
]),
);
}
}

380
lib/activities/home.dart Normal file
View File

@ -0,0 +1,380 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:oktoast/oktoast.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:weiman/activities/dataConvert.dart';
import 'package:weiman/db/setting.dart';
import 'package:weiman/provider/theme.dart';
import 'package:weiman/activities/checkData.dart';
import 'package:weiman/activities/hot.dart';
import 'package:weiman/activities/search/search.dart';
import 'package:weiman/activities/test2.dart';
import 'package:weiman/classes/book.dart';
import 'package:weiman/main.dart';
import 'package:weiman/provider/favoriteData.dart';
import 'package:weiman/widgets/checkConnect/checkConnect.dart';
import 'package:weiman/widgets/favorites.dart';
import 'package:weiman/widgets/histories.dart';
import 'package:weiman/widgets/quick.dart';
import 'checkDB.dart';
import 'setting/setting.dart';
class ActivityHome extends StatefulWidget {
@override
State<StatefulWidget> createState() => HomeState();
}
class HomeState extends State<ActivityHome> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
final List<Widget> histories = [];
final List<Book> quick = [];
final GlobalKey<QuickState> _quickState = GlobalKey();
bool showFavorite = true;
@override
void initState() {
super.initState();
analytics.setCurrentScreen(screenName: '/activity_home');
///
SchedulerBinding.instance.addPostFrameCallback((_) async {
autoSwitchTheme();
FavoriteData favData = Provider.of<FavoriteData>(context, listen: false);
await favData.loadBooksList();
final updated = await favData.checkUpdate();
if (updated > 0)
showToast(
'$updated 本藏书有更新',
textPadding: EdgeInsets.all(10),
);
});
}
void autoSwitchTheme() async {}
void gotoSearch() {
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: '/activity_search'),
builder: (context) => ActivitySearch()));
}
void gotoRecommend() {
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: '/activity_recommend'),
builder: (_) => ActivityRank(),
));
}
void gotoPatreon() {
launch('https://www.patreon.com/nrop19');
}
bool isEdit = false;
void _draggableModeChanged(bool mode) {
print('mode changed $mode');
isEdit = mode;
setState(() {});
}
Widget themeButton() {
final system = FontAwesomeIcons.cloudSun,
light = FontAwesomeIcons.solidSun,
dark = FontAwesomeIcons.solidMoon;
final theme = Provider.of<ThemeProvider>(context, listen: false);
Widget themeIcon;
switch (theme.themeMode) {
case ThemeMode.light:
themeIcon = Icon(light);
break;
case ThemeMode.dark:
themeIcon = Icon(dark);
break;
default:
themeIcon = Icon(system);
break;
}
return IconButton(
onPressed: () {
switch (theme.themeMode) {
case ThemeMode.light:
theme.changeTheme(ThemeMode.dark);
break;
case ThemeMode.dark:
theme.changeTheme(ThemeMode.system);
break;
default:
theme.changeTheme(ThemeMode.light);
}
Provider.of<Setting>(context, listen: false)
.setThemeMode(theme.themeMode);
showToastWidget(
Container(
padding: EdgeInsets.all(10),
color: Colors.black.withOpacity(0.7),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(mainAxisSize: MainAxisSize.min, children: [
Icon(
system,
size: 14,
color: Colors.white,
),
SizedBox(width: 10),
Text('跟随系统,自动切换明暗模式\n如果系统不支持,默认为明亮模式'),
]),
SizedBox(height: 10),
Row(mainAxisSize: MainAxisSize.min, children: [
Icon(
light,
size: 14,
color: Colors.white,
),
SizedBox(width: 10),
Text('为明亮模式'),
]),
SizedBox(height: 10),
Row(mainAxisSize: MainAxisSize.min, children: [
Icon(
dark,
size: 14,
color: Colors.white,
),
SizedBox(width: 10),
Text('为暗黑模式'),
]),
],
),
),
dismissOtherToast: true,
duration: Duration(seconds: 4),
);
},
icon: themeIcon,
);
}
@override
Widget build(BuildContext context) {
final media = MediaQuery.of(context);
final width = (media.size.width * 0.8).roundToDouble();
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text('微漫 v' + packageInfo.version),
automaticallyImplyLeading: false,
leading: isEdit
? IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: () {
_quickState.currentState.exit();
},
)
: null,
actions: <Widget>[
///
themeButton(),
SizedBox(width: 20),
///
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: '/activity_setting'),
builder: (_) => ActivitySetting()));
},
icon: Icon(FontAwesomeIcons.cog),
),
///
IconButton(
onPressed: () {
showFavorite = true;
_scaffoldKey.currentState.openEndDrawer();
},
icon: Icon(
Icons.favorite,
color: Colors.red,
),
),
///
IconButton(
onPressed: () {
showFavorite = false;
// getHistory();
_scaffoldKey.currentState.openEndDrawer();
},
icon: Icon(Icons.history),
),
],
),
drawerEnableOpenDragGesture: false,
endDrawerEnableOpenDragGesture: false,
endDrawer: Drawer(
child: LayoutBuilder(
builder: (_, constraints) {
if (showFavorite) {
return FavoriteList();
} else {
return Histories();
}
},
),
),
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.only(left: 40, right: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
child: OutlineButton(
onPressed: gotoSearch,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.search,
color: Colors.blue,
),
Text(
'搜索漫画',
style: TextStyle(color: Colors.blue),
)
],
),
borderSide: BorderSide(color: Colors.blue, width: 2),
shape: StadiumBorder(),
),
),
Row(
children: [
Expanded(
flex: 7,
child: OutlineButton(
onPressed: gotoRecommend,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.whatshot,
color: Colors.red,
),
Text(
'热门漫画',
style: TextStyle(color: Colors.red),
)
],
),
borderSide: BorderSide(color: Colors.red, width: 2),
shape: StadiumBorder(),
),
),
],
),
Center(
child: Quick(
key: _quickState,
width: width,
draggableModeChanged: _draggableModeChanged,
),
),
CheckConnectWidget(),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () async {
launch('https://bbs.level-plus.net/');
},
child: Text(
'魂+论坛首发',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.blue[200],
decoration: TextDecoration.underline,
),
),
),
SizedBox(width: 20),
GestureDetector(
onTap: () async {
if (await canLaunch('tg://resolve?domain=weiman_app'))
launch('tg://resolve?domain=weiman_app');
else
launch('https://t.me/weiman_app');
},
child: Text(
'Telegram 广播频道',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.blue[200],
decoration: TextDecoration.underline,
),
),
),
],
),
Visibility(
visible: isDevMode,
child: FlatButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => ActivityCheckData()));
},
child: Text('操作 收藏列表数据'),
),
),
Visibility(
visible: isDevMode,
child: FlatButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => ActivityCheckDB()));
},
child: Text('操作 DB数据'),
),
),
Visibility(
visible: isDevMode,
child: FlatButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ActivityDataConvert()));
},
child: Text('进入旧数据处理功能'),
),
),
],
),
),
),
floatingActionButton: isDevMode
? FloatingActionButton(
child: Text('测试'),
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (_) => ActivityTest()));
},
)
: null,
);
}
}

238
lib/activities/hot.dart Normal file
View File

@ -0,0 +1,238 @@
import 'package:flutter/material.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
import 'package:weiman/widgets/animatedLogo.dart';
import 'package:weiman/crawler/http.dart';
import 'package:weiman/crawler/http18Comic.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/widgets/book.dart';
import 'package:weiman/widgets/pullToRefreshHeader.dart';
class ActivityRank extends StatefulWidget {
@override
_ActivityRank createState() => _ActivityRank();
}
class _ActivityRank extends State<ActivityRank>
with SingleTickerProviderStateMixin {
TabController controller;
@override
void initState() {
controller = TabController(length: 2, vsync: this);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('热门漫画'),
bottom: TabBar(controller: controller, tabs: [
Tab(text: '韩漫'),
Tab(text: '全部'),
]),
),
body: TabBarView(controller: controller, children: [
HotTab(http: Http18Comic.instance, type: '/hanman'),
HotTab(http: Http18Comic.instance, type: ''),
]),
);
}
}
class SourceList extends LoadingMoreBase<Book> {
final String type;
final HttpBook http;
int page = 1;
String firstBookId;
bool hasMore = true;
SourceList({this.type, this.http});
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
try {
final books = await http.hotBooks(type, page);
if (books.isEmpty) {
hasMore = false;
} else {
if (firstBookId == books[0].aid) {
hasMore = false;
} else {
firstBookId = books[0].aid;
page++;
this.addAll(books);
}
}
return true;
} catch (e) {
return false;
}
}
@override
Future<bool> refresh([bool notifyStateChanged = false]) {
hasMore = true;
page = 1;
return super.refresh(notifyStateChanged);
}
}
class HotTab extends StatefulWidget {
final String type;
final HttpBook http;
const HotTab({Key key, this.type, this.http}) : super(key: key);
@override
_HotTab createState() => _HotTab();
}
class _HotTab extends State<HotTab> {
SourceList sourceList;
@override
void initState() {
sourceList = SourceList(type: widget.type, http: widget.http);
super.initState();
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
PullToRefreshContainer(
(info) => SliverPullToRefreshHeader(info: info),
),
LoadingMoreSliverList(SliverListConfig<Book>(
sourceList: sourceList,
indicatorBuilder: indicatorBuilder,
itemBuilder: (_, book, __) => WidgetBook(
book,
subtitle: book.authors?.join('/'),
),
)),
],
);
}
Widget book(Book book) {
return WidgetBook(book, subtitle: book.authors?.join('/'));
}
Widget indicatorBuilder(context, IndicatorStatus status) {
print('indicatorBuilder $status');
bool isSliver = true;
Widget widget;
switch (status) {
case IndicatorStatus.none:
widget = SizedBox();
break;
case IndicatorStatus.loadingMoreBusying:
widget = Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
AnimatedLogoWidget(width: 20, height: 30),
SizedBox(width: 10),
Text("正在读取")
],
);
widget = _setbackground(false, widget, 35.0);
break;
case IndicatorStatus.fullScreenBusying:
widget = Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedLogoWidget(width: 25, height: 30),
Text('读取中'),
],
),
);
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
}
break;
case IndicatorStatus.error:
case IndicatorStatus.fullScreenError:
widget = Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'读取失败\n你可能需要用梯子',
textAlign: TextAlign.center,
),
RaisedButton(
child: Text('再次重试'),
onPressed: sourceList.errorRefresh,
)
],
);
final height = status == IndicatorStatus.error ? 35.0 : double.infinity;
widget = _setbackground(false, widget, height);
if (status == IndicatorStatus.fullScreenError) {
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
}
break;
case IndicatorStatus.noMoreLoad:
widget = Text("已经显示全部搜索结果");
widget = _setbackground(false, widget, 35.0);
break;
case IndicatorStatus.empty:
widget = Text(
'没有内容',
);
widget = _setbackground(true, widget, double.infinity);
if (isSliver) {
widget = SliverToBoxAdapter(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
break;
}
return widget;
}
Widget _setbackground(bool full, Widget widget, double height) {
widget = Container(
width: double.infinity,
height: kToolbarHeight,
child: widget,
alignment: Alignment.center,
);
return widget;
}
Widget getIndicator(BuildContext context) {
return CircularProgressIndicator(
strokeWidth: 2.0,
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
);
}
}

View File

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:focus_widget/focus_widget.dart';
import 'package:weiman/crawler/http18Comic.dart';
import 'tab.dart';
class ActivitySearch extends StatefulWidget {
final String search;
const ActivitySearch({Key key, this.search = ''}) : super(key: key);
@override
State<StatefulWidget> createState() {
return SearchState();
}
}
class SearchState extends State<ActivitySearch>
with SingleTickerProviderStateMixin {
TextEditingController _controller;
GlobalKey<SearchTabState> key = GlobalKey<SearchTabState>();
@override
initState() {
_controller = TextEditingController(text: widget.search);
super.initState();
}
@override
dispose() {
_controller.dispose();
super.dispose();
}
void search() {
key.currentState.search = _controller.text;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (RawKeyEvent event) {
print('is enter: ${LogicalKeyboardKey.enter == event.logicalKey}');
if (_controller.text.isEmpty) return;
if (event.runtimeType == RawKeyUpEvent &&
LogicalKeyboardKey.enter == event.logicalKey) {
print('回车键搜索');
search();
}
},
child: FocusWidget.builder(
context,
builder: (_, focusNode) => TextField(
focusNode: focusNode,
style: TextStyle(color: Colors.white),
cursorColor: Colors.white,
decoration: InputDecoration(
hintText: '搜索书名',
prefixIcon: IconButton(
onPressed: search,
icon: Icon(Icons.search, color: Colors.white),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
border: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
),
textAlign: TextAlign.left,
controller: _controller,
autofocus: widget.search.isEmpty,
textInputAction: TextInputAction.search,
onSubmitted: (String name) {
focusNode.unfocus();
print('onSubmitted');
search();
},
keyboardType: TextInputType.text,
onEditingComplete: () {
focusNode.unfocus();
print('onEditingComplete');
search();
},
),
),
),
),
body: SearchTab(
http: Http18Comic.instance,
search: _controller.text,
key: key,
),
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/crawler/http.dart';
class SearchSourceList extends LoadingMoreBase<Book> {
final HttpBook http;
String search;
int page = 1;
bool hasMore = true;
String eachPageFirstBookId;
SearchSourceList({
@required this.http,
this.search = '',
});
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
print('搜书 $search');
if (search == null || search.isEmpty) return true;
final list = await http.searchBook(search, page);
if (list.isEmpty) {
hasMore = false;
} else if (list[0].aid == eachPageFirstBookId) {
hasMore = false;
} else {
eachPageFirstBookId = list[0].aid;
hasMore = true;
page++;
this.addAll(list);
}
return true;
}
@override
Future<bool> refresh([bool notifyStateChanged = false]) {
page = 1;
hasMore = true;
eachPageFirstBookId = null;
clear();
print('refresh $page $hasMore');
return super.refresh(notifyStateChanged);
}
}

View File

@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:weiman/activities/search/source.dart';
import 'package:weiman/crawler/http.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/widgets/book.dart';
class SearchTab extends StatefulWidget {
final HttpBook http;
final String search;
const SearchTab({
Key key,
@required this.http,
this.search,
}) : super(key: key);
@override
SearchTabState createState() => SearchTabState();
}
class SearchTabState extends State<SearchTab>
with AutomaticKeepAliveClientMixin {
SearchSourceList sourceList;
@override
void initState() {
sourceList = SearchSourceList(http: widget.http, search: widget.search);
super.initState();
}
Widget book(Book book) {
return WidgetBook(book, subtitle: book.authors.join('/'));
}
Future<bool> refresh() async {
return sourceList.refresh(true);
}
get search => sourceList.search;
set search(String value) {
print('tab search $value');
sourceList.search = value;
sourceList.refresh(true);
}
@override
Widget build(BuildContext context) {
super.build(context);
return LoadingMoreList(
ListConfig(
sourceList: sourceList,
itemBuilder: (_, item, index) => book(item),
autoLoadMore: true,
indicatorBuilder: indicatorBuilder,
),
);
}
Widget indicatorBuilder(context, IndicatorStatus status) {
bool isSliver = false;
Widget widget;
switch (status) {
case IndicatorStatus.none:
widget = SizedBox();
break;
case IndicatorStatus.loadingMoreBusying:
widget = Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
margin: EdgeInsets.only(right: 5.0),
height: 15.0,
width: 15.0,
child: getIndicator(context),
),
Text("正在读取")
],
);
widget = _setbackground(false, widget, 35.0);
break;
case IndicatorStatus.fullScreenBusying:
widget = widget = _setbackground(
false,
Text(
'正在读取',
),
35.0);
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
}
break;
case IndicatorStatus.error:
widget = _setbackground(
false,
Text(
'网络错误\n点击重试',
),
35.0);
widget = GestureDetector(
onTap: () {
sourceList.errorRefresh();
},
child: widget,
);
break;
case IndicatorStatus.fullScreenError:
widget = Text(
'读取失败,如果失败的次数太多可能需要用梯子',
);
widget = _setbackground(true, widget, double.infinity);
widget = GestureDetector(
onTap: () {
sourceList.errorRefresh();
},
child: widget,
);
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
break;
case IndicatorStatus.noMoreLoad:
widget = Text("已经显示全部搜索结果");
widget = _setbackground(false, widget, 35.0);
break;
case IndicatorStatus.empty:
widget = Text(
sourceList.search.isEmpty ? '请输入搜索内容' : '搜索不到任何内容',
);
widget = _setbackground(true, widget, double.infinity);
if (isSliver) {
widget = SliverToBoxAdapter(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
break;
}
return widget;
}
Widget _setbackground(bool full, Widget widget, double height) {
widget = Container(
width: double.infinity,
height: kToolbarHeight,
child: widget,
alignment: Alignment.center,
);
return widget;
}
Widget getIndicator(BuildContext context) {
return CircularProgressIndicator(
strokeWidth: 2.0,
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
);
}
@override
bool get wantKeepAlive => true;
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:weiman/db/setting.dart';
class HideStatusBar extends StatelessWidget {
final options = {
'自动': HideOption.auto,
'全程隐藏': HideOption.always,
'不隐藏': HideOption.none,
};
final Function(HideOption option) onChanged;
final HideOption option;
HideStatusBar({Key key, @required this.onChanged, @required this.option})
: super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
title: Text('看漫画时隐藏状态栏'),
subtitle: Text('自动:随着图片列表的上下滚动而自动显示或隐藏状态栏\n'
'全程隐藏:进入看图界面就隐藏状态栏,退出就显示状态栏\n'
'不隐藏:就是不隐藏状态栏咯'),
trailing: DropdownButton<HideOption>(
value: option,
items: options.keys
.map((key) => DropdownMenuItem(
child: Text(key),
value: options[key],
))
.toList(),
onChanged: onChanged,
),
);
}
}

View File

@ -0,0 +1,192 @@
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:oktoast/oktoast.dart';
import 'package:provider/provider.dart';
import 'package:weiman/activities/setting/hideStatusBar.dart';
import 'package:weiman/activities/setting/web.dart';
import 'package:weiman/db/setting.dart';
import 'package:weiman/main.dart';
class ActivitySetting extends StatefulWidget {
@override
_ActivitySetting createState() => _ActivitySetting();
}
class _ActivitySetting extends State<ActivitySetting> {
int imagesCount, sizeCount;
bool isClearing = false;
@override
void initState() {
super.initState();
imageCaches();
}
Future<void> imageCaches() async {
final files = imageCacheDir.listSync();
imagesCount = files.length;
sizeCount = 0;
files.forEach((file) => sizeCount += file.statSync().size);
if (mounted) setState(() {});
}
Future<void> clearDiskCachedImages() async {
await imageCacheDir.delete(recursive: true);
await imageCacheDir.create();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('设置')),
body: Consumer<Setting>(builder: (_, data, __) {
print('代理 ${data.getProxy()}');
return ListView(
children: ListTile.divideTiles(
context: context,
tiles: [
///
HideStatusBar(
option: data.getHideOption(),
onChanged: (option) => data.setHideOption(option),
),
///
ListTile(
title: Text('设置代理'),
subtitle: Text(data.getProxy() ?? ''),
onTap: () async {
var proxy = await showDialog<String>(
context: context,
builder: (_) {
final _c = TextEditingController(text: data.getProxy());
return WillPopScope(
child: AlertDialog(
title: Text('设置网络代理'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'只支持http代理\nSS,SSR,V2Ray,Trojan(Clash)\n这些梯子App都有提供Http代理功能'),
TextField(
controller: _c,
decoration: InputDecoration(
hintText: '例如Clash提供的127.0.0.1:7890'),
),
]),
actions: [
FlatButton(
child: Text('清空'),
onPressed: () {
_c.clear();
},
),
FlatButton(
child: Text('确定'),
onPressed: () {
Navigator.pop(context, _c.text);
},
),
],
),
onWillPop: () {
Navigator.pop(context, '-1');
return Future.value(false);
},
);
});
print('用户输入 $proxy');
if (proxy == '-1') return;
//
if (proxy != null) {
proxy = proxy
.trim()
.replaceFirst('http://', '')
.replaceFirst('https://', '');
}
//
if (proxy == null || proxy.isEmpty) {
proxy = null;
}
print('设置代理 $proxy');
await data.setProxy(proxy);
},
),
///
ListTile(
title: Text('清除所有图片缓存'),
subtitle: isClearing
? Text('清理中')
: Text.rich(
TextSpan(
children: [
TextSpan(text: '图片数量:'),
TextSpan(
text: imagesCount == null
? '读取中'
: '$imagesCount'),
TextSpan(text: '\n'),
TextSpan(text: '存储容量:'),
TextSpan(
text: sizeCount == null
? '读取中'
: '${filesize(sizeCount)}'),
],
),
),
onTap: () async {
if (isClearing == true) return;
final sure = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('确认清除所有图片缓存?'),
actions: [
RaisedButton(
child: Text('确认'),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
if (sure == true) {
showToast('正在清理图片缓存');
isClearing = true;
setState(() {});
await clearDiskCachedImages();
isClearing = false;
if (mounted) {
setState(() {});
await imageCaches();
}
showToast('成功清理图片缓存');
}
},
),
ListTile(
title: Text('查看最新版'),
subtitle: Text('当前版本为 ${packageInfo.version}'),
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => ActivityWeb()));
},
),
///
/* ListTile(
title: Text('清空漫画数据缓存'),
subtitle: Text('正常情况是不需要清空的'),
onTap: () async {
await HttpBook.dataCache.clearAll();
showToast('成功清空漫画数据缓存', textPadding: EdgeInsets.all(10));
},
),*/
],
).toList(),
);
}),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:weiman/main.dart';
class ActivityWeb extends StatefulWidget {
@override
_State createState() => _State();
}
class _State extends State<ActivityWeb> {
LoadState state = LoadState.loading;
@override
void initState() {
analytics.setCurrentScreen(screenName: '/activity_update_web');
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('最新版本'),
),
body: Stack(
alignment: Alignment.center,
children: [
WebView(
initialUrl: 'https://nrop19.github.io/weiman_app',
onWebViewCreated: (controller) {
state = LoadState.loading;
setState(() {});
},
onPageFinished: (_) {
state = LoadState.completed;
setState(() {});
},
),
if (state == LoadState.loading)
Container(
color: Colors.grey.withOpacity(0.3),
padding: EdgeInsets.all(20),
child: CircularProgressIndicator(),
),
],
),
);
}
}

139
lib/classes/book.dart Normal file
View File

@ -0,0 +1,139 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:weiman/crawler/http.dart';
import 'data.dart';
class Book {
final String http;
final String aid; // ID
final String name; //
final String avatar; //
final String author; //
final String description; //
final List<Chapter> chapters;
final int chapterCount;
final int version;
History history;
Book({
@required this.http,
@required this.name,
@required this.aid,
@required this.avatar,
this.author,
this.description,
this.chapters: const [],
this.chapterCount: 0,
this.history,
this.version: 0,
});
@override
String toString() {
return jsonEncode(toJson());
}
bool isFavorite() {
var books = Data.getFavorites();
return books.containsKey(aid);
}
Map<String, dynamic> toJson() {
print('book toJson');
final Map<String, dynamic> data = {
'http': http,
'aid': aid,
'name': name,
'avatar': avatar,
'author': author,
'chapterCount': chapterCount,
'version': version,
};
if (history != null) data['history'] = history.toJson();
return data;
}
factory Book.fromJson(Map<String, dynamic> json) {
final book = Book(
http: json['http'],
aid: json['aid'],
name: json['name'],
avatar: json['avatar'],
author: json['author'],
description: json['description'],
chapterCount: json['chapterCount'] ?? 0,
version: json['version'] ?? 0);
if (json.containsKey('history'))
book.history = History.fromJson(json['history']);
return book;
}
}
class Chapter {
final HttpBook http;
final String cid; // cid
final String cname; //
final String avatar; //
Chapter({
@required this.http,
@required this.cid,
@required this.cname,
@required this.avatar,
});
@override
String toString() {
final Map<String, String> data = {
'cid': cid,
'cname': cname,
'avatar': avatar,
};
return jsonEncode(data);
}
}
class History {
final String cid;
final String cname;
final int time;
final int image;
History({
@required this.cid,
@required this.cname,
@required this.time,
this.image = 0,
});
@override
String toString() => jsonEncode(toJson());
Map<String, dynamic> toJson() {
return {
'cid': cid,
'cname': cname,
'time': time,
'image': image,
};
}
static History fromJson(Map<String, dynamic> json) {
return History(
cid: json['cid'],
cname: json['cname'],
time: json['time'],
image: json['image'] ?? 0,
);
}
static History fromChapter(Chapter chapter) {
return History(
cid: chapter.cid,
cname: chapter.cname,
time: DateTime.now().millisecondsSinceEpoch,
);
}
}

24
lib/classes/chapter.dart Normal file
View File

@ -0,0 +1,24 @@
import 'dart:convert';
import 'package:flutter/material.dart';
class Chapter {
final String cid; // cid
final String cname; //
final DateTime time; //
Chapter({
@required this.cid,
@required this.cname,
this.time,
});
@override
String toString() {
final Map<String, String> data = {
'cid': cid,
'cname': cname,
};
return jsonEncode(data);
}
}

View File

@ -0,0 +1,11 @@
class ChapterContent {
final List<String> images;
final bool hasNextPage;
ChapterContent(this.images, this.hasNextPage);
@override
String toString() {
return 'ChapterContent images:${images.length} nexPage:$hasNextPage';
}
}

160
lib/classes/data.dart Normal file
View File

@ -0,0 +1,160 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'book.dart';
class Data {
static SharedPreferences instance;
static final favoriteBooksKey = 'favorite_books';
static final viewHistoryKey = 'view_history';
static final quickKey = 'quick_list';
static Future init() async {
instance = await SharedPreferences.getInstance();
}
static set<T>(String key, T value) {
if (value is String) {
instance.setString(key, value);
} else if (value is int) {
instance.setInt(key, value);
} else if (value is bool) {
instance.setBool(key, value);
} else if (value is List<String>) {
instance.setStringList(key, value);
} else if (value is double) {
instance.setDouble(key, value);
} else if (value is Map) {
instance.setString(key, json.encode(value));
}
}
static dynamic get(String key) {
return instance.get(key);
}
static bool hasData() {
return instance.containsKey(favoriteBooksKey) ||
instance.containsKey(viewHistoryKey);
}
static Map<String, Book> getFavorites() {
if (has(favoriteBooksKey)) {
final String str = instance.getString(favoriteBooksKey);
Map<String, Object> data = jsonDecode(str);
Map<String, Book> res = {};
data.keys.forEach((key) {
res[key] = Book.fromJson(data[key]);
});
return res;
}
return {};
}
static void addFavorite(Book book) {
var books = getFavorites();
books[book.aid] = book;
set<Map>(favoriteBooksKey, books);
}
static void removeFavorite(Book book) {
var books = getFavorites();
if (books.containsKey(book.aid)) {
books.remove(book.aid);
set<Map>(favoriteBooksKey, books);
reQuick();
}
}
static clear() {
instance.clear();
}
static bool has(String key) {
return instance.containsKey(key);
}
static remove(String key) {
instance.remove(key);
}
static Map<String, Book> getHistories() {
if (has(viewHistoryKey)) {
var data =
jsonDecode(instance.getString(viewHistoryKey)) as Map<String, Object>;
final Map<String, Book> histories = {};
data.forEach((key, value) {
histories[key] = Book.fromJson(value);
});
return histories;
}
return {};
}
static addHistory(Book book, Chapter chapter) {
book.history = History(
cid: chapter.cid,
cname: chapter.cname,
time: DateTime.now().millisecondsSinceEpoch);
final books = getHistories();
books[book.aid] = book;
set(viewHistoryKey, books);
// print('保存历史\n' + books.toString());
}
static removeHistory(bool Function(Book book) isDelete) {
var books = getHistories();
books.keys
.where((key) => isDelete(books[key]))
.toList()
.forEach(books.remove);
set(viewHistoryKey, books);
}
static removeHistoryFromBook(Book book) {
final books = getHistories();
books.remove(book.aid);
set(viewHistoryKey, books);
}
/// id
static List<String> quickIdList() {
if (instance.containsKey(quickKey)) {
return instance.getStringList(quickKey);
}
return [];
}
///
static List<Book> quickList() {
final books = getFavorites();
final ids = books.keys;
final List<String> quickIds = quickIdList();
print('快捷 $quickIds');
return quickIds
.where((id) => ids.contains(id))
.map((id) => books[id])
.toList();
}
///
static addQuick(Book book) {
final list = quickIdList();
list.add(book.aid);
instance.setStringList(quickKey, list.toSet().toList());
}
static addQuickAll(List<String> id) {
print('保存qid $id');
instance.setStringList(quickKey, id.toSet().toList());
}
/// Quick的id列表
static reQuick() {
final books = getFavorites();
final quickIds = quickIdList();
instance.setStringList(
quickKey, quickIds.where(books.keys.contains).toSet().toList());
}
}

34
lib/classes/history.dart Normal file
View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:weiman/classes/chapter.dart';
class History extends Chapter {
DateTime time; //
History({
@required cid,
@required cname,
@required this.time,
}) : super(cid: cid, cname: cname);
Map<String, dynamic> toJson() {
return {'cid': cid, 'cname': cname, 'time': time};
}
factory History.fromJson(Map<String, dynamic> map) {
if (map == null) return null;
return History(
cid: map['cid'],
cname: map['cname'],
time: map['time'],
);
}
factory History.fromChapter(Chapter chapter) {
return History(
cid: chapter.cid,
cname: chapter.cname,
time: DateTime.now(),
);
}
}

View File

@ -0,0 +1,95 @@
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:weiman/crawler/http.dart';
/// The dart:io implementation of [image_provider.NetworkImage].
class NetworkImageSSL extends ImageProvider<NetworkImage>
implements NetworkImage {
/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
const NetworkImageSSL(
this.http,
this.url, {
this.scale = 1.0,
this.headers,
this.timeout = 8,
this.reSort = false,
}) : assert(url != null),
assert(scale != null);
final HttpBook http;
final int timeout;
@override
final String url;
@override
final double scale;
@override
final Map<String, String> headers;
final bool reSort;
static void init(ByteData data) {}
@override
Future<NetworkImageSSL> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<NetworkImageSSL>(this);
}
@override
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
// Ownership of this controller is handed off to [_loadAsync]; it is that
// method's responsibility to close the controller's stream when the image
// has been loaded or an error is thrown.
final StreamController<ImageChunkEvent> chunkEvents =
StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<NetworkImage>('Image key', key),
];
},
);
}
Future<Codec> _loadAsync(
NetworkImageSSL key,
StreamController<ImageChunkEvent> chunkEvents,
DecoderCallback decode,
) async {
try {
assert(key == this);
final Uint8List bytes = await http.getImage(url, reSort: reSort);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $url');
return decode(bytes);
} finally {
chunkEvents.close();
}
}
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) return false;
final NetworkImageSSL typedOther = other;
return url == typedOther.url && scale == typedOther.scale;
}
@override
int get hashCode => hashValues(url, scale);
@override
String toString() => '$runtimeType("$url", scale: $scale)';
}

80
lib/crawler/http.dart Normal file
View File

@ -0,0 +1,80 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/classes/chapterContent.dart';
import 'package:weiman/db/book.dart';
import 'http18Comic.dart';
final headers = {
'user-agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
'accept':
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,zh-HK;q=0.7',
'cache-control': 'no-cache',
'pragma': 'no-cache',
};
class MyHttpClient {
static Map<String, HttpBook> clients = {};
static init(String proxy, int timeout) {
Http18Comic.instance = Http18Comic(
baseUrls.values.first,
name: baseUrls.keys.first,
headers: headers,
timeout: timeout,
);
clients[Http18Comic.instance.id] = Http18Comic.instance;
setGlobalProxy(proxy);
}
}
abstract class HttpBook {
final String id;
final String name;
final Dio dio;
HttpBook(this.id, this.name, this.dio);
Future<List<Book>> searchBook(String name, [int page]);
Future<Book> getBook(String aid);
Future<List<String>> getChapterImages(Book book, Chapter chapter);
Future<ChapterContent> getChapterContent(Book book, Chapter chapter);
Future<List<int>> getImage(String url, {bool reSort = false});
Future<List<Book>> hotBooks([String type = '', int page]);
}
class MyProxyHttpOverride extends HttpOverrides {
final String proxy;
MyProxyHttpOverride(this.proxy);
@override
HttpClient createHttpClient(SecurityContext context) {
return super.createHttpClient(context)
..findProxy = (uri) {
return 'PROXY $proxy;';
}
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
}
void setGlobalProxy(String proxy) {
print('setGlobalProxy $proxy');
if (proxy != null)
HttpOverrides.global = MyProxyHttpOverride(proxy);
else
HttpOverrides.global = null;
}

178
lib/db/book.dart Normal file
View File

@ -0,0 +1,178 @@
import 'package:hive/hive.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/classes/history.dart';
import 'package:weiman/crawler/http.dart';
import 'package:weiman/db/group.dart';
part 'book.g.dart';
const BookName = 'book';
enum BookUpdateStatus {
not, //
no, //
had, //
fail, //
wait, //
loading, //
old, //
}
@HiveType(typeId: 1)
class Book extends HiveObject {
static Box<Book> bookBox;
@HiveField(0)
String aid;
@HiveField(1)
String name;
@HiveField(2)
String avatar;
@HiveField(3)
List<String> authors;
@HiveField(4)
String description;
@HiveField(5)
int chapterCount;
// [][]
int newChapterCount;
BookUpdateStatus status;
List<Chapter> chapters;
List<String> tags;
@HiveField(6)
bool favorite;
@HiveField(7)
bool needUpdate;
@HiveField(8)
bool hasUpdate;
@HiveField(9)
DateTime updatedAt;
//
@HiveField(10)
int quick;
@HiveField(11)
Map<String, dynamic> _history;
@HiveField(12)
int groupId;
@HiveField(13)
String httpId;
bool look = false;
Group get group =>
groupId == null ? null : Group.groupBox.get(groupId, defaultValue: null);
HttpBook get http => MyHttpClient.clients[httpId];
History get history => History.fromJson(_history);
Future setFavorite(bool value) {
favorite = value;
return save();
}
Future setHistory(Chapter value) {
if (value == null) {
_history = null;
} else {
_history = History.fromChapter(value).toJson();
}
return save();
}
Book({
this.httpId,
this.aid,
this.name,
this.groupId,
this.avatar,
this.authors,
this.description,
this.chapterCount,
this.favorite = false,
this.needUpdate = false,
this.quick,
this.chapters = const [],
this.tags = const [],
Map<String, dynamic> history,
}) : _history = history;
@override
String toString() {
return 'Book:${toJson()}';
}
toJson() {
return {
'key': key,
'aid': aid,
'name': name,
'httpId': httpId,
'groupId': groupId,
'favorite': favorite,
'history': _history,
'status': status,
'chapterCount': chapterCount,
};
}
bool needToSave() {
return favorite == true || _history != null || quick != null;
}
@override
Future<void> save() {
if (needToSave()) {
return bookBox.put(aid, this);
}
return bookBox.delete(aid);
}
Future<bool> load() async {
if (httpId == null) return false;
final newBook = await this.http.getBook(aid);
print('load newBook:${newBook.httpId}');
chapters = newBook.chapters;
chapterCount = newBook.chapterCount;
authors = newBook.authors;
description = newBook.description;
httpId = newBook.httpId;
tags = newBook.tags;
print('book httpId $httpId');
return true;
}
Future<List<String>> loadChapter(Chapter chapter) async {
if (httpId == null) return null;
return this.http.getChapterImages(this, chapter);
}
Future<void> update() async {
try {
final newBook = await this.http.getBook(aid);
print('$name$chapterCount${newBook.chapterCount}');
newChapterCount = newBook.chapterCount - chapterCount;
status = newChapterCount > 0 ? BookUpdateStatus.had : BookUpdateStatus.no;
} catch (e) {
status = BookUpdateStatus.fail;
}
print('book update $status');
}
}

80
lib/db/book.g.dart Normal file
View File

@ -0,0 +1,80 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'book.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class BookAdapter extends TypeAdapter<Book> {
@override
final int typeId = 1;
@override
Book read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Book(
httpId: fields[13] as String,
aid: fields[0] as String,
name: fields[1] as String,
groupId: fields[12] as int,
avatar: fields[2] as String,
authors: (fields[3] as List)?.cast<String>(),
description: fields[4] as String,
chapterCount: fields[5] as int,
favorite: fields[6] as bool,
needUpdate: fields[7] as bool,
quick: fields[10] as int,
)
..hasUpdate = fields[8] as bool
..updatedAt = fields[9] as DateTime
.._history = (fields[11] as Map)?.cast<String, dynamic>();
}
@override
void write(BinaryWriter writer, Book obj) {
writer
..writeByte(14)
..writeByte(0)
..write(obj.aid)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.avatar)
..writeByte(3)
..write(obj.authors)
..writeByte(4)
..write(obj.description)
..writeByte(5)
..write(obj.chapterCount)
..writeByte(6)
..write(obj.favorite)
..writeByte(7)
..write(obj.needUpdate)
..writeByte(8)
..write(obj.hasUpdate)
..writeByte(9)
..write(obj.updatedAt)
..writeByte(10)
..write(obj.quick)
..writeByte(11)
..write(obj._history)
..writeByte(12)
..write(obj.groupId)
..writeByte(13)
..write(obj.httpId);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BookAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

36
lib/db/group.dart Normal file
View File

@ -0,0 +1,36 @@
import 'package:hive/hive.dart';
import 'book.dart';
part 'group.g.dart';
const GroupName = 'group';
@HiveType(typeId: 0)
class Group extends HiveObject {
static Box<Group> groupBox;
static Box<Book> bookBox;
@HiveField(0)
String name;
@HiveField(1)
bool expended;
Group(this.name, [this.expended = false]);
List<Book> get books => bookBox.values
.where((book) => book.favorite && book.groupId == this.key)
.toList();
@override
String toString() {
return 'Group:${{'key': key, 'name': name, 'books': books.length}}';
}
@override
Future<void> save() {
if (!isInBox) return groupBox.add(this);
return super.save();
}
}

44
lib/db/group.g.dart Normal file
View File

@ -0,0 +1,44 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'group.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class GroupAdapter extends TypeAdapter<Group> {
@override
final int typeId = 0;
@override
Group read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Group(
fields[0] as String,
fields[1] as bool,
);
}
@override
void write(BinaryWriter writer, Group obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.expended);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GroupAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

17
lib/db/historyOffset.dart Normal file
View File

@ -0,0 +1,17 @@
import 'package:hive/hive.dart';
const HistoryOffsetName = 'history';
class HistoryOffset {
static Box box;
static double get(String cid) {
print('get $cid');
return box.get(cid) ?? 0.0;
}
static Future<void> save(String cid, double offset) {
print('save $cid $offset');
return box.put(cid, offset);
}
}

79
lib/db/setting.dart Normal file
View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:weiman/crawler/http.dart';
import 'package:weiman/crawler/http18Comic.dart';
enum HideOption {
none,
auto,
always,
}
class Setting with ChangeNotifier {
static final String name = 'setting';
static Box settingBox;
Http18Comic http;
Setting() {
MyHttpClient.init(getProxy(), 10000);
}
HideOption getHideOption() {
final index =
settingBox.get('hideOption', defaultValue: HideOption.auto.index);
return HideOption.values[index];
}
Future setHideOption(HideOption option) async {
await settingBox.put('hideOption', option.index);
notifyListeners();
}
String getProxy() {
print('getProxy');
return settingBox.get('proxy', defaultValue: null);
}
Future setProxy(String proxy) async {
print('db/setting.setProxy $proxy');
await settingBox.put('proxy', proxy);
MyHttpClient.init(proxy, 10000);
notifyListeners();
}
ThemeMode getThemeMode() {
final int index = settingBox.get('theme', defaultValue: -1);
if (index == -1) return ThemeMode.system;
return ThemeMode.values[index];
}
Future setThemeMode(ThemeMode mode) {
return settingBox.put('theme', mode.index);
}
void refresh() {
notifyListeners();
}
Http18Comic getHttp() {
final String name =
settingBox.get('http', defaultValue: baseUrls.keys.first);
final http = Http18Comic(baseUrls[name], name: name, headers: headers);
setProxy(getProxy());
return http;
}
Future setHttp(HttpBook http) async {
await settingBox.put('http', http.name);
notifyListeners();
}
bool getViewerSwitch() {
return settingBox.get('viewerSwitch', defaultValue: true);
}
Future setViewerSwitch(bool value) async {
await settingBox.put('viewerSwitch', value);
notifyListeners();
}
}

134
lib/main.dart Normal file
View File

@ -0,0 +1,134 @@
import 'dart:async';
import 'dart:io';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:oktoast/oktoast.dart';
import 'package:package_info/package_info.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:weiman/activities/dataConvert.dart';
import 'package:weiman/activities/home.dart';
import 'package:weiman/classes/data.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/group.dart';
import 'package:weiman/db/historyOffset.dart';
import 'package:weiman/db/setting.dart';
import 'package:weiman/provider/favoriteData.dart';
import 'package:weiman/provider/theme.dart';
FirebaseAnalytics analytics;
FirebaseAnalyticsObserver observer;
const bool isDevMode = !bool.fromEnvironment('dart.vm.product');
int version;
BoxDecoration border;
Directory imageCacheDir;
String imageCacheDirPath;
PackageInfo packageInfo;
void main() async {
print("开发模式 $isDevMode");
FlutterError.onError = (FlutterErrorDetails details) {};
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
getTemporaryDirectory().then((dir) {
imageCacheDir = Directory(path.join(dir.path, 'images'));
imageCacheDirPath = imageCacheDir.path;
if (imageCacheDir.existsSync() == false) imageCacheDir.createSync();
print('图片缓存目录 $imageCacheDirPath');
});
try {
analytics = FirebaseAnalytics();
observer = FirebaseAnalyticsObserver(analytics: analytics);
} catch (e) {}
await Future.wait([
Hive.initFlutter(),
Data.init(),
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
]);
Hive.registerAdapter<Group>(GroupAdapter());
Hive.registerAdapter<Book>(BookAdapter());
await Future.wait([
Hive.openBox<Group>(GroupName).then((value) => Group.groupBox = value),
Hive.openBox<Book>(BookName)
.then((value) => Book.bookBox = Group.bookBox = value),
Hive.openBox(HistoryOffsetName).then((value) => HistoryOffset.box = value),
Hive.openBox(Setting.name).then((value) => Setting.settingBox = value),
]);
packageInfo = await PackageInfo.fromPlatform();
version = int.parse(packageInfo.buildNumber);
runApp(Main());
}
class Main extends StatefulWidget {
@override
_Main createState() => _Main();
}
class _Main extends State<Main> with WidgetsBindingObserver {
@override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void didChangePlatformBrightness() {
super.didChangePlatformBrightness();
Provider.of<ThemeProvider>(context, listen: false).update(context);
}
@override
Widget build(BuildContext context) {
border = BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context, color: Colors.grey)));
return OKToast(
child: MultiProvider(
providers: [
ChangeNotifierProvider<Setting>(
lazy: false,
create: (_) => Setting(),
),
ChangeNotifierProvider<FavoriteData>(
lazy: false,
create: (_) => FavoriteData(),
),
ChangeNotifierProvider<ThemeProvider>(
lazy: true,
create: (_) => ThemeProvider(_),
),
],
child: Consumer<ThemeProvider>(
builder: (_, theme, __) => MaterialApp(
title: '微漫 v${packageInfo.version}',
themeMode: theme.themeMode,
theme: ThemeData.light(),
darkTheme: ThemeData(
brightness: Brightness.dark,
accentColor: Colors.redAccent,
),
home: Data.hasData() ? ActivityDataConvert() : ActivityHome(),
// home: ActivityHome(),
debugShowCheckedModeBanner: isDevMode,
navigatorObservers: <NavigatorObserver>[observer],
),
),
),
);
}
}

View File

@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/group.dart';
class FavoriteData extends ChangeNotifier {
final List<Book> all = [], others = [];
final Map<Group, List<Book>> groups = {};
FavoriteData() {
loadBooksList();
}
Future<void> loadBooksList([notify = false]) async {
final groupList = Group.groupBox.values.toList();
final groupMap = {for (final group in groupList) group.key: group};
groups.clear();
groupList.forEach((group) {
groups[group] = [];
});
all.clear();
others.clear();
// if(isDevMode){
// final temp = [
// Book(
// aid: '180454',
// name: '朋友,女朋友',
// avatar:
// 'https://cdn-msp.18comic.org/media/albums/206567.jpg',
// chapterCount: 0,
// httpId: '18',
// needUpdate: false,
// authors: [],
// ),
// Book(
// aid: '206567',
// name: '抑欲人妻',
// avatar:
// 'https://cdn-msp.18comic.org/media/albums/206567.jpg',
// chapterCount: 0,
// httpId: '18',
// needUpdate: true,
// authors: [],
// ),
// Book(
// aid: '147335',
// name: '亲爱的大叔',
// avatar:
// 'https://cdn-msp.msp-comic.xyz/media/albums/147335.jpg',
// chapterCount: 0,
// httpId: '18',
// needUpdate: true,
// authors: [],
// ),
// ];
// all.addAll(temp);
// others.addAll(temp);
// }
Book.bookBox.values.forEach((book) {
if (book.favorite != true) return;
all.add(book);
if (groupMap.containsKey(book.groupId)) {
//
groups[groupMap[book.groupId]].add(book);
} else {
//
others.add(book);
}
});
print({'all': all.length, 'other': others.length});
if (notify) notifyListeners();
}
Future<int> checkUpdate() async {
final groupList = [others, ...groups.values];
for (final array in groupList) {
for (final book in array) {
if (book.httpId == null) {
book.status = BookUpdateStatus.old;
} else if (book.needUpdate != true) {
book.status = BookUpdateStatus.not;
} else {
book.status = BookUpdateStatus.wait;
}
notifyListeners();
if (book.status != BookUpdateStatus.wait) continue;
book.status = BookUpdateStatus.loading;
notifyListeners();
await book.update();
if (book.status == BookUpdateStatus.had) sort(array, book);
notifyListeners();
}
}
return all.where((book) => book.status == BookUpdateStatus.had).length;
}
///
void sort(List<Book> array, Book book) {
print('sort ${book.name}');
array.remove(book);
array.insert(0, book);
}
Future<void> deleteBook(Book book) async {
book.favorite = false;
await book.save();
// print('删书 ${book.name} 成功');
loadBooksList(true);
}
Future<void> deleteGroup(Group group, [bool deleteBooks = false]) async {
if (deleteBooks && groups.containsKey(group)) {
await Future.wait(groups[group].map((book) => book.setFavorite(false)));
}
await Group.groupBox.delete(group.key);
await loadBooksList(true);
}
Future<void> addGroup(Group group) async {
group.save();
await loadBooksList(true);
}
Future<void> addBook(Book book) async {
book.favorite = true;
await book.save();
loadBooksList(true);
}
}

29
lib/provider/theme.dart Normal file
View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weiman/db/setting.dart';
class ThemeProvider extends ChangeNotifier {
ThemeMode themeMode = ThemeMode.system; //
ThemeProvider(BuildContext context) {
themeMode = Provider.of<Setting>(context, listen: false).getThemeMode();
}
void changeTheme(ThemeMode mode) {
print('改变主题 $mode');
themeMode = mode;
notifyListeners();
}
void update(BuildContext context) {
final bright = MediaQuery.platformBrightnessOf(context);
switch (bright) {
case Brightness.light:
changeTheme(ThemeMode.light);
break;
case Brightness.dark:
changeTheme(ThemeMode.dark);
}
print('update $bright');
}
}

50
lib/utils.dart Normal file
View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:weiman/activities/book/book.dart';
import 'package:weiman/activities/chapter/activity.dart';
import 'package:weiman/activities/search/search.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/db/book.dart';
final weekTime = Duration.millisecondsPerDay * 7;
void openSearch(BuildContext context, String word) {}
Future openBook(BuildContext context, Book book, String heroTag) {
print('openBook $book');
if (book.http == null) {
return Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: '/activity_search/${book.name}'),
builder: (_) => ActivitySearch(search: book.name),
),
);
}
return Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: '/activity_book/${book.name}'),
builder: (_) => ActivityBook(book: book, heroTag: heroTag),
),
);
}
Future<void> openChapter(BuildContext context, Book book, Chapter chapter) {
return Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(
name: '/activity_chapter/${book.name}/${chapter.cname}'),
builder: (_) => ActivityChapter(book, chapter),
),
);
}
void showStatusBar() {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
void hideStatusBar() {
SystemChrome.setEnabledSystemUIOverlays([]);
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:sa_anicoto/sa_anicoto.dart';
class AnimatedLogoWidget extends StatefulWidget {
final double width, height;
const AnimatedLogoWidget({
Key key,
@required this.width,
@required this.height,
}) : super(key: key);
@override
_AnimatedLogoWidget createState() => _AnimatedLogoWidget();
}
class _AnimatedLogoWidget extends State<AnimatedLogoWidget>
with AnimationMixin {
Animation<double> size; // Declare animation variable
@override
void initState() {
size = Tween<double>(begin: 0, end: widget.height - 20).animate(controller);
controller.mirror(
duration: Duration(seconds: 1)); // Start the animation playback
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: size.value,
child: Image.asset(
'assets/logo.png',
width: 20,
height: 20,
),
),
],
),
);
}
}

192
lib/widgets/book.dart Normal file
View File

@ -0,0 +1,192 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/utils.dart';
class WidgetBook extends StatelessWidget {
final Book book;
final String subtitle;
final Function(Book) onTap;
const WidgetBook(
this.book, {
Key key,
@required this.subtitle,
this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final isLiked = book.favorite;
return ListTile(
title: Text(
book.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
dense: true,
leading: Hero(
tag: 'bookAvatar${book.aid}',
child: ExtendedImage(image: NetworkImageSSL(book.http, book.avatar)),
),
trailing: Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
color: isLiked ? Colors.red : Colors.grey,
size: 12,
),
onTap: () {
if (onTap != null) return onTap(book);
openBook(context, book, 'bookAvatar${book.aid}');
},
);
}
}
final dateFormat = DateFormat('yyyy-MM-dd');
class WidgetChapter extends StatelessWidget {
static final double height = kToolbarHeight;
final Chapter chapter;
final Function(Chapter) onTap;
final bool read;
WidgetChapter({
Key key,
this.chapter,
this.onTap,
this.read = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final children = <InlineSpan>[TextSpan(text: chapter.cname)];
if (read) {
children.insert(
0,
TextSpan(
text: '[已看]',
style: TextStyle(color: Colors.orange),
));
}
return ListTile(
onTap: () {
if (onTap != null) onTap(chapter);
},
title: RichText(
text: TextSpan(
children: children,
style: Theme.of(context).textTheme.bodyText2,
),
softWrap: true,
maxLines: 2,
),
subtitle: chapter.time == null
? null
: Text('更新时间 ${dateFormat.format(chapter.time)}'),
);
}
}
class WidgetHistory extends StatelessWidget {
final Book book;
final Function(Book book) onTap;
WidgetHistory(this.book, this.onTap);
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: ListTile(
onTap: () {
if (onTap != null) onTap(book);
},
title: Text(book.name),
leading: Image(
image: ExtendedNetworkImageProvider(book.avatar, cache: true),
fit: BoxFit.fitHeight,
),
subtitle: Text(book.history.cname),
),
);
}
}
class WidgetBookCheckNew extends StatefulWidget {
final Book book;
const WidgetBookCheckNew({Key key, this.book}) : super(key: key);
@override
_WidgetBookCheckNew createState() => _WidgetBookCheckNew();
}
class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
bool loading = true, hasError = false;
int news;
@override
void initState() {
super.initState();
load();
}
void load() async {
// loading = true;
// try {
// final book = await Http18Comic.instance
// .getBook(widget.book.aid)
// .timeout(Duration(seconds: 2));
// news = book.chapterCount - widget.book.chapterCount;
// hasError = false;
// } catch (e) {
// hasError = true;
// }
// loading = false;
// setState(() {});
}
@override
Widget build(BuildContext context) {
final children = <Widget>[];
if (widget.book.history != null)
children.add(Text(
widget.book.history.cname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
));
if (loading)
children.add(Text('检查更新中'));
else if (hasError)
children.add(Text('网络错误'));
else if (news > 0)
children.add(Text('$news 章更新'));
else
children.add(Text('没有更新'));
return ListTile(
onTap: () =>
openBook(context, widget.book, 'checkBook${widget.book.aid}'),
leading: Hero(
tag: 'checkBook${widget.book.aid}',
child: Image(
image:
ExtendedNetworkImageProvider(widget.book.avatar, cache: true)),
),
dense: true,
isThreeLine: true,
title: Text(widget.book.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
);
}
}

103
lib/widgets/bookGroup.dart Normal file
View File

@ -0,0 +1,103 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:weiman/db/group.dart';
class BookGroupHeader extends StatefulWidget {
final Group group;
final int count;
final List<Widget> actions;
final Color divideColor;
final double height;
final IndexedWidgetBuilder builder;
final List<Widget> slideActions;
const BookGroupHeader({
Key key,
@required this.group,
@required this.count,
@required this.builder,
this.actions = const [],
this.divideColor = Colors.grey,
this.height = kToolbarHeight,
this.slideActions,
}) : assert(group != null),
assert(builder != null),
super(key: key);
@override
_State createState() => _State();
}
class _State extends State<BookGroupHeader> {
bool expended;
@override
void initState() {
expended = widget.group.expended ?? false;
super.initState();
}
@override
Widget build(BuildContext context) {
Decoration _decoration = BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context, color: widget.divideColor),
),
);
Widget header = InkWell(
child: Container(
height: widget.height,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
),
child: Row(children: [
Transform.rotate(
angle: expended ? 0 : math.pi,
child: Icon(
Icons.arrow_drop_down,
color: Colors.grey,
),
),
Expanded(child: Text('${widget.group.name}(${widget.count})')),
...widget.actions,
]),
),
onTap: () {
expended = !expended;
widget.group
..expended = expended
..save();
setState(() {});
},
);
if (widget.slideActions != null && widget.slideActions.length > 0) {
header = Slidable(
child: header,
actionPane: SlidableDrawerActionPane(),
secondaryActions: widget.slideActions,
);
}
return SliverStickyHeader(
header: header,
sliver: expended
? SliverList(
delegate: SliverChildBuilderDelegate(
(ctx, i) {
if (i < widget.count - 1) {
return DecoratedBox(
decoration: _decoration,
child: widget.builder(context, i),
);
}
return widget.builder(context, i);
},
childCount: widget.count,
))
: null,
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/group.dart';
import 'package:weiman/provider/favoriteData.dart';
import 'package:weiman/widgets/groupFormDialog.dart';
Future showBookSettingDialog(BuildContext context, Book book) {
return showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('藏书《${book.name}》的设置'),
scrollable: true,
content: WidgetSetting(book: book),
),
);
}
class WidgetSetting extends StatefulWidget {
final Book book;
const WidgetSetting({Key key, this.book}) : super(key: key);
@override
_WidgetSetting createState() => _WidgetSetting();
}
class _WidgetSetting extends State<WidgetSetting> {
static final updateMenus = {true: '自动', false: '不检查'};
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: ListTile.divideTiles(context: context, tiles: [
ListTile(
title: Text('检查更新'),
trailing: DropdownButton<bool>(
value: widget.book.needUpdate,
items: updateMenus.keys
.map((key) =>
DropdownMenuItem(value: key, child: Text(updateMenus[key])))
.toList(),
onChanged: changeUpdate,
),
),
ListTile(
title: Text('分组'),
trailing: DropdownButton<Group>(
hint: Text('没有分组'),
value: widget.book.group,
items: [
DropdownMenuItem(
child: Text('新建'),
value: null,
),
...Group.groupBox.values
.map((e) => DropdownMenuItem(value: e, child: Text(e.name)))
.toList(),
],
onChanged: changeGroup,
),
),
]).toList(),
);
}
changeUpdate(bool needUpdate) async {
widget.book.needUpdate = needUpdate;
await widget.book.save();
setState(() {});
}
changeGroup(Group group) async {
if (group == null) {
group = await showGroupFormDialog(context);
}
widget.book.groupId = group == null ? widget.book.groupId : group.key;
await widget.book.save();
setState(() {});
}
changeFavorite() async {
await widget.book.setFavorite(!widget.book.favorite);
setState(() {});
}
removeHistory() async {
if (widget.book.history != null) await widget.book.setHistory(null);
setState(() {});
}
@override
void setState(fn) {
final fav = Provider.of<FavoriteData>(context, listen: false);
fav.loadBooksList(true);
super.setState(fn);
}
}

View File

@ -0,0 +1,314 @@
import 'package:dio/dio.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:html/parser.dart';
import 'package:provider/provider.dart';
import 'package:weiman/crawler/http.dart';
import 'package:weiman/crawler/http18Comic.dart';
import 'package:weiman/db/setting.dart';
class CheckConnectWidget extends StatefulWidget {
@override
_CheckConnectWidget createState() => _CheckConnectWidget();
}
class _CheckConnectWidget extends State<CheckConnectWidget> {
LoadState state = LoadState.loading;
final List<_Check> https = [];
String lastProxy;
@override
void initState() {
final setting = Provider.of<Setting>(context, listen: false);
lastProxy = setting.getProxy();
createHttps();
super.initState();
setting.addListener(() {
final proxy = setting.getProxy();
if (lastProxy != proxy) {
lastProxy = proxy;
createHttps();
}
});
}
void createHttps() {
print('重建http池 proxy:$lastProxy');
https.clear();
https.addAll(
baseUrls.keys.map(
(key) => _Check(
name: key,
url: baseUrls[key],
proxy: lastProxy,
),
),
);
check();
}
void check() async {
setState(() {
state = LoadState.loading;
});
https.forEach((http) => http.load());
await Future.wait(https.map((http) => http.load()));
final bool hasCompleted =
https.where((http) => http.state == LoadState.completed).isNotEmpty;
state = hasCompleted ? LoadState.completed : LoadState.failed;
if (hasCompleted) {
final sort = https.toList()..sort((a, b) => a.time.compareTo(b.time));
Http18Comic.instance = sort.first.http;
}
setState(() {});
}
void _showDialog(String title) async {
await showDialog(
context: context,
builder: (_) => Dialog(
title: title,
https: https,
retry: check,
),
);
}
@override
Widget build(BuildContext context) {
Widget row;
switch (state) {
case LoadState.loading:
row = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 10),
Text('正在尝试连接漫画网站'),
],
);
break;
case LoadState.failed:
row = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: Icon(Icons.error, color: Colors.red),
),
SizedBox(width: 10),
Text('连接不上漫画网站,点击查看错误'),
],
);
break;
default:
row = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: Icon(Icons.check_circle, color: Colors.green),
),
SizedBox(width: 10),
Text('成功连接到漫画网站,点击查看结果'),
],
);
}
return Padding(
padding: EdgeInsets.only(top: 10, bottom: 15),
child: GestureDetector(
child: row,
onTap: () => _showDialog('测试结果,选择源'),
),
);
}
}
class Dialog extends StatefulWidget {
final String title;
final List<_Check> https;
final Function retry;
const Dialog({Key key, this.title, this.https, this.retry}) : super(key: key);
@override
State<StatefulWidget> createState() => _Dialog();
}
class _Dialog extends State<Dialog> {
@override
Widget build(BuildContext context) {
final proxy = widget.https[0].proxy;
return AlertDialog(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.title),
if (proxy != null)
Text('正在使用代理:$proxy', style: TextStyle(fontSize: 14)),
],
),
content: Container(
width: 300,
height: 300,
child: ListView(
physics: ClampingScrollPhysics(),
shrinkWrap: true,
children: ListTile.divideTiles(
context: context,
tiles: widget.https.map(
(e) => e.build(onTap: () => setState(() {})),
)).toList(),
),
),
actions: [
FlatButton(
child: Text('再次测试'),
onPressed: () {
widget.retry();
setState(() {});
},
),
],
);
}
}
class _Check {
final String name;
final String proxy;
Http18Comic http;
Future future;
Duration time;
String error;
LoadState state;
_Check({
String url,
@required this.name,
@required this.proxy,
}) {
http = Http18Comic(
url,
name: name,
headers: headers,
proxy: proxy,
);
}
Future load() {
future = this._load();
return future;
}
Future _load() async {
state = LoadState.loading;
final now = DateTime.now();
try {
final Response<String> res = await http.dio.get<String>('/');
final $ = parse(res.data);
final $title = $.querySelector('title');
if (res.data.contains('Restricted') ||
$title == null ||
$title.text.indexOf('禁漫天堂') == -1) {
throw DioError(
request: res.request,
response: res,
error: '你使用的IP被漫画网站禁止访问请更换网络IP\n不要使用日本IP。',
);
}
state = LoadState.completed;
} catch (e) {
print(e);
if (e.runtimeType == DioError) {
final DioError error = e as DioError;
switch (error.type) {
case DioErrorType.CONNECT_TIMEOUT:
case DioErrorType.RECEIVE_TIMEOUT:
case DioErrorType.SEND_TIMEOUT:
this.error = '连接超时';
break;
default:
this.error = error.error.toString();
}
if (error.response?.data != null) {
this.error += '\n接收到的内容:\n' + error.response.data;
}
} else {
this.error = e.toString();
}
state = LoadState.failed;
print('$name 结果 $state');
}
time = DateTime.now().difference(now);
}
Widget build({Function onTap}) {
return FutureBuilder(
future: future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
final Widget title = Text(name);
switch (snapshot.connectionState) {
case ConnectionState.active:
case ConnectionState.waiting:
return ListTile(
title: title,
subtitle: Row(children: [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
SizedBox(width: 5),
Text('读取中'),
]),
);
break;
case ConnectionState.done:
if (state == LoadState.failed) {
return ListTile(
title: title,
subtitle: Text('连接失败,点击查看原因'),
onTap: () {
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text('$name 错误内容'),
content: Text(error),
);
});
},
);
}
final _time = time.inMilliseconds;
final timeString = _time > 1000
? '${(time.inMilliseconds / 1000).toStringAsFixed(2)}'
: '${time.inMilliseconds} 毫秒';
return CheckboxListTile(
title: title,
subtitle: Text('连接成功\n耗时:$timeString'),
isThreeLine: true,
value: Http18Comic.instance?.name == name,
onChanged: (name) {
Http18Comic.instance = http;
MyHttpClient.clients[http.id] = http;
onTap();
},
);
break;
default:
return ListTile(title: title, subtitle: Text('还没有开始网络请求'));
}
},
);
}
}

View File

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
class DBSourceListWidget extends StatefulWidget {
@override
_DBSourceListWidget createState() => _DBSourceListWidget();
}
class _DBSourceListWidget extends State<DBSourceListWidget> {
@override
Widget build(BuildContext context) {
return ListView(children: []);
}
}

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weiman/db/group.dart';
import 'package:weiman/provider/favoriteData.dart';
Future showDeleteGroupDialog(BuildContext context, Group group) {
return showDialog(
context: context,
builder: (_) => DeleteGroupWidget(group: group),
);
}
class DeleteGroupWidget extends StatefulWidget {
final Group group;
const DeleteGroupWidget({Key key, this.group}) : super(key: key);
@override
_DeleteGroupWidget createState() => _DeleteGroupWidget();
}
class _DeleteGroupWidget extends State<DeleteGroupWidget> {
bool deleteBooks = false;
@override
Widget build(BuildContext context) {
final length = widget.group.books.length;
return AlertDialog(
title: Text('删除分组 ${widget.group.name}'),
scrollable: true,
content: Column(
children: ListTile.divideTiles(context: context, tiles: [
if (length > 0)
ListTile(
title: Text('删除藏书'),
subtitle: Text('$length 本藏书'),
trailing: Checkbox(
value: deleteBooks,
onChanged: (v) => setState(() => deleteBooks = v),
),
)
]).toList(),
),
actions: [
FlatButton(
child: Text('确认'),
onPressed: () async {
await Provider.of<FavoriteData>(context, listen: false)
.deleteGroup(widget.group, deleteBooks);
Navigator.pop(context);
},
),
RaisedButton(
child: Text('取消'), onPressed: () => Navigator.pop(context)),
],
);
}
}

240
lib/widgets/favorites.dart Normal file
View File

@ -0,0 +1,240 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:oktoast/oktoast.dart';
import 'package:provider/provider.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/group.dart';
import 'package:weiman/provider/favoriteData.dart';
import 'package:weiman/utils.dart';
import 'package:weiman/widgets/bookGroup.dart';
import 'package:weiman/widgets/bookSettingDialog.dart';
import 'package:weiman/widgets/deleteGroupDialog.dart';
import 'package:weiman/widgets/groupFormDialog.dart';
import 'package:weiman/widgets/sliverExpandableGroup.dart';
import 'package:weiman/widgets/utils.dart';
class FavoriteList extends StatefulWidget {
@override
_FavoriteList createState() => _FavoriteList();
}
class _FavoriteList extends State<FavoriteList> {
static bool showTip = true;
@override
initState() {
super.initState();
if (showTip) {
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
showToast(
'下拉收藏列表检查更新\n分组和藏书左滑显示更多操作',
textPadding: EdgeInsets.all(10),
duration: Duration(seconds: 4),
);
showTip = false;
});
}
}
Widget bookBuilder(Book book) {
return FBookItem(
book: book,
onDelete: deleteBook,
);
}
deleteBook(Book book) async {
final sure = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('删除藏书 ${book.name} ?'),
actions: [
FlatButton(
child: Text('确认'),
onPressed: () {
Navigator.pop(context, true);
}),
],
),
);
print('删书 $sure');
if (sure != true) return;
await Provider.of<FavoriteData>(context, listen: false).deleteBook(book);
}
Future<void> deleteGroup(Group group) async {
await showDeleteGroupDialog(context, group);
setState(() {});
}
Future<void> groupRename(Group group) async {
await showGroupFormDialog(context, group);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Consumer<FavoriteData>(builder: (_, favorite, __) {
if (favorite.all.isEmpty && favorite.groups.keys.isEmpty)
return Center(child: Text('没有收藏'));
return ClipRect(
child: RefreshIndicator(
onRefresh: favorite.checkUpdate,
child: SafeArea(
child: CustomScrollView(
slivers: [
...favorite.groups.keys.map((group) {
final list = favorite.groups[group];
return BookGroupHeader(
group: group,
count: list.length,
builder: (ctx, i) => bookBuilder(favorite.groups[group][i]),
slideActions: [
IconSlideAction(
caption: '重命名',
color: Colors.blue,
icon: Icons.edit,
onTap: () => groupRename(group),
),
IconSlideAction(
caption: '删除',
color: Colors.red,
icon: Icons.delete,
onTap: () => deleteGroup(group),
),
],
);
}),
SliverExpandableGroup(
title: Text('没有分组的藏书(${favorite.others.length})'),
expanded: false,
count: favorite.others.length,
builder: (ctx, i) => bookBuilder(favorite.others[i]),
),
],
),
),
),
);
});
}
}
final bookStatusWidgets = {
BookUpdateStatus.loading:
TextSpan(text: '正在读取网络数据', style: TextStyle(color: Colors.blue)),
BookUpdateStatus.not:
TextSpan(text: '该藏书设置为不更新', style: TextStyle(color: Colors.grey)),
BookUpdateStatus.no:
TextSpan(text: '该藏书没有新章节', style: TextStyle(color: Colors.grey)),
BookUpdateStatus.wait:
TextSpan(text: '处于更新队列,等待更新', style: TextStyle(color: Colors.grey)),
BookUpdateStatus.old:
TextSpan(text: '旧藏书不检查更新', style: TextStyle(color: Colors.redAccent)),
BookUpdateStatus.fail:
TextSpan(text: '网络问题,检查更新失败', style: TextStyle(color: Colors.redAccent)),
};
class FBookItem extends StatefulWidget {
final Book book;
final void Function(Book book) onDelete;
const FBookItem({
Key key,
@required this.book,
@required this.onDelete,
}) : super(key: key);
@override
_FBookItem createState() => _FBookItem();
}
class _FBookItem extends State<FBookItem> {
@override
Widget build(BuildContext context) {
TextSpan subtitle =
bookStatusWidgets[widget.book.status ?? BookUpdateStatus.no];
if (widget.book.status == BookUpdateStatus.had) {
final _subtitle = '${widget.book.newChapterCount} 章更新';
subtitle = TextSpan(
text: _subtitle,
style: TextStyle(
color: widget.book.look ? Colors.grey : Colors.green,
),
);
}
return Slidable(
actionPane: SlidableDrawerActionPane(),
closeOnScroll: true,
actionExtentRatio: 0.25,
secondaryActions: [
IconSlideAction(
caption: '设置',
color: Colors.blue,
icon: Icons.settings,
onTap: () async {
final before = widget.book.needUpdate;
await showBookSettingDialog(context, widget.book);
if (before != widget.book.needUpdate) {
widget.book.status = widget.book.needUpdate
? BookUpdateStatus.no
: BookUpdateStatus.not;
}
if (mounted) setState(() {});
},
),
if (widget.book.status == BookUpdateStatus.had &&
widget.book.look == false)
IconSlideAction(
caption: '已读',
color: Colors.greenAccent,
iconWidget: SizedBox(
width: 24,
height: 24,
child: Icon(
FontAwesomeIcons.bellSlash,
size: 20,
color: Colors.white,
),
),
foregroundColor: Colors.white,
onTap: () async {
widget.book.chapterCount += widget.book.newChapterCount;
widget.book.look = true;
await widget.book.save();
setState(() {});
},
),
IconSlideAction(
caption: '删除',
color: Colors.red,
icon: Icons.delete,
onTap: () => widget.onDelete(widget.book),
),
],
child: ListTile(
onTap: () async {
await openBook(context, widget.book, 'fb ${widget.book.aid}');
setState(() {});
},
// onLongPress: () => onDelete(book),
leading: Hero(
tag: 'fb ${widget.book.aid}',
child: widget.book.http == null
? oldBookAvatar(text: '旧书', width: 50.0, height: 80.0)
: ExtendedImage(
image: NetworkImageSSL(widget.book.http, widget.book.avatar),
width: 50.0,
height: 80.0),
),
title: Text(widget.book.name),
subtitle: RichText(text: subtitle),
),
);
}
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:weiman/db/group.dart';
Future<Group> showGroupFormDialog(BuildContext context, [Group group]) {
return showDialog(
context: context,
builder: (_) {
return GroupFormDialog(group: group);
},
);
}
class GroupFormDialog extends StatefulWidget {
final Group group;
const GroupFormDialog({Key key, this.group}) : super(key: key);
@override
_GroupFormDialog createState() => _GroupFormDialog();
}
class _GroupFormDialog extends State<GroupFormDialog> {
final _form = GlobalKey<FormState>();
TextEditingController _nameController;
Group group;
@override
void initState() {
group = widget.group;
_nameController = TextEditingController(text: widget.group?.name ?? '');
super.initState();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.group == null ? '创建分组' : '分组重命名'),
content: Form(
key: _form,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
autofocus: true,
controller: _nameController,
decoration: InputDecoration.collapsed(
hintText: group == null ? '输入分组名称' : '原名 ${group.name}',
),
validator: (value) {
value = value.trim();
if (value.isEmpty) {
return '分组名称不能为空';
}
final sameName =
Group.groupBox.values.firstWhere((Group group) {
return group.name == value && group.key != this.group?.key;
}, orElse: () => null);
if (sameName != null) {
return '已经存在同名的分组';
}
return null;
},
)
],
),
),
actions: [
FlatButton(
child: Text('确认'),
onPressed: () async {
if (group == null) {
group = Group(_nameController.text);
} else {
group.name = _nameController.text;
}
await group.save();
Navigator.pop(context, group);
},
),
RaisedButton(
child: Text('取消'),
textColor: Colors.white,
color: Colors.blue,
onPressed: () {
Navigator.pop(context, group);
},
),
],
);
}
}

149
lib/widgets/histories.dart Normal file
View File

@ -0,0 +1,149 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:oktoast/oktoast.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/utils.dart';
import 'package:weiman/widgets/sliverExpandableGroup.dart';
import 'package:weiman/widgets/utils.dart';
class Histories extends StatefulWidget {
@override
_Histories createState() => _Histories();
}
class _Histories extends State<Histories> {
static bool _showTips = true;
final List<Book> inWeek = [], other = [];
@override
void initState() {
super.initState();
loadBook();
if (_showTips)
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
_showTips = false;
showToast(
'阅读记录和时间分组\n往左滑显示更多操作',
textPadding: EdgeInsets.all(10),
duration: Duration(seconds: 4),
);
});
}
void loadBook() {
inWeek.clear();
other.clear();
final list =
Book.bookBox.values.where((book) => book.history != null).toList();
final now = DateTime.now().millisecondsSinceEpoch;
list.sort((a, b) => b.history.time.compareTo(a.history.time));
list.forEach((book) {
if ((now - book.history.time.millisecondsSinceEpoch) < weekTime) {
inWeek.add(book);
} else {
other.add(book);
}
});
}
void clear(bool inWeek) async {
final title = '确认清空 ' + (inWeek ? '7天内的' : '更早的') + '浏览记录 ?';
final res = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text(title),
actions: [
FlatButton(
textColor: Colors.grey,
child: Text('取消'),
onPressed: () => Navigator.pop(context, false),
),
FlatButton(
child: Text('确认'),
onPressed: () => Navigator.pop(context, true),
),
],
));
print('清理历史 $inWeek $res');
if (res == false) return;
List<Book> list = inWeek ? this.inWeek : this.other;
await Future.wait(list.map((book) => book.setHistory(null)));
setState(() {
loadBook();
});
}
Widget book(List array, int index) {
final Book book = array[index];
return Slidable(
child: ListTile(
leading: book.http == null
? oldBookAvatar(text: '\n', width: 50.0, height: 80.0)
: ExtendedImage(
image: NetworkImageSSL(book.http, book.avatar),
width: 50.0,
height: 80.0),
title: Text(book.name),
subtitle: Text(book.history.cname),
onTap: () => openBook(context, book, 'fb ${book.aid}'),
),
actionPane: SlidableDrawerActionPane(),
secondaryActions: [
IconSlideAction(
caption: '删除',
color: Colors.red,
icon: Icons.delete,
onTap: () async {
await book.setHistory(null);
setState(() {
array.remove(book);
});
},
),
],
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: ClipRect(
child: CustomScrollView(
slivers: [
SliverExpandableGroup(
title: Text('7天内的浏览历史 (${inWeek.length})'),
expanded: true,
count: inWeek.length,
builder: (ctx, i) => book(inWeek, i),
slideActions: [
IconSlideAction(
caption: '清空',
color: Colors.red,
icon: Icons.delete,
onTap: () => clear(true),
),
],
),
SliverExpandableGroup(
title: Text('更早的浏览历史 (${other.length})'),
count: other.length,
builder: (ctx, i) => book(other, i),
slideActions: [
IconSlideAction(
caption: '清空',
color: Colors.red,
icon: Icons.delete,
onTap: () => clear(false),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
import 'package:weiman/widgets/animatedLogo.dart';
class SliverPullToRefreshHeader extends StatelessWidget {
static final double height = kToolbarHeight * 2;
final PullToRefreshScrollNotificationInfo info;
final void Function() onTap;
final double fontSize;
const SliverPullToRefreshHeader({
Key key,
@required this.info,
this.onTap,
this.fontSize = 16,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (info == null) return SliverToBoxAdapter(child: SizedBox());
double dragOffset = info?.dragOffset ?? 0.0;
Widget widget;
if (info.mode == RefreshIndicatorMode.error) {
widget = Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('读取网络数据失败\n你可能需要梯子'),
RaisedButton.icon(
icon: Icon(Icons.refresh),
onPressed: onTap,
label: Text('再次尝试'),
),
],
);
} else if (info.mode == RefreshIndicatorMode.refresh ||
info.mode == RefreshIndicatorMode.snap) {
widget = Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedLogoWidget(width: 20, height: 30),
SizedBox(width: 5),
Text('读取中,请稍候'),
],
);
} else if ([
RefreshIndicatorMode.drag,
RefreshIndicatorMode.armed,
RefreshIndicatorMode.snap
].contains(info.mode)) {
widget = Text('下拉刷新');
} else {
widget = SizedBox();
}
return SliverToBoxAdapter(
child: Container(
height: dragOffset,
alignment: Alignment.center,
child: widget,
),
);
}
}

219
lib/widgets/quick.dart Normal file
View File

@ -0,0 +1,219 @@
import 'package:draggable_container/draggable_container.dart';
import 'package:flutter/material.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/utils.dart';
import 'selectFavoriteBooks.dart';
import 'utils.dart';
class QuickBook extends DraggableItem {
static const heroTag = 'quickBookAvatar';
Widget child;
final BuildContext context;
final Book book;
final double width, height;
QuickBook(this.width, this.height,
{@required this.book, @required this.context}) {
child = GestureDetector(
onTap: () {
openBook(context, book, '$heroTag ${book.aid}');
},
child: Stack(
children: <Widget>[
book.http == null
? oldBookAvatar(width: width, height: height)
: SizedBox(
width: width,
height: height,
child: Hero(
tag: '$heroTag ${book.aid}',
child: Image(
image: NetworkImageSSL(book.http, book.avatar),
fit: BoxFit.cover,
),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: EdgeInsets.only(left: 2, right: 2, top: 2, bottom: 2),
color: Colors.black.withOpacity(0.5),
child: Text(
book.name,
softWrap: true,
maxLines: 1,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 10),
overflow: TextOverflow.ellipsis,
),
),
)
],
),
);
}
}
class Quick extends StatefulWidget {
final double width, height;
final Function(bool mode) draggableModeChanged;
const Quick(
{Key key, this.width, this.height, @required this.draggableModeChanged})
: super(key: key);
@override
QuickState createState() => QuickState();
}
class QuickState extends State<Quick> {
final int count = 8;
final List<DraggableItem> _draggableItems = [];
DraggableItem _addButton;
GlobalKey<DraggableContainerState> _key =
GlobalKey<DraggableContainerState>();
double width = 0, height = 0;
void exit() {
_key.currentState.draggableMode = false;
}
QuickState() {
_addButton = DraggableItem(
deletable: false,
fixed: true,
child: FlatButton(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.add,
color: Colors.grey,
),
Text(
'添加',
style: TextStyle(
fontSize: 10,
color: Colors.grey,
),
)
],
),
onPressed: () async {
final items = _key.currentState.items;
final buttonIndex = items.indexOf(_addButton);
print('add $buttonIndex');
if (buttonIndex > -1) {
final book = await showFavoriteBooksDialog(context);
print('选择了 $book');
if (book == null) return;
book
..quick = buttonIndex
..save();
_key.currentState.insteadOfIndex(buttonIndex,
QuickBook(width, height, book: book, context: context),
force: true);
}
},
),
);
}
int length() {
return _key.currentState.items.where((item) => item is QuickBook).length;
}
@override
void initState() {
super.initState();
width = widget.width / 4 - 10;
height = (width / 0.7).roundToDouble();
final list = <Book>[];
Book.bookBox.values.forEach((book) {
if (book.quick != null && list.length < count) {
list.add(book);
} else {
book.quick = null;
book.save();
}
});
print('quick book length ${list.length}');
list.sort((a, b) => a.quick.compareTo(b.quick));
_draggableItems.addAll(list.map((book) {
return QuickBook(width, height, book: book, context: context);
}));
if (_draggableItems.length < count) _draggableItems.add(_addButton);
for (var i = count - _draggableItems.length; i > 0; i--) {
_draggableItems.add(null);
}
}
@override
Widget build(BuildContext context) {
print('quick build');
return Column(
children: <Widget>[
Container(
margin: EdgeInsets.only(top: 8, bottom: 4, left: 8),
width: widget.width,
child: Text(
'快速导航(长按编辑)',
textAlign: TextAlign.left,
style: TextStyle(color: Colors.grey, fontSize: 12),
),
),
DraggableContainer(
key: _key,
slotMargin: EdgeInsets.only(bottom: 8, left: 6, right: 6),
slotSize: Size(width, height),
slotDecoration: BoxDecoration(color: Colors.grey.withOpacity(0.3)),
dragDecoration: BoxDecoration(
boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]),
items: _draggableItems,
onDraggableModeChanged: widget.draggableModeChanged,
onBeforeDelete: (index, item) async {
if (item is QuickBook) {
print('on before delete ${item.book.name}');
item.book.quick = null;
item.book.save();
}
return true;
},
onChanged: (List<DraggableItem> items) {
final nullIndex = items.indexOf(null);
final buttonIndex = items.indexOf(_addButton);
print('null $nullIndex, button $buttonIndex');
if (nullIndex > -1 && buttonIndex == -1) {
print('显示添加按钮 1');
_key.currentState.insteadOfIndex(
nullIndex, _addButton,
triggerEvent: false, force: true);
print('显示添加按钮 2');
setState(() {});
} else if (nullIndex > -1 &&
buttonIndex > -1 &&
nullIndex < buttonIndex) {
_key.currentState.removeItem(_addButton);
_key.currentState
.insteadOfIndex(nullIndex, _addButton, triggerEvent: false);
}
var quick = 0;
items.forEach((item) {
if (item is QuickBook) {
item.book
..quick = quick
..save();
quick++;
}
});
},
),
],
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/provider/favoriteData.dart';
Future<Book> showFavoriteBooksDialog(BuildContext context) {
return showDialog<Book>(
context: context,
builder: (_) => FavoriteBooksDialog(title: '将藏书添加到快速导航'),
);
}
class FavoriteBooksDialog extends StatelessWidget {
final String title;
const FavoriteBooksDialog({
Key key,
@required this.title,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final fav = Provider.of<FavoriteData>(context, listen: false);
return AlertDialog(
title: Text(title),
scrollable: true,
content: Column(
children: ListTile.divideTiles(
context: context,
tiles: fav.all
.where((book) => book.quick == null)
.map(
(book) => ListTile(
title: Text(book.name),
leading: ExtendedImage(
image: NetworkImageSSL(book.http, book.avatar),
fit: BoxFit.cover,
width: 40,
),
onTap: () => Navigator.pop(context, book),
),
)
.toList(),
).toList(),
),
);
}
}

View File

@ -0,0 +1,106 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
class SliverExpandableBuilder {
final int count;
final WidgetBuilder builder;
const SliverExpandableBuilder(this.count, this.builder);
}
class SliverExpandableGroup extends StatefulWidget {
final Widget title;
final bool expanded;
final List<Widget> actions;
final Color divideColor;
final double height;
final int count;
final IndexedWidgetBuilder builder;
final List<Widget> slideActions;
const SliverExpandableGroup({
Key key,
@required this.title,
@required this.count,
@required this.builder,
this.expanded = false,
this.actions = const [],
this.divideColor = Colors.grey,
this.height = kToolbarHeight,
this.slideActions,
}) : assert(title != null),
assert(builder != null),
super(key: key);
@override
_SliverExpandableGroup createState() => _SliverExpandableGroup();
}
class _SliverExpandableGroup extends State<SliverExpandableGroup> {
bool _expanded;
@override
initState() {
super.initState();
_expanded = widget.expanded;
}
@override
Widget build(BuildContext context) {
Decoration _decoration = BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context, color: widget.divideColor),
),
);
Widget header = InkWell(
child: Container(
height: widget.height,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
),
child: Row(children: [
Transform.rotate(
angle: _expanded ? 0 : math.pi,
child: Icon(
Icons.arrow_drop_down,
color: Colors.grey,
),
),
Expanded(child: widget.title),
...widget.actions,
]),
),
onTap: () {
setState(() {
_expanded = !_expanded;
});
},
);
if (widget.slideActions != null && widget.slideActions.length > 0) {
header = Slidable(
child: header,
actionPane: SlidableDrawerActionPane(),
secondaryActions: widget.slideActions,
);
}
return SliverStickyHeader(
header: header,
sliver: _expanded
? SliverList(
delegate: SliverChildBuilderDelegate((ctx, i) {
if (i < widget.count - 1) {
return DecoratedBox(
decoration: _decoration,
child: widget.builder(context, i),
);
}
return widget.builder(context, i);
}, childCount: widget.count))
: null,
);
}
}

45
lib/widgets/utils.dart Normal file
View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class TextDivider extends StatelessWidget {
final String text;
final double leftPadding, padding;
final List<Widget> actions;
const TextDivider({
Key key,
@required this.text,
this.padding = 5,
this.leftPadding = 15,
this.actions = const [],
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding:
EdgeInsets.only(left: leftPadding, top: padding, bottom: padding),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(child: Text(text, style: TextStyle(color: Colors.grey))),
...actions,
],
),
);
}
}
Widget oldBookAvatar({
String text = '\n\n',
width = double.infinity,
height = double.infinity,
}) {
return Container(
width: width,
height: height,
alignment: Alignment.center,
color: Colors.greenAccent,
child: Text(text),
);
}

122
pubspec.yaml Normal file
View File

@ -0,0 +1,122 @@
name: weiman
description: 微漫App
# The following line prevents the package from being accidentally published to
# pub.dev using `pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.1.4+2007
environment:
sdk: ">=2.9.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dio: any
dio_http_cache: any
image: any
intl: any
async: any
http: any
encrypt: any
html: any
hive: any
sa_anicoto: any
hive_flutter: any
shared_preferences: any
random_string: any
filesize: any
oktoast: any
path_provider: any
draggable_container: any
sticky_headers: any
flutter_sticky_header: any
extended_nested_scroll_view: any
package_info: any
url_launcher: any
font_awesome_flutter: any
webview_flutter: any
loadmore: any
pull_to_refresh_notification: any
http_client_helper: any
extended_image: any
screenshot: any
focus_widget: any
provider: any
loading_more_list: any
flutter_slidable: any
firebase_core: any
firebase_analytics: any
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.3
dev_dependencies:
flutter_test:
sdk: flutter
hive_generator: any
build_runner: any
#dependency_overrides:
# analyzer: '0.39.14'
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
assets:
- assets/logo.png
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages