1
0
mirror of https://github.com/nrop19/weiman_app.git synced 2025-08-03 15:22:47 +08:00
This commit is contained in:
nrop19 2020-02-26 05:39:36 +08:00
parent 038f6d5eaf
commit f68617e44b
16 changed files with 997 additions and 507 deletions

View File

@ -15,15 +15,12 @@ class BookState extends State<ActivityBook> {
ScrollController _scrollController; ScrollController _scrollController;
bool _reverse = false; bool _reverse = false;
bool isFavorite = false;
bool isLoading = true, isSuccess = false;
Book book; Book book;
List<Chapter> chapters = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
isFavorite = widget.book.isFavorite(); book = widget.book;
SchedulerBinding.instance.addPostFrameCallback((_) { SchedulerBinding.instance.addPostFrameCallback((_) {
_refresh.currentState _refresh.currentState
.show(notificationDragOffset: SliverPullToRefreshHeader.height); .show(notificationDragOffset: SliverPullToRefreshHeader.height);
@ -38,47 +35,36 @@ class BookState extends State<ActivityBook> {
} }
Future<bool> loadBook() async { Future<bool> loadBook() async {
setState(() { final cacheBook = await Book.loadBookCache(widget.book.aid);
isLoading = true; if (cacheBook == null) {
isSuccess = false; print('没有缓存');
}); try {
try { await loadBookFromInternet();
book = await UserAgentClient.instance } catch (e) {
.getBook(aid: widget.book.aid) return false;
.timeout(Duration(seconds: 5)); }
book.history = Data.getHistories()[book.aid]?.history; } else {
chapters print('有缓存');
..clear() book = cacheBook;
..addAll(book.chapters); updateBook();
if (_reverse) chapters = chapters.reversed.toList(); loadBookFromInternet().then((_) => updateBook());
///
if (isFavorite) Data.addFavorite(book);
_scrollToRead();
isLoading = false;
isSuccess = true;
} catch (e) {
isLoading = false;
isSuccess = false;
return false;
} }
print('刷新 $book');
setState(() {});
return true; return true;
} }
void _scrollToRead() { Future<dynamic> loadBookFromInternet() async {
if (book.history != null) { final internetBook = await HttpHHMH39.instance
final history = book.chapters .getBook(widget.book.aid)
.firstWhere((chapter) => chapter.cid == book.history.cid); .timeout(Duration(seconds: 10));
SchedulerBinding.instance.addPostFrameCallback((_) { book = internetBook;
_scrollController.animateTo( if (book.isFavorite()) Data.addFavorite(book);
WidgetChapter.height * chapters.indexOf(history).toDouble(), book.saveBookCache();
duration: Duration(milliseconds: 500), updateBook();
curve: Curves.linear); }
});
} void updateBook() {
book.history = Data.getHistories()[book.aid]?.history;
if (mounted) setState(() {});
} }
_openChapter(Chapter chapter) { _openChapter(Chapter chapter) {
@ -88,60 +74,100 @@ class BookState extends State<ActivityBook> {
}); });
} }
favoriteBook() { favoriteBook() async {
widget.book.favorite(); final fav = Provider.of<FavoriteData>(context, listen: false);
isFavorite = !isFavorite; if (book.isFavorite()) {
final isFavoriteBook = Data._quickIdList().contains(book.aid);
if (isFavoriteBook) {
final sure = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('确认取消收藏?'),
content: Text('删除这本藏书后,首页的快速导航也会删除这本藏书'),
actions: [
FlatButton(
child: Text('确认'),
onPressed: () => Navigator.pop(context, true),
),
FlatButton(
child: Text('取消'),
textColor: Colors.blue,
onPressed: () => Navigator.pop(context, false),
),
],
));
if (!sure) return;
}
fav.remove(book);
} else
fav.add(book);
setState(() {}); setState(() {});
} }
void _sort() { List<Chapter> _sort() {
setState(() { final List<Chapter> list = List.from(book.chapters);
_reverse = !_reverse; if (_reverse) return list.reversed.toList();
chapters = chapters.reversed.toList();
_scrollToRead();
});
}
List<Widget> chapterWidgets() {
final book = this.book ?? widget.book;
List<Widget> list = [];
chapters.forEach((chapter) {
final isRead = chapter.cid == book.history?.cid;
list.add(WidgetChapter(
chapter: chapter,
onTap: _openChapter,
read: isRead,
));
});
return list; return list;
} }
Widget buildChapter(BuildContext context, int index) { IndexedWidgetBuilder buildChapters(List<Chapter> chapters) {
final book = this.book ?? widget.book; IndexedWidgetBuilder builder = (BuildContext context, int index) {
final chapter = chapters[index]; final chapter = chapters[index];
final isRead = chapter.cid == book.history?.cid; Widget child;
if (index < chapters.length - 1) { if (chapter.avatar == null) {
return DecoratedBox( child = ListTile(
decoration: _Main._border, leading: Text('[已看]', style: TextStyle(color: Colors.orange)),
child: WidgetChapter( title: Text(chapter.cname),
onTap: () {},
);
} else {
child = WidgetChapter(
chapter: chapter, chapter: chapter,
onTap: _openChapter, onTap: _openChapter,
read: isRead, read: chapter.cid == book.history?.cid,
), );
); }
} if (index < chapters.length - 1)
return WidgetChapter( child = DecoratedBox(
chapter: chapter, decoration: _Main._border,
onTap: _openChapter, child: child,
read: isRead, );
); return child;
};
return builder;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isFavorite = book.isFavorite();
Color color = isFavorite ? Colors.red : Colors.white; Color color = isFavorite ? Colors.red : Colors.white;
IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border; IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border;
final book = this.book ?? widget.book; final List<Chapter> chapters = _sort();
final history = <Widget>[];
if (book.history != null && book.chapters.length > 0) {
final chapter = book.chapters
.firstWhere((chapter) => chapter.cid == book.history.cid);
history.add(ListTile(title: Text('阅读历史')));
history.add(WidgetChapter(
chapter: chapter,
onTap: _openChapter,
read: true,
));
history.add(ListTile(title: Text('下一章')));
final nextIndex = book.chapters.indexOf(chapter) + 1;
if (nextIndex < book.chapterCount) {
history.add(WidgetChapter(
chapter: book.chapters[nextIndex],
onTap: _openChapter,
read: false,
));
} else {
history.add(ListTile(subtitle: Text('没有了')));
}
history.add(SizedBox(height: 20));
}
history.add(ListTile(title: Text('章节列表')));
return Scaffold( return Scaffold(
body: PullToRefreshNotification( body: PullToRefreshNotification(
key: _refresh, key: _refresh,
@ -153,11 +179,16 @@ class BookState extends State<ActivityBook> {
SliverAppBar( SliverAppBar(
floating: true, floating: true,
pinned: true, pinned: true,
title: Text(widget.book.name), title: Text(book.name),
expandedHeight: 200, expandedHeight: 200,
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
onPressed: _sort, onPressed: () {
setState(() {
_reverse = !_reverse;
setState(() {});
});
},
icon: Icon(_reverse icon: Icon(_reverse
? FontAwesomeIcons.sortNumericDown ? FontAwesomeIcons.sortNumericDown
: FontAwesomeIcons.sortNumericDownAlt)), : FontAwesomeIcons.sortNumericDownAlt)),
@ -175,8 +206,12 @@ class BookState extends State<ActivityBook> {
height: 160, height: 160,
child: Hero( child: Hero(
tag: widget.heroTag, tag: widget.heroTag,
child: child: Image(
Image(image: NetworkImageSSL(widget.book.avatar)), image: ExtendedNetworkImageProvider(
book.avatar,
cache: true,
),
),
), ),
), ),
Expanded( Expanded(
@ -211,10 +246,16 @@ class BookState extends State<ActivityBook> {
onTap: () => _refresh.currentState.show( onTap: () => _refresh.currentState.show(
notificationDragOffset: SliverPullToRefreshHeader.height), notificationDragOffset: SliverPullToRefreshHeader.height),
)), )),
SliverToBoxAdapter(
child: Column(
children: history,
crossAxisAlignment: CrossAxisAlignment.start,
),
),
SliverList( SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
buildChapter, buildChapters(chapters),
childCount: book.chapters.length, childCount: chapters.length,
), ),
), ),
], ],

