1
0
mirror of https://github.com/nrop19/weiman_app.git synced 2025-08-02 23:05:48 +08:00
This commit is contained in:
nrop19 2020-01-04 04:29:47 +08:00
parent 87b0d9897b
commit 582d231063
23 changed files with 2844 additions and 2 deletions

73
.gitignore vendored Normal file
View File

@ -0,0 +1,73 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.packages
.pub-cache/
.pub/
/build/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

View File

@ -1,2 +1,11 @@
# weiman_app
微漫
# weiman v1.0.4
### 微漫脱敏后的开源代码
#### 不解答任何代码上的问题
#### App的问题请到 [Telegram群讨论](https://t.me/boring_programer)
- 删除了android端文件夹涉及到apk签名等敏感文件
- 删除了ios端文件夹
- 删除了lib/classes/http.dart文件里的网站域名和爬虫逻辑保护被爬网站的同时防止被爬网站加大防爬难度。

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

331
lib/activities/book.dart Normal file
View File

@ -0,0 +1,331 @@
part of '../main.dart';
class ActivityBook extends StatefulWidget {
final Book book;
final String heroTag;
ActivityBook({@required this.book, @required this.heroTag});
@override
BookState createState() => BookState();
}
class BookState extends State<ActivityBook> {
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
GlobalKey<NestedScrollViewState> _key = GlobalKey<NestedScrollViewState>();
bool _reverse = false;
bool isFavorite = false;
bool isLoading = true, isSuccess = false;
Book book;
List<Chapter> chapters = [];
@override
void initState() {
super.initState();
isFavorite = widget.book.isFavorite();
SchedulerBinding.instance.addPostFrameCallback((_) {
_refresh.currentState.show();
});
}
Future<bool> loadBook() async {
setState(() {
isLoading = true;
isSuccess = false;
});
try {
book = await UserAgentClient.instance
.getBook(aid: widget.book.aid)
.timeout(Duration(seconds: 5));
book.history = Data.getHistories()[book.aid]?.history;
chapters
..clear()
..addAll(book.chapters);
if (_reverse) chapters = chapters.reversed.toList();
///
if (isFavorite) Data.addFavorite(book);
_scrollToRead();
isSuccess = true;
} catch (e) {
isSuccess = false;
return false;
}
isLoading = false;
print('刷新 $book');
setState(() {});
return true;
}
void _scrollToRead() {
if (book.history != null) {
final history = book.chapters
.firstWhere((chapter) => chapter.cid == book.history.cid);
SchedulerBinding.instance.addPostFrameCallback((_) {
_key.currentState.currentInnerPosition.animateTo(
WidgetChapter.height * chapters.indexOf(history).toDouble(),
duration: Duration(milliseconds: 500),
curve: Curves.linear);
});
}
}
_openChapter(Chapter chapter) {
setState(() {
book.history = History(cid: chapter.cid, cname: chapter.cname, time: 0);
openChapter(context, book, chapter);
});
}
favoriteBook() {
widget.book.favorite();
isFavorite = !isFavorite;
setState(() {});
}
void _sort() {
setState(() {
_reverse = !_reverse;
chapters = chapters.reversed.toList();
_scrollToRead();
});
}
List<Widget> _headerBuilder(BuildContext context, bool innerBoxIsScrolled) {
Color color = isFavorite ? Colors.red : Colors.white;
IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border;
final book = this.book ?? widget.book;
return <Widget>[
SliverAppBar(
floating: true,
pinned: true,
snap: false,
title: Text(widget.book.name),
expandedHeight: 200,
actions: <Widget>[
IconButton(
onPressed: _sort,
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: Image.network(
widget.book.avatar,
),
),
),
Expanded(
child: Container(
padding: EdgeInsets.only(top: 50, right: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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),
),
],
),
)),
],
),
),
),
)
];
}
List<Widget> chapterWidgets() {
final book = this.book ?? widget.book;
List<Widget> list = [];
chapters.forEach((chapter) {
final isRead = chapter.cid == book.history?.cid;
list.add(WidgetChapter(
chapter: chapter,
onTap: _openChapter,
read: isRead,
));
});
return list;
}
Widget buildChapter(BuildContext context, int index) {
final book = this.book ?? widget.book;
final chapter = chapters[index];
final isRead = chapter.cid == book.history?.cid;
return WidgetChapter(
chapter: chapter,
onTap: _openChapter,
read: isRead,
);
}
@override
Widget build(BuildContext context) {
Color color = isFavorite ? Colors.red : Colors.white;
IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border;
final book = this.book ?? widget.book;
return Scaffold(
body: PullToRefreshNotification(
key: _refresh,
onRefresh: loadBook,
maxDragOffset: kToolbarHeight * 2,
child: NestedScrollView(
key: _key,
headerSliverBuilder: (_, __) => [],
physics: AlwaysScrollableClampingScrollPhysics(),
body: CustomScrollView(
physics: AlwaysScrollableClampingScrollPhysics(),
slivers: [
SliverAppBar(
floating: true,
pinned: false,
title: Text(widget.book.name),
expandedHeight: 200,
actions: <Widget>[
IconButton(
onPressed: _sort,
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: Image.network(
widget.book.avatar,
),
),
),
Expanded(
child: Container(
padding: EdgeInsets.only(top: 50, right: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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: kToolbarHeight * 2),
)),
NestedScrollViewInnerScrollPositionKeyWidget(
Key('0'),
SliverList(
delegate: SliverChildBuilderDelegate(
buildChapter,
childCount: book.chapters.length,
),
),
),
],
),
),
),
);
}
Widget build1(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
var pinnedHeaderHeight =
//statusBar height
statusBarHeight +
//pinned SliverAppBar height in header
kToolbarHeight;
return Scaffold(
body: NestedScrollViewRefreshIndicator(
key: _refresh,
onRefresh: loadBook,
child: NestedScrollView(
key: _key,
pinnedHeaderSliverHeightBuilder: () => pinnedHeaderHeight,
headerSliverBuilder: _headerBuilder,
body: LayoutBuilder(
builder: (_, __) {
if (isLoading)
return Container();
else if (isSuccess) {
return ListView(
children: ListTile.divideTiles(
context: context,
color: Colors.grey,
tiles: chapterWidgets())
.toList());
}
return Container(
constraints: BoxConstraints.expand(),
alignment: Alignment.center,
child: Center(
child: Text(
'读取失败,下拉刷新\n如果多次失败,请检查网络',
textAlign: TextAlign.center,
),
),
);
},
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_key.currentState.currentInnerPosition.animateTo(0,
duration: Duration(milliseconds: 100), curve: Curves.linear);
},
child: Icon(FontAwesomeIcons.angleDoubleUp),
),
);
}
}

323
lib/activities/chapter.dart Normal file
View File

@ -0,0 +1,323 @@
part of '../main.dart';
enum LoadState {
Loading,
Finish,
Timeout,
}
class LoadMoreListSource extends LoadingMoreBase<int> {
@override
Future<bool> loadData([bool isloadMoreAction = false]) {
return Future.delayed(Duration(seconds: 1), () {
for (var i = 0; i < 10; i++) {
this.add(0);
}
return true;
});
}
}
class ActivityChapter extends StatefulWidget {
final Book book;
final Chapter chapter;
ActivityChapter(this.book, this.chapter);
@override
ChapterState createState() => ChapterState();
}
class ChapterState extends State<ActivityChapter> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
PageController _pageController;
int showIndex = 0;
bool hasNextImage = true;
@override
void initState() {
super.initState();
_pageController = PageController(
keepPage: false,
initialPage: widget.book.chapters.indexOf(widget.chapter));
}
@override
void dispose() {
_pageController?.dispose();
super.dispose();
}
@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,
itemBuilder: (ctx, index) {
return ChapterContentView(
actions: [
IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openEndDrawer();
},
),
],
book: widget.book,
chapter: widget.book.chapters[index],
);
}),
// floatingActionButton: FloatingActionButton(
// child: Text('下一章'),
// onPressed: () {
// if (hasNextChapter)
// return openChapter(widget.book.chapters[chapterIndex + 1]);
// Fluttertoast.showToast(msg: '已经是最后一章了');
// },
// ),
);
}
}
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();
_controller = ScrollController();
updateRead();
SchedulerBinding.instance.addPostFrameCallback((_) {
_controller.jumpTo(WidgetChapter.height * read);
});
}
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
void dispose() {
_controller.dispose();
super.dispose();
}
@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 = [];
int chapterIndex = -1;
bool hasNextChapter = false;
@override
initState() {
super.initState();
chapterIndex = widget.book.chapters.indexOf(widget.chapter);
hasNextChapter = widget.book.chapters.last != widget.chapter;
Data.addHistory(widget.book, widget.chapter);
SchedulerBinding.instance.addPostFrameCallback((_) => _refresh?.currentState
?.show(notificationDragOffset: kToolbarHeight * 2));
}
Future<bool> fetchImages() async {
print('fetchImages');
setState(() {});
images.clear();
try {
images.addAll(await UserAgentClient.instance
.getImages(aid: widget.book.aid, cid: widget.chapter.cid)
.timeout(const Duration(seconds: 5)));
if (images.length < 5) {
// print('图片 前:' + images.toString());
var list = await checkImage(images.last);
images.addAll(list);
}
} catch (e) {
print('错误');
return false;
// throw(e);
}
// print('所有图片:' + images.toString());
setState(() {});
return true;
}
@override
Widget build(BuildContext context) {
return PullToRefreshNotification(
key: _refresh,
onRefresh: fetchImages,
maxDragOffset: kToolbarHeight * 2,
child: CustomScrollView(
physics: AlwaysScrollableClampingScrollPhysics(),
slivers: [
SliverAppBar(
title: Text(widget.chapter.cname),
pinned: false,
floating: true,
actions: widget.actions,
),
PullToRefreshContainer((info) => SliverPullToRefreshHeader(
info: info,
onTap: () => _refresh.currentState
.show(notificationDragOffset: kToolbarHeight * 2),
)),
SliverList(
delegate: SliverChildBuilderDelegate(
(ctx, i) => Image.network(images[i]),
childCount: images.length),
),
],
),
);
}
}
Future<dynamic> checkImage(String last) async {
final response = new ReceivePort();
await Isolate.spawn(_checkImage, response.sendPort);
final sendPort = await response.first as SendPort;
//ReceivePort
final answer = new ReceivePort();
//
sendPort.send([answer.sendPort, last]);
return answer.first;
}
void _checkImage(SendPort initialReplyTo) {
UserAgentClient.instance = UserAgentClient('chrome');
final port = new ReceivePort();
initialReplyTo.send(port.sendPort);
port.listen((message) async {
//
final send = message[0] as SendPort;
final last = message[1] as String;
//
final uri = Uri.parse(last);
// print({'scheme': uri.scheme, 'host': uri.host, 'path': uri.path});
final a = uri.scheme + '://' + uri.host;
final b = uri.pathSegments.take(uri.pathSegments.length - 1).join('/');
// print({'a': a, 'b': b});
//
final file = uri.pathSegments.last.split('.');
final fileName = file[0];
//
final fileFormat = file[1];
final list = <String>[];
int plus = 1;
//print('最后的图片:' + last);
while (true) {
final String file1 =
getFileName(name: fileName, divider: '_', plus: plus),
file2 = getFileName(name: fileName, divider: '_', plus: plus + 1);
var url1 = '$a/$b/$file1.$fileFormat', url2 = '$a/$b/$file2.$fileFormat';
// print('正在测试:\n' + url1 + '\n' + url2);
final res = await Future.wait([
UserAgentClient.instance.head(url1),
UserAgentClient.instance.head(url2)
]);
if (res[0].statusCode != 200) break;
list.add(url1);
if (res[1].statusCode != 200) {
break;
}
list.add(url2);
plus += 2;
}
// print('最后的图片数量: ' + number.toString());
send.send(list);
});
}
String getFileName(
{@required String name, @required String divider, @required int plus}) {
List<String> data = name.split(divider), newName = [];
for (var i = 0; i < data.length; i++) {
try {
int number = int.parse(data[i]) + plus;
newName.add(number.toString());
} catch (e) {
newName.add(data[i]);
}
}
return newName.join(divider);
}

