mirror of
https://github.com/nrop19/weiman_app.git
synced 2025-08-02 23:05:48 +08:00
Github Action自动发布
This commit is contained in:
commit
6a92a5505e
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 nrop19
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
11
README.md
Normal file
11
README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# 微漫 v1.1.0 [宣传页面](https://nrop19.github.io/weiman_app)
|
||||||
|
|
||||||
|
### 微漫脱敏后的开源代码
|
||||||
|
|
||||||
|
#### 不解答任何代码上的问题
|
||||||
|
|
||||||
|
#### App的问题请到 [Telegram群](https://t.me/boring_programer) 讨论
|
||||||
|
|
||||||
|
- 删除了android端文件夹,涉及到apk签名等敏感文件
|
||||||
|
- 删除了ios端文件夹
|
||||||
|
- 删除了lib/crawler/里的其它文件,保护被爬网站的同时防止被爬网站加大防爬难度。
|
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
246
lib/activities/book.dart
Normal file
246
lib/activities/book.dart
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import 'package:extended_image/extended_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||||
|
|
||||||
|
import '../classes/book.dart';
|
||||||
|
import '../classes/data.dart';
|
||||||
|
import '../classes/networkImageSSL.dart';
|
||||||
|
import '../main.dart';
|
||||||
|
import '../utils.dart';
|
||||||
|
import '../widgets/book.dart';
|
||||||
|
import '../widgets/favorites.dart';
|
||||||
|
import '../widgets/pullToRefreshHeader.dart';
|
||||||
|
|
||||||
|
class ActivityBook extends StatefulWidget {
|
||||||
|
final Book book;
|
||||||
|
final String heroTag;
|
||||||
|
|
||||||
|
ActivityBook({@required this.book, @required this.heroTag});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ActivityBook createState() => _ActivityBook();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActivityBook extends State<ActivityBook> {
|
||||||
|
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
|
||||||
|
ScrollController _scrollController;
|
||||||
|
|
||||||
|
bool _reverse = false;
|
||||||
|
Book book;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
book = widget.book;
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_refresh.currentState
|
||||||
|
.show(notificationDragOffset: SliverPullToRefreshHeader.height);
|
||||||
|
});
|
||||||
|
_scrollController = ScrollController();
|
||||||
|
print('${widget.book.toJson()}');
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> loadBook() async {
|
||||||
|
book = await book.http.getBook(book.aid);
|
||||||
|
book.history = Data.getHistories()[book.aid]?.history;
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openChapter(Chapter chapter) {
|
||||||
|
setState(() {
|
||||||
|
book.history = History(cid: chapter.cid, cname: chapter.cname, time: 0);
|
||||||
|
openChapter(context, book, chapter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
favoriteBook() async {
|
||||||
|
final fav = Provider.of<FavoriteData>(context, listen: false);
|
||||||
|
if (book.isFavorite()) {
|
||||||
|
final sure = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: Text('确认取消收藏?'),
|
||||||
|
// content: Text('删除这本藏书后,首页的快速导航也会删除这本藏书'),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('确认'),
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
child: Text('取消'),
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
if (sure == true) {
|
||||||
|
final inQuickList = Data.quickIdList().contains(book.aid);
|
||||||
|
if (inQuickList) {}
|
||||||
|
fav.remove(book);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fav.add(book);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Chapter> _sort() {
|
||||||
|
final List<Chapter> list = List.from(book.chapters);
|
||||||
|
if (_reverse) return list.reversed.toList();
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexedWidgetBuilder buildChapters(List<Chapter> chapters) {
|
||||||
|
IndexedWidgetBuilder builder = (BuildContext context, int index) {
|
||||||
|
final chapter = chapters[index];
|
||||||
|
Widget child = WidgetChapter(
|
||||||
|
chapter: chapter,
|
||||||
|
onTap: _openChapter,
|
||||||
|
read: chapter.cid == book.history?.cid,
|
||||||
|
);
|
||||||
|
if (index < chapters.length - 1)
|
||||||
|
child = DecoratedBox(
|
||||||
|
decoration: border,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
return child;
|
||||||
|
};
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isFavorite = book.isFavorite();
|
||||||
|
Color color = isFavorite ? Colors.red : Colors.white;
|
||||||
|
IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border;
|
||||||
|
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(
|
||||||
|
body: PullToRefreshNotification(
|
||||||
|
key: _refresh,
|
||||||
|
onRefresh: loadBook,
|
||||||
|
maxDragOffset: kToolbarHeight * 2,
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
pinned: true,
|
||||||
|
title: Text(book.name),
|
||||||
|
expandedHeight: 200,
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_reverse = !_reverse;
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_reverse
|
||||||
|
? FontAwesomeIcons.sortNumericDown
|
||||||
|
: FontAwesomeIcons.sortNumericDownAlt)),
|
||||||
|
IconButton(
|
||||||
|
onPressed: favoriteBook, icon: Icon(icon, color: color))
|
||||||
|
],
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
background: SafeArea(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
top: 50, left: 20, right: 10, bottom: 20),
|
||||||
|
height: 160,
|
||||||
|
child: Hero(
|
||||||
|
tag: widget.heroTag,
|
||||||
|
child: ExtendedImage(
|
||||||
|
width: 100,
|
||||||
|
image: NetworkImageSSL(
|
||||||
|
widget.book.http,
|
||||||
|
widget.book.avatar,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(top: 50, right: 20),
|
||||||
|
child: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'作者:' + (book.author ?? ''),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'简介:\n' + (book.description ?? ''),
|
||||||
|
softWrap: true,
|
||||||
|
style:
|
||||||
|
TextStyle(color: Colors.white, height: 1.2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PullToRefreshContainer((info) => SliverPullToRefreshHeader(
|
||||||
|
info: info,
|
||||||
|
onTap: () => _refresh.currentState.show(
|
||||||
|
notificationDragOffset: SliverPullToRefreshHeader.height),
|
||||||
|
)),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
children: history,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
buildChapters(chapters),
|
||||||
|
childCount: chapters.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
352
lib/activities/chapter.dart
Normal file
352
lib/activities/chapter.dart
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
import 'package:extended_image/extended_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:oktoast/oktoast.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||||
|
import 'package:sticky_headers/sticky_headers/widget.dart';
|
||||||
|
import 'package:weiman/activities/setting/hideStatusBar.dart';
|
||||||
|
import 'package:weiman/activities/setting/setting.dart';
|
||||||
|
|
||||||
|
import '../classes/book.dart';
|
||||||
|
import '../classes/data.dart';
|
||||||
|
import '../classes/networkImageSSL.dart';
|
||||||
|
import '../utils.dart';
|
||||||
|
import '../widgets/book.dart';
|
||||||
|
import '../widgets/pullToRefreshHeader.dart';
|
||||||
|
|
||||||
|
class ActivityChapter extends StatefulWidget {
|
||||||
|
final Book book;
|
||||||
|
final Chapter chapter;
|
||||||
|
|
||||||
|
ActivityChapter(this.book, this.chapter);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ActivityChapter createState() => _ActivityChapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActivityChapter extends State<ActivityChapter> {
|
||||||
|
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
PageController _pageController;
|
||||||
|
int showIndex = 0;
|
||||||
|
bool hasNextImage = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_pageController = PageController(
|
||||||
|
keepPage: false,
|
||||||
|
initialPage: widget.book.chapters.indexOf(widget.chapter));
|
||||||
|
super.initState();
|
||||||
|
saveHistory(widget.chapter);
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
final hide = Provider.of<SettingData>(context, listen: false).hide;
|
||||||
|
if (hide == HideOption.always) {
|
||||||
|
hideStatusBar();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController?.dispose();
|
||||||
|
showStatusBar();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void pageChanged(int page) {
|
||||||
|
saveHistory(widget.book.chapters[page]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveHistory(Chapter chapter) {
|
||||||
|
Data.addHistory(widget.book, chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
endDrawer: ChapterDrawer(
|
||||||
|
book: widget.book,
|
||||||
|
onTap: (chapter) {
|
||||||
|
_pageController.jumpToPage(widget.book.chapters.indexOf(chapter));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
body: PageView.builder(
|
||||||
|
physics: AlwaysScrollableClampingScrollPhysics(),
|
||||||
|
controller: _pageController,
|
||||||
|
itemCount: widget.book.chapters.length,
|
||||||
|
onPageChanged: pageChanged,
|
||||||
|
itemBuilder: (ctx, index) {
|
||||||
|
return ChapterContentView(
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.menu),
|
||||||
|
onPressed: () {
|
||||||
|
_scaffoldKey.currentState.openEndDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
book: widget.book,
|
||||||
|
chapter: widget.book.chapters[index],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterDrawer extends StatefulWidget {
|
||||||
|
final Book book;
|
||||||
|
final void Function(Chapter chapter) onTap;
|
||||||
|
|
||||||
|
const ChapterDrawer({
|
||||||
|
Key key,
|
||||||
|
@required this.book,
|
||||||
|
@required this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ChapterDrawer createState() => _ChapterDrawer();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChapterDrawer extends State<ChapterDrawer> {
|
||||||
|
ScrollController _controller;
|
||||||
|
int read;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
updateRead();
|
||||||
|
_controller =
|
||||||
|
ScrollController(initialScrollOffset: WidgetChapter.height * read);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRead() {
|
||||||
|
final readChapter = widget.book.chapters
|
||||||
|
.firstWhere((chapter) => widget.book.history?.cid == chapter.cid);
|
||||||
|
read = widget.book.chapters.indexOf(readChapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void scrollToRead() {
|
||||||
|
setState(() {
|
||||||
|
updateRead();
|
||||||
|
});
|
||||||
|
_controller.animateTo(
|
||||||
|
WidgetChapter.height * read,
|
||||||
|
duration: Duration(milliseconds: 200),
|
||||||
|
curve: Curves.linear,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Drawer(
|
||||||
|
child: SafeArea(
|
||||||
|
child: ListView(
|
||||||
|
controller: _controller,
|
||||||
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: widget.book.chapters.map((chapter) {
|
||||||
|
final isRead = widget.book.history?.cid == chapter.cid;
|
||||||
|
return WidgetChapter(
|
||||||
|
chapter: chapter,
|
||||||
|
onTap: (chapter) {
|
||||||
|
if (widget.onTap != null) widget.onTap(chapter);
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
scrollToRead();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
read: isRead,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterContentView extends StatefulWidget {
|
||||||
|
final Book book;
|
||||||
|
final Chapter chapter;
|
||||||
|
final List<Widget> actions;
|
||||||
|
|
||||||
|
const ChapterContentView({Key key, this.book, this.chapter, this.actions})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ChapterContentView createState() => _ChapterContentView();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChapterContentView extends State<ChapterContentView> {
|
||||||
|
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
|
||||||
|
final List<String> images = [];
|
||||||
|
TextStyle _style = TextStyle(color: Colors.white);
|
||||||
|
BoxDecoration _decoration =
|
||||||
|
BoxDecoration(color: Colors.black.withOpacity(0.4));
|
||||||
|
|
||||||
|
bool loading = true;
|
||||||
|
|
||||||
|
ScrollController scrollController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
scrollController = ScrollController();
|
||||||
|
super.initState();
|
||||||
|
Data.addHistory(widget.book, widget.chapter);
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_refresh?.currentState
|
||||||
|
?.show(notificationDragOffset: SliverPullToRefreshHeader.height);
|
||||||
|
final hide = Provider.of<SettingData>(context, listen: false).hide;
|
||||||
|
if (hide == HideOption.auto) {
|
||||||
|
scrollController.addListener(() {
|
||||||
|
final isUp = scrollController.position.userScrollDirection ==
|
||||||
|
ScrollDirection.forward;
|
||||||
|
if (isUp)
|
||||||
|
showStatusBar();
|
||||||
|
else
|
||||||
|
hideStatusBar();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> fetchImages() async {
|
||||||
|
print('fetchImages');
|
||||||
|
loading = true;
|
||||||
|
images.clear();
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
try {
|
||||||
|
images.addAll(await widget.book.http
|
||||||
|
.getChapterImages(widget.book, widget.chapter)
|
||||||
|
.timeout(Duration(seconds: 10)));
|
||||||
|
} catch (e) {
|
||||||
|
print('错误 $e');
|
||||||
|
showToastWidget(
|
||||||
|
GestureDetector(
|
||||||
|
child: Container(
|
||||||
|
child: Text('读取章节内容出现错误\n点击复制错误内容'),
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await Clipboard.setData(ClipboardData(text: e.toString()));
|
||||||
|
final content = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
|
print('粘贴板 ${content.text}');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
duration: Duration(seconds: 5),
|
||||||
|
handleTouch: true,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
// throw(e);
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PullToRefreshNotification(
|
||||||
|
key: _refresh,
|
||||||
|
onRefresh: fetchImages,
|
||||||
|
maxDragOffset: kToolbarHeight * 2,
|
||||||
|
child: CustomScrollView(
|
||||||
|
physics: AlwaysScrollableClampingScrollPhysics(),
|
||||||
|
controller: scrollController,
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
title: Text(widget.chapter.cname),
|
||||||
|
pinned: false,
|
||||||
|
floating: true,
|
||||||
|
actions: widget.actions,
|
||||||
|
),
|
||||||
|
PullToRefreshContainer(
|
||||||
|
(info) => SliverPullToRefreshHeader(
|
||||||
|
info: info,
|
||||||
|
onTap: () => _refresh.currentState.show(
|
||||||
|
notificationDragOffset: SliverPullToRefreshHeader.height),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(ctx, i) {
|
||||||
|
return StickyHeader(
|
||||||
|
overlapHeaders: true,
|
||||||
|
header: SafeArea(
|
||||||
|
top: true,
|
||||||
|
bottom: false,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(5),
|
||||||
|
decoration: _decoration,
|
||||||
|
child: Text(
|
||||||
|
'${i + 1} / ${images.length}',
|
||||||
|
style: _style,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: ExtendedImage(
|
||||||
|
image: NetworkImageSSL(widget.book.http, images[i]),
|
||||||
|
loadStateChanged: (state) {
|
||||||
|
switch (state.extendedImageLoadState) {
|
||||||
|
case LoadState.loading:
|
||||||
|
return SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case LoadState.failed:
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 300,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text('图片读取失败'),
|
||||||
|
RaisedButton(
|
||||||
|
child: Text('重试'),
|
||||||
|
onPressed: state.reLoadImage,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return ExtendedRawImage(
|
||||||
|
image: state.extendedImageInfo?.image,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: images.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
136
lib/activities/checkData.dart
Normal file
136
lib/activities/checkData.dart
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:oktoast/oktoast.dart';
|
||||||
|
|
||||||
|
import '../classes/data.dart';
|
||||||
|
|
||||||
|
class ActivityCheckData extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_State createState() => _State();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CheckState {
|
||||||
|
Uncheck,
|
||||||
|
Pass,
|
||||||
|
Fail,
|
||||||
|
}
|
||||||
|
|
||||||
|
final titleTextStyle = TextStyle(fontSize: 14, color: Colors.blue),
|
||||||
|
passStyle = TextStyle(color: Colors.green),
|
||||||
|
failStyle = TextStyle(color: Colors.red);
|
||||||
|
|
||||||
|
class _State extends State<ActivityCheckData> {
|
||||||
|
CheckState firstState;
|
||||||
|
int firstLength = 0;
|
||||||
|
final TextSpan secondResults = TextSpan();
|
||||||
|
TextEditingController _outputController, _inputController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_outputController = TextEditingController();
|
||||||
|
_inputController = TextEditingController();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSpan first() {
|
||||||
|
String text;
|
||||||
|
switch (firstState) {
|
||||||
|
case CheckState.Pass:
|
||||||
|
text = '有数据, 一共 $firstLength 本收藏';
|
||||||
|
break;
|
||||||
|
case CheckState.Fail:
|
||||||
|
text = '没有收藏数据';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text = '未检查';
|
||||||
|
}
|
||||||
|
return TextSpan(
|
||||||
|
text: text,
|
||||||
|
style: firstState == CheckState.Pass ? passStyle : failStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final firstChildren = [
|
||||||
|
Text('检查漫画收藏列表'),
|
||||||
|
RaisedButton(
|
||||||
|
child: Text('检查'),
|
||||||
|
color: Colors.blue,
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () {
|
||||||
|
final has = Data.has(Data.favoriteBooksKey);
|
||||||
|
if (has) {
|
||||||
|
final String str = Data.instance.getString(Data.favoriteBooksKey);
|
||||||
|
final Map<String, Object> map = jsonDecode(str);
|
||||||
|
firstLength = map.keys.length;
|
||||||
|
_outputController.text = str;
|
||||||
|
}
|
||||||
|
firstState = firstLength > 0 ? CheckState.Pass : CheckState.Fail;
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: '结果:',
|
||||||
|
children: [first()],
|
||||||
|
style: TextStyle(color: Colors.black)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if (firstState == CheckState.Pass) {
|
||||||
|
firstChildren.add(Text('点击复制'));
|
||||||
|
firstChildren.add(TextField(
|
||||||
|
maxLines: 8,
|
||||||
|
controller: _outputController,
|
||||||
|
onTap: () {
|
||||||
|
showToast('已经复制');
|
||||||
|
Clipboard.setData(ClipboardData(text: _outputController.text));
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('收藏数据检修'),
|
||||||
|
),
|
||||||
|
body: ListView(children: [
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(5),
|
||||||
|
child: Container(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: firstChildren,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('导入收藏数据'),
|
||||||
|
TextField(
|
||||||
|
controller: _inputController,
|
||||||
|
maxLines: 8,
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
child: Text('导入'),
|
||||||
|
onPressed: () {
|
||||||
|
if (_inputController.text.length > 0) {
|
||||||
|
Data.instance.setString(
|
||||||
|
Data.favoriteBooksKey, _inputController.text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
302
lib/activities/home.dart
Normal file
302
lib/activities/home.dart
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import 'package:dynamic_theme/dynamic_theme.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:oktoast/oktoast.dart';
|
||||||
|
import 'package:package_info/package_info.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../activities/checkData.dart';
|
||||||
|
import '../activities/hot.dart';
|
||||||
|
import '../activities/search/search.dart';
|
||||||
|
import '../activities/test.dart';
|
||||||
|
import '../classes/book.dart';
|
||||||
|
import '../main.dart';
|
||||||
|
import '../widgets/favorites.dart';
|
||||||
|
import '../widgets/histories.dart';
|
||||||
|
import '../widgets/quick.dart';
|
||||||
|
import 'setting/setting.dart';
|
||||||
|
|
||||||
|
class ActivityHome extends StatefulWidget {
|
||||||
|
final PackageInfo packageInfo;
|
||||||
|
|
||||||
|
const ActivityHome(this.packageInfo, {Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => HomeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeState extends State<ActivityHome> {
|
||||||
|
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
final List<Widget> histories = [];
|
||||||
|
final List<Book> quick = [];
|
||||||
|
final GlobalKey<QuickState> _quickState = GlobalKey();
|
||||||
|
|
||||||
|
bool showFavorite = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
analytics.setCurrentScreen(screenName: '/activity_home');
|
||||||
|
|
||||||
|
/// 提前检查一次藏书的更新情况
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
autoSwitchTheme();
|
||||||
|
FavoriteData favData = Provider.of<FavoriteData>(context, listen: false);
|
||||||
|
await favData.loadBooksList();
|
||||||
|
await favData.checkNews(
|
||||||
|
Provider.of<SettingData>(context, listen: false).autoCheck);
|
||||||
|
final updated =
|
||||||
|
favData.hasNews.values.where((int count) => count > 0).length;
|
||||||
|
if (updated > 0)
|
||||||
|
showToast(
|
||||||
|
'$updated 本藏书有更新',
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.5),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void autoSwitchTheme() async {
|
||||||
|
final isDark = await DynamicTheme.of(context).loadBrightness();
|
||||||
|
final nowIsDark = DynamicTheme.of(context).brightness == Brightness.dark;
|
||||||
|
if (isDark != nowIsDark)
|
||||||
|
DynamicTheme.of(context)
|
||||||
|
.setBrightness(isDark ? Brightness.dark : Brightness.light);
|
||||||
|
}
|
||||||
|
|
||||||
|
void gotoSearch() {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: '/activity_search'),
|
||||||
|
builder: (context) => ActivitySearch()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void gotoRecommend() {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: '/activity_recommend'),
|
||||||
|
builder: (_) => ActivityRank(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void gotoPatreon() {
|
||||||
|
launch('https://www.patreon.com/nrop19');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEdit = false;
|
||||||
|
|
||||||
|
void _draggableModeChanged(bool mode) {
|
||||||
|
print('mode changed $mode');
|
||||||
|
isEdit = mode;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final media = MediaQuery.of(context);
|
||||||
|
final width = (media.size.width * 0.8).roundToDouble();
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('微漫 v' + widget.packageInfo.version),
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
leading: isEdit
|
||||||
|
? IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back_ios),
|
||||||
|
onPressed: () {
|
||||||
|
_quickState.currentState.exit();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
actions: <Widget>[
|
||||||
|
/// 黑白样式切换
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
DynamicTheme.of(context).setBrightness(
|
||||||
|
Theme.of(context).brightness == Brightness.dark
|
||||||
|
? Brightness.light
|
||||||
|
: Brightness.dark);
|
||||||
|
},
|
||||||
|
icon: Icon(Theme.of(context).brightness == Brightness.light
|
||||||
|
? FontAwesomeIcons.lightbulb
|
||||||
|
: FontAwesomeIcons.solidLightbulb),
|
||||||
|
),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
|
||||||
|
/// 设置界面
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: '/activity_setting'),
|
||||||
|
builder: (_) => ActivitySetting()));
|
||||||
|
},
|
||||||
|
icon: Icon(FontAwesomeIcons.cog),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 收藏列表
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showFavorite = true;
|
||||||
|
_scaffoldKey.currentState.openEndDrawer();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.favorite,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 浏览历史列表
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showFavorite = false;
|
||||||
|
// getHistory();
|
||||||
|
_scaffoldKey.currentState.openEndDrawer();
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.history),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
endDrawer: Drawer(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (_, constraints) {
|
||||||
|
if (showFavorite) {
|
||||||
|
return FavoriteList();
|
||||||
|
} else {
|
||||||
|
return Histories();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.only(left: 40, right: 40),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
child: OutlineButton(
|
||||||
|
onPressed: gotoSearch,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'搜索漫画',
|
||||||
|
style: TextStyle(color: Colors.blue),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(color: Colors.blue, width: 2),
|
||||||
|
shape: StadiumBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 7,
|
||||||
|
child: OutlineButton(
|
||||||
|
onPressed: gotoRecommend,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.whatshot,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'热门漫画',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(color: Colors.red, width: 2),
|
||||||
|
shape: StadiumBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
launch('https://bbs.level-plus.net/');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'在 level-plus.net 论坛首发',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue[200],
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
if (await canLaunch('tg://resolve?domain=weiman_app'))
|
||||||
|
launch('tg://resolve?domain=weiman_app');
|
||||||
|
else
|
||||||
|
launch('https://t.me/weiman_app');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'关注 Telegram 广播频道',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue[200],
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Visibility(
|
||||||
|
visible: isDevMode,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(context,
|
||||||
|
MaterialPageRoute(builder: (_) => ActivityCheckData()));
|
||||||
|
},
|
||||||
|
child: Text('操作 收藏列表数据'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: isDevMode
|
||||||
|
? FloatingActionButton(
|
||||||
|
child: Text('测试'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context, MaterialPageRoute(builder: (_) => ActivityTest()));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
254
lib/activities/hot.dart
Normal file
254
lib/activities/hot.dart
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:loading_more_list/loading_more_list.dart';
|
||||||
|
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||||
|
|
||||||
|
import '../classes/book.dart';
|
||||||
|
import '../crawler/http.dart';
|
||||||
|
import '../crawler/http18Comic.dart';
|
||||||
|
import '../widgets/book.dart';
|
||||||
|
import '../widgets/pullToRefreshHeader.dart';
|
||||||
|
|
||||||
|
class ActivityRank extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_ActivityRank createState() => _ActivityRank();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActivityRank extends State<ActivityRank>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
TabController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
controller = TabController(length: 2, vsync: this);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('热门漫画'),
|
||||||
|
bottom: TabBar(controller: controller, tabs: [
|
||||||
|
Tab(text: '韩漫'),
|
||||||
|
Tab(text: '全部'),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
body: TabBarView(controller: controller, children: [
|
||||||
|
HotTab(http: Http18Comic.instance, type: '/hanman'),
|
||||||
|
HotTab(http: Http18Comic.instance, type: ''),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SourceList extends LoadingMoreBase<Book> {
|
||||||
|
final String type;
|
||||||
|
final HttpBook http;
|
||||||
|
int page = 1;
|
||||||
|
String firstBookId = null;
|
||||||
|
|
||||||
|
bool hasMore = true;
|
||||||
|
|
||||||
|
SourceList({this.type, this.http});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> loadData([bool isloadMoreAction = false]) async {
|
||||||
|
try {
|
||||||
|
final books = await http.hotBooks(type, page);
|
||||||
|
if (books.isEmpty) {
|
||||||
|
hasMore = false;
|
||||||
|
} else {
|
||||||
|
if (firstBookId == books[0].aid) {
|
||||||
|
hasMore = false;
|
||||||
|
} else {
|
||||||
|
firstBookId = books[0].aid;
|
||||||
|
page++;
|
||||||
|
this.addAll(books);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> refresh([bool notifyStateChanged = false]) {
|
||||||
|
hasMore = true;
|
||||||
|
page = 1;
|
||||||
|
return super.refresh(notifyStateChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HotTab extends StatefulWidget {
|
||||||
|
final String type;
|
||||||
|
final HttpBook http;
|
||||||
|
|
||||||
|
const HotTab({Key key, this.type, this.http}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_HotTab createState() => _HotTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HotTab extends State<HotTab> {
|
||||||
|
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
|
||||||
|
SourceList sourceList;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
sourceList = SourceList(type: widget.type, http: widget.http);
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) => _refresh?.currentState
|
||||||
|
?.show(notificationDragOffset: SliverPullToRefreshHeader.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PullToRefreshNotification(
|
||||||
|
key: _refresh,
|
||||||
|
pullBackOnRefresh: true,
|
||||||
|
onRefresh: () => sourceList.refresh(),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
PullToRefreshContainer(
|
||||||
|
(info) => SliverPullToRefreshHeader(info: info),
|
||||||
|
),
|
||||||
|
LoadingMoreSliverList(SliverListConfig<Book>(
|
||||||
|
sourceList: sourceList,
|
||||||
|
indicatorBuilder: indicatorBuilder,
|
||||||
|
itemBuilder: (_, book, __) => WidgetBook(
|
||||||
|
book,
|
||||||
|
subtitle: book.author,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
LoadingMoreList(ListConfig<Book>(
|
||||||
|
sourceList: sourceList,
|
||||||
|
autoLoadMore: true,
|
||||||
|
itemBuilder: (_, item, index) => book(item),
|
||||||
|
indicatorBuilder: indicatorBuilder,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget book(Book book) {
|
||||||
|
return WidgetBook(book, subtitle: book.author);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget indicatorBuilder(context, IndicatorStatus status) {
|
||||||
|
print('indicatorBuilder $status');
|
||||||
|
bool isSliver = true;
|
||||||
|
Widget widget;
|
||||||
|
switch (status) {
|
||||||
|
case IndicatorStatus.none:
|
||||||
|
widget = SizedBox();
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.loadingMoreBusying:
|
||||||
|
widget = Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(right: 5.0),
|
||||||
|
height: 15.0,
|
||||||
|
width: 15.0,
|
||||||
|
child: getIndicator(context),
|
||||||
|
),
|
||||||
|
Text("正在读取")
|
||||||
|
],
|
||||||
|
);
|
||||||
|
widget = _setbackground(false, widget, 35.0);
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.fullScreenBusying:
|
||||||
|
widget = SizedBox();
|
||||||
|
if (isSliver) {
|
||||||
|
widget = SliverFillRemaining(
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.error:
|
||||||
|
widget = Text(
|
||||||
|
'读取失败,如果失败的次数太多可能需要用梯子',
|
||||||
|
);
|
||||||
|
widget = _setbackground(false, widget, 35.0);
|
||||||
|
|
||||||
|
widget = GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
sourceList.errorRefresh();
|
||||||
|
},
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.fullScreenError:
|
||||||
|
widget = Text(
|
||||||
|
'读取失败,如果失败的次数太多可能需要用梯子',
|
||||||
|
);
|
||||||
|
widget = _setbackground(true, widget, double.infinity);
|
||||||
|
widget = GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
sourceList.errorRefresh();
|
||||||
|
},
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
if (isSliver) {
|
||||||
|
widget = SliverFillRemaining(
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
widget = CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverFillRemaining(
|
||||||
|
child: widget,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.noMoreLoad:
|
||||||
|
widget = Text("已经显示全部搜索结果");
|
||||||
|
widget = _setbackground(false, widget, 35.0);
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.empty:
|
||||||
|
widget = Text(
|
||||||
|
'没有内容',
|
||||||
|
);
|
||||||
|
widget = _setbackground(true, widget, double.infinity);
|
||||||
|
if (isSliver) {
|
||||||
|
widget = SliverToBoxAdapter(
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
widget = CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverFillRemaining(
|
||||||
|
child: widget,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _setbackground(bool full, Widget widget, double height) {
|
||||||
|
widget = Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: kToolbarHeight,
|
||||||
|
child: widget,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
);
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget getIndicator(BuildContext context) {
|
||||||
|
return CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.0,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
94
lib/activities/search/search.dart
Normal file
94
lib/activities/search/search.dart
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:focus_widget/focus_widget.dart';
|
||||||
|
|
||||||
|
import '../../crawler/http18Comic.dart';
|
||||||
|
import 'tab.dart';
|
||||||
|
|
||||||
|
class ActivitySearch extends StatefulWidget {
|
||||||
|
final String search;
|
||||||
|
|
||||||
|
const ActivitySearch({Key key, this.search = ''}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return SearchState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchState extends State<ActivitySearch>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
TextEditingController _controller;
|
||||||
|
GlobalKey<SearchTabState> key = GlobalKey<SearchTabState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
_controller = TextEditingController(text: widget.search);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void search() {
|
||||||
|
key.currentState.search = _controller.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: RawKeyboardListener(
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
onKey: (RawKeyEvent event) {
|
||||||
|
print('is enter: ${LogicalKeyboardKey.enter == event.logicalKey}');
|
||||||
|
if (_controller.text.isEmpty) return;
|
||||||
|
if (event.runtimeType == RawKeyUpEvent &&
|
||||||
|
LogicalKeyboardKey.enter == event.logicalKey) {
|
||||||
|
print('回车键搜索');
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: FocusWidget.builder(
|
||||||
|
context,
|
||||||
|
builder: (_, focusNode) => TextField(
|
||||||
|
focusNode: focusNode,
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '搜索书名',
|
||||||
|
prefixIcon: IconButton(
|
||||||
|
onPressed: search,
|
||||||
|
icon: Icon(Icons.search, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
controller: _controller,
|
||||||
|
autofocus: widget.search.isEmpty,
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
|
onSubmitted: (String name) {
|
||||||
|
focusNode.unfocus();
|
||||||
|
print('onSubmitted');
|
||||||
|
search();
|
||||||
|
},
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
onEditingComplete: () {
|
||||||
|
focusNode.unfocus();
|
||||||
|
print('onEditingComplete');
|
||||||
|
search();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SearchTab(
|
||||||
|
name: Http18Comic.instance.name,
|
||||||
|
http: Http18Comic.instance,
|
||||||
|
search: _controller.text,
|
||||||
|
key: key,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
51
lib/activities/search/source.dart
Normal file
51
lib/activities/search/source.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:loading_more_list/loading_more_list.dart';
|
||||||
|
|
||||||
|
import '../../classes/book.dart';
|
||||||
|
import '../../crawler/http.dart';
|
||||||
|
|
||||||
|
class SearchSourceList extends LoadingMoreBase<Book> {
|
||||||
|
final HttpBook http;
|
||||||
|
String search;
|
||||||
|
int page = 1;
|
||||||
|
bool hasMore = true;
|
||||||
|
String eachPageFirstBookId;
|
||||||
|
|
||||||
|
SearchSourceList({
|
||||||
|
@required this.http,
|
||||||
|
this.search = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> loadData([bool isloadMoreAction = false]) async {
|
||||||
|
print('搜书 $search');
|
||||||
|
if (search == null || search.isEmpty) return true;
|
||||||
|
var list;
|
||||||
|
try {
|
||||||
|
list = await http.searchBook(search, page);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (list.isEmpty) {
|
||||||
|
hasMore = false;
|
||||||
|
} else if (list[0].aid == eachPageFirstBookId) {
|
||||||
|
hasMore = false;
|
||||||
|
} else {
|
||||||
|
eachPageFirstBookId = list[0].aid;
|
||||||
|
hasMore = true;
|
||||||
|
page++;
|
||||||
|
this.addAll(list);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> refresh([bool notifyStateChanged = false]) {
|
||||||
|
page = 1;
|
||||||
|
hasMore = true;
|
||||||
|
eachPageFirstBookId = null;
|
||||||
|
clear();
|
||||||
|
print('refresh $page $hasMore');
|
||||||
|
return super.refresh(notifyStateChanged);
|
||||||
|
}
|
||||||
|
}
|
186
lib/activities/search/tab.dart
Normal file
186
lib/activities/search/tab.dart
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:loading_more_list/loading_more_list.dart';
|
||||||
|
|
||||||
|
import './source.dart';
|
||||||
|
import '../../classes/book.dart';
|
||||||
|
import '../../crawler/http.dart';
|
||||||
|
import '../../widgets/book.dart';
|
||||||
|
|
||||||
|
class SearchTab extends StatefulWidget {
|
||||||
|
final String name;
|
||||||
|
final HttpBook http;
|
||||||
|
final String search;
|
||||||
|
|
||||||
|
const SearchTab({
|
||||||
|
Key key,
|
||||||
|
@required this.name,
|
||||||
|
@required this.http,
|
||||||
|
this.search,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SearchTabState createState() => SearchTabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchTabState extends State<SearchTab>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
SearchSourceList sourceList;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
sourceList = SearchSourceList(http: widget.http, search: widget.search);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget book(Book book) {
|
||||||
|
return WidgetBook(book, subtitle: book.author);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> refresh() async {
|
||||||
|
return sourceList.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
get search => sourceList.search;
|
||||||
|
|
||||||
|
set search(String value) {
|
||||||
|
print('tab search $value');
|
||||||
|
sourceList.search = value;
|
||||||
|
sourceList.refresh(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return LoadingMoreList(
|
||||||
|
ListConfig(
|
||||||
|
sourceList: sourceList,
|
||||||
|
itemBuilder: (_, item, index) => book(item),
|
||||||
|
autoLoadMore: true,
|
||||||
|
indicatorBuilder: indicatorBuilder,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget indicatorBuilder(context, IndicatorStatus status) {
|
||||||
|
bool isSliver = false;
|
||||||
|
Widget widget;
|
||||||
|
switch (status) {
|
||||||
|
case IndicatorStatus.none:
|
||||||
|
widget = SizedBox();
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.loadingMoreBusying:
|
||||||
|
widget = Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(right: 5.0),
|
||||||
|
height: 15.0,
|
||||||
|
width: 15.0,
|
||||||
|
child: getIndicator(context),
|
||||||
|
),
|
||||||
|
Text("正在读取")
|
||||||
|
],
|
||||||
|
);
|
||||||
|
widget = _setbackground(false, widget, 35.0);
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.fullScreenBusying:
|
||||||
|
widget = widget = _setbackground(
|
||||||
|
false,
|
||||||
|
Text(
|
||||||
|
'正在读取',
|
||||||
|
),
|
||||||
|
35.0);
|
||||||
|
if (isSliver) {
|
||||||
|
widget = SliverFillRemaining(
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.error:
|
||||||
|
widget = _setbackground(
|
||||||
|
false,
|
||||||
|
Text(
|
||||||
|
'网络错误\n点击重试',
|
||||||
|
),
|
||||||
|
35.0);
|
||||||
|
|
||||||
|
widget = GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
sourceList.errorRefresh();
|
||||||
|
},
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.fullScreenError:
|
||||||
|
widget = Text(
|
||||||
|
'读取失败,如果失败的次数太多可能需要用梯子',
|
||||||
|
);
|
||||||
|
widget = _setbackground(true, widget, double.infinity);
|
||||||
|
widget = GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
sourceList.errorRefresh();
|
||||||
|
},
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
if (isSliver) {
|
||||||
|
widget = SliverFillRemaining(
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
widget = CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverFillRemaining(
|
||||||
|
child: widget,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.noMoreLoad:
|
||||||
|
widget = Text("已经显示全部搜索结果");
|
||||||
|
widget = _setbackground(false, widget, 35.0);
|
||||||
|
break;
|
||||||
|
case IndicatorStatus.empty:
|
||||||
|
widget = Text(
|
||||||
|
sourceList.search.isEmpty ? '请输入搜索内容' : '搜索不到任何内容',
|
||||||
|
);
|
||||||
|
widget = _setbackground(true, widget, double.infinity);
|
||||||
|
if (isSliver) {
|
||||||
|
widget = SliverToBoxAdapter(
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
widget = CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverFillRemaining(
|
||||||
|
child: widget,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _setbackground(bool full, Widget widget, double height) {
|
||||||
|
widget = Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: kToolbarHeight,
|
||||||
|
child: widget,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
);
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget getIndicator(BuildContext context) {
|
||||||
|
return CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.0,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
40
lib/activities/setting/hideStatusBar.dart
Normal file
40
lib/activities/setting/hideStatusBar.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
enum HideOption {
|
||||||
|
none,
|
||||||
|
auto,
|
||||||
|
always,
|
||||||
|
}
|
||||||
|
|
||||||
|
class HideStatusBar extends StatelessWidget {
|
||||||
|
final options = {
|
||||||
|
'自动': HideOption.auto,
|
||||||
|
'全程隐藏': HideOption.always,
|
||||||
|
'不隐藏': HideOption.none,
|
||||||
|
};
|
||||||
|
final Function(HideOption option) onChanged;
|
||||||
|
final HideOption option;
|
||||||
|
|
||||||
|
HideStatusBar({Key key, @required this.onChanged, @required this.option})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text('看漫画时隐藏状态栏'),
|
||||||
|
subtitle: Text('自动:随着图片列表的上下滚动而自动显示或隐藏状态栏\n'
|
||||||
|
'全程隐藏:进入看图界面就隐藏状态栏,退出就显示状态栏\n'
|
||||||
|
'不隐藏:就是不隐藏状态栏咯'),
|
||||||
|
trailing: DropdownButton<HideOption>(
|
||||||
|
value: option,
|
||||||
|
items: options.keys
|
||||||
|
.map((key) => DropdownMenuItem(
|
||||||
|
child: Text(key),
|
||||||
|
value: options[key],
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
283
lib/activities/setting/setting.dart
Normal file
283
lib/activities/setting/setting.dart
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:filesize/filesize.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:oktoast/oktoast.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:weiman/activities/setting/hideStatusBar.dart';
|
||||||
|
|
||||||
|
import '../../classes/data.dart';
|
||||||
|
import '../../crawler/http.dart';
|
||||||
|
import '../../main.dart';
|
||||||
|
|
||||||
|
enum AutoCheckLevel {
|
||||||
|
none,
|
||||||
|
onlyInWeek,
|
||||||
|
all,
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingData extends ChangeNotifier {
|
||||||
|
static final String key = 'setting_data';
|
||||||
|
AutoCheckLevel _autoCheck;
|
||||||
|
HideOption _hide;
|
||||||
|
String _proxy;
|
||||||
|
Directory imageCacheDir;
|
||||||
|
|
||||||
|
SettingData() {
|
||||||
|
final Map<String, dynamic> data =
|
||||||
|
jsonDecode(Data.instance.getString(key) ?? '{}');
|
||||||
|
print('SettingData $data');
|
||||||
|
_autoCheck = data['autoCheck'] == null
|
||||||
|
? AutoCheckLevel.onlyInWeek
|
||||||
|
: AutoCheckLevel.values[data['autoCheck']];
|
||||||
|
_hide = data['hide'] == null
|
||||||
|
? HideOption.auto
|
||||||
|
: HideOption.values[data['hide']];
|
||||||
|
_proxy = data['proxy'];
|
||||||
|
|
||||||
|
MyHttpClient.init(_proxy, 10000, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
get autoCheck => _autoCheck;
|
||||||
|
|
||||||
|
set autoCheck(AutoCheckLevel val) {
|
||||||
|
_autoCheck = val;
|
||||||
|
notifyListeners();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
String get proxy => _proxy;
|
||||||
|
|
||||||
|
set proxy(String value) {
|
||||||
|
print('set proxy $value');
|
||||||
|
_proxy = value;
|
||||||
|
notifyListeners();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
HideOption get hide => _hide;
|
||||||
|
|
||||||
|
set hide(HideOption value) {
|
||||||
|
_hide = value;
|
||||||
|
notifyListeners();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'autoCheck': _autoCheck.index,
|
||||||
|
'proxy': _proxy,
|
||||||
|
'hide': _hide.index
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void save() {
|
||||||
|
MyHttpClient.init(_proxy, 10000, 30000);
|
||||||
|
print('save ${toJson()}');
|
||||||
|
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 files = imageCacheDir.listSync();
|
||||||
|
imagesCount = files.length;
|
||||||
|
sizeCount = 0;
|
||||||
|
files.forEach((file) => sizeCount += file.statSync().size);
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearDiskCachedImages() async {
|
||||||
|
await imageCacheDir.delete(recursive: true);
|
||||||
|
await imageCacheDir.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text('设置')),
|
||||||
|
body: Consumer<SettingData>(builder: (_, data, __) {
|
||||||
|
print('代理 ${data.proxy}');
|
||||||
|
return ListView(
|
||||||
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: [
|
||||||
|
/// 更新设置
|
||||||
|
autoCheck(data),
|
||||||
|
|
||||||
|
/// 隐藏状态栏设置
|
||||||
|
HideStatusBar(
|
||||||
|
option: data.hide,
|
||||||
|
onChanged: (option) => data.hide = option,
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 设置代理
|
||||||
|
ListTile(
|
||||||
|
title: Text('设置代理'),
|
||||||
|
subtitle: Text(data.proxy ?? '无'),
|
||||||
|
onTap: () async {
|
||||||
|
var proxy = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
final _c = TextEditingController(text: data.proxy);
|
||||||
|
return WillPopScope(
|
||||||
|
child: AlertDialog(
|
||||||
|
title: Text('设置网络代理'),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'只支持http代理\nSS,SSR,V2Ray,Trojan(Clash)\n这些梯子App都有提供Http代理功能'),
|
||||||
|
TextField(
|
||||||
|
controller: _c,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '例如Clash提供的127.0.0.1:7890'),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('清空'),
|
||||||
|
onPressed: () {
|
||||||
|
_c.clear();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('确定'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, _c.text);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onWillPop: () {
|
||||||
|
Navigator.pop(context, '-1');
|
||||||
|
return Future.value(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
print('用户输入 $proxy');
|
||||||
|
if (proxy == '-1') return;
|
||||||
|
if (proxy != null) {
|
||||||
|
proxy = proxy
|
||||||
|
.trim()
|
||||||
|
.replaceFirst('http://', '')
|
||||||
|
.replaceFirst('https://', '');
|
||||||
|
}
|
||||||
|
if (proxy == null || proxy.isEmpty) {
|
||||||
|
proxy = null;
|
||||||
|
}
|
||||||
|
print('设置代理 $proxy');
|
||||||
|
data.proxy = proxy;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 清空图片缓存
|
||||||
|
ListTile(
|
||||||
|
title: Text('清除所有图片缓存'),
|
||||||
|
subtitle: isClearing
|
||||||
|
? Text('清理中')
|
||||||
|
: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(text: '图片数量:'),
|
||||||
|
TextSpan(
|
||||||
|
text: imagesCount == null
|
||||||
|
? '读取中'
|
||||||
|
: '$imagesCount 张'),
|
||||||
|
TextSpan(text: '\n'),
|
||||||
|
TextSpan(text: '存储容量:'),
|
||||||
|
TextSpan(
|
||||||
|
text: sizeCount == null
|
||||||
|
? '读取中'
|
||||||
|
: '${filesize(sizeCount)}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
if (isClearing == true) return;
|
||||||
|
final sure = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: Text('确认清除所有图片缓存?'),
|
||||||
|
actions: [
|
||||||
|
RaisedButton(
|
||||||
|
child: Text('确认'),
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (sure == true) {
|
||||||
|
showToast('正在清理图片缓存');
|
||||||
|
isClearing = true;
|
||||||
|
setState(() {});
|
||||||
|
await clearDiskCachedImages();
|
||||||
|
isClearing = false;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
await imageCaches();
|
||||||
|
}
|
||||||
|
showToast('成功清理图片缓存');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 清空数据缓存
|
||||||
|
ListTile(
|
||||||
|
title: Text('清空漫画数据缓存'),
|
||||||
|
subtitle: Text('正常情况是不需要清空的'),
|
||||||
|
onTap: () async {
|
||||||
|
await HttpBook.dataCache.clearAll();
|
||||||
|
showToast('成功清空漫画数据缓存', textPadding: EdgeInsets.all(10));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).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(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
48
lib/activities/test.dart
Normal file
48
lib/activities/test.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
|
|
||||||
|
import '../classes/data.dart';
|
||||||
|
import '../crawler/http18Comic.dart';
|
||||||
|
|
||||||
|
class ActivityTest extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('测试'),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
onPressed: read,
|
||||||
|
child: Text('读取'),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: clear,
|
||||||
|
child: Text('清空数据'),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: httpTest,
|
||||||
|
child: Text('Http请求参数测试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void read() {
|
||||||
|
var books = Data.getFavorites();
|
||||||
|
print(jsonEncode(books));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
Data.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> httpTest() async {
|
||||||
|
final books = await Http18Comic.instance.searchBook('冲突');
|
||||||
|
print('搜索漫画 ${books[0].toJson()}');
|
||||||
|
}
|
||||||
|
}
|
138
lib/classes/book.dart
Normal file
138
lib/classes/book.dart
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../crawler/http.dart';
|
||||||
|
import '../main.dart' as main;
|
||||||
|
import 'data.dart';
|
||||||
|
|
||||||
|
class Author {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const Author(this.id, this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Book {
|
||||||
|
final String _http;
|
||||||
|
final String aid; // 漫画的数据库ID
|
||||||
|
final String name; // 书本名称
|
||||||
|
final String avatar; // 书本封面
|
||||||
|
final String author; // 画家
|
||||||
|
final String description; // 描述
|
||||||
|
final List<Chapter> chapters;
|
||||||
|
final int chapterCount;
|
||||||
|
final int version;
|
||||||
|
|
||||||
|
History history;
|
||||||
|
|
||||||
|
Book({
|
||||||
|
@required String http,
|
||||||
|
@required this.name,
|
||||||
|
@required this.aid,
|
||||||
|
@required this.avatar,
|
||||||
|
this.author,
|
||||||
|
this.description,
|
||||||
|
this.chapters: const [],
|
||||||
|
this.chapterCount: 0,
|
||||||
|
this.history,
|
||||||
|
this.version: 0,
|
||||||
|
}) : _http = http;
|
||||||
|
|
||||||
|
HttpBook get http => MyHttpClient.clients[_http];
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return jsonEncode(toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isFavorite() {
|
||||||
|
var books = Data.getFavorites();
|
||||||
|
return books.containsKey(aid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
print('book toJson');
|
||||||
|
final Map<String, dynamic> data = {
|
||||||
|
'http': _http,
|
||||||
|
'aid': aid,
|
||||||
|
'name': name,
|
||||||
|
'avatar': avatar,
|
||||||
|
'author': author,
|
||||||
|
'chapterCount': chapterCount,
|
||||||
|
'version': version,
|
||||||
|
};
|
||||||
|
if (history != null) data['history'] = history.toJson();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Book.fromJson(Map<String, dynamic> json) {
|
||||||
|
final book = Book(
|
||||||
|
http: json['http'],
|
||||||
|
aid: json['aid'],
|
||||||
|
name: json['name'],
|
||||||
|
avatar: json['avatar'],
|
||||||
|
author: json['author'],
|
||||||
|
description: json['description'],
|
||||||
|
chapterCount: json['chapterCount'] ?? 0,
|
||||||
|
version: json['version'] ?? 0);
|
||||||
|
if (json.containsKey('history'))
|
||||||
|
book.history = History.fromJson(json['history']);
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Chapter {
|
||||||
|
final HttpBook http;
|
||||||
|
final String cid; // 章节cid
|
||||||
|
final String cname; // 章节名称
|
||||||
|
final String avatar; // 章节封面
|
||||||
|
|
||||||
|
Chapter({
|
||||||
|
@required this.http,
|
||||||
|
@required this.cid,
|
||||||
|
@required this.cname,
|
||||||
|
@required this.avatar,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final Map<String, String> data = {
|
||||||
|
'cid': cid,
|
||||||
|
'cname': cname,
|
||||||
|
'avatar': avatar,
|
||||||
|
};
|
||||||
|
return jsonEncode(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class History {
|
||||||
|
final String cid;
|
||||||
|
final String cname;
|
||||||
|
final int time;
|
||||||
|
|
||||||
|
History({@required this.cid, @required this.cname, @required this.time});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => jsonEncode(toJson());
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'cid': cid,
|
||||||
|
'cname': cname,
|
||||||
|
'time': time,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static History fromJson(Map<String, dynamic> json) {
|
||||||
|
return History(cid: json['cid'], cname: json['cname'], time: json['time']);
|
||||||
|
}
|
||||||
|
|
||||||
|
static History fromChapter(Chapter chapter) {
|
||||||
|
return History(
|
||||||
|
cid: chapter.cid,
|
||||||
|
cname: chapter.cname,
|
||||||
|
time: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
155
lib/classes/data.dart
Normal file
155
lib/classes/data.dart
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'book.dart';
|
||||||
|
|
||||||
|
class Data {
|
||||||
|
static SharedPreferences instance;
|
||||||
|
static final favoriteBooksKey = 'favorite_books';
|
||||||
|
static final viewHistoryKey = 'view_history';
|
||||||
|
static final quickKey = 'quick_list';
|
||||||
|
|
||||||
|
static Future init() async {
|
||||||
|
instance = await SharedPreferences.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
static set<T>(String key, T value) {
|
||||||
|
if (value is String) {
|
||||||
|
instance.setString(key, value);
|
||||||
|
} else if (value is int) {
|
||||||
|
instance.setInt(key, value);
|
||||||
|
} else if (value is bool) {
|
||||||
|
instance.setBool(key, value);
|
||||||
|
} else if (value is List<String>) {
|
||||||
|
instance.setStringList(key, value);
|
||||||
|
} else if (value is double) {
|
||||||
|
instance.setDouble(key, value);
|
||||||
|
} else if (value is Map) {
|
||||||
|
instance.setString(key, json.encode(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static dynamic get(String key) {
|
||||||
|
return instance.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Book> getFavorites() {
|
||||||
|
if (has(favoriteBooksKey)) {
|
||||||
|
final String str = instance.getString(favoriteBooksKey);
|
||||||
|
Map<String, Object> data = jsonDecode(str);
|
||||||
|
Map<String, Book> res = {};
|
||||||
|
data.keys.forEach((key) {
|
||||||
|
res[key] = Book.fromJson(data[key]);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addFavorite(Book book) {
|
||||||
|
var books = getFavorites();
|
||||||
|
books[book.aid] = book;
|
||||||
|
set<Map>(favoriteBooksKey, books);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void removeFavorite(Book book) {
|
||||||
|
var books = getFavorites();
|
||||||
|
if (books.containsKey(book.aid)) {
|
||||||
|
books.remove(book.aid);
|
||||||
|
set<Map>(favoriteBooksKey, books);
|
||||||
|
reQuick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static clear() {
|
||||||
|
instance.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool has(String key) {
|
||||||
|
return instance.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static remove(String key) {
|
||||||
|
instance.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Book> getHistories() {
|
||||||
|
if (has(viewHistoryKey)) {
|
||||||
|
var data =
|
||||||
|
jsonDecode(instance.getString(viewHistoryKey)) as Map<String, Object>;
|
||||||
|
final Map<String, Book> histories = {};
|
||||||
|
data.forEach((key, value) {
|
||||||
|
histories[key] = Book.fromJson(value);
|
||||||
|
});
|
||||||
|
return histories;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static addHistory(Book book, Chapter chapter) {
|
||||||
|
book.history = History(
|
||||||
|
cid: chapter.cid,
|
||||||
|
cname: chapter.cname,
|
||||||
|
time: DateTime.now().millisecondsSinceEpoch);
|
||||||
|
final books = getHistories();
|
||||||
|
books[book.aid] = book;
|
||||||
|
set(viewHistoryKey, books);
|
||||||
|
// print('保存历史\n' + books.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeHistory(bool Function(Book book) isDelete) {
|
||||||
|
var books = getHistories();
|
||||||
|
books.keys
|
||||||
|
.where((key) => isDelete(books[key]))
|
||||||
|
.toList()
|
||||||
|
.forEach(books.remove);
|
||||||
|
set(viewHistoryKey, books);
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeHistoryFromBook(Book book) {
|
||||||
|
final books = getHistories();
|
||||||
|
books.remove(book.aid);
|
||||||
|
set(viewHistoryKey, books);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 快速导航 id 列表,内部方法
|
||||||
|
static List<String> quickIdList() {
|
||||||
|
if (instance.containsKey(quickKey)) {
|
||||||
|
return instance.getStringList(quickKey);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 快速导航列表
|
||||||
|
static List<Book> quickList() {
|
||||||
|
final books = getFavorites();
|
||||||
|
final ids = books.keys;
|
||||||
|
final List<String> quickIds = quickIdList();
|
||||||
|
print('快捷 $quickIds');
|
||||||
|
return quickIds
|
||||||
|
.where((id) => ids.contains(id))
|
||||||
|
.map((id) => books[id])
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 增加快速导航
|
||||||
|
static addQuick(Book book) {
|
||||||
|
final list = quickIdList();
|
||||||
|
list.add(book.aid);
|
||||||
|
instance.setStringList(quickKey, list.toSet().toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
static addQuickAll(List<String> id) {
|
||||||
|
print('保存qid $id');
|
||||||
|
instance.setStringList(quickKey, id.toSet().toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重新整理Quick的id列表
|
||||||
|
static reQuick() {
|
||||||
|
final books = getFavorites();
|
||||||
|
final quickIds = quickIdList();
|
||||||
|
instance.setStringList(
|
||||||
|
quickKey, quickIds.where(books.keys.contains).toSet().toList());
|
||||||
|
}
|
||||||
|
}
|
93
lib/classes/networkImageSSL.dart
Normal file
93
lib/classes/networkImageSSL.dart
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../crawler/http.dart';
|
||||||
|
|
||||||
|
/// The dart:io implementation of [image_provider.NetworkImage].
|
||||||
|
class NetworkImageSSL extends ImageProvider<NetworkImage>
|
||||||
|
implements NetworkImage {
|
||||||
|
/// Creates an object that fetches the image at the given URL.
|
||||||
|
///
|
||||||
|
/// The arguments [url] and [scale] must not be null.
|
||||||
|
const NetworkImageSSL(
|
||||||
|
this.http,
|
||||||
|
this.url, {
|
||||||
|
this.scale = 1.0,
|
||||||
|
this.headers,
|
||||||
|
this.timeout = 8,
|
||||||
|
}) : assert(url != null),
|
||||||
|
assert(scale != null);
|
||||||
|
|
||||||
|
final HttpBook http;
|
||||||
|
|
||||||
|
final int timeout;
|
||||||
|
@override
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final double scale;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Map<String, String> headers;
|
||||||
|
|
||||||
|
static void init(ByteData data) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<NetworkImageSSL> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture<NetworkImageSSL>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
|
||||||
|
// Ownership of this controller is handed off to [_loadAsync]; it is that
|
||||||
|
// method's responsibility to close the controller's stream when the image
|
||||||
|
// has been loaded or an error is thrown.
|
||||||
|
final StreamController<ImageChunkEvent> chunkEvents =
|
||||||
|
StreamController<ImageChunkEvent>();
|
||||||
|
|
||||||
|
return MultiFrameImageStreamCompleter(
|
||||||
|
codec: _loadAsync(key, chunkEvents, decode),
|
||||||
|
chunkEvents: chunkEvents.stream,
|
||||||
|
scale: key.scale,
|
||||||
|
informationCollector: () {
|
||||||
|
return <DiagnosticsNode>[
|
||||||
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
|
DiagnosticsProperty<NetworkImage>('Image key', key),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Codec> _loadAsync(
|
||||||
|
NetworkImageSSL key,
|
||||||
|
StreamController<ImageChunkEvent> chunkEvents,
|
||||||
|
DecoderCallback decode,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
assert(key == this);
|
||||||
|
final Uint8List bytes = await http.getImage(url);
|
||||||
|
if (bytes.lengthInBytes == 0)
|
||||||
|
throw Exception('NetworkImage is an empty file: $url');
|
||||||
|
return decode(bytes);
|
||||||
|
} finally {
|
||||||
|
chunkEvents.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
final NetworkImageSSL typedOther = other;
|
||||||
|
return url == typedOther.url && scale == typedOther.scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues(url, scale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '$runtimeType("$url", scale: $scale)';
|
||||||
|
}
|
71
lib/crawler/http.dart
Normal file
71
lib/crawler/http.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/adapter.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:dio_http_cache/dio_http_cache.dart';
|
||||||
|
|
||||||
|
import '../classes/book.dart';
|
||||||
|
import 'http18Comic.dart';
|
||||||
|
|
||||||
|
class MyHttpClient {
|
||||||
|
static Map<String, HttpBook> clients = {};
|
||||||
|
|
||||||
|
static init(String proxy, int timeout, int imageTimeout) {
|
||||||
|
final headers = {
|
||||||
|
"user-agent":
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36",
|
||||||
|
"accept":
|
||||||
|
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||||
|
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,zh-HK;q=0.7",
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
"pragma": "no-cache",
|
||||||
|
};
|
||||||
|
|
||||||
|
var http = Http18Comic(
|
||||||
|
proxy: proxy,
|
||||||
|
headers: headers,
|
||||||
|
timeout: timeout,
|
||||||
|
);
|
||||||
|
clients[http.id] = http;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class HttpBook {
|
||||||
|
static final DioCacheManager dataCache = DioCacheManager(CacheConfig(
|
||||||
|
databaseName: 'data',
|
||||||
|
defaultMaxAge: Duration(days: 30),
|
||||||
|
));
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
final Dio dio;
|
||||||
|
|
||||||
|
HttpBook(this.id, this.name, this.dio);
|
||||||
|
|
||||||
|
Future<List<Book>> searchBook(String name, [int page]);
|
||||||
|
|
||||||
|
Future<Book> getBook(String aid);
|
||||||
|
|
||||||
|
Future<List<String>> getChapterImages(Book book, Chapter chapter);
|
||||||
|
|
||||||
|
Future<List<int>> getImage(String url);
|
||||||
|
|
||||||
|
Future<List<Book>> hotBooks([String type = '', int page]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetProxy(Dio dio, String proxy) {
|
||||||
|
if (proxy != null) {
|
||||||
|
proxy = 'PROXY $proxy';
|
||||||
|
// print('setProxy $proxy');
|
||||||
|
final adapter = DefaultHttpClientAdapter();
|
||||||
|
adapter.onHttpClientCreate = (HttpClient client) {
|
||||||
|
client.findProxy = (uri) {
|
||||||
|
//proxy all request to localhost:8888
|
||||||
|
return proxy;
|
||||||
|
};
|
||||||
|
client.badCertificateCallback =
|
||||||
|
(X509Certificate cert, String host, int port) => true;
|
||||||
|
};
|
||||||
|
dio.httpClientAdapter = adapter;
|
||||||
|
}
|
||||||
|
}
|
97
lib/main.dart
Normal file
97
lib/main.dart
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dynamic_theme/dynamic_theme.dart';
|
||||||
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
|
import 'package:firebase_analytics/observer.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:oktoast/oktoast.dart';
|
||||||
|
import 'package:package_info/package_info.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'activities/home.dart';
|
||||||
|
import 'activities/setting/setting.dart';
|
||||||
|
import 'classes/data.dart';
|
||||||
|
import 'widgets/favorites.dart';
|
||||||
|
|
||||||
|
FirebaseAnalytics analytics;
|
||||||
|
FirebaseAnalyticsObserver observer;
|
||||||
|
|
||||||
|
const bool isDevMode = !bool.fromEnvironment('dart.vm.product');
|
||||||
|
int version;
|
||||||
|
BoxDecoration border;
|
||||||
|
|
||||||
|
Directory imageCacheDir;
|
||||||
|
String imageCacheDirPath;
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
FlutterError.onError = (FlutterErrorDetails details) {};
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
getTemporaryDirectory().then((dir) {
|
||||||
|
imageCacheDir = Directory(path.join(dir.path, 'images'));
|
||||||
|
imageCacheDirPath = imageCacheDir.path;
|
||||||
|
if (imageCacheDir.existsSync() == false) imageCacheDir.createSync();
|
||||||
|
print('图片缓存目录 $imageCacheDirPath');
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
analytics = FirebaseAnalytics();
|
||||||
|
observer = FirebaseAnalyticsObserver(analytics: analytics);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
Data.init(),
|
||||||
|
SystemChrome.setPreferredOrientations(
|
||||||
|
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
||||||
|
]);
|
||||||
|
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
version = int.parse(packageInfo.buildNumber);
|
||||||
|
runApp(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider<SettingData>(create: (_) => SettingData()),
|
||||||
|
ChangeNotifierProvider<FavoriteData>(create: (_) => FavoriteData()),
|
||||||
|
],
|
||||||
|
child: Main(packageInfo: packageInfo),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Main extends StatefulWidget {
|
||||||
|
final PackageInfo packageInfo;
|
||||||
|
|
||||||
|
const Main({Key key, this.packageInfo}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_Main createState() => _Main();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Main extends State<Main> with WidgetsBindingObserver {
|
||||||
|
static BoxDecoration _border;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
border = BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: Divider.createBorderSide(context, color: Colors.grey)));
|
||||||
|
return DynamicTheme(
|
||||||
|
defaultBrightness: Brightness.dark,
|
||||||
|
data: (brightness) => new ThemeData(
|
||||||
|
brightness: brightness,
|
||||||
|
),
|
||||||
|
themedWidgetBuilder: (context, theme) {
|
||||||
|
return OKToast(
|
||||||
|
child: MaterialApp(
|
||||||
|
title: '微漫 v${widget.packageInfo.version}',
|
||||||
|
theme: theme,
|
||||||
|
home: ActivityHome(widget.packageInfo),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
50
lib/utils.dart
Normal file
50
lib/utils.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import './main.dart';
|
||||||
|
import 'activities/book.dart';
|
||||||
|
import 'activities/chapter.dart';
|
||||||
|
import 'activities/search/search.dart';
|
||||||
|
import 'classes/book.dart';
|
||||||
|
|
||||||
|
final weekTime = Duration.millisecondsPerDay * 7;
|
||||||
|
|
||||||
|
void openBook(BuildContext context, Book book, String heroTag) {
|
||||||
|
print('openBook ${book.name} version:${book.version}');
|
||||||
|
if (book.version == null || book.version < version || book.http == null) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: '/activity_search/${book.name}'),
|
||||||
|
builder: (_) => ActivitySearch(search: book.name),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: '/activity_book/${book.name}'),
|
||||||
|
builder: (_) => ActivityBook(book: book, heroTag: heroTag),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void openChapter(BuildContext context, Book book, Chapter chapter) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: '/activity_chapter/${book.name}/${chapter.cname}'),
|
||||||
|
builder: (_) => ActivityChapter(book, chapter),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showStatusBar() {
|
||||||
|
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hideStatusBar() {
|
||||||
|
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||||
|
}
|
196
lib/widgets/book.dart
Normal file
196
lib/widgets/book.dart
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import 'package:extended_image/extended_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../classes/book.dart';
|
||||||
|
import '../classes/networkImageSSL.dart';
|
||||||
|
import '../utils.dart';
|
||||||
|
|
||||||
|
class WidgetBook extends StatelessWidget {
|
||||||
|
final Book book;
|
||||||
|
final String subtitle;
|
||||||
|
final Function(Book) onTap;
|
||||||
|
|
||||||
|
const WidgetBook(
|
||||||
|
this.book, {
|
||||||
|
Key key,
|
||||||
|
@required this.subtitle,
|
||||||
|
this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var isLiked = book.isFavorite();
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
book.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
subtitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
leading: Hero(
|
||||||
|
tag: 'bookAvatar${book.aid}',
|
||||||
|
child: ExtendedImage(image: NetworkImageSSL(book.http, book.avatar)),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
isLiked ? Icons.favorite : Icons.favorite_border,
|
||||||
|
color: isLiked ? Colors.red : Colors.grey,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) return onTap(book);
|
||||||
|
openBook(context, book, 'bookAvatar${book.aid}');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WidgetChapter extends StatelessWidget {
|
||||||
|
static final double height = kToolbarHeight;
|
||||||
|
final Chapter chapter;
|
||||||
|
final Function(Chapter) onTap;
|
||||||
|
final bool read;
|
||||||
|
|
||||||
|
WidgetChapter({
|
||||||
|
Key key,
|
||||||
|
this.chapter,
|
||||||
|
this.onTap,
|
||||||
|
this.read = false,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final children = <InlineSpan>[TextSpan(text: chapter.cname)];
|
||||||
|
if (read) {
|
||||||
|
children.insert(
|
||||||
|
0,
|
||||||
|
TextSpan(
|
||||||
|
text: '[已看]',
|
||||||
|
style: TextStyle(color: Colors.orange),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return ListTile(
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) onTap(chapter);
|
||||||
|
},
|
||||||
|
title: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: children,
|
||||||
|
style: Theme.of(context).textTheme.body1,
|
||||||
|
),
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
leading: chapter.avatar == null
|
||||||
|
? null
|
||||||
|
: Image(
|
||||||
|
image: ExtendedNetworkImageProvider(
|
||||||
|
chapter.avatar,
|
||||||
|
cache: true,
|
||||||
|
),
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
width: 100,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WidgetHistory extends StatelessWidget {
|
||||||
|
final Book book;
|
||||||
|
final Function(Book book) onTap;
|
||||||
|
|
||||||
|
WidgetHistory(this.book, this.onTap);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) onTap(book);
|
||||||
|
},
|
||||||
|
title: Text(book.name),
|
||||||
|
leading: Image(
|
||||||
|
image: ExtendedNetworkImageProvider(book.avatar, cache: true),
|
||||||
|
fit: BoxFit.fitHeight,
|
||||||
|
),
|
||||||
|
subtitle: Text(book.history.cname),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WidgetBookCheckNew extends StatefulWidget {
|
||||||
|
final Book book;
|
||||||
|
|
||||||
|
const WidgetBookCheckNew({Key key, this.book}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_WidgetBookCheckNew createState() => _WidgetBookCheckNew();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
|
||||||
|
bool loading = true, hasError = false;
|
||||||
|
int news;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
void load() async {
|
||||||
|
// loading = true;
|
||||||
|
// try {
|
||||||
|
// final book = await Http18Comic.instance
|
||||||
|
// .getBook(widget.book.aid)
|
||||||
|
// .timeout(Duration(seconds: 2));
|
||||||
|
// news = book.chapterCount - widget.book.chapterCount;
|
||||||
|
// hasError = false;
|
||||||
|
// } catch (e) {
|
||||||
|
// hasError = true;
|
||||||
|
// }
|
||||||
|
// loading = false;
|
||||||
|
// setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final children = <Widget>[];
|
||||||
|
if (widget.book.history != null)
|
||||||
|
children.add(Text(
|
||||||
|
widget.book.history.cname,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
children.add(Text('检查更新中'));
|
||||||
|
else if (hasError)
|
||||||
|
children.add(Text('网络错误'));
|
||||||
|
else if (news > 0)
|
||||||
|
children.add(Text('有 $news 章更新'));
|
||||||
|
else
|
||||||
|
children.add(Text('没有更新'));
|
||||||
|
return ListTile(
|
||||||
|
onTap: () =>
|
||||||
|
openBook(context, widget.book, 'checkBook${widget.book.aid}'),
|
||||||
|
leading: Hero(
|
||||||
|
tag: 'checkBook${widget.book.aid}',
|
||||||
|
child: Image(
|
||||||
|
image:
|
||||||
|
ExtendedNetworkImageProvider(widget.book.avatar, cache: true)),
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
isThreeLine: true,
|
||||||
|
title: Text(widget.book.name),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
13
lib/widgets/dbSourceListWidget.dart
Normal file
13
lib/widgets/dbSourceListWidget.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class DBSourceListWidget extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_DBSourceListWidget createState() => _DBSourceListWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DBSourceListWidget extends State<DBSourceListWidget> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(children: []);
|
||||||
|
}
|
||||||
|
}
|
272
lib/widgets/favorites.dart
Normal file
272
lib/widgets/favorites.dart
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
import 'package:extended_image/extended_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../activities/search/search.dart';
|
||||||
|
import '../activities/setting/setting.dart';
|
||||||
|
import '../classes/book.dart';
|
||||||
|
import '../classes/data.dart';
|
||||||
|
import '../classes/networkImageSSL.dart';
|
||||||
|
import '../utils.dart';
|
||||||
|
import '../widgets/sliverExpandableGroup.dart';
|
||||||
|
import '../widgets/utils.dart';
|
||||||
|
|
||||||
|
class FavoriteData extends ChangeNotifier {
|
||||||
|
/// -3 旧的收藏数据,跳过检查,-2 在队列中等待检查,-1读取错误,0 没有更新,> 0 更新的章节数量
|
||||||
|
final Map<String, int> hasNews = {}; // 漫画的状态
|
||||||
|
final Map<String, Book> all = {}, // 所有收藏
|
||||||
|
inWeek = {}, // 7天内看过的收藏
|
||||||
|
other = {}; // 其他收藏
|
||||||
|
|
||||||
|
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)];
|
||||||
|
if (currentBook.version == 0 || currentBook.http == null) {
|
||||||
|
hasNews[currentBook.aid] = -3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
newBook = await currentBook.http
|
||||||
|
.getBook(currentBook.aid)
|
||||||
|
.timeout(Duration(seconds: 8));
|
||||||
|
int different = newBook.chapterCount - currentBook.chapterCount;
|
||||||
|
hasNews[currentBook.aid] = different;
|
||||||
|
} catch (e) {
|
||||||
|
hasNews[currentBook.aid] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FavoriteList extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_FavoriteList createState() => _FavoriteList();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavoriteList extends State<FavoriteList> {
|
||||||
|
static bool showTip = false;
|
||||||
|
|
||||||
|
static final loadFailTextSpan = TextSpan(
|
||||||
|
text: '读取失败,下拉刷新', style: TextStyle(color: Colors.redAccent)),
|
||||||
|
waitToCheck =
|
||||||
|
TextSpan(text: '等待检查更新', style: TextStyle(color: Colors.grey)),
|
||||||
|
unCheck =
|
||||||
|
TextSpan(text: '请下拉列表检查更新', style: TextStyle(color: Colors.grey)),
|
||||||
|
noUpdate = TextSpan(text: '没有更新', style: TextStyle(color: Colors.grey)),
|
||||||
|
outDate = TextSpan(
|
||||||
|
text: '旧版本的收藏数据,不检查更新', style: TextStyle(color: Colors.redAccent));
|
||||||
|
|
||||||
|
Widget bookBuilder(Book book, int state) {
|
||||||
|
TextSpan _state = unCheck;
|
||||||
|
if (state == null) {
|
||||||
|
_state = unCheck;
|
||||||
|
} else if (state > 0) {
|
||||||
|
_state =
|
||||||
|
TextSpan(text: '有 $state 章更新', style: TextStyle(color: Colors.green));
|
||||||
|
} else if (state == 0) {
|
||||||
|
_state = noUpdate;
|
||||||
|
} else if (state == -1) {
|
||||||
|
_state = loadFailTextSpan;
|
||||||
|
} else if (state == -2) {
|
||||||
|
_state = waitToCheck;
|
||||||
|
} else if (state == -3) {
|
||||||
|
_state = outDate;
|
||||||
|
}
|
||||||
|
return FBookItem(
|
||||||
|
book: book,
|
||||||
|
subtitle: _state,
|
||||||
|
onDelete: deleteBook,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteBook(Book book) async {
|
||||||
|
final sure = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: Text('确认删除 ${book.name} ?'),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('确认'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (sure == true)
|
||||||
|
Provider.of<FavoriteData>(context, listen: false).remove(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer2<SettingData, FavoriteData>(
|
||||||
|
builder: (_, setting, favorite, __) {
|
||||||
|
if (favorite.all.isEmpty) return Center(child: Text('没有收藏'));
|
||||||
|
List<Book> inWeekUpdated = [],
|
||||||
|
inWeekUnUpdated = [],
|
||||||
|
otherUpdated = [],
|
||||||
|
otherUnUpdated = [];
|
||||||
|
favorite.inWeek.forEach((aid, 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 ClipRect(
|
||||||
|
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],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FBookItem extends StatelessWidget {
|
||||||
|
final Book book;
|
||||||
|
final TextSpan subtitle;
|
||||||
|
final void Function(Book book) onDelete;
|
||||||
|
|
||||||
|
const FBookItem({
|
||||||
|
Key key,
|
||||||
|
@required this.book,
|
||||||
|
@required this.subtitle,
|
||||||
|
@required this.onDelete,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Slidable(
|
||||||
|
actionPane: SlidableDrawerActionPane(),
|
||||||
|
closeOnScroll: true,
|
||||||
|
actionExtentRatio: 0.25,
|
||||||
|
secondaryActions: [
|
||||||
|
IconSlideAction(
|
||||||
|
caption: '删除',
|
||||||
|
color: Colors.red,
|
||||||
|
icon: Icons.delete,
|
||||||
|
onTap: () => onDelete(book),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
if (book.http != null)
|
||||||
|
return openBook(context, book, 'fb ${book.aid}');
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ActivitySearch(search: book.name),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
// onLongPress: () => onDelete(book),
|
||||||
|
leading: Hero(
|
||||||
|
tag: 'fb ${book.aid}',
|
||||||
|
child: book.http == null
|
||||||
|
? oldBookAvatar(text: '旧书', width: 50.0, height: 80.0)
|
||||||
|
: ExtendedImage(
|
||||||
|
image: NetworkImageSSL(book.http, book.avatar),
|
||||||
|
width: 50.0,
|
||||||
|
height: 80.0),
|
||||||
|
),
|
||||||
|
title: Text(book.name),
|
||||||
|
subtitle: RichText(text: subtitle),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
131
lib/widgets/histories.dart
Normal file
131
lib/widgets/histories.dart
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import 'package:extended_image/extended_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
|
|
||||||
|
import '../classes/book.dart';
|
||||||
|
import '../classes/data.dart';
|
||||||
|
import '../classes/networkImageSSL.dart';
|
||||||
|
import '../utils.dart';
|
||||||
|
import '../widgets/sliverExpandableGroup.dart';
|
||||||
|
import '../widgets/utils.dart';
|
||||||
|
|
||||||
|
class Histories extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_Histories createState() => _Histories();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Histories extends State<Histories> {
|
||||||
|
final List<Book> inWeek = [], other = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
loadBook();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadBook() {
|
||||||
|
inWeek.clear();
|
||||||
|
other.clear();
|
||||||
|
final list = Data.getHistories().values.toList();
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
list.sort((a, b) => b.history.time.compareTo(a.history.time));
|
||||||
|
list.forEach((book) {
|
||||||
|
if ((now - book.history.time) < weekTime) {
|
||||||
|
inWeek.add(book);
|
||||||
|
} else {
|
||||||
|
other.add(book);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear(bool inWeek) async {
|
||||||
|
final title = '确认清空 ' + (inWeek ? '7天内的' : '更早的') + '浏览记录 ?';
|
||||||
|
final res = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
textColor: Colors.grey,
|
||||||
|
child: Text('取消'),
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('确认'),
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
print('清理历史 $inWeek $res');
|
||||||
|
if (res == false) return;
|
||||||
|
List<Book> list = inWeek ? this.inWeek : this.other;
|
||||||
|
list.forEach((book) => Data.removeHistoryFromBook(book));
|
||||||
|
setState(() {
|
||||||
|
loadBook();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget book(List array, int index) {
|
||||||
|
final Book book = array[index];
|
||||||
|
return Slidable(
|
||||||
|
child: ListTile(
|
||||||
|
leading: book.http == null
|
||||||
|
? oldBookAvatar(text: '旧\n书', width: 50.0, height: 80.0)
|
||||||
|
: ExtendedImage(
|
||||||
|
image: NetworkImageSSL(book.http, book.avatar),
|
||||||
|
width: 50.0,
|
||||||
|
height: 80.0),
|
||||||
|
title: Text(book.name),
|
||||||
|
subtitle: Text(book.history.cname),
|
||||||
|
onTap: () => openBook(context, book, 'fb ${book.aid}'),
|
||||||
|
),
|
||||||
|
actionPane: SlidableDrawerActionPane(),
|
||||||
|
secondaryActions: [
|
||||||
|
IconSlideAction(
|
||||||
|
caption: '删除',
|
||||||
|
color: Colors.red,
|
||||||
|
icon: Icons.delete,
|
||||||
|
onTap: () => setState(() {
|
||||||
|
array.removeAt(index);
|
||||||
|
Data.removeHistoryFromBook(book);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: ClipRect(
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverExpandableGroup(
|
||||||
|
title: Text('7天内的浏览历史 (${inWeek.length})'),
|
||||||
|
expanded: true,
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('清空'),
|
||||||
|
onPressed: inWeek.length == 0 ? null : () => clear(true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
count: inWeek.length,
|
||||||
|
builder: (ctx, i) => book(inWeek, i),
|
||||||
|
),
|
||||||
|
SliverExpandableGroup(
|
||||||
|
title: Text('更早的浏览历史 (${other.length})'),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('清空'),
|
||||||
|
onPressed: other.length == 0 ? null : () => clear(false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
count: other.length,
|
||||||
|
builder: (ctx, i) => book(other, i),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
74
lib/widgets/pullToRefreshHeader.dart
Normal file
74
lib/widgets/pullToRefreshHeader.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||||
|
|
||||||
|
class SliverPullToRefreshHeader extends StatelessWidget {
|
||||||
|
static final double height = kToolbarHeight * 2;
|
||||||
|
final PullToRefreshScrollNotificationInfo info;
|
||||||
|
final void Function() onTap;
|
||||||
|
final double fontSize;
|
||||||
|
|
||||||
|
const SliverPullToRefreshHeader({
|
||||||
|
Key key,
|
||||||
|
@required this.info,
|
||||||
|
this.onTap,
|
||||||
|
this.fontSize = 16,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (info == null) return SliverToBoxAdapter(child: SizedBox());
|
||||||
|
double dragOffset = info?.dragOffset ?? 0.0;
|
||||||
|
TextSpan text = TextSpan(
|
||||||
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
fontSize: fontSize,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
baseline: TextBaseline.alphabetic,
|
||||||
|
child: Padding(
|
||||||
|
child: Image.asset("assets/logo.png", height: 20),
|
||||||
|
padding: EdgeInsets.only(right: 5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
if (info.mode == RefreshIndicatorMode.error) {
|
||||||
|
text.children.addAll([
|
||||||
|
TextSpan(
|
||||||
|
text: '读取失败\n当失败次数太多可能是网络出现问题\n',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
WidgetSpan(
|
||||||
|
child: RaisedButton.icon(
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
onPressed: onTap,
|
||||||
|
label: Text('再次尝试'))),
|
||||||
|
]);
|
||||||
|
} else if (info.mode == RefreshIndicatorMode.refresh ||
|
||||||
|
info.mode == RefreshIndicatorMode.snap) {
|
||||||
|
text.children.addAll([
|
||||||
|
TextSpan(text: '读取中,请稍候'),
|
||||||
|
]);
|
||||||
|
} else if ([
|
||||||
|
RefreshIndicatorMode.drag,
|
||||||
|
RefreshIndicatorMode.armed,
|
||||||
|
RefreshIndicatorMode.snap
|
||||||
|
].contains(info.mode)) {
|
||||||
|
text.children.add(TextSpan(text: '重新读取'));
|
||||||
|
} else {
|
||||||
|
text.children.add(TextSpan(text: 'Bye~'));
|
||||||
|
}
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
height: dragOffset,
|
||||||
|
child: Center(
|
||||||
|
child: Text.rich(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
244
lib/widgets/quick.dart
Normal file
244
lib/widgets/quick.dart
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import 'package:draggable_container/draggable_container.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../classes/book.dart';
|
||||||
|
import '../classes/data.dart';
|
||||||
|
import '../classes/networkImageSSL.dart';
|
||||||
|
import '../utils.dart';
|
||||||
|
import '../widgets/favorites.dart';
|
||||||
|
import '../widgets/utils.dart';
|
||||||
|
|
||||||
|
class QuickBook extends DraggableItem {
|
||||||
|
static const heroTag = 'quickBookAvatar';
|
||||||
|
Widget child;
|
||||||
|
final BuildContext context;
|
||||||
|
final Book book;
|
||||||
|
final double width, height;
|
||||||
|
|
||||||
|
QuickBook(this.width, this.height,
|
||||||
|
{@required this.book, @required this.context}) {
|
||||||
|
child = GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
openBook(context, book, '$heroTag ${book.aid}');
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
book.http == null
|
||||||
|
? oldBookAvatar(width: width, height: height)
|
||||||
|
: SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: Hero(
|
||||||
|
tag: '$heroTag ${book.aid}',
|
||||||
|
child: Image(
|
||||||
|
image: NetworkImageSSL(book.http, book.avatar),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(left: 2, right: 2, top: 2, bottom: 2),
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
child: Text(
|
||||||
|
book.name,
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: 1,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 10),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Quick extends StatefulWidget {
|
||||||
|
final double width, height;
|
||||||
|
final Function(bool mode) draggableModeChanged;
|
||||||
|
|
||||||
|
const Quick(
|
||||||
|
{Key key, this.width, this.height, @required this.draggableModeChanged})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
QuickState createState() => QuickState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuickState extends State<Quick> {
|
||||||
|
final List<String> id = [];
|
||||||
|
final int count = 8;
|
||||||
|
final List<DraggableItem> _draggableItems = [];
|
||||||
|
DraggableItem _addButton;
|
||||||
|
GlobalKey<DraggableContainerState> _key =
|
||||||
|
GlobalKey<DraggableContainerState>();
|
||||||
|
double width = 0, height = 0;
|
||||||
|
|
||||||
|
void exit() {
|
||||||
|
_key.currentState.draggableMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showSelectBookDialog() async {
|
||||||
|
final books = Data.getFavorites();
|
||||||
|
final list = books.values
|
||||||
|
.where((book) => !id.contains(book.aid))
|
||||||
|
.map((book) => ListTile(
|
||||||
|
title: Text(book.name),
|
||||||
|
leading: Image(image: NetworkImageSSL(book.http, book.avatar)),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context, book);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
return showDialog<Book>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('将收藏的漫画添加到快速导航'),
|
||||||
|
content: Container(
|
||||||
|
width: double.maxFinite,
|
||||||
|
height: 300,
|
||||||
|
child: list.isNotEmpty
|
||||||
|
? ListView(
|
||||||
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: list,
|
||||||
|
).toList(),
|
||||||
|
)
|
||||||
|
: Center(child: Text('没有了')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QuickState() {
|
||||||
|
_addButton = DraggableItem(
|
||||||
|
deletable: false,
|
||||||
|
fixed: true,
|
||||||
|
child: FlatButton(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'添加',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
final items = _key.currentState.items;
|
||||||
|
final buttonIndex = items.indexOf(_addButton);
|
||||||
|
print('add $buttonIndex');
|
||||||
|
if (buttonIndex > -1) {
|
||||||
|
final book = await _showSelectBookDialog();
|
||||||
|
print('选择了 $book');
|
||||||
|
if (book == null) return;
|
||||||
|
_key.currentState.insteadOfIndex(buttonIndex,
|
||||||
|
QuickBook(width, height, book: book, context: context),
|
||||||
|
force: true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int length() {
|
||||||
|
return _key.currentState.items.where((item) => item is QuickBook).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
width = widget.width / 4 - 10;
|
||||||
|
height = (width / 0.7).roundToDouble();
|
||||||
|
_draggableItems.addAll(Data.quickList().map((book) {
|
||||||
|
id.add(book.aid);
|
||||||
|
return QuickBook(width, height, book: book, context: context);
|
||||||
|
}));
|
||||||
|
if (_draggableItems.length < count) _draggableItems.add(_addButton);
|
||||||
|
for (var i = count - _draggableItems.length; i > 0; i--) {
|
||||||
|
_draggableItems.add(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 8, bottom: 4, left: 8),
|
||||||
|
width: widget.width,
|
||||||
|
child: Text(
|
||||||
|
'快速导航(长按编辑)',
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DraggableContainer(
|
||||||
|
key: _key,
|
||||||
|
slotMargin: EdgeInsets.only(bottom: 8, left: 6, right: 6),
|
||||||
|
slotSize: Size(width, height),
|
||||||
|
slotDecoration: BoxDecoration(color: Colors.grey.withOpacity(0.3)),
|
||||||
|
dragDecoration: BoxDecoration(
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]),
|
||||||
|
items: _draggableItems,
|
||||||
|
onDraggableModeChanged: widget.draggableModeChanged,
|
||||||
|
onChanged: (List<DraggableItem> items) {
|
||||||
|
id.clear();
|
||||||
|
items.forEach((item) {
|
||||||
|
if (item is QuickBook) id.add(item.book.aid);
|
||||||
|
});
|
||||||
|
Data.addQuickAll(id);
|
||||||
|
final nullIndex = items.indexOf(null);
|
||||||
|
final buttonIndex = items.indexOf(_addButton);
|
||||||
|
print('null $nullIndex, button $buttonIndex');
|
||||||
|
if (nullIndex > -1 && buttonIndex == -1) {
|
||||||
|
_key.currentState
|
||||||
|
.insteadOfIndex(nullIndex, _addButton, triggerEvent: false);
|
||||||
|
} else if (nullIndex > -1 &&
|
||||||
|
buttonIndex > -1 &&
|
||||||
|
nullIndex < buttonIndex) {
|
||||||
|
_key.currentState.removeItem(_addButton);
|
||||||
|
_key.currentState
|
||||||
|
.insteadOfIndex(nullIndex, _addButton, triggerEvent: false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
95
lib/widgets/sliverExpandableGroup.dart
Normal file
95
lib/widgets/sliverExpandableGroup.dart
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||||
|
|
||||||
|
class SliverExpandableBuilder {
|
||||||
|
final int count;
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
|
const SliverExpandableBuilder(this.count, this.builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SliverExpandableGroup extends StatefulWidget {
|
||||||
|
final Widget title;
|
||||||
|
final bool expanded;
|
||||||
|
final List<Widget> actions;
|
||||||
|
final Color divideColor;
|
||||||
|
final double height;
|
||||||
|
final int count;
|
||||||
|
final IndexedWidgetBuilder builder;
|
||||||
|
|
||||||
|
const SliverExpandableGroup({
|
||||||
|
Key key,
|
||||||
|
@required this.title,
|
||||||
|
@required this.count,
|
||||||
|
@required this.builder,
|
||||||
|
this.expanded = false,
|
||||||
|
this.actions = const [],
|
||||||
|
this.divideColor = Colors.grey,
|
||||||
|
this.height = kToolbarHeight,
|
||||||
|
}) : assert(title != null),
|
||||||
|
assert(builder != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SliverExpandableGroup createState() => _SliverExpandableGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliverExpandableGroup extends State<SliverExpandableGroup> {
|
||||||
|
bool _expanded;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_expanded = widget.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Decoration _decoration = BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: Divider.createBorderSide(context, color: widget.divideColor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return SliverStickyHeader(
|
||||||
|
header: InkWell(
|
||||||
|
child: Container(
|
||||||
|
height: widget.height,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).dialogBackgroundColor,
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Transform.rotate(
|
||||||
|
angle: _expanded ? 0 : math.pi,
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: widget.title),
|
||||||
|
...widget.actions,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_expanded = !_expanded;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
sliver: _expanded
|
||||||
|
? SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate((ctx, i) {
|
||||||
|
if (i < widget.count - 1) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: _decoration,
|
||||||
|
child: widget.builder(context, i),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return widget.builder(context, i);
|
||||||
|
}, childCount: widget.count))
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
45
lib/widgets/utils.dart
Normal file
45
lib/widgets/utils.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class TextDivider extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final double leftPadding, padding;
|
||||||
|
final List<Widget> actions;
|
||||||
|
|
||||||
|
const TextDivider({
|
||||||
|
Key key,
|
||||||
|
@required this.text,
|
||||||
|
this.padding = 5,
|
||||||
|
this.leftPadding = 15,
|
||||||
|
this.actions = const [],
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding:
|
||||||
|
EdgeInsets.only(left: leftPadding, top: padding, bottom: padding),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(text, style: TextStyle(color: Colors.grey))),
|
||||||
|
...actions,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget oldBookAvatar({
|
||||||
|
String text = '旧\n藏\n书',
|
||||||
|
width = double.infinity,
|
||||||
|
height = double.infinity,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: Colors.greenAccent,
|
||||||
|
child: Text(text),
|
||||||
|
);
|
||||||
|
}
|
106
pubspec.yaml
Normal file
106
pubspec.yaml
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
name: weiman
|
||||||
|
description: 微漫App
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
version: 1.1.0+2007
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.3.0 <3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
dio: any
|
||||||
|
dio_http_cache: any
|
||||||
|
cupertino_icons: any
|
||||||
|
async: any
|
||||||
|
http: any
|
||||||
|
encrypt: any
|
||||||
|
html: any
|
||||||
|
shared_preferences: any
|
||||||
|
random_string: any
|
||||||
|
filesize: any
|
||||||
|
oktoast: any
|
||||||
|
path_provider: any
|
||||||
|
draggable_container: any
|
||||||
|
sticky_headers: any
|
||||||
|
flutter_sticky_header: any
|
||||||
|
extended_nested_scroll_view: any
|
||||||
|
dynamic_theme: any
|
||||||
|
package_info: any
|
||||||
|
url_launcher: any
|
||||||
|
font_awesome_flutter: any
|
||||||
|
loadmore: any
|
||||||
|
pull_to_refresh_notification: any
|
||||||
|
http_client_helper: any
|
||||||
|
extended_image: any
|
||||||
|
screenshot: any
|
||||||
|
focus_widget: any
|
||||||
|
provider: any
|
||||||
|
loading_more_list: any
|
||||||
|
flutter_slidable: any
|
||||||
|
|
||||||
|
firebase_core: any
|
||||||
|
firebase_analytics: any
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
floor_generator: any
|
||||||
|
build_runner: any
|
||||||
|
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter.
|
||||||
|
flutter:
|
||||||
|
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- assets/logo.png
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||||
|
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/custom-fonts/#from-packages
|
Loading…
x
Reference in New Issue
Block a user