View File

@ -19,6 +19,7 @@ class ChapterState extends State<ActivityChapter> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
saveHistory(widget.chapter);
_pageController = PageController( _pageController = PageController(
keepPage: false, keepPage: false,
initialPage: widget.book.chapters.indexOf(widget.chapter)); initialPage: widget.book.chapters.indexOf(widget.chapter));
@ -30,6 +31,15 @@ class ChapterState extends State<ActivityChapter> {
super.dispose(); super.dispose();
} }
void pageChanged(int page) {
saveHistory(widget.book.chapters[page]);
widget.book.saveBookCache();
}
void saveHistory(Chapter chapter) {
Data.addHistory(widget.book, chapter);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -41,23 +51,25 @@ class ChapterState extends State<ActivityChapter> {
}, },
), ),
body: PageView.builder( body: PageView.builder(
physics: AlwaysScrollableClampingScrollPhysics(), physics: AlwaysScrollableClampingScrollPhysics(),
controller: _pageController, controller: _pageController,
itemCount: widget.book.chapters.length, itemCount: widget.book.chapters.length,
itemBuilder: (ctx, index) { onPageChanged: pageChanged,
return ChapterContentView( itemBuilder: (ctx, index) {
actions: [ return ChapterContentView(
IconButton( actions: [
icon: Icon(Icons.menu), IconButton(
onPressed: () { icon: Icon(Icons.menu),
_scaffoldKey.currentState.openEndDrawer(); onPressed: () {
}, _scaffoldKey.currentState.openEndDrawer();
), },
], ),
book: widget.book, ],
chapter: widget.book.chapters[index], book: widget.book,
); chapter: widget.book.chapters[index],
}), );
},
),
); );
} }
} }
@ -170,19 +182,19 @@ class _ChapterContentView extends State<ChapterContentView> {
Future<bool> fetchImages() async { Future<bool> fetchImages() async {
print('fetchImages'); print('fetchImages');
if (mounted) setState(() {});
loading = true; loading = true;
images.clear(); images.clear();
if (mounted) setState(() {});
final _images = Chapter.fromCache(widget.book, widget.chapter);
if (_images != null) {
print('章节 有缓存');
images.addAll(_images);
return true;
}
try { try {
images.addAll(await UserAgentClient.instance images.addAll(await HttpHHMH39.instance
.getImages(aid: widget.book.aid, cid: widget.chapter.cid) .getChapterImages(widget.book, widget.chapter)
.timeout(Duration(seconds: 5))); .timeout(Duration(seconds: 10)));
if (images.length < 5) {
// print('图片 前:' + images.toString());
final list =
await checkImage(images.last).timeout(Duration(seconds: 15));
images.addAll(list);
}
} catch (e) { } catch (e) {
print('错误 $e'); print('错误 $e');
showToastWidget( showToastWidget(
@ -206,6 +218,7 @@ class _ChapterContentView extends State<ChapterContentView> {
} }
loading = false; loading = false;
// print('所有图片:' + images.toString()); // print('所有图片:' + images.toString());
Chapter.saveCache(widget.book, widget.chapter, images);
if (mounted) setState(() {}); if (mounted) setState(() {});
return true; return true;
} }
@ -264,8 +277,9 @@ class _ChapterContentView extends State<ChapterContentView> {
], ],
), ),
), ),
content: ExtendedImage( content: ExtendedImage.network(
image: NetworkImageSSL(images[i]), images[i],
cache: true,
enableLoadState: true, enableLoadState: true,
enableMemoryCache: true, enableMemoryCache: true,
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
@ -303,22 +317,6 @@ class _ChapterContentView extends State<ChapterContentView> {
} }
}, },
), ),
// content: Image(
// image: NetworkImageSSL(images[i]),
// loadingBuilder: (_, child, loadingProgress) {
// if (loadingProgress == null) return child;
// return SizedBox(
// height: 400,
// child: Center(
// child: CircularProgressIndicator(
// value: loadingProgress.expectedTotalBytes != null
// ? loadingProgress.cumulativeBytesLoaded /
// loadingProgress.expectedTotalBytes
// : null,
// ),
// ),
// );
// }),
); );
}, },
childCount: images.length, childCount: images.length,
@ -349,10 +347,8 @@ Future<List<String>> checkImage(String last) async {
file2 = getFileName(name: fileName, divider: '_', plus: plus + 1); file2 = getFileName(name: fileName, divider: '_', plus: plus + 1);
var url1 = '$a/$b/$file1.$fileFormat', url2 = '$a/$b/$file2.$fileFormat'; var url1 = '$a/$b/$file1.$fileFormat', url2 = '$a/$b/$file2.$fileFormat';
// print('正在测试:\n' + url1 + '\n' + url2); // print('正在测试:\n' + url1 + '\n' + url2);
final res = await Future.wait([ final res = await Future.wait(
UserAgentClient.instance.head(url1), [HttpHHMH39.instance.head(url1), HttpHHMH39.instance.head(url2)]);
UserAgentClient.instance.head(url2)
]);
if (res[0].statusCode != 200) break; if (res[0].statusCode != 200) break;
list.add(url1); list.add(url1);
if (res[1].statusCode != 200) { if (res[1].statusCode != 200) {

View File

@ -17,7 +17,7 @@ final titleTextStyle = TextStyle(fontSize: 14, color: Colors.blue),
class _State extends State<ActivityCheckData> { class _State extends State<ActivityCheckData> {
CheckState firstState; CheckState firstState;
int firstLength; int firstLength = 0;
final TextSpan secondResults = TextSpan(); final TextSpan secondResults = TextSpan();
TextEditingController _outputController, _inputController; TextEditingController _outputController, _inputController;

View File

@ -26,10 +26,11 @@ class HomeState extends State<ActivityHome> {
/// ///
SchedulerBinding.instance.addPostFrameCallback((_) async { SchedulerBinding.instance.addPostFrameCallback((_) async {
autoSwitchTheme(); autoSwitchTheme();
_FavoriteList.getBooks(); FavoriteData favData = Provider.of<FavoriteData>(context,listen: false);
await _FavoriteList.checkNews(); await favData.loadBooksList();
final updated = _FavoriteList.hasNews.values await favData.checkNews(Provider.of<SettingData>(context, listen: false).autoCheck);
.where((int updatedChapters) => updatedChapters > 0) final updated = favData.hasNews.values
.where((int count) => count > 0)
.length; .length;
if (updated > 0) if (updated > 0)
showToast( showToast(
@ -59,11 +60,15 @@ class HomeState extends State<ActivityHome> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: RouteSettings(name: '/activity_recommend/'), settings: RouteSettings(name: '/activity_recommend'),
builder: (_) => ActivityRank(), builder: (_) => ActivityRank(),
)); ));
} }
void gotoPatreon() {
launch('https://www.patreon.com/nrop19');
}
bool isEdit = false; bool isEdit = false;
void _draggableModeChanged(bool mode) { void _draggableModeChanged(bool mode) {
@ -104,6 +109,18 @@ class HomeState extends State<ActivityHome> {
), ),
SizedBox(width: 20), SizedBox(width: 20),
///
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: '/activity_setting'),
builder: (_) => ActivitySetting()));
},
icon: Icon(FontAwesomeIcons.cog),
),
/// ///
IconButton( IconButton(
onPressed: () { onPressed: () {
@ -138,110 +155,125 @@ class HomeState extends State<ActivityHome> {
}, },
), ),
), ),
body: Container( body: Center(
alignment: Alignment.center, child: SingleChildScrollView(
padding: EdgeInsets.only(left: 40, right: 40), padding: EdgeInsets.only(left: 40, right: 40),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: <Widget>[ children: <Widget>[
Container( Container(
child: OutlineButton( child: OutlineButton(
onPressed: gotoSearch, onPressed: gotoSearch,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Icon( Icon(
Icons.search, Icons.search,
color: Colors.blue, color: Colors.blue,
), ),
Text( Text(
'搜索漫画', '搜索漫画',
style: TextStyle(color: Colors.blue), style: TextStyle(color: Colors.blue),
) )
], ],
),
borderSide: BorderSide(color: Colors.blue, width: 2),
shape: StadiumBorder(),
), ),
borderSide: BorderSide(color: Colors.blue, width: 2),
shape: StadiumBorder(),
), ),
), Row(
Row( children: [
children: [ Expanded(
Expanded( flex: 7,
child: OutlineButton( child: OutlineButton(
onPressed: gotoRecommend, onPressed: gotoRecommend,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Icon( Icon(
Icons.whatshot, Icons.whatshot,
color: Colors.red, color: Colors.red,
), ),
Text( Text(
'月排行榜', '月排行榜',
style: TextStyle(color: Colors.red), style: TextStyle(color: Colors.red),
) )
], ],
),
borderSide: BorderSide(color: Colors.red, width: 2),
shape: StadiumBorder(),
), ),
borderSide: BorderSide(color: Colors.red, width: 2), ),
shape: StadiumBorder(), // SizedBox(width: 10),
// Expanded(
// flex: 3,
// child: OutlineButton(
// onPressed: gotoPatreon,
// child: Text.rich(
// TextSpan(children: [TextSpan(text: '赞助')]),
// style: TextStyle(color: Colors.red),
// ),
// borderSide: BorderSide(color: Colors.red, width: 2),
// shape: StadiumBorder(),
// ),
// ),
],
),
Center(
child: Quick(
key: _quickState,
width: width,
draggableModeChanged: _draggableModeChanged,
),
),
Container(
margin: EdgeInsets.only(bottom: 10),
child: Text(
'在 level-plus.net 论坛首发',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[500]),
),
),
GestureDetector(
onTap: () async {
if (await canLaunch('tg://resolve?domain=weiman_app'))
launch('tg://resolve?domain=weiman_app');
else
launch('https://t.me/weiman_app');
},
child: Text(
'关注 Telegram 广播频道',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.blue[200],
decoration: TextDecoration.underline,
), ),
), ),
],
),
Center(
child: Quick(
key: _quickState,
width: width,
draggableModeChanged: _draggableModeChanged,
), ),
), Visibility(
Container( visible: isDevMode,
margin: EdgeInsets.only(bottom: 10), child: FlatButton(
child: Text( onPressed: () {
'在 level-plus.net 论坛首发', Navigator.push(context,
textAlign: TextAlign.center, MaterialPageRoute(builder: (_) => ActivityTest()));
style: TextStyle(color: Colors.grey[500]), },
), child: Text('测试界面'),
),
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(
Visibility( visible: isDevMode,
visible: isDevMode, child: FlatButton(
child: FlatButton( onPressed: () {
onPressed: () { Navigator.push(context,
Navigator.push(context, MaterialPageRoute(builder: (_) => ActivityCheckData()));
MaterialPageRoute(builder: (_) => ActivityTest())); },
}, child: Text('操作 收藏列表数据'),
child: Text('测试界面'), ),
), ),
), ],
Visibility( ),
visible: isDevMode,
child: FlatButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => ActivityCheckData()));
},
child: Text('操作 收藏列表数据'),
),
),
],
), ),
), ),
); );