View File

@ -0,0 +1,135 @@
part of '../main.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;
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 = json.decode(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: () {
Fluttertoast.showToast(
msg: '已经复制',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIos: 1,
);
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);
}
},
),
],
),
),
),
]),
);
}
}

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

@ -0,0 +1,258 @@
part of '../main.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();
static final weekTime = 7 * 24 * 3600000;
bool showFavorite = true;
@override
void initState() {
super.initState();
analytics.setCurrentScreen(screenName: '/activity_home');
///
SchedulerBinding.instance.addPostFrameCallback((_) async {
_FavoriteList.getBooks();
await _FavoriteList.checkNews();
final updated = _FavoriteList.hasNews.values
.where((int updatedChapters) => updatedChapters > 0)
.length;
if (updated > 0)
Fluttertoast.showToast(
msg: '$updated 本藏书有更新',
gravity: ToastGravity.CENTER,
backgroundColor: Colors.black.withOpacity(0.5),
);
});
}
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: (_) => ActivityRecommend(),
));
}
bool isEdit = false;
void _draggableModeChanged(bool mode) {
print('mode changed $mode');
isEdit = mode;
setState(() {});
}
@override
Widget build(BuildContext context) {
var media = MediaQuery.of(context);
var width = media.size.width;
width = width * .8;
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: () {
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: (_, __) {
if (showFavorite) {
return FavoriteList();
} else {
return Histories();
}
},
),
),
body: Container(
alignment: Alignment.center,
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(
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(),
),
),
],
),
Quick(
key: _quickState,
width: width,
draggableModeChanged: _draggableModeChanged,
),
Container(
margin: EdgeInsets.only(bottom: 10),
child: Text(
'在 level-plus.net 论坛首发',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[500]),
),
),
GestureDetector(
onTap: () async {
if (await canLaunch('tg://resolve?domain=weiman_app'))
launch('tg://resolve?domain=weiman_app');
else
launch('https://t.me/weiman_app');
},
child: Text(
'Telegram广播频道',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.blue[200],
decoration: TextDecoration.underline,
),
),
),
Visibility(
visible: isDevMode,
child: FlatButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => ActivityTest()));
},
child: Text('测试界面'),
),
),
Visibility(
visible: isDevMode,
child: FlatButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => ActivityCheckData()));
},
child: Text('操作 收藏列表数据'),
),
),
],
),
),
);
}
}
Iterable<Widget> favoriteTiles(context, Iterable<Book> books,
{void Function(Book book) onTap}) {
return books.map((book) => ListTile(
onTap: () {
onTap(book);
},
title: Text(book.name),
leading: Image.network(book.avatar),
subtitle: Text(
'作者:' + book.author,
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
));
}

View File

@ -0,0 +1,83 @@
part of '../main.dart';
class ActivityRecommend extends StatefulWidget {
@override
_ActivityRecommend createState() => _ActivityRecommend();
}
class _ActivityRecommend extends State<ActivityRecommend> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('月排行榜'),
),
body: BookList(),
);
}
}
class BookList extends StatefulWidget {
const BookList({Key key}) : super(key: key);
@override
_BookList createState() => _BookList();
}
class _BookList extends State<BookList> {
final GlobalKey<RefreshIndicatorState> _refresh = GlobalKey();
final List<Book> books = [];
bool loadFail = false;
@override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
_refresh.currentState.show();
});
}
Future<void> loadBooks() async {
loadFail = false;
try {
final books = await UserAgentClient.instance
.getMonthList()
.timeout(Duration(seconds: 5));
this.books
..clear()
..addAll(books);
} catch (e) {
loadFail = true;
}
setState(() {});
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
key: _refresh,
onRefresh: loadBooks,
child: loadFail
? CustomScrollView(
slivers: [
SliverFillRemaining(
child: Center(child: Text('读取失败,下拉刷新')),
)
],
)
: ListView(
children: ListTile.divideTiles(
context: context,
tiles: books.map((book) => WidgetBook(
book,
subtitle: book.author,
))).toList(),
),
);
}
}

