mirror of
https://github.com/nrop19/weiman_app.git
synced 2025-08-02 15:04:50 +08:00
Github Action自动发布
This commit is contained in:
commit
bd98e57779
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.1 [宣传页面](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);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
297
lib/activities/home.dart
Normal file
297
lib/activities/home.dart
Normal file
@ -0,0 +1,297 @@
|
||||
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/checkConnect.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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: Quick(
|
||||
key: _quickState,
|
||||
width: width,
|
||||
draggableModeChanged: _draggableModeChanged,
|
||||
),
|
||||
),
|
||||
CheckConnectWidget(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
launch('https://bbs.level-plus.net/');
|
||||
},
|
||||
child: Text(
|
||||
'魂+论坛首发',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.blue[200],
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (await canLaunch('tg://resolve?domain=weiman_app'))
|
||||
launch('tg://resolve?domain=weiman_app');
|
||||
else
|
||||
launch('https://t.me/weiman_app');
|
||||
},
|
||||
child: Text(
|
||||
'Telegram 广播频道',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.blue[200],
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Visibility(
|
||||
visible: isDevMode,
|
||||
child: FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (_) => ActivityCheckData()));
|
||||
},
|
||||
child: Text('操作 收藏列表数据'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
46
lib/activities/search/source.dart
Normal file
46
lib/activities/search/source.dart
Normal file
@ -0,0 +1,46 @@
|
||||
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;
|
||||
final list = await http.searchBook(search, page);
|
||||
if (list.isEmpty) {
|
||||
hasMore = false;
|
||||
} else if (list[0].aid == eachPageFirstBookId) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
eachPageFirstBookId = list[0].aid;
|
||||
hasMore = true;
|
||||
page++;
|
||||
this.addAll(list);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> refresh([bool notifyStateChanged = false]) {
|
||||
page = 1;
|
||||
hasMore = true;
|
||||
eachPageFirstBookId = null;
|
||||
clear();
|
||||
print('refresh $page $hasMore');
|
||||
return super.refresh(notifyStateChanged);
|
||||
}
|
||||
}
|
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(true);
|
||||
}
|
||||
|
||||
get search => sourceList.search;
|
||||
|
||||
set search(String value) {
|
||||
print('tab search $value');
|
||||
sourceList.search = value;
|
||||
sourceList.refresh(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return LoadingMoreList(
|
||||
ListConfig(
|
||||
sourceList: sourceList,
|
||||
itemBuilder: (_, item, index) => book(item),
|
||||
autoLoadMore: true,
|
||||
indicatorBuilder: indicatorBuilder,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget indicatorBuilder(context, IndicatorStatus status) {
|
||||
bool isSliver = false;
|
||||
Widget widget;
|
||||
switch (status) {
|
||||
case IndicatorStatus.none:
|
||||
widget = SizedBox();
|
||||
break;
|
||||
case IndicatorStatus.loadingMoreBusying:
|
||||
widget = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(right: 5.0),
|
||||
height: 15.0,
|
||||
width: 15.0,
|
||||
child: getIndicator(context),
|
||||
),
|
||||
Text("正在读取")
|
||||
],
|
||||
);
|
||||
widget = _setbackground(false, widget, 35.0);
|
||||
break;
|
||||
case IndicatorStatus.fullScreenBusying:
|
||||
widget = widget = _setbackground(
|
||||
false,
|
||||
Text(
|
||||
'正在读取',
|
||||
),
|
||||
35.0);
|
||||
if (isSliver) {
|
||||
widget = SliverFillRemaining(
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case IndicatorStatus.error:
|
||||
widget = _setbackground(
|
||||
false,
|
||||
Text(
|
||||
'网络错误\n点击重试',
|
||||
),
|
||||
35.0);
|
||||
|
||||
widget = GestureDetector(
|
||||
onTap: () {
|
||||
sourceList.errorRefresh();
|
||||
},
|
||||
child: widget,
|
||||
);
|
||||
break;
|
||||
case IndicatorStatus.fullScreenError:
|
||||
widget = Text(
|
||||
'读取失败,如果失败的次数太多可能需要用梯子',
|
||||
);
|
||||
widget = _setbackground(true, widget, double.infinity);
|
||||
widget = GestureDetector(
|
||||
onTap: () {
|
||||
sourceList.errorRefresh();
|
||||
},
|
||||
child: widget,
|
||||
);
|
||||
if (isSliver) {
|
||||
widget = SliverFillRemaining(
|
||||
child: widget,
|
||||
);
|
||||
} else {
|
||||
widget = CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverFillRemaining(
|
||||
child: widget,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
break;
|
||||
case IndicatorStatus.noMoreLoad:
|
||||
widget = Text("已经显示全部搜索结果");
|
||||
widget = _setbackground(false, widget, 35.0);
|
||||
break;
|
||||
case IndicatorStatus.empty:
|
||||
widget = Text(
|
||||
sourceList.search.isEmpty ? '请输入搜索内容' : '搜索不到任何内容',
|
||||
);
|
||||
widget = _setbackground(true, widget, double.infinity);
|
||||
if (isSliver) {
|
||||
widget = SliverToBoxAdapter(
|
||||
child: widget,
|
||||
);
|
||||
} else {
|
||||
widget = CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverFillRemaining(
|
||||
child: widget,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
Widget _setbackground(bool full, Widget widget, double height) {
|
||||
widget = Container(
|
||||
width: double.infinity,
|
||||
height: kToolbarHeight,
|
||||
child: widget,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
return widget;
|
||||
}
|
||||
|
||||
Widget getIndicator(BuildContext context) {
|
||||
return CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
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;
|
||||
}
|
||||
}
|
101
lib/main.dart
Normal file
101
lib/main.dart
Normal file
@ -0,0 +1,101 @@
|
||||
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(),
|
||||
lazy: false,
|
||||
),
|
||||
ChangeNotifierProvider<FavoriteData>(
|
||||
create: (_) => FavoriteData(), lazy: false),
|
||||
],
|
||||
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 {
|
||||
@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),
|
||||
debugShowCheckedModeBanner: isDevMode,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
138
lib/widgets/checkConnect.dart
Normal file
138
lib/widgets/checkConnect.dart
Normal file
@ -0,0 +1,138 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_http_cache/dio_http_cache.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../crawler/http18Comic.dart';
|
||||
|
||||
class CheckConnectWidget extends StatefulWidget {
|
||||
@override
|
||||
_CheckConnectWidget createState() => _CheckConnectWidget();
|
||||
}
|
||||
|
||||
class _CheckConnectWidget extends State<CheckConnectWidget> {
|
||||
LoadState state = LoadState.loading;
|
||||
String error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
check();
|
||||
}
|
||||
|
||||
Future<void> check() async {
|
||||
setState(() {
|
||||
state = LoadState.loading;
|
||||
});
|
||||
try {
|
||||
final res = await Http18Comic.instance.dio.head(
|
||||
'/',
|
||||
options: buildCacheOptions(
|
||||
Duration(seconds: 1),
|
||||
forceRefresh: true,
|
||||
),
|
||||
);
|
||||
assert(res.statusCode == 200);
|
||||
setState(() {
|
||||
state = LoadState.completed;
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.runtimeType == DioError) {
|
||||
final DioError error = e as DioError;
|
||||
switch (error.type) {
|
||||
case DioErrorType.CONNECT_TIMEOUT:
|
||||
case DioErrorType.RECEIVE_TIMEOUT:
|
||||
case DioErrorType.SEND_TIMEOUT:
|
||||
this.error = '连接超时';
|
||||
break;
|
||||
default:
|
||||
this.error = error.error.toString();
|
||||
}
|
||||
} else {
|
||||
this.error = e.toString();
|
||||
}
|
||||
setState(() {
|
||||
state = LoadState.failed;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void showError() async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
title: Text('错误内容'),
|
||||
content: Text(error.toString()),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('再次尝试'),
|
||||
onPressed: () {
|
||||
check();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget row;
|
||||
switch (state) {
|
||||
case LoadState.loading:
|
||||
row = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('正在尝试连接漫画网站'),
|
||||
],
|
||||
);
|
||||
break;
|
||||
case LoadState.failed:
|
||||
row = GestureDetector(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Icon(Icons.error, color: Colors.red),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('连接不上漫画网站,点击查看错误'),
|
||||
],
|
||||
),
|
||||
onTap: showError,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
row = GestureDetector(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Icon(Icons.check_circle, color: Colors.green),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('成功连接到漫画网站,点击重新测试'),
|
||||
],
|
||||
),
|
||||
onTap: check,
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 10, bottom: 15),
|
||||
child: row,
|
||||
);
|
||||
}
|
||||
}
|
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
251
lib/widgets/quick.dart
Normal file
251
lib/widgets/quick.dart
Normal file
@ -0,0 +1,251 @@
|
||||
import 'package:draggable_container/draggable_container.dart';
|
||||
import 'package:extended_image/extended_image.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 {
|
||||
print('添加漫画到快速导航');
|
||||
final books = Data.getFavorites();
|
||||
final list = books.values
|
||||
.where((book) => !id.contains(book.aid))
|
||||
.map((book) => ListTile(
|
||||
title: Text(book.name),
|
||||
leading: ExtendedImage(
|
||||
image: NetworkImageSSL(book.http, book.avatar),
|
||||
fit: BoxFit.cover,
|
||||
width: 40,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, book);
|
||||
},
|
||||
));
|
||||
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) {
|
||||
print('quick build');
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 8, bottom: 4, left: 8),
|
||||
width: widget.width,
|
||||
child: Text(
|
||||
'快速导航(长按编辑)',
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
),
|
||||
DraggableContainer(
|
||||
key: _key,
|
||||
slotMargin: EdgeInsets.only(bottom: 8, left: 6, right: 6),
|
||||
slotSize: Size(width, height),
|
||||
slotDecoration: BoxDecoration(color: Colors.grey.withOpacity(0.3)),
|
||||
dragDecoration: BoxDecoration(
|
||||
boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]),
|
||||
items: _draggableItems,
|
||||
onDraggableModeChanged: widget.draggableModeChanged,
|
||||
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.1+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