86
lib/activities/rank.dart Normal file
View File

@ -0,0 +1,86 @@
part of '../main.dart';
class ActivityRank extends StatefulWidget {
@override
_ActivityRank createState() => _ActivityRank();
}
class _ActivityRank extends State<ActivityRank> {
final List<Book> books = [];
int page = 1;
LoadMoreD _d;
@override
void initState() {
super.initState();
_d = LoadMoreD(reload);
}
void reload() {
onLoadMore();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('月排行榜')),
body: Container(
child: LoadMore(
isFinish: page >= 10,
onLoadMore: onLoadMore,
delegate: _d,
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return WidgetBook(
books[index],
subtitle: books[index].author,
);
},
itemCount: books.length,
),
),
),
);
}
Future<bool> onLoadMore() async {
bool isSuccess = false;
try {
books.addAll(await HttpHHMH39.instance.getMonthList(page: page));
isSuccess = true;
print('load $page');
page++;
setState(() {});
} catch (e) {
print('$e $page');
}
return isSuccess;
}
}
class LoadMoreD extends LoadMoreDelegate {
final void Function() reload;
LoadMoreD(this.reload);
@override
Widget buildChild(LoadMoreStatus status,
{builder = DefaultLoadMoreTextBuilder.chinese}) {
Widget widget;
switch (status) {
case LoadMoreStatus.idle:
widget = SizedBox();
break;
case LoadMoreStatus.loading:
widget = SafeArea(child: Center(child: Text('读取中')));
break;
case LoadMoreStatus.fail:
widget = SafeArea(child: Center(child: Text('读取失败,点击再次尝试')));
break;
case LoadMoreStatus.nomore:
widget = SafeArea(child: Center(child: Text('就这些了')));
break;
}
return widget;
}
}