130
lib/activities/search.dart Normal file
View File

@ -0,0 +1,130 @@
part of '../main.dart';
class ActivitySearch extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Search();
}
}
class Search extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return SearchState();
}
}
enum _SearchState {
None,
Searching,
Done,
Error,
}
class SearchState extends State<Search> {
Future<List<Book>> search;
TextEditingController _controller = TextEditingController();
CancelableOperation _searcher;
_SearchState _state = _SearchState.None;
final List<Book> _books = [];
void startSearch() {
print('搜索漫画: ' + _controller.text);
if (_searcher != null) _searcher.cancel();
_books.clear();
setState(() {
_state = _SearchState.Searching;
});
_searcher = CancelableOperation.fromFuture(
UserAgentClient.instance.searchBook(_controller.text))
.then((books) {
setState(() {
print('搜索完成: ' + books.length.toString());
_books.addAll(books);
_state = _SearchState.Done;
});
});
}
@override
Widget build(BuildContext context) {
search = null;
return Scaffold(
appBar: AppBar(
title: Text('搜索漫画'),
),
body: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (RawKeyEvent event) {
if (event.runtimeType == RawKeyUpEvent &&
event.logicalKey.debugName.toLowerCase() == 'enter') {
if (_controller.text.isEmpty) return;
startSearch();
}
},
child: TextField(
decoration: InputDecoration(
hintText: '搜索书名', prefixIcon: Icon(Icons.search)),
textAlign: TextAlign.left,
controller: _controller,
autofocus: true,
textInputAction: TextInputAction.search,
onSubmitted: (String name) {
print('onSubmitted');
startSearch();
},
keyboardType: TextInputType.text,
onEditingComplete: () {
print('onEditingComplete');
startSearch();
},
),
),
),
],
),
Expanded(
flex: 1,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
switch (_state) {
case _SearchState.Searching:
return Center(child: CircularProgressIndicator());
case _SearchState.None:
return Center(
child: Icon(
Icons.search,
color: Colors.grey,
));
default:
if (_books.length == 0)
return Center(
child: Text(
'一本也找不到',
style: TextStyle(color: Colors.blueGrey),
));
List<Widget> list = _books
.map((book) => WidgetBook(
book,
subtitle: book.author,
))
.toList();
return ListView(
children:
ListTile.divideTiles(context: context, tiles: list)
.toList(),
);
}
},
),
),
],
),
);
}
}

46
lib/activities/test.dart Normal file
View File

@ -0,0 +1,46 @@
part of '../main.dart';
class ActivityTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('asd'),
),
body: Column(
children: <Widget>[
FlatButton(
onPressed: save,
child: Text('保存'),
),
FlatButton(
onPressed: read,
child: Text('读取'),
),
FlatButton(
onPressed: clear,
child: Text('清空数据'),
),
],
),
);
}
void save() {
Data.addFavorite(Book(
aid: '123',
name: 'name',
avatar: 'avatar',
description: '',
author: ''));
}
void read() {
var books = Data.getFavorites();
print(jsonEncode(books));
}
void clear() {
Data.clear();
}
}

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

@ -0,0 +1,110 @@
part of '../main.dart';
class Book {
final String aid; // ID
final String name; //
final String avatar; //
final String author; //
final String description; //
final List<Chapter> chapters;
final int chapterCount;
History history;
Book({
@required this.name,
@required this.aid,
@required this.avatar,
this.author,
this.description,
this.chapters: const [],
this.chapterCount: 0,
});
@override
String toString() {
return jsonEncode(toJson());
}
bool isFavorite() {
var books = Data.getFavorites();
return books.containsKey(aid);
}
favorite() {
if (isFavorite())
Data.removeFavorite(this);
else
Data.addFavorite(this);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {
'aid': aid,
'name': name,
'avatar': avatar,
'author': author,
'chapterCount': chapterCount,
};
if (history != null) data['history'] = history.toJson();
return data;
}
static Book fromJson(Map<String, dynamic> json) {
final book = Book(
aid: json['aid'],
name: json['name'],
avatar: json['avatar'],
author: json['author'],
description: json['description'],
chapterCount: json['chapterCount'] ?? 0,
);
if (json.containsKey('history'))
book.history = History.fromJson(json['history']);
return book;
}
}
class Chapter {
final String cid; // cid
final String cname; //
final String avatar; //
Chapter({@required this.cid, @required this.cname, @required this.avatar});
@override
String toString() {
return jsonEncode({cid: cid, cname: cname, avatar: avatar});
}
}
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,
);
}
}

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

@ -0,0 +1,151 @@
part of '../main.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, jsonEncode(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());
}
}

47
lib/classes/http.dart Normal file
View File

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

157
lib/main.dart Normal file
View File

@ -0,0 +1,157 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:math' as math;
import 'package:async/async.dart';
import 'package:draggable_container/draggable_container.dart';
import 'package:dynamic_theme/dynamic_theme.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart';
import 'package:flutter/material.dart' hide NestedScrollView;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:html/parser.dart' as html;
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:package_info/package_info.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'
hide CircularProgressIndicator;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
part './activities/book.dart';
part './activities/chapter.dart';
part './activities/checkData.dart';
part './activities/home.dart';
part './activities/recommend.dart';
part './activities/search.dart';
part './activities/test.dart';
part './classes/book.dart';
part './classes/data.dart';
part './classes/http.dart';
part './widgets/book.dart';
part './widgets/favorites.dart';
part './widgets/histories.dart';
part './widgets/pullToRefreshHeader.dart';
part './widgets/quick.dart';
part './widgets/sliverExpandableGroup.dart';
part './widgets/utils.dart';
part 'utils.dart';
FirebaseAnalytics analytics;
FirebaseAnalyticsObserver observer;
const bool isDevMode = !bool.fromEnvironment('dart.vm.product');
void main() async {
WidgetsFlutterBinding.ensureInitialized();
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();
UserAgentClient.init(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36');
runApp(Main(packageInfo: packageInfo));
// runApp(MaterialApp(
// title: '微漫',
// theme: ThemeData.light(),
// darkTheme: ThemeData.dark(),
// themeMode: ThemeMode.system,
// debugShowCheckedModeBanner: false,
// navigatorObservers: [observer],
// home: ActivityHome(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 {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return DynamicTheme(
defaultBrightness: ThemeMode.system == ThemeMode.light
? Brightness.light
: Brightness.dark,
data: (brightness) => new ThemeData(
brightness: brightness,
),
themedWidgetBuilder: (context, theme) {
return new MaterialApp(
title: 'Flutter Demo',
theme: theme,
home: ActivityHome(widget.packageInfo),
);
});
// return MaterialApp(
// title: '微漫',
// theme: ThemeData(
// brightness: Brightness.light,
// ),
// darkTheme: ThemeData(
// brightness: Brightness.dark,
// ),
// themeMode: ThemeMode.system,
// debugShowCheckedModeBanner: false,
// navigatorObservers: [observer],
// home: ActivityHome(widget.packageInfo),
// );
}
@override
void didChangePlatformBrightness() {
print('改变亮度');
setState(() {});
}
}

