1
0
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:
github-actions 2020-08-09 00:35:35 +00:00
commit bd98e57779
30 changed files with 3943 additions and 0 deletions

21
LICENSE Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

246
lib/activities/book.dart Normal file
View 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
View 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,
),
),
],
),
);
}
}

View 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
View 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
View 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),
);
}
}

View 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,
),
);
}
}

View 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);
}
}

View 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;
}

View 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,
),
);
}
}

View 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
View 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
View 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
View 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());
}
}

View 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
View 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
View 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
View 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
View 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,
),
);
}
}

View 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,
);
}
}

View 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
View 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 -10 > 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
View 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),
),
],
),
),
);
}
}

View 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
View 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 QuickBookdelete : ${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);
}
},
),
],
);
}
}

View 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
View 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
View 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