mirror of
https://github.com/nrop19/weiman_app.git
synced 2025-08-03 15:22:47 +08:00
v1.0.8
This commit is contained in:
parent
038f6d5eaf
commit
f68617e44b
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
86
lib/activities/rank.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
160
lib/activities/setting.dart
Normal 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(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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'],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
30
lib/crawler/http.dart
Normal 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);
|
||||||
|
}
|
@ -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),
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
@ -1,15 +1,88 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class FavoriteData extends ChangeNotifier {
|
||||||
|
/// -2 在队列中等待检查,-1读取错误,0 没有更新,> 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),
|
||||||
);
|
);
|
||||||
|
@ -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 QuickBook,delete : ${id.contains(item.book.aid)}');
|
||||||
|
if (!id.contains(item.book.aid)) {
|
||||||
|
_key.currentState.insteadOfIndex(i, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
24
pubspec.yaml
24
pubspec.yaml
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user