24
lib/utils.dart Normal file
View File

@ -0,0 +1,24 @@
part of 'main.dart';
final weekTime = Duration.millisecondsPerDay * 7;
void openBook(BuildContext context, Book book, String heroTag) {
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),
),
);
}

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

@ -0,0 +1,187 @@
part of '../main.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: Image.network(
book.avatar,
height: 200,
fit: BoxFit.scaleDown,
)),
trailing: Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
color: isLiked ? Colors.red : Colors.grey,
size: 12,
),
onTap: () {
if (onTap != null) onTap(book);
openBook(context, book, 'bookAvatar${book.aid}');
},
);
}
}
class WidgetChapter extends StatelessWidget {
static final double height = 56;
final Chapter chapter;
final Function(Chapter) onTap;
final bool read;
WidgetChapter({
Key key,
this.chapter,
this.onTap,
this.read,
}) : 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: Image.network(
chapter.avatar,
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.network(
book.avatar,
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 UserAgentClient.instance
.getBook(aid: 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.network(widget.book.avatar),
),
dense: true,
isThreeLine: true,
title: Text(widget.book.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
);
}
}

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

@ -0,0 +1,182 @@
part of '../main.dart';
class FavoriteList extends StatefulWidget {
@override
_FavoriteList createState() => _FavoriteList();
}
class _FavoriteList extends State<FavoriteList> {
static final Map<String, int> hasNews = {};
static final List<Book> all = [], //
inWeek = [], // 7
other = []; //
static bool showTip = false;
static 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));
static void getBooks() {
all.clear();
inWeek.clear();
other.clear();
all.addAll(Data.getFavorites().values);
if (all.isNotEmpty) {
final now = DateTime.now().millisecondsSinceEpoch;
all.forEach((book) {
if (book.history != null && (now - book.history.time) < weekTime) {
inWeek.add(book);
} else {
other.add(book);
}
});
}
}
@override
void initState() {
super.initState();
getBooks();
if (all.isNotEmpty) {
if (showTip == false) {
showTip = true;
Fluttertoast.showToast(
msg: '下拉列表可以检查漫画更新',
gravity: ToastGravity.CENTER,
backgroundColor: Colors.black.withOpacity(0.5),
);
}
}
}
void _openBook(book) {
openBook(context, book, 'fb ${book.aid}');
}
static Future<void> checkNews() async {
hasNews.clear();
Book currentBook, newBook;
int different;
for (var i = 0; i < all.length; i++) {
currentBook = all[i];
try {
newBook = await UserAgentClient.instance
.getBook(aid: currentBook.aid)
.timeout(Duration(seconds: 2));
different = newBook.chapterCount - currentBook.chapterCount;
hasNews[currentBook.aid] = different;
} catch (e) {
hasNews[currentBook.aid] = -1;
}
}
}
Widget bookBuilder(Book book) {
TextSpan state;
if (hasNews.isEmpty) {
state = unCheck;
} else {
if (hasNews.containsKey(book.aid)) {
if (hasNews[book.aid] > 0) {
state = TextSpan(
text: '${hasNews[book.aid]} 章更新',
style: TextStyle(color: Colors.green));
} else if (hasNews[book.aid] == -1) {
state = loadFailTextSpan;
} else if (hasNews[book.aid] == 0) {
state = noUpdate;
}
} else {
state = waitToCheck;
}
}
return FBookItem(
book: book,
subtitle: state,
onTap: _openBook,
);
}
@override
Widget build(BuildContext context) {
List<Book> inWeekUpdated = [],
inWeekUnUpdated = [],
otherUpdated = [],
otherUnUpdated = [];
inWeek.forEach((book) {
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0)
inWeekUpdated.add(book);
else
inWeekUnUpdated.add(book);
});
other.forEach((book) {
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0)
otherUpdated.add(book);
else
otherUnUpdated.add(book);
});
return Drawer(
child: RefreshIndicator(
onRefresh: () async {
await checkNews();
setState(() {});
},
child: all.isEmpty
? Center(child: Text('没有收藏'))
: SafeArea(
child: CustomScrollView(
slivers: [
SliverExpandableGroup(
title: Text('7天内看过并且有更新的藏书(${inWeekUpdated.length})'),
expanded: true,
count: inWeekUpdated.length,
builder: (ctx, i) => bookBuilder(inWeekUpdated[i]),
),
SliverExpandableGroup(
title: Text('7天内看过的藏书(${inWeekUnUpdated.length})'),
count: inWeekUnUpdated.length,
builder: (ctx, i) => bookBuilder(inWeekUnUpdated[i]),
),
SliverExpandableGroup(
title: Text('有更新的藏书(${otherUpdated.length})'),
count: otherUpdated.length,
builder: (ctx, i) => bookBuilder(otherUpdated[i]),
),
SliverExpandableGroup(
title: Text('没有更新的藏书(${otherUnUpdated.length})'),
count: otherUnUpdated.length,
builder: (ctx, i) => bookBuilder(otherUnUpdated[i]),
),
],
)),
),
);
}
}
class FBookItem extends StatelessWidget {
final Book book;
final TextSpan subtitle;
final void Function(Book book) onTap;
const FBookItem({
Key key,
@required this.book,
@required this.subtitle,
@required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
onTap: () => onTap(book),
leading: Hero(tag: 'fb ${book.aid}', child: Image.network(book.avatar)),
title: Text(book.name, style: Theme.of(context).textTheme.body1),
subtitle: RichText(text: subtitle),
);
}
}

View File

@ -0,0 +1,97 @@
part of '../main.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();
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
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) => WidgetBook(
inWeek[i],
subtitle: inWeek[i].history.cname,
),
),
SliverExpandableGroup(
title: Text('更早的浏览历史 (${other.length})'),
actions: [
FlatButton(
child: Text('清空'),
onPressed: other.length == 0 ? null : () => clear(false),
),
],
count: other.length,
builder: (ctx, i) => WidgetBook(
other[i],
subtitle: other[i].history.cname,
),
),
],
),
);
}
}

View File

@ -0,0 +1,72 @@
part of '../main.dart';
class SliverPullToRefreshHeader extends StatelessWidget {
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("images/logo.png", height: 20),
padding: EdgeInsets.only(right: 5),
),
),
]);
if (info.mode == RefreshIndicatorMode.error) {
text.children.addAll([
TextSpan(
text: '读取失败\n当失败次数太多请检查网络情况\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,
),
),
),
);
}
}

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

@ -0,0 +1,215 @@
part of '../main.dart';
class QuickBook extends DraggableItem {
static const heroTag = 'quickBookAvatar';
Widget child;
final BuildContext context;
final Book book;
QuickBook({@required this.book, @required this.context}) {
child = GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (ctx) => ActivityBook(
book: book,
heroTag: '$heroTag ${book.aid}',
)));
},
child: Stack(
children: <Widget>[
Hero(
tag: '$heroTag ${book.aid}',
child: Image.network(book.avatar),
),
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,
),
),
)
],
),
);
}
checkUpdate() {
UserAgentClient.instance.getBook(aid: book.aid);
}
}
class Quick extends StatefulWidget {
final double width, height;
final Function(bool mode) draggableModeChanged;
const Quick(
{Key key, this.width, this.height, @required this.draggableModeChanged})
: super(key: key);
@override
QuickState createState() => QuickState();
}
class QuickState extends State<Quick> {
final int count = 8;
final List<DraggableItem> _draggableItems = [];
DraggableItem _addButton;
GlobalKey<DraggableContainerState> _key = GlobalKey();
final List<String> id = [];
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.network(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(book: book, context: context),
force: true);
}
},
),
);
}
int length() {
// print(_key.currentState.items);
// return 0;
return _key.currentState.items.where((item) => item is QuickBook).length;
}
@override
void initState() {
super.initState();
_draggableItems.addAll(Data.quickList().map((book) {
id.add(book.aid);
return QuickBook(book: book, context: context);
}));
if (_draggableItems.length < count) _draggableItems.add(_addButton);
for (var i = count - _draggableItems.length; i > 0; i--) {
_draggableItems.add(null);
}
}
@override
Widget build(BuildContext context) {
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),
),
),
Container(
width: widget.width,
child: DraggableContainer(
key: _key,
slotMargin: EdgeInsets.only(bottom: 8, left: 7, right: 7),
slotSize: Size(72, 100),
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);
}
},
)),
],
);
}
}

View File

@ -0,0 +1,91 @@
part of '../main.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,
);
}
}

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

@ -0,0 +1,30 @@
part of '../main.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,
],
),
);
}
}

91
pubspec.yaml Normal file
View File

@ -0,0 +1,91 @@
name: weiman
description: 微漫
# 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.0.4
environment:
sdk: ">=2.3.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
async: any
cupertino_icons: any
http: any
encrypt: any
html: any
shared_preferences: any
fluttertoast: any
draggable_container: any
flutter_sticky_header: any
extended_nested_scroll_view: any
dynamic_theme: any
package_info: any
url_launcher: any
font_awesome_flutter: any
loading_more_list: any
pull_to_refresh_notification: any
firebase_core: any
firebase_analytics: any
e2e: any
dev_dependencies:
flutter_test:
sdk: flutter
# 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:
- images/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