View File

@ -32,7 +32,7 @@ class SearchState extends State<Search> {
}); });
_books.clear(); _books.clear();
try { try {
final List<Book> books = await UserAgentClient.instance final List<Book> books = await HttpHHMH39.instance
.searchBook(_controller.text) .searchBook(_controller.text)
.timeout(Duration(seconds: 5)); .timeout(Duration(seconds: 5));
_books.addAll(books); _books.addAll(books);
@ -47,52 +47,56 @@ class SearchState extends State<Search> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( 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('回车键搜索');
submit();
}
},
child: FocusWidget.builder(
context,
(_, focusNode) => TextField(
focusNode: focusNode,
decoration: InputDecoration(
hintText: '搜索书名',
prefixIcon: IconButton(
onPressed: () {
_refresh.currentState.show(
notificationDragOffset:
SliverPullToRefreshHeader.height);
},
icon: Icon(Icons.search),
),
),
textAlign: TextAlign.left,
controller: _controller,
autofocus: true,
textInputAction: TextInputAction.search,
onSubmitted: (String name) {
focusNode.unfocus();
print('onSubmitted');
submit();
},
keyboardType: TextInputType.text,
onEditingComplete: () {
focusNode.unfocus();
print('onEditingComplete');
submit();
},
),
),
),
),
body: PullToRefreshNotification( body: PullToRefreshNotification(
key: _refresh, key: _refresh,
onRefresh: startSearch, onRefresh: startSearch,
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
SliverAppBar(
pinned: true,
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('回车键搜索');
submit();
}
},
child: TextField(
decoration: InputDecoration(
hintText: '搜索书名',
prefixIcon: IconButton(
onPressed: () {
_refresh.currentState.show(
notificationDragOffset:
SliverPullToRefreshHeader.height);
},
icon: Icon(Icons.search),
),
),
textAlign: TextAlign.left,
controller: _controller,
autofocus: true,
textInputAction: TextInputAction.search,
onSubmitted: (String name) {
print('onSubmitted');
submit();
},
keyboardType: TextInputType.text,
onEditingComplete: () {
print('onEditingComplete');
submit();
},
),
),
),
PullToRefreshContainer((info) => SliverPullToRefreshHeader( PullToRefreshContainer((info) => SliverPullToRefreshHeader(
info: info, info: info,
onTap: submit, onTap: submit,

160
lib/activities/setting.dart Normal file
View File

@ -0,0 +1,160 @@
part of '../main.dart';
enum AutoCheckLevel {
none,
onlyInWeek,
all,
}
class SettingData extends ChangeNotifier {
static final String key = 'setting_data';
AutoCheckLevel _autoCheck;
SettingData() {
final Map<String, dynamic> data =
jsonDecode(Data.instance.getString(key) ?? '{}');
_autoCheck = data['autoCheck'] == null
? AutoCheckLevel.onlyInWeek
: AutoCheckLevel.values[data['autoCheck']];
print('SettingData');
print(toJson());
}
get autoCheck => _autoCheck;
set autoCheck(AutoCheckLevel val) {
_autoCheck = val;
notifyListeners();
save();
}
Map<String, dynamic> toJson() {
return {'autoCheck': _autoCheck.index};
}
void save() {
Data.instance.setString(key, jsonEncode(toJson()));
}
}
class ActivitySetting extends StatefulWidget {
@override
_ActivitySetting createState() => _ActivitySetting();
}
class _ActivitySetting extends State<ActivitySetting> {
static final Map<String, AutoCheckLevel> levels = {
'不检查': AutoCheckLevel.none,
'7天内看过': AutoCheckLevel.onlyInWeek,
'全部': AutoCheckLevel.all
};
int imagesCount, sizeCount;
bool isClearing = false;
@override
void initState() {
super.initState();
imageCaches();
}
Future<void> imageCaches() async {
final dir = await getTemporaryDirectory();
final cacheDir = Directory(path.join(dir.path, CacheImageFolderName));
if (cacheDir.existsSync() == false) cacheDir.createSync();
final files = cacheDir.listSync();
imagesCount = files.length;
sizeCount = 0;
files.forEach((file) => sizeCount += file.statSync().size);
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('设置')),
body: Consumer<SettingData>(
builder: (_, data, __) => ListView(
children: ListTile.divideTiles(
context: context,
tiles: [
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 {
final makeSure = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('确认清除所有图片缓存?'),
actions: [
FlatButton(
child: Text('取消'),
onPressed: () => Navigator.pop(context, false),
),
RaisedButton(
child: Text('确认'),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
if (makeSure == false) return;
showToast('正在清理图片缓存');
isClearing = true;
setState(() {});
await clearDiskCachedImages();
isClearing = false;
if (mounted) {
setState(() {});
await imageCaches();
}
showToast('成功清理图片缓存');
},
),
autoCheck(data),
],
).toList(),
),
),
);
}
Widget autoCheck(SettingData data) {
return ListTile(
title: Text('自动检查收藏漫画的更新'),
subtitle: Text('每次启动App后检查一次更新\n有很多漫画收藏的建议只检查7天内看过的漫画'),
trailing: DropdownButton<AutoCheckLevel>(
value: data.autoCheck,
items: levels.keys
.map(
(key) => DropdownMenuItem(
child: Text(key),
value: levels[key],
),
)
.toList(),
onChanged: (level) {
data.autoCheck = level;
// setState(() {});
},
),
);
}
}

View File

@ -1,6 +1,7 @@
part of '../main.dart'; part of '../main.dart';
class Book { class Book {
static String cachePath;
final String aid; // ID final String aid; // ID
final String name; // final String name; //
final String avatar; // final String avatar; //
@ -8,6 +9,7 @@ class Book {
final String description; // final String description; //
final List<Chapter> chapters; final List<Chapter> chapters;
final int chapterCount; final int chapterCount;
final bool fromCache;
History history; History history;
@ -19,8 +21,16 @@ class Book {
this.description, this.description,
this.chapters: const [], this.chapters: const [],
this.chapterCount: 0, this.chapterCount: 0,
this.fromCache: false,
this.history,
}); });
static Future<void> initCachePath() async {
final Directory dir = await getTemporaryDirectory();
if (!dir.existsSync()) dir.createSync();
cachePath = path.join(dir.path, 'books');
}
@override @override
String toString() { String toString() {
return jsonEncode(toJson()); return jsonEncode(toJson());
@ -31,13 +41,6 @@ class Book {
return books.containsKey(aid); return books.containsKey(aid);
} }
favorite() {
if (isFavorite())
Data.removeFavorite(this);
else
Data.addFavorite(this);
}
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = { final Map<String, dynamic> data = {
'aid': aid, 'aid': aid,
@ -63,6 +66,48 @@ class Book {
book.history = History.fromJson(json['history']); book.history = History.fromJson(json['history']);
return book; return book;
} }
Future<void> saveBookCache() async {
final file = File(path.join(cachePath, '$aid.json'));
if (!file.existsSync()) file.createSync(recursive: true);
await file.writeAsString(
jsonEncode(
{
'aid': aid,
'name': name,
'author': author,
'avatar': avatar,
'description': description,
'chapters':
chapters.map<String>((chapter) => chapter.toString()).toList(),
},
),
);
}
static Future<Book> loadBookCache(String aid) async {
final file = File(path.join(cachePath, '$aid.json'));
print('loadBookCache ${file.path}');
if (file.existsSync()) {
final json = jsonDecode(await file.readAsString());
final List<dynamic> chapters = json['chapters'] ?? [];
print('chapters ${json['chapters'][0]}');
return Book(
aid: json['aid'],
name: json['name'],
avatar: json['avatar'],
description: json['description'],
author: json['author'],
fromCache: true,
chapters: chapters.map((str) => Chapter.fromJsonString(str)).toList(),
chapterCount: chapters.length,
history: json['history'] == null
? null
: History.fromJson(jsonDecode(json['history'])),
);
}
return null;
}
} }
class Chapter { class Chapter {
@ -70,11 +115,48 @@ class Chapter {
final String cname; // final String cname; //
final String avatar; // final String avatar; //
Chapter({@required this.cid, @required this.cname, @required this.avatar}); Chapter({
@required this.cid,
@required this.cname,
@required this.avatar,
});
@override @override
String toString() { String toString() {
return jsonEncode({cid: cid, cname: cname, avatar: avatar}); final Map<String, String> data = {
'cid': cid,
'cname': cname,
'avatar': avatar,
};
return jsonEncode(data);
}
static Chapter fromJsonString(String str) {
return fromJson(jsonDecode(str));
}
static List<String> fromCache(Book book, Chapter chapter) {
final file =
File(path.join(Book.cachePath, '${book.aid}_${chapter.cid}.json'));
if (file.existsSync()) return List<String>.from(jsonDecode(file.readAsStringSync()));
return null;
}
static Future<void> saveCache(
Book book, Chapter chapter, List<String> images) async {
print('chapter save cache ${chapter.cid}');
final file =
File(path.join(Book.cachePath, '${book.aid}_${chapter.cid}.json'));
if (file.existsSync() == false) file.createSync();
return file.writeAsString(jsonEncode(images));
}
static fromJson(data) {
return Chapter(
cid: data['cid'],
cname: data['cname'],
avatar: data['avatar'],
);
} }
} }

View File

@ -1,48 +0,0 @@
part of '../main.dart';
const domain = '';
class UserAgentClient extends http.BaseClient {
http.Client _inner = http.Client();
String lastKey;
int lastKeyTime = 0;
static UserAgentClient instance;
// UserAgentClient(this.userAgent);
UserAgentClient(String userAgent, ByteData data) {
}
Future<String> getKey() async {
}
@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
}
Future<List<String>> getImages(
{@required String aid, @required String cid}) async {
}
Future<Book> getBook({String aid}) async {
}
static String _decrypt({String key, String content}) {
}
Future<List<Book>> searchBook(String name) async {
}
static void init() async {
}
Future<http.Response> _get(url, {Map<String, String> headers}) async {
}
Future<List<Book>> getMonthList() async {
}
Future<List<Book>> getIndexRandomBooks() async {
}
}

View File

@ -7,10 +7,15 @@ class NetworkImageSSL
/// Creates an object that fetches the image at the given URL. /// Creates an object that fetches the image at the given URL.
/// ///
/// The arguments [url] and [scale] must not be null. /// The arguments [url] and [scale] must not be null.
const NetworkImageSSL(this.url, {this.scale = 1.0, this.headers}) const NetworkImageSSL(
: assert(url != null), this.url, {
this.scale = 1.0,
this.headers,
this.timeout = 8,
}) : assert(url != null),
assert(scale != null); assert(scale != null);
final int timeout;
@override @override
final String url; final String url;
@ -72,8 +77,9 @@ class NetworkImageSSL
assert(key == this); assert(key == this);
final Uri resolved = Uri.base.resolve(key.url); final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = final HttpClientRequest request = await _httpClient
await _httpClient.getUrl(resolved).timeout(Duration(seconds: 5)); .getUrl(resolved)
.timeout(Duration(seconds: timeout));
headers?.forEach((String name, String value) { headers?.forEach((String name, String value) {
request.headers.add(name, value); request.headers.add(name, value);
}); });

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

@ -0,0 +1,30 @@
part of '../main.dart';
class MyHttpClient {
static Future init() async {
final ca = await rootBundle.load('assets/ca.crt');
final SecurityContext context = SecurityContext.defaultContext;
context.setTrustedCertificatesBytes(ca.buffer.asUint8List());
final version = Random().nextInt(10) + 72;
final userAgent =
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$version.0.4056.3 Mobile Safari/537.36';
final ioClient = new HttpClient()
..badCertificateCallback = (_, __, ___) => true;
ioClient.userAgent = userAgent;
final client = IOClient(ioClient);
}
}
abstract class UserAgentClient extends http.BaseClient {
final http.Client inner;
UserAgentClient(this.inner);
Future<List<Book>> searchBook(String name);
Future<Book> getBook(String aid);
Future<List<String>> getChapterImages(Book book, Chapter chapter);
}

View File

@ -3,36 +3,39 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:math'; import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:draggable_container/draggable_container.dart'; import 'package:draggable_container/draggable_container.dart';
import 'package:dynamic_theme/dynamic_theme.dart'; import 'package:dynamic_theme/dynamic_theme.dart';
import 'package:encrypt/encrypt.dart' as encrypt; import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:extended_image/extended_image.dart'; import 'package:extended_image/extended_image.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:filesize/filesize.dart';
import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart'; import 'package:firebase_analytics/observer.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide NestedScrollView; import 'package:flutter/material.dart' hide NestedScrollView;
import 'package:flutter/painting.dart' as image_provider;
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:focus_widget/focus_widget.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:html/parser.dart' as html; import 'package:html/parser.dart' as html;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:http/io_client.dart'; import 'package:http/io_client.dart';
import 'package:http_client_helper/http_client_helper.dart';
import 'package:loadmore/loadmore.dart';
import 'package:oktoast/oktoast.dart'; import 'package:oktoast/oktoast.dart';
import 'package:package_info/package_info.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:pull_to_refresh_notification/pull_to_refresh_notification.dart' import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'
hide CircularProgressIndicator; hide CircularProgressIndicator;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:sticky_headers/sticky_headers/widget.dart'; import 'package:sticky_headers/sticky_headers/widget.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart' as image_provider;
part './activities/book.dart'; part './activities/book.dart';
@ -46,16 +49,22 @@ part './activities/rank.dart';
part './activities/search.dart'; part './activities/search.dart';
part './activities/setting.dart';
part './activities/test.dart'; part './activities/test.dart';
part './classes/book.dart'; part './classes/book.dart';
part './classes/data.dart'; part './classes/data.dart';
part './classes/http.dart';
part './classes/networkImageSSL.dart'; part './classes/networkImageSSL.dart';
part './crawler/hhmh39.dart';
part './crawler/http.dart';
part './crawler/httpHanManJia.dart';
part './widgets/book.dart'; part './widgets/book.dart';
part './widgets/favorites.dart'; part './widgets/favorites.dart';
@ -88,13 +97,22 @@ void main() async {
await Future.wait([ await Future.wait([
Data.init(), Data.init(),
Book.initCachePath(),
MyHttpClient.init(),
SystemChrome.setPreferredOrientations( SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]) [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
]); ]);
final PackageInfo packageInfo = await PackageInfo.fromPlatform(); final PackageInfo packageInfo = await PackageInfo.fromPlatform();
UserAgentClient.init(); runApp(
runApp(Main(packageInfo: packageInfo)); MultiProvider(
providers: [
ChangeNotifierProvider<SettingData>(create: (_) => SettingData()),
ChangeNotifierProvider<FavoriteData>(create: (_) => FavoriteData()),
],
child: Main(packageInfo: packageInfo),
),
);
} }
class Main extends StatefulWidget { class Main extends StatefulWidget {
@ -109,6 +127,12 @@ class Main extends StatefulWidget {
class _Main extends State<Main> with WidgetsBindingObserver { class _Main extends State<Main> with WidgetsBindingObserver {
static BoxDecoration _border; static BoxDecoration _border;
@override
initState() {
super.initState();
HttpClientHelper().set(HttpHHMH39.instance.inner);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_border == null) if (_border == null)
@ -123,7 +147,7 @@ class _Main extends State<Main> with WidgetsBindingObserver {
themedWidgetBuilder: (context, theme) { themedWidgetBuilder: (context, theme) {
return OKToast( return OKToast(
child: MaterialApp( child: MaterialApp(
title: 'Flutter Demo', title: '微漫 v${widget.packageInfo.version}',
theme: theme, theme: theme,
home: ActivityHome(widget.packageInfo), home: ActivityHome(widget.packageInfo),
), ),

View File

@ -28,12 +28,13 @@ class WidgetBook extends StatelessWidget {
), ),
dense: true, dense: true,
leading: Hero( leading: Hero(
tag: 'bookAvatar${book.aid}', tag: 'bookAvatar${book.aid}',
child: Image(image:NetworkImageSSL( child: Image(
book.avatar), image: ExtendedNetworkImageProvider(book.avatar, cache: true),
height: 200, height: 200,
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
)), ),
),
trailing: Icon( trailing: Icon(
isLiked ? Icons.favorite : Icons.favorite_border, isLiked ? Icons.favorite : Icons.favorite_border,
color: isLiked ? Colors.red : Colors.grey, color: isLiked ? Colors.red : Colors.grey,
@ -57,7 +58,7 @@ class WidgetChapter extends StatelessWidget {
Key key, Key key,
this.chapter, this.chapter,
this.onTap, this.onTap,
this.read, this.read = false,
}) : super(key: key); }) : super(key: key);
@override @override
@ -83,11 +84,16 @@ class WidgetChapter extends StatelessWidget {
softWrap: true, softWrap: true,
maxLines: 2, maxLines: 2,
), ),
leading: Image(image:NetworkImageSSL( leading: chapter.avatar == null
chapter.avatar), ? null
fit: BoxFit.fitWidth, : Image(
width: 100, image: ExtendedNetworkImageProvider(
), chapter.avatar,
cache: true,
),
fit: BoxFit.fitWidth,
width: 100,
),
); );
} }
} }
@ -106,8 +112,8 @@ class WidgetHistory extends StatelessWidget {
if (onTap != null) onTap(book); if (onTap != null) onTap(book);
}, },
title: Text(book.name), title: Text(book.name),
leading: Image(image:NetworkImageSSL( leading: Image(
book.avatar), image: ExtendedNetworkImageProvider(book.avatar, cache: true),
fit: BoxFit.fitHeight, fit: BoxFit.fitHeight,
), ),
subtitle: Text(book.history.cname), subtitle: Text(book.history.cname),
@ -138,8 +144,8 @@ class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
void load() async { void load() async {
loading = true; loading = true;
try { try {
final book = await UserAgentClient.instance final book = await HttpHHMH39.instance
.getBook(aid: widget.book.aid) .getBook(widget.book.aid)
.timeout(Duration(seconds: 2)); .timeout(Duration(seconds: 2));
news = book.chapterCount - widget.book.chapterCount; news = book.chapterCount - widget.book.chapterCount;
hasError = false; hasError = false;
@ -173,7 +179,9 @@ class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
openBook(context, widget.book, 'checkBook${widget.book.aid}'), openBook(context, widget.book, 'checkBook${widget.book.aid}'),
leading: Hero( leading: Hero(
tag: 'checkBook${widget.book.aid}', tag: 'checkBook${widget.book.aid}',
child: Image(image:NetworkImageSSL(widget.book.avatar)), child: Image(
image:
ExtendedNetworkImageProvider(widget.book.avatar, cache: true)),
), ),
dense: true, dense: true,
isThreeLine: true, isThreeLine: true,

View File

@ -1,15 +1,88 @@
part of '../main.dart'; part of '../main.dart';
class FavoriteData extends ChangeNotifier {
/// -2 -10 > 0
final Map<String, int> hasNews = {}; //
final Map<String, Book> all = {}, //
inWeek = {}, // 7
other = {}; //
bool _loading;
FavoriteData() {
loadBooksList();
}
Future<void> loadBooksList() async {
all
..clear()
..addAll(Data.getFavorites());
calcBookHistory();
}
void add(Book book) {
Data.addFavorite(book);
all[book.aid] = book;
calcBookHistory();
}
void remove(Book book) {
Data.removeFavorite(book);
all.remove(book.aid);
calcBookHistory();
}
void calcBookHistory() {
inWeek.clear();
other.clear();
if (all.isNotEmpty) {
final now = DateTime.now().millisecondsSinceEpoch;
all.forEach((aid, book) {
if (book.history != null && (now - book.history.time) < weekTime) {
inWeek[aid] = book;
} else {
other[aid] = book;
}
});
}
notifyListeners();
}
Future<void> checkNews(AutoCheckLevel level) async {
if (level == AutoCheckLevel.none) return;
final _books = level == AutoCheckLevel.onlyInWeek ? inWeek : all;
final keys = _books.keys;
hasNews
..clear()
..addAll(_books.map((aid, book) => MapEntry(aid, -2)));
notifyListeners();
Book currentBook, newBook;
for (var i = 0; i < _books.length; i++) {
currentBook = _books[keys.elementAt(i)];
try {
newBook = await HttpHHMH39.instance
.getBook(currentBook.aid)
.timeout(Duration(seconds: 8));
int different = newBook.chapterCount - currentBook.chapterCount;
hasNews[currentBook.aid] = different;
if (different > 0) {
newBook.history = Data.getHistories()[newBook.aid]?.history;
Data.addFavorite(newBook);
newBook.saveBookCache();
}
} catch (e) {
hasNews[currentBook.aid] = -1;
}
}
notifyListeners();
}
}
class FavoriteList extends StatefulWidget { class FavoriteList extends StatefulWidget {
@override @override
_FavoriteList createState() => _FavoriteList(); _FavoriteList createState() => _FavoriteList();
} }
class _FavoriteList extends State<FavoriteList> { class _FavoriteList extends State<FavoriteList> {
static final Map<String, int> hasNews = {};
static final List<Book> all = [], //
inWeek = [], // 7
other = []; //
static bool showTip = false; static bool showTip = false;
static final loadFailTextSpan = TextSpan( static final loadFailTextSpan = TextSpan(
@ -20,140 +93,100 @@ class _FavoriteList extends State<FavoriteList> {
TextSpan(text: '请下拉列表检查更新', style: TextStyle(color: Colors.grey)), TextSpan(text: '请下拉列表检查更新', style: TextStyle(color: Colors.grey)),
noUpdate = TextSpan(text: '没有更新', style: TextStyle(color: Colors.grey)); noUpdate = TextSpan(text: '没有更新', style: TextStyle(color: Colors.grey));
static void getBooks() {
all.clear();
inWeek.clear();
other.clear();
all.addAll(Data.getFavorites().values);
if (all.isNotEmpty) {
final now = DateTime.now().millisecondsSinceEpoch;
all.forEach((book) {
if (book.history != null && (now - book.history.time) < weekTime) {
inWeek.add(book);
} else {
other.add(book);
}
});
}
}
@override
void initState() {
super.initState();
getBooks();
if (all.isNotEmpty) {
if (showTip == false) {
showTip = true;
showToast(
'下拉列表可以检查漫画更新',
backgroundColor: Colors.black.withOpacity(0.5),
);
}
}
}
void _openBook(book) { void _openBook(book) {
openBook(context, book, 'fb ${book.aid}'); openBook(context, book, 'fb ${book.aid}');
} }
static Future<void> checkNews() async { Widget bookBuilder(Book book, int state) {
hasNews.clear(); TextSpan _state = unCheck;
Book currentBook, newBook; if (state == null) {
int different; _state = unCheck;
for (var i = 0; i < all.length; i++) { } else if (state > 0) {
currentBook = all[i]; _state =
try { TextSpan(text: '$state 章更新', style: TextStyle(color: Colors.green));
newBook = await UserAgentClient.instance } else if (state == 0) {
.getBook(aid: currentBook.aid) _state = noUpdate;
.timeout(Duration(seconds: 2)); } else if (state == -1) {
different = newBook.chapterCount - currentBook.chapterCount; _state = loadFailTextSpan;
hasNews[currentBook.aid] = different; } else if (state == -2) {
} catch (e) { _state = waitToCheck;
hasNews[currentBook.aid] = -1;
}
}
}
Widget bookBuilder(Book book) {
TextSpan state;
if (hasNews.isEmpty) {
state = unCheck;
} else {
if (hasNews.containsKey(book.aid)) {
if (hasNews[book.aid] > 0) {
state = TextSpan(
text: '${hasNews[book.aid]} 章更新',
style: TextStyle(color: Colors.green));
} else if (hasNews[book.aid] == -1) {
state = loadFailTextSpan;
} else if (hasNews[book.aid] == 0) {
state = noUpdate;
}
} else {
state = waitToCheck;
}
} }
return FBookItem( return FBookItem(
book: book, book: book,
subtitle: state, subtitle: _state,
onTap: _openBook, onTap: _openBook,
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Book> inWeekUpdated = [], return Consumer2<SettingData, FavoriteData>(
inWeekUnUpdated = [], builder: (_, setting, favorite, __) {
otherUpdated = [], if (favorite.all.isEmpty) return Center(child: Text('没有收藏'));
otherUnUpdated = []; List<Book> inWeekUpdated = [],
inWeek.forEach((book) { inWeekUnUpdated = [],
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0) otherUpdated = [],
inWeekUpdated.add(book); otherUnUpdated = [];
else favorite.inWeek.forEach((aid, book) {
inWeekUnUpdated.add(book); if (favorite.hasNews.containsKey(book.aid) &&
favorite.hasNews[book.aid] > 0)
inWeekUpdated.add(book);
else
inWeekUnUpdated.add(book);
});
favorite.other.forEach((aid, book) {
if (favorite.hasNews.containsKey(book.aid) &&
favorite.hasNews[book.aid] > 0)
otherUpdated.add(book);
else
otherUnUpdated.add(book);
});
return Drawer(
child: RefreshIndicator(
onRefresh: () async {
favorite.checkNews(AutoCheckLevel.all);
},
child: SafeArea(
child: CustomScrollView(
slivers: [
SliverExpandableGroup(
title: Text('7天内看过并且有更新的藏书(${inWeekUpdated.length})'),
expanded: true,
count: inWeekUpdated.length,
builder: (ctx, i) => bookBuilder(
inWeekUpdated[i],
favorite.hasNews[inWeekUpdated[i].aid],
),
),
SliverExpandableGroup(
title: Text('7天内看过的藏书(${inWeekUnUpdated.length})'),
count: inWeekUnUpdated.length,
builder: (ctx, i) => bookBuilder(
inWeekUnUpdated[i],
favorite.hasNews[inWeekUnUpdated[i].aid],
),
),
SliverExpandableGroup(
title: Text('有更新的藏书(${otherUpdated.length})'),
count: otherUpdated.length,
builder: (ctx, i) => bookBuilder(
otherUpdated[i],
favorite.hasNews[otherUpdated[i].aid],
),
),
SliverExpandableGroup(
title: Text('没有更新的藏书(${otherUnUpdated.length})'),
count: otherUnUpdated.length,
builder: (ctx, i) => bookBuilder(
otherUnUpdated[i],
favorite.hasNews[otherUnUpdated[i].aid],
),
),
],
)),
),
);
}); });
other.forEach((book) {
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0)
otherUpdated.add(book);
else
otherUnUpdated.add(book);
});
return Drawer(
child: RefreshIndicator(
onRefresh: () async {
await checkNews();
setState(() {});
},
child: all.isEmpty
? Center(child: Text('没有收藏'))
: SafeArea(
child: CustomScrollView(
slivers: [
SliverExpandableGroup(
title: Text('7天内看过并且有更新的藏书(${inWeekUpdated.length})'),
expanded: true,
count: inWeekUpdated.length,
builder: (ctx, i) => bookBuilder(inWeekUpdated[i]),
),
SliverExpandableGroup(
title: Text('7天内看过的藏书(${inWeekUnUpdated.length})'),
count: inWeekUnUpdated.length,
builder: (ctx, i) => bookBuilder(inWeekUnUpdated[i]),
),
SliverExpandableGroup(
title: Text('有更新的藏书(${otherUpdated.length})'),
count: otherUpdated.length,
builder: (ctx, i) => bookBuilder(otherUpdated[i]),
),
SliverExpandableGroup(
title: Text('没有更新的藏书(${otherUnUpdated.length})'),
count: otherUnUpdated.length,
builder: (ctx, i) => bookBuilder(otherUnUpdated[i]),
),
],
)),
),
);
} }
} }
@ -174,8 +207,14 @@ class FBookItem extends StatelessWidget {
return ListTile( return ListTile(
onTap: () => onTap(book), onTap: () => onTap(book),
leading: Hero( leading: Hero(
tag: 'fb ${book.aid}', tag: 'fb ${book.aid}',
child: Image(image: NetworkImageSSL(book.avatar))), child: Image(
image: ExtendedNetworkImageProvider(
book.avatar,
cache: true,
),
),
),
title: Text(book.name, style: Theme.of(context).textTheme.body1), title: Text(book.name, style: Theme.of(context).textTheme.body1),
subtitle: RichText(text: subtitle), subtitle: RichText(text: subtitle),
); );

View File

@ -55,7 +55,7 @@ class QuickBook extends DraggableItem {
} }
checkUpdate() { checkUpdate() {
UserAgentClient.instance.getBook(aid: book.aid); HttpHHMH39.instance.getBook(book.aid);
} }
} }
@ -72,11 +72,12 @@ class Quick extends StatefulWidget {
} }
class QuickState extends State<Quick> { class QuickState extends State<Quick> {
final List<String> id = [];
final int count = 8; final int count = 8;
final List<DraggableItem> _draggableItems = []; final List<DraggableItem> _draggableItems = [];
DraggableItem _addButton; DraggableItem _addButton;
GlobalKey<DraggableContainerState> _key = GlobalKey(); GlobalKey<DraggableContainerState> _key =
final List<String> id = []; GlobalKey<DraggableContainerState>();
double width = 0, height = 0; double width = 0, height = 0;
void exit() { void exit() {
@ -171,6 +172,25 @@ class QuickState extends State<Quick> {
for (var i = count - _draggableItems.length; i > 0; i--) { for (var i = count - _draggableItems.length; i > 0; i--) {
_draggableItems.add(null); _draggableItems.add(null);
} }
SchedulerBinding.instance.addPostFrameCallback((_) {
print('添加监听');
Provider.of<FavoriteData>(context, listen: false).addListener(refresh);
});
}
void refresh() {
final id = Data._quickIdList();
// print('refresh $id');
for (var i = 0; i < _draggableItems.length; i++) {
final item = _draggableItems[i];
if (item is QuickBook) {
// print('is QuickBookdelete : ${id.contains(item.book.aid)}');
if (!id.contains(item.book.aid)) {
_key.currentState.insteadOfIndex(i, null);
}
}
}
} }
@override @override

View File

@ -1,5 +1,5 @@
name: weiman name: weiman
description: 微漫 description: 微漫App
# The following defines the version and build number for your application. # The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43 # A version number is three numbers separated by dots, like 1.2.43
@ -11,7 +11,7 @@ description: 微漫
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.4 version: 1.0.8+2005
environment: environment:
sdk: ">=2.3.0 <3.0.0" sdk: ">=2.3.0 <3.0.0"
@ -20,26 +20,34 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
async: any
cupertino_icons: any cupertino_icons: any
async: any
http: any http: any
encrypt: any encrypt: any
html: any html: any
shared_preferences: any shared_preferences: any
fluttertoast: any random_string: any
filesize: any
oktoast: any
path_provider: any
draggable_container: any draggable_container: any
sticky_headers: any
flutter_sticky_header: any flutter_sticky_header: any
extended_nested_scroll_view: any extended_nested_scroll_view: any
dynamic_theme: any dynamic_theme: any
package_info: any package_info: any
url_launcher: any url_launcher: any
font_awesome_flutter: any font_awesome_flutter: any
loading_more_list: any loadmore: any
pull_to_refresh_notification: any pull_to_refresh_notification: any
http_client_helper: any
extended_image: any
screenshot: any
focus_widget: any
provider: any
firebase_core: any firebase_core: any
firebase_analytics: any firebase_analytics: any
e2e: any
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -58,7 +66,9 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- images/logo.png - assets/logo.png
- assets/ca.crt
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
# assets: # assets:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg