1
0
mirror of https://github.com/nrop19/weiman_app.git synced 2025-08-02 06:52:36 +08:00

Compare commits

...

No commits in common. "v1.0.8" and "master" have entirely different histories.

60 changed files with 4574 additions and 1562 deletions

73
.gitignore vendored
View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,225 +0,0 @@
part of '../main.dart';
class ActivityBook extends StatefulWidget {
final Book book;
final String heroTag;
ActivityBook({@required this.book, @required this.heroTag});
@override
BookState createState() => BookState();
}
class BookState extends State<ActivityBook> {
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
ScrollController _scrollController;
bool _reverse = false;
bool isFavorite = false;
bool isLoading = true, isSuccess = false;
Book book;
List<Chapter> chapters = [];
@override
void initState() {
super.initState();
isFavorite = widget.book.isFavorite();
SchedulerBinding.instance.addPostFrameCallback((_) {
_refresh.currentState
.show(notificationDragOffset: SliverPullToRefreshHeader.height);
});
_scrollController = ScrollController();
}
@override
dispose() {
_scrollController.dispose();
super.dispose();
}
Future<bool> loadBook() async {
setState(() {
isLoading = true;
isSuccess = false;
});
try {
book = await UserAgentClient.instance
.getBook(aid: widget.book.aid)
.timeout(Duration(seconds: 5));
book.history = Data.getHistories()[book.aid]?.history;
chapters
..clear()
..addAll(book.chapters);
if (_reverse) chapters = chapters.reversed.toList();
///
if (isFavorite) Data.addFavorite(book);
_scrollToRead();
isLoading = false;
isSuccess = true;
} catch (e) {
isLoading = false;
isSuccess = false;
return false;
}
print('刷新 $book');
setState(() {});
return true;
}
void _scrollToRead() {
if (book.history != null) {
final history = book.chapters
.firstWhere((chapter) => chapter.cid == book.history.cid);
SchedulerBinding.instance.addPostFrameCallback((_) {
_scrollController.animateTo(
WidgetChapter.height * chapters.indexOf(history).toDouble(),
duration: Duration(milliseconds: 500),
curve: Curves.linear);
});
}
}
_openChapter(Chapter chapter) {
setState(() {
book.history = History(cid: chapter.cid, cname: chapter.cname, time: 0);
openChapter(context, book, chapter);
});
}
favoriteBook() {
widget.book.favorite();
isFavorite = !isFavorite;
setState(() {});
}
void _sort() {
setState(() {
_reverse = !_reverse;
chapters = chapters.reversed.toList();
_scrollToRead();
});
}
List<Widget> chapterWidgets() {
final book = this.book ?? widget.book;
List<Widget> list = [];
chapters.forEach((chapter) {
final isRead = chapter.cid == book.history?.cid;
list.add(WidgetChapter(
chapter: chapter,
onTap: _openChapter,
read: isRead,
));
});
return list;
}
Widget buildChapter(BuildContext context, int index) {
final book = this.book ?? widget.book;
final chapter = chapters[index];
final isRead = chapter.cid == book.history?.cid;
if (index < chapters.length - 1) {
return DecoratedBox(
decoration: _Main._border,
child: WidgetChapter(
chapter: chapter,
onTap: _openChapter,
read: isRead,
),
);
}
return WidgetChapter(
chapter: chapter,
onTap: _openChapter,
read: isRead,
);
}
@override
Widget build(BuildContext context) {
Color color = isFavorite ? Colors.red : Colors.white;
IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border;
final book = this.book ?? widget.book;
return Scaffold(
body: PullToRefreshNotification(
key: _refresh,
onRefresh: loadBook,
maxDragOffset: kToolbarHeight * 2,
child: CustomScrollView(
controller: _scrollController,
slivers: [
SliverAppBar(
floating: true,
pinned: true,
title: Text(widget.book.name),
expandedHeight: 200,
actions: <Widget>[
IconButton(
onPressed: _sort,
icon: Icon(_reverse
? FontAwesomeIcons.sortNumericDown
: FontAwesomeIcons.sortNumericDownAlt)),
IconButton(
onPressed: favoriteBook, icon: Icon(icon, color: color))
],
flexibleSpace: FlexibleSpaceBar(
background: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
margin: EdgeInsets.only(
top: 50, left: 20, right: 10, bottom: 20),
height: 160,
child: Hero(
tag: widget.heroTag,
child:
Image(image: NetworkImageSSL(widget.book.avatar)),
),
),
Expanded(
child: Container(
padding: EdgeInsets.only(top: 50, right: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'作者:' + (book.author ?? ''),
style: TextStyle(color: Colors.white),
),
Container(
margin: EdgeInsets.only(top: 10),
),
Text(
'简介:\n' + (book.description ?? ''),
softWrap: true,
style:
TextStyle(color: Colors.white, height: 1.2),
),
],
),
)),
],
),
),
),
),
PullToRefreshContainer((info) => SliverPullToRefreshHeader(
info: info,
onTap: () => _refresh.currentState.show(
notificationDragOffset: SliverPullToRefreshHeader.height),
)),
SliverList(
delegate: SliverChildBuilderDelegate(
buildChapter,
childCount: book.chapters.length,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,274 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
import 'package:weiman/activities/book/tapToSearch.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/main.dart';
import 'package:weiman/provider/favoriteData.dart';
import 'package:weiman/utils.dart';
import 'package:weiman/widgets/book.dart';
import 'package:weiman/widgets/bookSettingDialog.dart';
import 'package:weiman/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;
@override
void initState() {
super.initState();
widget.book.look = true;
_scrollController = ScrollController();
print('${widget.book}');
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
_refresh.currentState
.show(notificationDragOffset: SliverPullToRefreshHeader.height);
});
}
@override
dispose() {
_scrollController.dispose();
super.dispose();
}
Future<bool> loadBook() async {
try {
final res = await widget.book.load();
if (mounted && widget.book.needToSave()) {
await widget.book.save();
// Provider.of<FavoriteData>(context, listen: false).loadBooksList(true);
}
if (mounted) setState(() {});
return res;
} catch (e) {
return false;
}
}
_openChapter(Chapter chapter) async {
await openChapter(context, widget.book, chapter);
setState(() {});
}
favoriteBook() async {
final fav = Provider.of<FavoriteData>(context, listen: false);
if (widget.book.favorite) {
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) {
fav.deleteBook(widget.book);
}
} else {
await fav.addBook(widget.book);
await showBookSettingDialog(context, widget.book);
if (widget.book.needUpdate == true) {
widget.book.status = BookUpdateStatus.no;
} else {
widget.book.status = BookUpdateStatus.not;
}
}
setState(() {});
}
List<Chapter> _sort() {
final List<Chapter> list = List.from(widget.book.chapters);
// print('sort ${list.length}');
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 == widget.book.history?.cid,
);
if (index < chapters.length - 1)
child = DecoratedBox(
decoration: border,
child: child,
);
return child;
};
return builder;
}
@override
Widget build(BuildContext context) {
Color color = widget.book.favorite ? Colors.red : Colors.white;
IconData icon =
widget.book.favorite ? Icons.favorite : Icons.favorite_border;
final List<Chapter> chapters = _sort();
final history = <Widget>[];
if (widget.book.history != null && widget.book.chapters.length > 0) {
final chapter = widget.book.chapters.firstWhere(
(chapter) => chapter.cid == widget.book.history.cid,
orElse: () => null,
);
if(chapter != null){
history.add(ListTile(title: Text('阅读历史')));
history.add(WidgetChapter(
chapter: chapter,
onTap: _openChapter,
read: true,
));
history.add(ListTile(title: Text('下一章')));
final nextIndex = widget.book.chapters.indexOf(chapter) + 1;
if (nextIndex < widget.book.chapterCount) {
history.add(WidgetChapter(
chapter: widget.book.chapters[nextIndex],
onTap: _openChapter,
read: false,
));
} else {
history.add(ListTile(subtitle: Text('没有了')));
}
}
history.add(SizedBox(height: 20));
}
history.add(
ListTile(
title: Row(
children: [
Text('章节列表'),
SizedBox(width: 10),
TextButton(
onPressed: () {
_reverse = !_reverse;
setState(() {});
},
child: Text('倒序'),
),
],
),
),
);
return Scaffold(
body: PullToRefreshNotification(
key: _refresh,
onRefresh: loadBook,
maxDragOffset: kToolbarHeight * 2,
child: CustomScrollView(
controller: _scrollController,
slivers: [
///
SliverAppBar(
floating: true,
pinned: true,
title: Text(widget.book.name),
expandedHeight: 200,
actions: <Widget>[
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>[
TapToSearchWidget(
leading: '作者', items: widget.book.authors),
TapToSearchWidget(
leading: '标签', items: widget.book.tags),
Container(
margin: EdgeInsets.only(top: 10),
),
Text(
widget.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,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:weiman/activities/search/search.dart';
class TapToSearchWidget extends StatelessWidget {
final String leading;
final List<String> items;
const TapToSearchWidget({
Key key,
this.leading,
this.items,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton(
child: Text('$leading'),
onPressed: null,
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
overlayColor:
MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.3)),
visualDensity: VisualDensity.comfortable,
),
),
Expanded(
child: Wrap(
spacing: 10,
crossAxisAlignment: WrapCrossAlignment.center,
children: items.map((e) => _Item(string: e)).toList(),
),
),
],
);
}
}
class _Item extends StatelessWidget {
final String string;
const _Item({Key key, @required this.string})
: assert(string != null),
super(key: key);
@override
Widget build(BuildContext context) {
return TextButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ActivitySearch(
search: string,
)));
},
icon: Icon(Icons.search, size: 14),
label: Text(string),
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
overlayColor:
MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.3)),
visualDensity: VisualDensity.comfortable,
),
);
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ActivitySearch(
search: string,
)));
},
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: string,
style: TextStyle(decoration: TextDecoration.underline)),
WidgetSpan(
child: Icon(
Icons.search,
color: Colors.white,
size: 14,
)),
],
),
style: TextStyle(
color: Colors.white,
textBaseline: TextBaseline.ideographic,
),
),
);
}
}

View File

@ -1,380 +0,0 @@
part of '../main.dart';
class ActivityChapter extends StatefulWidget {
final Book book;
final Chapter chapter;
ActivityChapter(this.book, this.chapter);
@override
ChapterState createState() => ChapterState();
}
class ChapterState extends State<ActivityChapter> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
PageController _pageController;
int showIndex = 0;
bool hasNextImage = true;
@override
void initState() {
super.initState();
_pageController = PageController(
keepPage: false,
initialPage: widget.book.chapters.indexOf(widget.chapter));
}
@override
void dispose() {
_pageController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
endDrawer: ChapterDrawer(
book: widget.book,
onTap: (chapter) {
_pageController.jumpToPage(widget.book.chapters.indexOf(chapter));
},
),
body: PageView.builder(
physics: AlwaysScrollableClampingScrollPhysics(),
controller: _pageController,
itemCount: widget.book.chapters.length,
itemBuilder: (ctx, index) {
return ChapterContentView(
actions: [
IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openEndDrawer();
},
),
],
book: widget.book,
chapter: widget.book.chapters[index],
);
}),
);
}
}
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;
@override
initState() {
super.initState();
Data.addHistory(widget.book, widget.chapter);
SchedulerBinding.instance.addPostFrameCallback((_) => _refresh?.currentState
?.show(notificationDragOffset: SliverPullToRefreshHeader.height));
}
Future<bool> fetchImages() async {
print('fetchImages');
if (mounted) setState(() {});
loading = true;
images.clear();
try {
images.addAll(await UserAgentClient.instance
.getImages(aid: widget.book.aid, cid: widget.chapter.cid)
.timeout(Duration(seconds: 5)));
if (images.length < 5) {
// print('图片 前:' + images.toString());
final list =
await checkImage(images.last).timeout(Duration(seconds: 15));
images.addAll(list);
}
} 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;
// print('所有图片:' + images.toString());
if (mounted) setState(() {});
return true;
}
@override
Widget build(BuildContext context) {
final list = <Widget>[];
if (!loading && images.length < 20) {
list.add(SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(5),
child: Text('只读取到少于20张图片友情提示\n'
'由于能力有限,可能没有办法识别出本章的所有图片,\n'
'敬请谅解。'))));
}
return PullToRefreshNotification(
key: _refresh,
onRefresh: fetchImages,
maxDragOffset: kToolbarHeight * 2,
child: CustomScrollView(
physics: AlwaysScrollableClampingScrollPhysics(),
slivers: [
SliverAppBar(
title: Text(widget.chapter.cname),
pinned: false,
floating: true,
actions: widget.actions,
),
PullToRefreshContainer(
(info) => SliverPullToRefreshHeader(
info: info,
onTap: () => _refresh.currentState.show(
notificationDragOffset: SliverPullToRefreshHeader.height),
),
),
...list,
SliverList(
delegate: SliverChildBuilderDelegate(
(ctx, i) {
print('item $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(images[i]),
enableLoadState: true,
enableMemoryCache: true,
fit: BoxFit.fitWidth,
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,
);
}
},
),
// content: Image(
// image: NetworkImageSSL(images[i]),
// loadingBuilder: (_, child, loadingProgress) {
// if (loadingProgress == null) return child;
// return SizedBox(
// height: 400,
// child: Center(
// child: CircularProgressIndicator(
// value: loadingProgress.expectedTotalBytes != null
// ? loadingProgress.cumulativeBytesLoaded /
// loadingProgress.expectedTotalBytes
// : null,
// ),
// ),
// );
// }),
);
},
childCount: images.length,
),
),
],
),
);
}
}
Future<List<String>> checkImage(String last) async {
final uri = Uri.parse(last);
// print({'scheme': uri.scheme, 'host': uri.host, 'path': uri.path});
final a = uri.scheme + '://' + uri.host;
final b = uri.pathSegments.take(uri.pathSegments.length - 1).join('/');
// print({'a': a, 'b': b});
//
final file = uri.pathSegments.last.split('.');
final fileName = file[0];
//
final fileFormat = file[1];
final List<String> list = [];
int plus = 1;
//print('最后的图片:' + last);
while (true) {
final String file1 = getFileName(name: fileName, divider: '_', plus: plus),
file2 = getFileName(name: fileName, divider: '_', plus: plus + 1);
var url1 = '$a/$b/$file1.$fileFormat', url2 = '$a/$b/$file2.$fileFormat';
// print('正在测试:\n' + url1 + '\n' + url2);
final res = await Future.wait([
UserAgentClient.instance.head(url1),
UserAgentClient.instance.head(url2)
]);
if (res[0].statusCode != 200) break;
list.add(url1);
if (res[1].statusCode != 200) {
break;
}
list.add(url2);
plus += 2;
}
// print('最后的图片数量: ' + number.toString());
return list;
}
String getFileName(
{@required String name, @required String divider, @required int plus}) {
List<String> data = name.split(divider), newName = [];
for (var i = 0; i < data.length; i++) {
try {
int number = int.parse(data[i]) + plus;
newName.add(number.toString());
} catch (e) {
newName.add(data[i]);
}
}
return newName.join(divider);
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
import 'package:weiman/activities/chapter/chapterTab.dart';
import 'package:weiman/activities/chapter/drawer.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/setting.dart';
import 'package:weiman/utils.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<Setting>(context, listen: false).getHideOption();
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) async {
await widget.book.setHistory(chapter);
}
@override
Widget build(BuildContext context) {
return Consumer<Setting>(builder: (_, data, __) {
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 ChapterTab(
actions: [
IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openEndDrawer();
},
),
],
book: widget.book,
chapter: widget.book.chapters[index],
);
},
),
);
});
}
}

View File

@ -0,0 +1,261 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:provider/provider.dart';
import 'package:weiman/activities/chapter/image.dart';
import 'package:weiman/activities/chapter/viewerSwitcherWidget.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/crawler/http18Comic.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/setting.dart';
import 'package:weiman/utils.dart';
import 'package:weiman/widgets/animatedLogo.dart';
class ChapterSourceList extends LoadingMoreBase<String> {
final Book book;
final Chapter chapter;
final Function onFirstLoaded;
bool firstLoad = true;
bool hasMore = true;
bool isMultiPage = false;
int page = 1;
ChapterSourceList({
this.book,
this.chapter,
this.onFirstLoaded,
});
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
final chapterContent = await Http18Comic.instance.getChapterContent(
book,
chapter,
page: page,
);
print(chapterContent.toString());
hasMore = chapterContent.hasNextPage;
this.addAll(chapterContent.images);
if (firstLoad) {
firstLoad = false;
isMultiPage = hasMore;
}
page++;
return true;
}
@override
Future<bool> refresh([bool notifyStateChanged = false]) {
firstLoad = true;
hasMore = true;
page = 1;
return super.refresh(notifyStateChanged);
}
}
class ChapterTab extends StatefulWidget {
final Book book;
final Chapter chapter;
final List<Widget> actions;
const ChapterTab({Key key, this.book, this.chapter, this.actions})
: super(key: key);
@override
_State createState() => _State();
}
class _State extends State<ChapterTab> {
ChapterSourceList sourceList;
ScrollController scrollController;
@override
initState() {
scrollController = ScrollController();
sourceList = ChapterSourceList(
book: widget.book,
chapter: widget.chapter,
);
widget.book.setHistory(widget.chapter);
super.initState();
// /
final setting = Provider.of<Setting>(context, listen: false);
final hide = setting.getHideOption();
if (hide == HideOption.auto) {
scrollController.addListener(() {
final isUp = scrollController.position.userScrollDirection ==
ScrollDirection.forward;
if (isUp)
showStatusBar();
else
hideStatusBar();
});
}
}
@override
dispose() {
widget.book.setHistory(widget.chapter);
scrollController?.dispose();
super.dispose();
}
Widget imageBuilder(ctx, String image, int index) {
index += 1;
bool reDraw = false;
try {
int cid = int.parse(widget.chapter.cid);
reDraw = cid >= 220980;
// print('创建图片 cid $cid reDraw $reDraw');
} catch (e) {}
return ImageWidget(
image: image,
index: index,
total: sourceList.length,
reSort: reDraw,
);
}
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>[
AnimatedLogoWidget(width: 20, height: 30),
SizedBox(width: 10),
Text("正在读取")
],
);
widget = Container(
width: double.infinity,
height: kToolbarHeight,
child: widget,
alignment: Alignment.center,
);
break;
case IndicatorStatus.fullScreenBusying:
widget = Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedLogoWidget(width: 25, height: 30),
Text('读取中'),
],
),
);
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
}
break;
case IndicatorStatus.error:
case IndicatorStatus.fullScreenError:
widget = Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'读取失败\n你可能需要用梯子',
textAlign: TextAlign.center,
),
RaisedButton(
child: Text('再次重试'),
onPressed: sourceList.errorRefresh,
)
],
);
widget = Container(
width: double.infinity,
height: kToolbarHeight,
child: widget,
alignment: Alignment.center,
);
if (status == IndicatorStatus.fullScreenError) {
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
}
break;
case IndicatorStatus.noMoreLoad:
widget = SizedBox();
break;
case IndicatorStatus.empty:
widget = Text(
'没有图片',
);
widget = Container(
width: double.infinity,
height: kToolbarHeight,
child: widget,
alignment: Alignment.center,
);
if (isSliver) {
widget = SliverToBoxAdapter(
child: widget,
);
} else {
widget = CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: widget,
)
],
);
}
break;
}
return widget;
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
controller: scrollController,
slivers: [
SliverAppBar(
snap: true,
floating: true,
title: Text(widget.chapter.cname),
actions: [
ViewerSwitcherWidget(),
IconButton(
icon: Icon(Icons.vertical_align_top),
onPressed: () => scrollController.jumpTo(0.0),
),
...widget.actions,
],
),
LoadingMoreSliverList(
SliverListConfig(
sourceList: sourceList,
itemBuilder: imageBuilder,
addSemanticIndexes: true,
semanticIndexOffset: 10,
autoLoadMore: true,
indicatorBuilder: indicatorBuilder,
),
),
],
);
}
}

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/widgets/book.dart';
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(),
),
),
);
}
}

View File

@ -0,0 +1,110 @@
import 'dart:ui' as ui;
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart' hide Image;
import 'package:provider/provider.dart';
import 'package:sticky_headers/sticky_headers/widget.dart';
import 'package:weiman/activities/chapter/viewer.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/crawler/http18Comic.dart';
import 'package:weiman/db/setting.dart';
class ImageWidget extends StatefulWidget {
final int index;
final int total;
final String image;
final bool reSort;
const ImageWidget({
Key key,
this.image,
this.index,
this.total,
this.reSort = false,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<ImageWidget> {
static TextStyle _style = TextStyle(color: Colors.white);
static BoxDecoration _decoration =
BoxDecoration(color: Colors.black.withOpacity(0.4));
String get tag {
return 'image_viewer_${widget.index}';
}
@override
Widget build(BuildContext context) {
return StickyHeader(
overlapHeaders: true,
header: SafeArea(
top: true,
bottom: false,
child: Row(
children: [
Container(
padding: EdgeInsets.all(5),
decoration: _decoration,
child: Text(
'${widget.index} / ${widget.total}',
style: _style,
),
),
],
),
),
content: ExtendedImage(
image: NetworkImageSSL(
Http18Comic.instance,
widget.image,
reSort: widget.reSort,
),
loadStateChanged: (ExtendedImageState state) {
Widget widget;
switch (state.extendedImageLoadState) {
case LoadState.loading:
widget = SizedBox(
height: 300,
child: Center(
child: CircularProgressIndicator(),
),
);
break;
case LoadState.completed:
widget = GestureDetector(
child: Hero(
child:
ExtendedRawImage(image: state.extendedImageInfo?.image),
tag: tag,
),
onTap: () => onTap(context),
);
break;
default:
}
return widget;
},
),
);
}
onTap(BuildContext context) {
final viewerSwitch =
Provider.of<Setting>(context, listen: false).getViewerSwitch();
// print('viewer $viewerSwitch');
if (!viewerSwitch) return;
Navigator.push(
context,
TransparentMaterialPageRoute(
builder: (_) => ActivityImageViewer(
url: this.widget.image,
heroTag: tag,
reSort: widget.reSort,
),
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/crawler/http18Comic.dart';
class ActivityImageViewer extends StatefulWidget {
final String url;
final String heroTag;
final bool reSort;
const ActivityImageViewer({
Key key,
this.url,
this.heroTag,
this.reSort = false,
}) : super(key: key);
@override
_State createState() => _State();
}
class _State extends State<ActivityImageViewer> {
double currentScale = 1.0;
@override
Widget build(BuildContext context) {
return ExtendedImageSlidePage(
slideAxis: SlideAxis.both,
slideType: SlideType.onlyImage,
child: Material(
color: Colors.transparent,
shadowColor: Colors.transparent,
child: Stack(
fit: StackFit.expand,
children: [
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: ExtendedImage(
image: NetworkImageSSL(
Http18Comic.instance,
widget.url,
reSort: widget.reSort,
),
enableSlideOutPage: true,
mode: ExtendedImageMode.gesture,
onDoubleTap: (status) {
currentScale = currentScale == 1 ? 3 : 1;
status.handleDoubleTap(scale: currentScale);
},
heroBuilderForSlidingPage: (child) {
return Hero(
child: child,
tag: widget.heroTag,
);
},
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weiman/db/setting.dart';
class ViewerSwitcherWidget extends StatefulWidget {
@override
ViewerSwitcherState createState() => ViewerSwitcherState();
}
class ViewerSwitcherState extends State<ViewerSwitcherWidget> {
@override
Widget build(BuildContext context) {
return Consumer<Setting>(builder: (_, data, __) {
final icon = data.getViewerSwitch()
? Icons.check_box_outlined
: Icons.check_box_outline_blank;
return
TextButton.icon(
icon: Icon(icon),
label: Text('看图'),
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
overlayColor:
MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.3)),
visualDensity: VisualDensity.compact,
),
onPressed: () {
data.setViewerSwitch(!data.getViewerSwitch());
setState(() {});
},
);
});
}
}

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:weiman/db/book.dart';
class ActivityCheckDB extends StatefulWidget {
@override
_State createState() => _State();
}
enum CheckState {
Uncheck,
Pass,
Fail,
}
class _State extends State<ActivityCheckDB> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('收藏数据检修'),
),
body: ListView(children: [
ListTile(
title: Text('所有藏书章节数量归零'),
onTap: () async {
for (final book in Book.bookBox.values) {
book.chapterCount = 0;
await book.save();
}
},
),
ListTile(
title: Text('清空漫画数据'),
subtitle: Text('${Book.bookBox.length}'),
onTap: () async {
await Book.bookBox.clear();
},
),
]),
);
}
}

View File

@ -1,4 +1,10 @@
part of '../main.dart';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:oktoast/oktoast.dart';
import 'package:weiman/classes/data.dart';
class ActivityCheckData extends StatefulWidget {
@override
@ -17,7 +23,7 @@ final titleTextStyle = TextStyle(fontSize: 14, color: Colors.blue),
class _State extends State<ActivityCheckData> {
CheckState firstState;
int firstLength;
int firstLength = 0;
final TextSpan secondResults = TextSpan();
TextEditingController _outputController, _inputController;
@ -57,7 +63,7 @@ class _State extends State<ActivityCheckData> {
final has = Data.has(Data.favoriteBooksKey);
if (has) {
final String str = Data.instance.getString(Data.favoriteBooksKey);
final Map<String, Object> map = json.decode(str);
final Map<String, Object> map = jsonDecode(str);
firstLength = map.keys.length;
_outputController.text = str;
}

View File

@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:oktoast/oktoast.dart';
import 'package:weiman/classes/book.dart';
import 'package:weiman/classes/data.dart';
import 'package:weiman/db/book.dart' as newBook;
import 'package:weiman/main.dart';
import 'home.dart';
class ActivityDataConvert extends StatefulWidget {
@override
_State createState() => _State();
}
class _State extends State<ActivityDataConvert> {
List<Book> quick;
Map<String, Book> favorites;
bool selectQ = true, selectH = true;
@override
void initState() {
analytics.setCurrentScreen(screenName: '/activity_data_convert');
favorites = Data.getFavorites();
quick = Data.quickList();
super.initState();
}
Future convert() async {
int quickIndex = 0;
int skip = 0;
final awaitList = <Future>[];
favorites.keys.forEach((id) {
if (newBook.Book.bookBox.containsKey(id)) return;
final oldBook = favorites[id];
final isQuick = selectQ && quick.contains(oldBook.aid);
final book = new newBook.Book(
httpId: null,
aid: oldBook.aid,
name: oldBook.name,
avatar: oldBook.avatar,
description: oldBook.description,
authors: [oldBook.author],
chapterCount: oldBook.chapterCount,
quick: isQuick ? quickIndex : null,
needUpdate: true,
favorite: true,
history: null,
);
if (isQuick) quickIndex++;
awaitList.add(book.save());
});
await Future.wait(awaitList);
showToast(
'成功转存 ${awaitList.length} 本小说\n跳过了 $skip',
textPadding: EdgeInsets.all(10),
);
}
Future clean() async {
await Data.instance.remove(Data.favoriteBooksKey);
await Data.instance.remove(Data.quickKey);
await Data.instance.remove(Data.viewHistoryKey);
}
void gotoHome() {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => ActivityHome(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('旧数据转存'),
),
body: ListView(children: [
ListTile(
title: Text('从v1.1.2开始,为了实现藏书分组功能,使用了新的数据存储方式'
'\n【旧书】打开后直接搜索同名漫画。'
'\n清空旧数据后这个界面不会再次出现。'
'\n需要将旧的藏书数据转存为新数据吗?'
'\n旧藏书不多的话,我个人建议直接清空,可以防止产生数据干扰')),
ListTile(
title: Text('收藏列表'),
subtitle: Text('一共有 ${favorites.length}'),
trailing: Checkbox(
value: true,
onChanged: null,
),
),
ListTile(
title: Text('快速导航'),
subtitle: Text('一共有 ${quick.length}'),
trailing: Checkbox(
value: selectQ,
onChanged: (value) {
setState(() {
selectQ = value;
});
},
),
),
]),
bottomNavigationBar: Row(children: [
SizedBox(width: 10),
Expanded(
child: OutlineButton(
child: Text('直接清空旧数据'),
onPressed: () async {
await clean();
gotoHome();
},
),
),
SizedBox(width: 10),
Expanded(
child: OutlineButton(
child: Text('转存并清空旧数据'),
onPressed: () async {
await convert();
await clean();
gotoHome();
},
),
),
SizedBox(width: 10),
]),
);
}
}

View File

@ -1,10 +1,28 @@
part of '../main.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:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:weiman/activities/dataConvert.dart';
import 'package:weiman/db/setting.dart';
import 'package:weiman/provider/theme.dart';
import 'package:weiman/activities/checkData.dart';
import 'package:weiman/activities/hot.dart';
import 'package:weiman/activities/search/search.dart';
import 'package:weiman/activities/test2.dart';
import 'package:weiman/classes/book.dart';
import 'package:weiman/main.dart';
import 'package:weiman/provider/favoriteData.dart';
import 'package:weiman/widgets/checkConnect/checkConnect.dart';
import 'package:weiman/widgets/favorites.dart';
import 'package:weiman/widgets/histories.dart';
import 'package:weiman/widgets/quick.dart';
import 'checkDB.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();
}
@ -14,7 +32,6 @@ class HomeState extends State<ActivityHome> {
final List<Widget> histories = [];
final List<Book> quick = [];
final GlobalKey<QuickState> _quickState = GlobalKey();
static final weekTime = 7 * 24 * 3600000;
bool showFavorite = true;
@ -26,26 +43,18 @@ class HomeState extends State<ActivityHome> {
///
SchedulerBinding.instance.addPostFrameCallback((_) async {
autoSwitchTheme();
_FavoriteList.getBooks();
await _FavoriteList.checkNews();
final updated = _FavoriteList.hasNews.values
.where((int updatedChapters) => updatedChapters > 0)
.length;
FavoriteData favData = Provider.of<FavoriteData>(context, listen: false);
await favData.loadBooksList();
final updated = await favData.checkUpdate();
if (updated > 0)
showToast(
'$updated 本藏书有更新',
backgroundColor: Colors.black.withOpacity(0.5),
textPadding: EdgeInsets.all(10),
);
});
}
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 autoSwitchTheme() async {}
void gotoSearch() {
Navigator.push(
@ -59,11 +68,15 @@ class HomeState extends State<ActivityHome> {
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: '/activity_recommend/'),
settings: RouteSettings(name: '/activity_recommend'),
builder: (_) => ActivityRank(),
));
}
void gotoPatreon() {
launch('https://www.patreon.com/nrop19');
}
bool isEdit = false;
void _draggableModeChanged(bool mode) {
@ -72,6 +85,85 @@ class HomeState extends State<ActivityHome> {
setState(() {});
}
Widget themeButton() {
final system = FontAwesomeIcons.cloudSun,
light = FontAwesomeIcons.solidSun,
dark = FontAwesomeIcons.solidMoon;
final theme = Provider.of<ThemeProvider>(context, listen: false);
Widget themeIcon;
switch (theme.themeMode) {
case ThemeMode.light:
themeIcon = Icon(light);
break;
case ThemeMode.dark:
themeIcon = Icon(dark);
break;
default:
themeIcon = Icon(system);
break;
}
return IconButton(
onPressed: () {
switch (theme.themeMode) {
case ThemeMode.light:
theme.changeTheme(ThemeMode.dark);
break;
case ThemeMode.dark:
theme.changeTheme(ThemeMode.system);
break;
default:
theme.changeTheme(ThemeMode.light);
}
Provider.of<Setting>(context, listen: false)
.setThemeMode(theme.themeMode);
showToastWidget(
Container(
padding: EdgeInsets.all(10),
color: Colors.black.withOpacity(0.7),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(mainAxisSize: MainAxisSize.min, children: [
Icon(
system,
size: 14,
color: Colors.white,
),
SizedBox(width: 10),
Text('跟随系统,自动切换明暗模式\n如果系统不支持,默认为明亮模式'),
]),
SizedBox(height: 10),
Row(mainAxisSize: MainAxisSize.min, children: [
Icon(
light,
size: 14,
color: Colors.white,
),
SizedBox(width: 10),
Text('为明亮模式'),
]),
SizedBox(height: 10),
Row(mainAxisSize: MainAxisSize.min, children: [
Icon(
dark,
size: 14,
color: Colors.white,
),
SizedBox(width: 10),
Text('为暗黑模式'),
]),
],
),
),
dismissOtherToast: true,
duration: Duration(seconds: 4),
);
},
icon: themeIcon,
);
}
@override
Widget build(BuildContext context) {
final media = MediaQuery.of(context);
@ -79,7 +171,7 @@ class HomeState extends State<ActivityHome> {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text('微漫 v' + widget.packageInfo.version),
title: Text('微漫 v' + packageInfo.version),
automaticallyImplyLeading: false,
leading: isEdit
? IconButton(
@ -91,18 +183,20 @@ class HomeState extends State<ActivityHome> {
: null,
actions: <Widget>[
///
themeButton(),
SizedBox(width: 20),
///
IconButton(
onPressed: () {
DynamicTheme.of(context).setBrightness(
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark);
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: '/activity_setting'),
builder: (_) => ActivitySetting()));
},
icon: Icon(Theme.of(context).brightness == Brightness.light
? FontAwesomeIcons.lightbulb
: FontAwesomeIcons.solidLightbulb),
icon: Icon(FontAwesomeIcons.cog),
),
SizedBox(width: 20),
///
IconButton(
@ -127,9 +221,11 @@ class HomeState extends State<ActivityHome> {
),
],
),
drawerEnableOpenDragGesture: false,
endDrawerEnableOpenDragGesture: false,
endDrawer: Drawer(
child: LayoutBuilder(
builder: (_, __) {
builder: (_, constraints) {
if (showFavorite) {
return FavoriteList();
} else {
@ -138,112 +234,147 @@ class HomeState extends State<ActivityHome> {
},
),
),
body: Container(
alignment: Alignment.center,
padding: EdgeInsets.only(left: 40, right: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
child: OutlineButton(
onPressed: gotoSearch,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.search,
color: Colors.blue,
),
Text(
'搜索漫画',
style: TextStyle(color: Colors.blue),
)
],
),
borderSide: BorderSide(color: Colors.blue, width: 2),
shape: StadiumBorder(),
),
),
Row(
children: [
Expanded(
child: OutlineButton(
onPressed: gotoRecommend,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.whatshot,
color: Colors.red,
),
Text(
'月排行榜',
style: TextStyle(color: Colors.red),
)
],
),
borderSide: BorderSide(color: Colors.red, width: 2),
shape: StadiumBorder(),
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),
)
],
),
),
],
),
Center(
child: Quick(
key: _quickState,
width: width,
draggableModeChanged: _draggableModeChanged,
),
),
Container(
margin: EdgeInsets.only(bottom: 10),
child: Text(
'在 level-plus.net 论坛首发',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[500]),
),
),
GestureDetector(
onTap: () async {
if (await canLaunch('tg://resolve?domain=weiman_app'))
launch('tg://resolve?domain=weiman_app');
else
launch('https://t.me/weiman_app');
},
child: Text(
'Telegram广播频道',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.blue[200],
decoration: TextDecoration.underline,
borderSide: BorderSide(color: Colors.blue, width: 2),
shape: StadiumBorder(),
),
),
),
Visibility(
visible: isDevMode,
child: FlatButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => ActivityTest()));
},
child: Text('测试界面'),
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(),
),
),
],
),
),
Visibility(
visible: isDevMode,
child: FlatButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => ActivityCheckData()));
},
child: Text('操作 收藏列表数据'),
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('操作 收藏列表数据'),
),
),
Visibility(
visible: isDevMode,
child: FlatButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => ActivityCheckDB()));
},
child: Text('操作 DB数据'),
),
),
Visibility(
visible: isDevMode,
child: FlatButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ActivityDataConvert()));
},
child: Text('进入旧数据处理功能'),
),
),
],
),
),
),
floatingActionButton: isDevMode
? FloatingActionButton(
child: Text('测试'),
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (_) => ActivityTest()));
},
)
: null,
);
}
}

238
lib/activities/hot.dart Normal file
View File

@ -0,0 +1,238 @@
import 'package:flutter/material.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
import 'package:weiman/widgets/animatedLogo.dart';
import 'package:weiman/crawler/http.dart';
import 'package:weiman/crawler/http18Comic.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/widgets/book.dart';
import 'package:weiman/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;
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> {
SourceList sourceList;
@override
void initState() {
sourceList = SourceList(type: widget.type, http: widget.http);
super.initState();
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
PullToRefreshContainer(
(info) => SliverPullToRefreshHeader(info: info),
),
LoadingMoreSliverList(SliverListConfig<Book>(
sourceList: sourceList,
indicatorBuilder: indicatorBuilder,
itemBuilder: (_, book, __) => WidgetBook(
book,
subtitle: book.authors?.join('/'),
),
)),
],
);
}
Widget book(Book book) {
return WidgetBook(book, subtitle: book.authors?.join('/'));
}
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>[
AnimatedLogoWidget(width: 20, height: 30),
SizedBox(width: 10),
Text("正在读取")
],
);
widget = _setbackground(false, widget, 35.0);
break;
case IndicatorStatus.fullScreenBusying:
widget = Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedLogoWidget(width: 25, height: 30),
Text('读取中'),
],
),
);
if (isSliver) {
widget = SliverFillRemaining(
child: widget,
);
}
break;
case IndicatorStatus.error:
case IndicatorStatus.fullScreenError:
widget = Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'读取失败\n你可能需要用梯子',
textAlign: TextAlign.center,
),
RaisedButton(
child: Text('再次重试'),
onPressed: sourceList.errorRefresh,
)
],
);
final height = status == IndicatorStatus.error ? 35.0 : double.infinity;
widget = _setbackground(false, widget, height);
if (status == IndicatorStatus.fullScreenError) {
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

@ -1,123 +0,0 @@
part of '../main.dart';
class ActivitySearch extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Search();
}
}
class Search extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return SearchState();
}
}
class SearchState extends State<Search> {
TextEditingController _controller = TextEditingController();
GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
final List<Book> _books = [];
bool loading;
void submit() {
_refresh.currentState
.show(notificationDragOffset: SliverPullToRefreshHeader.height);
}
Future<bool> startSearch() async {
print('搜索漫画: ' + _controller.text);
setState(() {
loading = true;
});
_books.clear();
try {
final List<Book> books = await UserAgentClient.instance
.searchBook(_controller.text)
.timeout(Duration(seconds: 5));
_books.addAll(books);
loading = false;
} catch (e) {
loading = false;
return false;
}
return true;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: PullToRefreshNotification(
key: _refresh,
onRefresh: startSearch,
child: CustomScrollView(slivers: [
SliverAppBar(
pinned: true,
title: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (RawKeyEvent event) {
print(
'is enter: ${LogicalKeyboardKey.enter == event.logicalKey}');
if (_controller.text.isEmpty) return;
if (event.runtimeType == RawKeyUpEvent &&
LogicalKeyboardKey.enter == event.logicalKey) {
print('回车键搜索');
submit();
}
},
child: TextField(
decoration: InputDecoration(
hintText: '搜索书名',
prefixIcon: IconButton(
onPressed: () {
_refresh.currentState.show(
notificationDragOffset:
SliverPullToRefreshHeader.height);
},
icon: Icon(Icons.search),
),
),
textAlign: TextAlign.left,
controller: _controller,
autofocus: true,
textInputAction: TextInputAction.search,
onSubmitted: (String name) {
print('onSubmitted');
submit();
},
keyboardType: TextInputType.text,
onEditingComplete: () {
print('onEditingComplete');
submit();
},
),
),
),
PullToRefreshContainer((info) => SliverPullToRefreshHeader(
info: info,
onTap: submit,
)),
SliverLayoutBuilder(
builder: (_, __) {
if (loading == null)
return SliverFillRemaining(
child: Center(child: Text('输入关键词搜索')));
if (loading) return SliverToBoxAdapter();
if (_books.length == 0) {
return SliverFillRemaining(child: Center(child: Text('一本也没有')));
}
return SliverList(
delegate: SliverChildBuilderDelegate((_, i) {
return WidgetBook(
_books[i],
subtitle: _books[i].author,
);
}, childCount: _books.length),
);
},
),
]),
),
);
}
}

View File

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:focus_widget/focus_widget.dart';
import 'package:weiman/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),
cursorColor: Colors.white,
decoration: InputDecoration(
hintText: '搜索书名',
prefixIcon: IconButton(
onPressed: search,
icon: Icon(Icons.search, color: Colors.white),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
border: UnderlineInputBorder(
borderSide: BorderSide(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(
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 'package:weiman/db/book.dart';
import 'package:weiman/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,183 @@
import 'package:flutter/material.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:weiman/activities/search/source.dart';
import 'package:weiman/crawler/http.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/widgets/book.dart';
class SearchTab extends StatefulWidget {
final HttpBook http;
final String search;
const SearchTab({
Key key,
@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.authors.join('/'));
}
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,35 @@
import 'package:flutter/material.dart';
import 'package:weiman/db/setting.dart';
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,192 @@
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 'package:weiman/activities/setting/web.dart';
import 'package:weiman/db/setting.dart';
import 'package:weiman/main.dart';
class ActivitySetting extends StatefulWidget {
@override
_ActivitySetting createState() => _ActivitySetting();
}
class _ActivitySetting extends State<ActivitySetting> {
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<Setting>(builder: (_, data, __) {
print('代理 ${data.getProxy()}');
return ListView(
children: ListTile.divideTiles(
context: context,
tiles: [
///
HideStatusBar(
option: data.getHideOption(),
onChanged: (option) => data.setHideOption(option),
),
///
ListTile(
title: Text('设置代理'),
subtitle: Text(data.getProxy() ?? ''),
onTap: () async {
var proxy = await showDialog<String>(
context: context,
builder: (_) {
final _c = TextEditingController(text: data.getProxy());
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');
await data.setProxy(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('当前版本为 ${packageInfo.version}'),
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => ActivityWeb()));
},
),
///
/* ListTile(
title: Text('清空漫画数据缓存'),
subtitle: Text('正常情况是不需要清空的'),
onTap: () async {
await HttpBook.dataCache.clearAll();
showToast('成功清空漫画数据缓存', textPadding: EdgeInsets.all(10));
},
),*/
],
).toList(),
);
}),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:weiman/main.dart';
class ActivityWeb extends StatefulWidget {
@override
_State createState() => _State();
}
class _State extends State<ActivityWeb> {
LoadState state = LoadState.loading;
@override
void initState() {
analytics.setCurrentScreen(screenName: '/activity_update_web');
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('最新版本'),
),
body: Stack(
alignment: Alignment.center,
children: [
WebView(
initialUrl: 'https://nrop19.github.io/weiman_app',
onWebViewCreated: (controller) {
state = LoadState.loading;
setState(() {});
},
onPageFinished: (_) {
state = LoadState.completed;
setState(() {});
},
),
if (state == LoadState.loading)
Container(
color: Colors.grey.withOpacity(0.3),
padding: EdgeInsets.all(20),
child: CircularProgressIndicator(),
),
],
),
);
}
}

View File

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

View File

@ -1,17 +1,24 @@
part of '../main.dart';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:weiman/crawler/http.dart';
import 'data.dart';
class Book {
final String aid; // ID
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 this.http,
@required this.name,
@required this.aid,
@required this.avatar,
@ -19,6 +26,8 @@ class Book {
this.description,
this.chapters: const [],
this.chapterCount: 0,
this.history,
this.version: 0,
});
@override
@ -31,34 +40,31 @@ class Book {
return books.containsKey(aid);
}
favorite() {
if (isFavorite())
Data.removeFavorite(this);
else
Data.addFavorite(this);
}
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;
}
static Book fromJson(Map<String, dynamic> json) {
factory Book.fromJson(Map<String, dynamic> json) {
final book = Book(
aid: json['aid'],
name: json['name'],
avatar: json['avatar'],
author: json['author'],
description: json['description'],
chapterCount: json['chapterCount'] ?? 0,
);
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;
@ -66,15 +72,26 @@ class Book {
}
class Chapter {
final HttpBook http;
final String cid; // cid
final String cname; //
final String avatar; //
Chapter({@required this.cid, @required this.cname, @required this.avatar});
Chapter({
@required this.http,
@required this.cid,
@required this.cname,
@required this.avatar,
});
@override
String toString() {
return jsonEncode({cid: cid, cname: cname, avatar: avatar});
final Map<String, String> data = {
'cid': cid,
'cname': cname,
'avatar': avatar,
};
return jsonEncode(data);
}
}
@ -82,8 +99,14 @@ class History {
final String cid;
final String cname;
final int time;
final int image;
History({@required this.cid, @required this.cname, @required this.time});
History({
@required this.cid,
@required this.cname,
@required this.time,
this.image = 0,
});
@override
String toString() => jsonEncode(toJson());
@ -93,11 +116,17 @@ class History {
'cid': cid,
'cname': cname,
'time': time,
'image': image,
};
}
static History fromJson(Map<String, dynamic> json) {
return History(cid: json['cid'], cname: json['cname'], time: json['time']);
return History(
cid: json['cid'],
cname: json['cname'],
time: json['time'],
image: json['image'] ?? 0,
);
}
static History fromChapter(Chapter chapter) {

24
lib/classes/chapter.dart Normal file
View File

@ -0,0 +1,24 @@
import 'dart:convert';
import 'package:flutter/material.dart';
class Chapter {
final String cid; // cid
final String cname; //
final DateTime time; //
Chapter({
@required this.cid,
@required this.cname,
this.time,
});
@override
String toString() {
final Map<String, String> data = {
'cid': cid,
'cname': cname,
};
return jsonEncode(data);
}
}

View File

@ -0,0 +1,11 @@
class ChapterContent {
final List<String> images;
final bool hasNextPage;
ChapterContent(this.images, this.hasNextPage);
@override
String toString() {
return 'ChapterContent images:${images.length} nexPage:$hasNextPage';
}
}

View File

@ -1,4 +1,8 @@
part of '../main.dart';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'book.dart';
class Data {
static SharedPreferences instance;
@ -22,7 +26,7 @@ class Data {
} else if (value is double) {
instance.setDouble(key, value);
} else if (value is Map) {
instance.setString(key, jsonEncode(value));
instance.setString(key, json.encode(value));
}
}
@ -30,6 +34,11 @@ class Data {
return instance.get(key);
}
static bool hasData() {
return instance.containsKey(favoriteBooksKey) ||
instance.containsKey(viewHistoryKey);
}
static Map<String, Book> getFavorites() {
if (has(favoriteBooksKey)) {
final String str = instance.getString(favoriteBooksKey);
@ -110,7 +119,7 @@ class Data {
}
/// id
static List<String> _quickIdList() {
static List<String> quickIdList() {
if (instance.containsKey(quickKey)) {
return instance.getStringList(quickKey);
}
@ -121,7 +130,7 @@ class Data {
static List<Book> quickList() {
final books = getFavorites();
final ids = books.keys;
final List<String> quickIds = _quickIdList();
final List<String> quickIds = quickIdList();
print('快捷 $quickIds');
return quickIds
.where((id) => ids.contains(id))
@ -131,7 +140,7 @@ class Data {
///
static addQuick(Book book) {
final list = _quickIdList();
final list = quickIdList();
list.add(book.aid);
instance.setStringList(quickKey, list.toSet().toList());
}
@ -144,7 +153,7 @@ class Data {
/// Quick的id列表
static reQuick() {
final books = getFavorites();
final quickIds = _quickIdList();
final quickIds = quickIdList();
instance.setStringList(
quickKey, quickIds.where(books.keys.contains).toSet().toList());
}

34
lib/classes/history.dart Normal file
View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:weiman/classes/chapter.dart';
class History extends Chapter {
DateTime time; //
History({
@required cid,
@required cname,
@required this.time,
}) : super(cid: cid, cname: cname);
Map<String, dynamic> toJson() {
return {'cid': cid, 'cname': cname, 'time': time};
}
factory History.fromJson(Map<String, dynamic> map) {
if (map == null) return null;
return History(
cid: map['cid'],
cname: map['cname'],
time: map['time'],
);
}
factory History.fromChapter(Chapter chapter) {
return History(
cid: chapter.cid,
cname: chapter.cname,
time: DateTime.now(),
);
}
}

View File

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

View File

@ -1,16 +1,30 @@
part of '../main.dart';
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:weiman/crawler/http.dart';
/// The dart:io implementation of [image_provider.NetworkImage].
class NetworkImageSSL
extends image_provider.ImageProvider<image_provider.NetworkImage>
implements 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.url, {this.scale = 1.0, this.headers})
: assert(url != null),
const NetworkImageSSL(
this.http,
this.url, {
this.scale = 1.0,
this.headers,
this.timeout = 8,
this.reSort = false,
}) : assert(url != null),
assert(scale != null);
final HttpBook http;
final int timeout;
@override
final String url;
@ -20,80 +34,46 @@ class NetworkImageSSL
@override
final Map<String, String> headers;
final bool reSort;
static void init(ByteData data) {}
@override
Future<NetworkImageSSL> obtainKey(
image_provider.ImageConfiguration configuration) {
Future<NetworkImageSSL> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<NetworkImageSSL>(this);
}
@override
image_provider.ImageStreamCompleter load(
image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
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<image_provider.ImageChunkEvent> chunkEvents =
StreamController<image_provider.ImageChunkEvent>();
final StreamController<ImageChunkEvent> chunkEvents =
StreamController<ImageChunkEvent>();
return image_provider.MultiFrameImageStreamCompleter(
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<image_provider.ImageProvider>(
'Image provider', this),
DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<NetworkImage>('Image key', key),
];
},
);
}
// Do not access this field directly; use [_httpClient] instead.
// We set `autoUncompress` to false to ensure that we can trust the value of
// the `Content-Length` HTTP header. We automatically uncompress the content
// in our call to [consolidateHttpClientResponseBytes].
static HttpClient _sharedHttpClient = HttpClient()
..autoUncompress = false
..badCertificateCallback = (_, __, ___) => true;
static HttpClient get _httpClient {
return _sharedHttpClient;
}
Future<ui.Codec> _loadAsync(
Future<Codec> _loadAsync(
NetworkImageSSL key,
StreamController<image_provider.ImageChunkEvent> chunkEvents,
image_provider.DecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
DecoderCallback decode,
) async {
try {
assert(key == this);
final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request =
await _httpClient.getUrl(resolved).timeout(Duration(seconds: 5));
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
throw image_provider.NetworkImageLoadException(
statusCode: response.statusCode, uri: resolved);
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
chunkEvents.add(image_provider.ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
final Uint8List bytes = await http.getImage(url, reSort: reSort);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
throw Exception('NetworkImage is an empty file: $url');
return decode(bytes);
} finally {
chunkEvents.close();
@ -108,7 +88,7 @@ class NetworkImageSSL
}
@override
int get hashCode => ui.hashValues(url, scale);
int get hashCode => hashValues(url, scale);
@override
String toString() => '$runtimeType("$url", scale: $scale)';

80
lib/crawler/http.dart Normal file
View File

@ -0,0 +1,80 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/classes/chapterContent.dart';
import 'package:weiman/db/book.dart';
import 'http18Comic.dart';
final headers = {
'user-agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 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',
};
class MyHttpClient {
static Map<String, HttpBook> clients = {};
static init(String proxy, int timeout) {
Http18Comic.instance = Http18Comic(
baseUrls.values.first,
name: baseUrls.keys.first,
headers: headers,
timeout: timeout,
);
clients[Http18Comic.instance.id] = Http18Comic.instance;
setGlobalProxy(proxy);
}
}
abstract class HttpBook {
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<ChapterContent> getChapterContent(Book book, Chapter chapter);
Future<List<int>> getImage(String url, {bool reSort = false});
Future<List<Book>> hotBooks([String type = '', int page]);
}
class MyProxyHttpOverride extends HttpOverrides {
final String proxy;
MyProxyHttpOverride(this.proxy);
@override
HttpClient createHttpClient(SecurityContext context) {
return super.createHttpClient(context)
..findProxy = (uri) {
return 'PROXY $proxy;';
}
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
}
void setGlobalProxy(String proxy) {
print('setGlobalProxy $proxy');
if (proxy != null)
HttpOverrides.global = MyProxyHttpOverride(proxy);
else
HttpOverrides.global = null;
}

178
lib/db/book.dart Normal file
View File

@ -0,0 +1,178 @@
import 'package:hive/hive.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/classes/history.dart';
import 'package:weiman/crawler/http.dart';
import 'package:weiman/db/group.dart';
part 'book.g.dart';
const BookName = 'book';
enum BookUpdateStatus {
not, //
no, //
had, //
fail, //
wait, //
loading, //
old, //
}
@HiveType(typeId: 1)
class Book extends HiveObject {
static Box<Book> bookBox;
@HiveField(0)
String aid;
@HiveField(1)
String name;
@HiveField(2)
String avatar;
@HiveField(3)
List<String> authors;
@HiveField(4)
String description;
@HiveField(5)
int chapterCount;
// [][]
int newChapterCount;
BookUpdateStatus status;
List<Chapter> chapters;
List<String> tags;
@HiveField(6)
bool favorite;
@HiveField(7)
bool needUpdate;
@HiveField(8)
bool hasUpdate;
@HiveField(9)
DateTime updatedAt;
//
@HiveField(10)
int quick;
@HiveField(11)
Map<String, dynamic> _history;
@HiveField(12)
int groupId;
@HiveField(13)
String httpId;
bool look = false;
Group get group =>
groupId == null ? null : Group.groupBox.get(groupId, defaultValue: null);
HttpBook get http => MyHttpClient.clients[httpId];
History get history => History.fromJson(_history);
Future setFavorite(bool value) {
favorite = value;
return save();
}
Future setHistory(Chapter value) {
if (value == null) {
_history = null;
} else {
_history = History.fromChapter(value).toJson();
}
return save();
}
Book({
this.httpId,
this.aid,
this.name,
this.groupId,
this.avatar,
this.authors,
this.description,
this.chapterCount,
this.favorite = false,
this.needUpdate = false,
this.quick,
this.chapters = const [],
this.tags = const [],
Map<String, dynamic> history,
}) : _history = history;
@override
String toString() {
return 'Book:${toJson()}';
}
toJson() {
return {
'key': key,
'aid': aid,
'name': name,
'httpId': httpId,
'groupId': groupId,
'favorite': favorite,
'history': _history,
'status': status,
'chapterCount': chapterCount,
};
}
bool needToSave() {
return favorite == true || _history != null || quick != null;
}
@override
Future<void> save() {
if (needToSave()) {
return bookBox.put(aid, this);
}
return bookBox.delete(aid);
}
Future<bool> load() async {
if (httpId == null) return false;
final newBook = await this.http.getBook(aid);
print('load newBook:${newBook.httpId}');
chapters = newBook.chapters;
chapterCount = newBook.chapterCount;
authors = newBook.authors;
description = newBook.description;
httpId = newBook.httpId;
tags = newBook.tags;
print('book httpId $httpId');
return true;
}
Future<List<String>> loadChapter(Chapter chapter) async {
if (httpId == null) return null;
return this.http.getChapterImages(this, chapter);
}
Future<void> update() async {
try {
final newBook = await this.http.getBook(aid);
print('$name$chapterCount${newBook.chapterCount}');
newChapterCount = newBook.chapterCount - chapterCount;
status = newChapterCount > 0 ? BookUpdateStatus.had : BookUpdateStatus.no;
} catch (e) {
status = BookUpdateStatus.fail;
}
print('book update $status');
}
}

80
lib/db/book.g.dart Normal file
View File

@ -0,0 +1,80 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'book.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class BookAdapter extends TypeAdapter<Book> {
@override
final int typeId = 1;
@override
Book read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Book(
httpId: fields[13] as String,
aid: fields[0] as String,
name: fields[1] as String,
groupId: fields[12] as int,
avatar: fields[2] as String,
authors: (fields[3] as List)?.cast<String>(),
description: fields[4] as String,
chapterCount: fields[5] as int,
favorite: fields[6] as bool,
needUpdate: fields[7] as bool,
quick: fields[10] as int,
)
..hasUpdate = fields[8] as bool
..updatedAt = fields[9] as DateTime
.._history = (fields[11] as Map)?.cast<String, dynamic>();
}
@override
void write(BinaryWriter writer, Book obj) {
writer
..writeByte(14)
..writeByte(0)
..write(obj.aid)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.avatar)
..writeByte(3)
..write(obj.authors)
..writeByte(4)
..write(obj.description)
..writeByte(5)
..write(obj.chapterCount)
..writeByte(6)
..write(obj.favorite)
..writeByte(7)
..write(obj.needUpdate)
..writeByte(8)
..write(obj.hasUpdate)
..writeByte(9)
..write(obj.updatedAt)
..writeByte(10)
..write(obj.quick)
..writeByte(11)
..write(obj._history)
..writeByte(12)
..write(obj.groupId)
..writeByte(13)
..write(obj.httpId);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BookAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

36
lib/db/group.dart Normal file
View File

@ -0,0 +1,36 @@
import 'package:hive/hive.dart';
import 'book.dart';
part 'group.g.dart';
const GroupName = 'group';
@HiveType(typeId: 0)
class Group extends HiveObject {
static Box<Group> groupBox;
static Box<Book> bookBox;
@HiveField(0)
String name;
@HiveField(1)
bool expended;
Group(this.name, [this.expended = false]);
List<Book> get books => bookBox.values
.where((book) => book.favorite && book.groupId == this.key)
.toList();
@override
String toString() {
return 'Group:${{'key': key, 'name': name, 'books': books.length}}';
}
@override
Future<void> save() {
if (!isInBox) return groupBox.add(this);
return super.save();
}
}

44
lib/db/group.g.dart Normal file
View File

@ -0,0 +1,44 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'group.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class GroupAdapter extends TypeAdapter<Group> {
@override
final int typeId = 0;
@override
Group read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Group(
fields[0] as String,
fields[1] as bool,
);
}
@override
void write(BinaryWriter writer, Group obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.expended);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GroupAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

17
lib/db/historyOffset.dart Normal file
View File

@ -0,0 +1,17 @@
import 'package:hive/hive.dart';
const HistoryOffsetName = 'history';
class HistoryOffset {
static Box box;
static double get(String cid) {
print('get $cid');
return box.get(cid) ?? 0.0;
}
static Future<void> save(String cid, double offset) {
print('save $cid $offset');
return box.put(cid, offset);
}
}

79
lib/db/setting.dart Normal file
View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:weiman/crawler/http.dart';
import 'package:weiman/crawler/http18Comic.dart';
enum HideOption {
none,
auto,
always,
}
class Setting with ChangeNotifier {
static final String name = 'setting';
static Box settingBox;
Http18Comic http;
Setting() {
MyHttpClient.init(getProxy(), 10000);
}
HideOption getHideOption() {
final index =
settingBox.get('hideOption', defaultValue: HideOption.auto.index);
return HideOption.values[index];
}
Future setHideOption(HideOption option) async {
await settingBox.put('hideOption', option.index);
notifyListeners();
}
String getProxy() {
print('getProxy');
return settingBox.get('proxy', defaultValue: null);
}
Future setProxy(String proxy) async {
print('db/setting.setProxy $proxy');
await settingBox.put('proxy', proxy);
MyHttpClient.init(proxy, 10000);
notifyListeners();
}
ThemeMode getThemeMode() {
final int index = settingBox.get('theme', defaultValue: -1);
if (index == -1) return ThemeMode.system;
return ThemeMode.values[index];
}
Future setThemeMode(ThemeMode mode) {
return settingBox.put('theme', mode.index);
}
void refresh() {
notifyListeners();
}
Http18Comic getHttp() {
final String name =
settingBox.get('http', defaultValue: baseUrls.keys.first);
final http = Http18Comic(baseUrls[name], name: name, headers: headers);
setProxy(getProxy());
return http;
}
Future setHttp(HttpBook http) async {
await settingBox.put('http', http.name);
notifyListeners();
}
bool getViewerSwitch() {
return settingBox.get('viewerSwitch', defaultValue: true);
}
Future setViewerSwitch(bool value) async {
await settingBox.put('viewerSwitch', value);
notifyListeners();
}
}

View File

@ -1,85 +1,53 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'dart:math';
import 'package:draggable_container/draggable_container.dart';
import 'package:dynamic_theme/dynamic_theme.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:extended_image/extended_image.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart';
import 'package:flutter/material.dart' hide NestedScrollView;
import 'package:flutter/scheduler.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:html/parser.dart' as html;
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:oktoast/oktoast.dart';
import 'package:package_info/package_info.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'
hide CircularProgressIndicator;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sticky_headers/sticky_headers/widget.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart' as image_provider;
part './activities/book.dart';
part './activities/chapter.dart';
part './activities/checkData.dart';
part './activities/home.dart';
part './activities/rank.dart';
part './activities/search.dart';
part './activities/test.dart';
part './classes/book.dart';
part './classes/data.dart';
part './classes/http.dart';
part './classes/networkImageSSL.dart';
part './widgets/book.dart';
part './widgets/favorites.dart';
part './widgets/histories.dart';
part './widgets/pullToRefreshHeader.dart';
part './widgets/quick.dart';
part './widgets/sliverExpandableGroup.dart';
part './widgets/utils.dart';
part 'utils.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:weiman/activities/dataConvert.dart';
import 'package:weiman/activities/home.dart';
import 'package:weiman/classes/data.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/group.dart';
import 'package:weiman/db/historyOffset.dart';
import 'package:weiman/db/setting.dart';
import 'package:weiman/provider/favoriteData.dart';
import 'package:weiman/provider/theme.dart';
FirebaseAnalytics analytics;
FirebaseAnalyticsObserver observer;
const bool isDevMode = !bool.fromEnvironment('dart.vm.product');
int version;
BoxDecoration border;
Directory imageCacheDir;
String imageCacheDirPath;
PackageInfo packageInfo;
void main() async {
print("开发模式 $isDevMode");
FlutterError.onError = (FlutterErrorDetails details) {};
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
getTemporaryDirectory().then((dir) {
imageCacheDir = Directory(path.join(dir.path, 'images'));
imageCacheDirPath = imageCacheDir.path;
if (imageCacheDir.existsSync() == false) imageCacheDir.createSync();
print('图片缓存目录 $imageCacheDirPath');
});
try {
analytics = FirebaseAnalytics();
@ -87,47 +55,80 @@ void main() async {
} catch (e) {}
await Future.wait([
Hive.initFlutter(),
Data.init(),
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
]);
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
UserAgentClient.init();
runApp(Main(packageInfo: packageInfo));
Hive.registerAdapter<Group>(GroupAdapter());
Hive.registerAdapter<Book>(BookAdapter());
await Future.wait([
Hive.openBox<Group>(GroupName).then((value) => Group.groupBox = value),
Hive.openBox<Book>(BookName)
.then((value) => Book.bookBox = Group.bookBox = value),
Hive.openBox(HistoryOffsetName).then((value) => HistoryOffset.box = value),
Hive.openBox(Setting.name).then((value) => Setting.settingBox = value),
]);
packageInfo = await PackageInfo.fromPlatform();
version = int.parse(packageInfo.buildNumber);
runApp(Main());
}
class Main extends StatefulWidget {
final PackageInfo packageInfo;
const Main({Key key, this.packageInfo}) : super(key: key);
@override
_Main createState() => _Main();
}
class _Main extends State<Main> with WidgetsBindingObserver {
static BoxDecoration _border;
@override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void didChangePlatformBrightness() {
super.didChangePlatformBrightness();
Provider.of<ThemeProvider>(context, listen: false).update(context);
}
@override
Widget build(BuildContext context) {
if (_border == null)
_border = BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context, color: Colors.grey)));
return DynamicTheme(
defaultBrightness: Brightness.dark,
data: (brightness) => new ThemeData(
brightness: brightness,
border = BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context, color: Colors.grey)));
return OKToast(
child: MultiProvider(
providers: [
ChangeNotifierProvider<Setting>(
lazy: false,
create: (_) => Setting(),
),
ChangeNotifierProvider<FavoriteData>(
lazy: false,
create: (_) => FavoriteData(),
),
ChangeNotifierProvider<ThemeProvider>(
lazy: true,
create: (_) => ThemeProvider(_),
),
],
child: Consumer<ThemeProvider>(
builder: (_, theme, __) => MaterialApp(
title: '微漫 v${packageInfo.version}',
themeMode: theme.themeMode,
theme: ThemeData.light(),
darkTheme: ThemeData(
brightness: Brightness.dark,
accentColor: Colors.redAccent,
),
themedWidgetBuilder: (context, theme) {
return OKToast(
child: MaterialApp(
title: 'Flutter Demo',
theme: theme,
home: ActivityHome(widget.packageInfo),
),
);
});
home: Data.hasData() ? ActivityDataConvert() : ActivityHome(),
// home: ActivityHome(),
debugShowCheckedModeBanner: isDevMode,
navigatorObservers: <NavigatorObserver>[observer],
),
),
),
);
}
}

View File

@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/group.dart';
class FavoriteData extends ChangeNotifier {
final List<Book> all = [], others = [];
final Map<Group, List<Book>> groups = {};
FavoriteData() {
loadBooksList();
}
Future<void> loadBooksList([notify = false]) async {
final groupList = Group.groupBox.values.toList();
final groupMap = {for (final group in groupList) group.key: group};
groups.clear();
groupList.forEach((group) {
groups[group] = [];
});
all.clear();
others.clear();
// if(isDevMode){
// final temp = [
// Book(
// aid: '180454',
// name: '朋友,女朋友',
// avatar:
// 'https://cdn-msp.18comic.org/media/albums/206567.jpg',
// chapterCount: 0,
// httpId: '18',
// needUpdate: false,
// authors: [],
// ),
// Book(
// aid: '206567',
// name: '抑欲人妻',
// avatar:
// 'https://cdn-msp.18comic.org/media/albums/206567.jpg',
// chapterCount: 0,
// httpId: '18',
// needUpdate: true,
// authors: [],
// ),
// Book(
// aid: '147335',
// name: '亲爱的大叔',
// avatar:
// 'https://cdn-msp.msp-comic.xyz/media/albums/147335.jpg',
// chapterCount: 0,
// httpId: '18',
// needUpdate: true,
// authors: [],
// ),
// ];
// all.addAll(temp);
// others.addAll(temp);
// }
Book.bookBox.values.forEach((book) {
if (book.favorite != true) return;
all.add(book);
if (groupMap.containsKey(book.groupId)) {
//
groups[groupMap[book.groupId]].add(book);
} else {
//
others.add(book);
}
});
print({'all': all.length, 'other': others.length});
if (notify) notifyListeners();
}
Future<int> checkUpdate() async {
final groupList = [others, ...groups.values];
for (final array in groupList) {
for (final book in array) {
if (book.httpId == null) {
book.status = BookUpdateStatus.old;
} else if (book.needUpdate != true) {
book.status = BookUpdateStatus.not;
} else {
book.status = BookUpdateStatus.wait;
}
notifyListeners();
if (book.status != BookUpdateStatus.wait) continue;
book.status = BookUpdateStatus.loading;
notifyListeners();
await book.update();
if (book.status == BookUpdateStatus.had) sort(array, book);
notifyListeners();
}
}
return all.where((book) => book.status == BookUpdateStatus.had).length;
}
///
void sort(List<Book> array, Book book) {
print('sort ${book.name}');
array.remove(book);
array.insert(0, book);
}
Future<void> deleteBook(Book book) async {
book.favorite = false;
await book.save();
// print('删书 ${book.name} 成功');
loadBooksList(true);
}
Future<void> deleteGroup(Group group, [bool deleteBooks = false]) async {
if (deleteBooks && groups.containsKey(group)) {
await Future.wait(groups[group].map((book) => book.setFavorite(false)));
}
await Group.groupBox.delete(group.key);
await loadBooksList(true);
}
Future<void> addGroup(Group group) async {
group.save();
await loadBooksList(true);
}
Future<void> addBook(Book book) async {
book.favorite = true;
await book.save();
loadBooksList(true);
}
}

29
lib/provider/theme.dart Normal file
View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weiman/db/setting.dart';
class ThemeProvider extends ChangeNotifier {
ThemeMode themeMode = ThemeMode.system; //
ThemeProvider(BuildContext context) {
themeMode = Provider.of<Setting>(context, listen: false).getThemeMode();
}
void changeTheme(ThemeMode mode) {
print('改变主题 $mode');
themeMode = mode;
notifyListeners();
}
void update(BuildContext context) {
final bright = MediaQuery.platformBrightnessOf(context);
switch (bright) {
case Brightness.light:
changeTheme(ThemeMode.light);
break;
case Brightness.dark:
changeTheme(ThemeMode.dark);
}
print('update $bright');
}
}

View File

@ -1,9 +1,27 @@
part of 'main.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:weiman/activities/book/book.dart';
import 'package:weiman/activities/chapter/activity.dart';
import 'package:weiman/activities/search/search.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/db/book.dart';
final weekTime = Duration.millisecondsPerDay * 7;
void openBook(BuildContext context, Book book, String heroTag) {
Navigator.push(
void openSearch(BuildContext context, String word) {}
Future openBook(BuildContext context, Book book, String heroTag) {
print('openBook $book');
if (book.http == null) {
return 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}'),
@ -12,8 +30,8 @@ void openBook(BuildContext context, Book book, String heroTag) {
);
}
void openChapter(BuildContext context, Book book, Chapter chapter) {
Navigator.push(
Future<void> openChapter(BuildContext context, Book book, Chapter chapter) {
return Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(
@ -22,3 +40,11 @@ void openChapter(BuildContext context, Book book, Chapter chapter) {
),
);
}
void showStatusBar() {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
void hideStatusBar() {
SystemChrome.setEnabledSystemUIOverlays([]);
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:sa_anicoto/sa_anicoto.dart';
class AnimatedLogoWidget extends StatefulWidget {
final double width, height;
const AnimatedLogoWidget({
Key key,
@required this.width,
@required this.height,
}) : super(key: key);
@override
_AnimatedLogoWidget createState() => _AnimatedLogoWidget();
}
class _AnimatedLogoWidget extends State<AnimatedLogoWidget>
with AnimationMixin {
Animation<double> size; // Declare animation variable
@override
void initState() {
size = Tween<double>(begin: 0, end: widget.height - 20).animate(controller);
controller.mirror(
duration: Duration(seconds: 1)); // Start the animation playback
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: size.value,
child: Image.asset(
'assets/logo.png',
width: 20,
height: 20,
),
),
],
),
);
}
}

View File

@ -1,4 +1,10 @@
part of '../main.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:weiman/classes/chapter.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/utils.dart';
class WidgetBook extends StatelessWidget {
final Book book;
@ -14,7 +20,7 @@ class WidgetBook extends StatelessWidget {
@override
Widget build(BuildContext context) {
var isLiked = book.isFavorite();
final isLiked = book.favorite;
return ListTile(
title: Text(
book.name,
@ -28,25 +34,24 @@ class WidgetBook extends StatelessWidget {
),
dense: true,
leading: Hero(
tag: 'bookAvatar${book.aid}',
child: Image(image:NetworkImageSSL(
book.avatar),
height: 200,
fit: BoxFit.scaleDown,
)),
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) onTap(book);
if (onTap != null) return onTap(book);
openBook(context, book, 'bookAvatar${book.aid}');
},
);
}
}
final dateFormat = DateFormat('yyyy-MM-dd');
class WidgetChapter extends StatelessWidget {
static final double height = kToolbarHeight;
final Chapter chapter;
@ -57,7 +62,7 @@ class WidgetChapter extends StatelessWidget {
Key key,
this.chapter,
this.onTap,
this.read,
this.read = false,
}) : super(key: key);
@override
@ -78,16 +83,14 @@ class WidgetChapter extends StatelessWidget {
title: RichText(
text: TextSpan(
children: children,
style: Theme.of(context).textTheme.body1,
style: Theme.of(context).textTheme.bodyText2,
),
softWrap: true,
maxLines: 2,
),
leading: Image(image:NetworkImageSSL(
chapter.avatar),
fit: BoxFit.fitWidth,
width: 100,
),
subtitle: chapter.time == null
? null
: Text('更新时间 ${dateFormat.format(chapter.time)}'),
);
}
}
@ -106,8 +109,8 @@ class WidgetHistory extends StatelessWidget {
if (onTap != null) onTap(book);
},
title: Text(book.name),
leading: Image(image:NetworkImageSSL(
book.avatar),
leading: Image(
image: ExtendedNetworkImageProvider(book.avatar, cache: true),
fit: BoxFit.fitHeight,
),
subtitle: Text(book.history.cname),
@ -136,18 +139,18 @@ class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
}
void load() async {
loading = true;
try {
final book = await UserAgentClient.instance
.getBook(aid: widget.book.aid)
.timeout(Duration(seconds: 2));
news = book.chapterCount - widget.book.chapterCount;
hasError = false;
} catch (e) {
hasError = true;
}
loading = false;
setState(() {});
// 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
@ -173,7 +176,9 @@ class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
openBook(context, widget.book, 'checkBook${widget.book.aid}'),
leading: Hero(
tag: 'checkBook${widget.book.aid}',
child: Image(image:NetworkImageSSL(widget.book.avatar)),
child: Image(
image:
ExtendedNetworkImageProvider(widget.book.avatar, cache: true)),
),
dense: true,
isThreeLine: true,

103
lib/widgets/bookGroup.dart Normal file
View File

@ -0,0 +1,103 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:weiman/db/group.dart';
class BookGroupHeader extends StatefulWidget {
final Group group;
final int count;
final List<Widget> actions;
final Color divideColor;
final double height;
final IndexedWidgetBuilder builder;
final List<Widget> slideActions;
const BookGroupHeader({
Key key,
@required this.group,
@required this.count,
@required this.builder,
this.actions = const [],
this.divideColor = Colors.grey,
this.height = kToolbarHeight,
this.slideActions,
}) : assert(group != null),
assert(builder != null),
super(key: key);
@override
_State createState() => _State();
}
class _State extends State<BookGroupHeader> {
bool expended;
@override
void initState() {
expended = widget.group.expended ?? false;
super.initState();
}
@override
Widget build(BuildContext context) {
Decoration _decoration = BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context, color: widget.divideColor),
),
);
Widget header = InkWell(
child: Container(
height: widget.height,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
),
child: Row(children: [
Transform.rotate(
angle: expended ? 0 : math.pi,
child: Icon(
Icons.arrow_drop_down,
color: Colors.grey,
),
),
Expanded(child: Text('${widget.group.name}(${widget.count})')),
...widget.actions,
]),
),
onTap: () {
expended = !expended;
widget.group
..expended = expended
..save();
setState(() {});
},
);
if (widget.slideActions != null && widget.slideActions.length > 0) {
header = Slidable(
child: header,
actionPane: SlidableDrawerActionPane(),
secondaryActions: widget.slideActions,
);
}
return SliverStickyHeader(
header: header,
sliver: expended
? 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,
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/group.dart';
import 'package:weiman/provider/favoriteData.dart';
import 'package:weiman/widgets/groupFormDialog.dart';
Future showBookSettingDialog(BuildContext context, Book book) {
return showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('藏书《${book.name}》的设置'),
scrollable: true,
content: WidgetSetting(book: book),
),
);
}
class WidgetSetting extends StatefulWidget {
final Book book;
const WidgetSetting({Key key, this.book}) : super(key: key);
@override
_WidgetSetting createState() => _WidgetSetting();
}
class _WidgetSetting extends State<WidgetSetting> {
static final updateMenus = {true: '自动', false: '不检查'};
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: ListTile.divideTiles(context: context, tiles: [
ListTile(
title: Text('检查更新'),
trailing: DropdownButton<bool>(
value: widget.book.needUpdate,
items: updateMenus.keys
.map((key) =>
DropdownMenuItem(value: key, child: Text(updateMenus[key])))
.toList(),
onChanged: changeUpdate,
),
),
ListTile(
title: Text('分组'),
trailing: DropdownButton<Group>(
hint: Text('没有分组'),
value: widget.book.group,
items: [
DropdownMenuItem(
child: Text('新建'),
value: null,
),
...Group.groupBox.values
.map((e) => DropdownMenuItem(value: e, child: Text(e.name)))
.toList(),
],
onChanged: changeGroup,
),
),
]).toList(),
);
}
changeUpdate(bool needUpdate) async {
widget.book.needUpdate = needUpdate;
await widget.book.save();
setState(() {});
}
changeGroup(Group group) async {
if (group == null) {
group = await showGroupFormDialog(context);
}
widget.book.groupId = group == null ? widget.book.groupId : group.key;
await widget.book.save();
setState(() {});
}
changeFavorite() async {
await widget.book.setFavorite(!widget.book.favorite);
setState(() {});
}
removeHistory() async {
if (widget.book.history != null) await widget.book.setHistory(null);
setState(() {});
}
@override
void setState(fn) {
final fav = Provider.of<FavoriteData>(context, listen: false);
fav.loadBooksList(true);
super.setState(fn);
}
}

View File

@ -0,0 +1,314 @@
import 'package:dio/dio.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:html/parser.dart';
import 'package:provider/provider.dart';
import 'package:weiman/crawler/http.dart';
import 'package:weiman/crawler/http18Comic.dart';
import 'package:weiman/db/setting.dart';
class CheckConnectWidget extends StatefulWidget {
@override
_CheckConnectWidget createState() => _CheckConnectWidget();
}
class _CheckConnectWidget extends State<CheckConnectWidget> {
LoadState state = LoadState.loading;
final List<_Check> https = [];
String lastProxy;
@override
void initState() {
final setting = Provider.of<Setting>(context, listen: false);
lastProxy = setting.getProxy();
createHttps();
super.initState();
setting.addListener(() {
final proxy = setting.getProxy();
if (lastProxy != proxy) {
lastProxy = proxy;
createHttps();
}
});
}
void createHttps() {
print('重建http池 proxy:$lastProxy');
https.clear();
https.addAll(
baseUrls.keys.map(
(key) => _Check(
name: key,
url: baseUrls[key],
proxy: lastProxy,
),
),
);
check();
}
void check() async {
setState(() {
state = LoadState.loading;
});
https.forEach((http) => http.load());
await Future.wait(https.map((http) => http.load()));
final bool hasCompleted =
https.where((http) => http.state == LoadState.completed).isNotEmpty;
state = hasCompleted ? LoadState.completed : LoadState.failed;
if (hasCompleted) {
final sort = https.toList()..sort((a, b) => a.time.compareTo(b.time));
Http18Comic.instance = sort.first.http;
}
setState(() {});
}
void _showDialog(String title) async {
await showDialog(
context: context,
builder: (_) => Dialog(
title: title,
https: https,
retry: check,
),
);
}
@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 = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: Icon(Icons.error, color: Colors.red),
),
SizedBox(width: 10),
Text('连接不上漫画网站,点击查看错误'),
],
);
break;
default:
row = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: Icon(Icons.check_circle, color: Colors.green),
),
SizedBox(width: 10),
Text('成功连接到漫画网站,点击查看结果'),
],
);
}
return Padding(
padding: EdgeInsets.only(top: 10, bottom: 15),
child: GestureDetector(
child: row,
onTap: () => _showDialog('测试结果,选择源'),
),
);
}
}
class Dialog extends StatefulWidget {
final String title;
final List<_Check> https;
final Function retry;
const Dialog({Key key, this.title, this.https, this.retry}) : super(key: key);
@override
State<StatefulWidget> createState() => _Dialog();
}
class _Dialog extends State<Dialog> {
@override
Widget build(BuildContext context) {
final proxy = widget.https[0].proxy;
return AlertDialog(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.title),
if (proxy != null)
Text('正在使用代理:$proxy', style: TextStyle(fontSize: 14)),
],
),
content: Container(
width: 300,
height: 300,
child: ListView(
physics: ClampingScrollPhysics(),
shrinkWrap: true,
children: ListTile.divideTiles(
context: context,
tiles: widget.https.map(
(e) => e.build(onTap: () => setState(() {})),
)).toList(),
),
),
actions: [
FlatButton(
child: Text('再次测试'),
onPressed: () {
widget.retry();
setState(() {});
},
),
],
);
}
}
class _Check {
final String name;
final String proxy;
Http18Comic http;
Future future;
Duration time;
String error;
LoadState state;
_Check({
String url,
@required this.name,
@required this.proxy,
}) {
http = Http18Comic(
url,
name: name,
headers: headers,
proxy: proxy,
);
}
Future load() {
future = this._load();
return future;
}
Future _load() async {
state = LoadState.loading;
final now = DateTime.now();
try {
final Response<String> res = await http.dio.get<String>('/');
final $ = parse(res.data);
final $title = $.querySelector('title');
if (res.data.contains('Restricted') ||
$title == null ||
$title.text.indexOf('禁漫天堂') == -1) {
throw DioError(
request: res.request,
response: res,
error: '你使用的IP被漫画网站禁止访问请更换网络IP\n不要使用日本IP。',
);
}
state = LoadState.completed;
} catch (e) {
print(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();
}
if (error.response?.data != null) {
this.error += '\n接收到的内容:\n' + error.response.data;
}
} else {
this.error = e.toString();
}
state = LoadState.failed;
print('$name 结果 $state');
}
time = DateTime.now().difference(now);
}
Widget build({Function onTap}) {
return FutureBuilder(
future: future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
final Widget title = Text(name);
switch (snapshot.connectionState) {
case ConnectionState.active:
case ConnectionState.waiting:
return ListTile(
title: title,
subtitle: Row(children: [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
SizedBox(width: 5),
Text('读取中'),
]),
);
break;
case ConnectionState.done:
if (state == LoadState.failed) {
return ListTile(
title: title,
subtitle: Text('连接失败,点击查看原因'),
onTap: () {
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text('$name 错误内容'),
content: Text(error),
);
});
},
);
}
final _time = time.inMilliseconds;
final timeString = _time > 1000
? '${(time.inMilliseconds / 1000).toStringAsFixed(2)}'
: '${time.inMilliseconds} 毫秒';
return CheckboxListTile(
title: title,
subtitle: Text('连接成功\n耗时:$timeString'),
isThreeLine: true,
value: Http18Comic.instance?.name == name,
onChanged: (name) {
Http18Comic.instance = http;
MyHttpClient.clients[http.id] = http;
onTap();
},
);
break;
default:
return ListTile(title: title, subtitle: Text('还没有开始网络请求'));
}
},
);
}
}

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: []);
}
}

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weiman/db/group.dart';
import 'package:weiman/provider/favoriteData.dart';
Future showDeleteGroupDialog(BuildContext context, Group group) {
return showDialog(
context: context,
builder: (_) => DeleteGroupWidget(group: group),
);
}
class DeleteGroupWidget extends StatefulWidget {
final Group group;
const DeleteGroupWidget({Key key, this.group}) : super(key: key);
@override
_DeleteGroupWidget createState() => _DeleteGroupWidget();
}
class _DeleteGroupWidget extends State<DeleteGroupWidget> {
bool deleteBooks = false;
@override
Widget build(BuildContext context) {
final length = widget.group.books.length;
return AlertDialog(
title: Text('删除分组 ${widget.group.name}'),
scrollable: true,
content: Column(
children: ListTile.divideTiles(context: context, tiles: [
if (length > 0)
ListTile(
title: Text('删除藏书'),
subtitle: Text('$length 本藏书'),
trailing: Checkbox(
value: deleteBooks,
onChanged: (v) => setState(() => deleteBooks = v),
),
)
]).toList(),
),
actions: [
FlatButton(
child: Text('确认'),
onPressed: () async {
await Provider.of<FavoriteData>(context, listen: false)
.deleteGroup(widget.group, deleteBooks);
Navigator.pop(context);
},
),
RaisedButton(
child: Text('取消'), onPressed: () => Navigator.pop(context)),
],
);
}
}

View File

@ -1,4 +1,21 @@
part of '../main.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:oktoast/oktoast.dart';
import 'package:provider/provider.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/db/group.dart';
import 'package:weiman/provider/favoriteData.dart';
import 'package:weiman/utils.dart';
import 'package:weiman/widgets/bookGroup.dart';
import 'package:weiman/widgets/bookSettingDialog.dart';
import 'package:weiman/widgets/deleteGroupDialog.dart';
import 'package:weiman/widgets/groupFormDialog.dart';
import 'package:weiman/widgets/sliverExpandableGroup.dart';
import 'package:weiman/widgets/utils.dart';
class FavoriteList extends StatefulWidget {
@override
@ -6,178 +23,218 @@ class FavoriteList extends StatefulWidget {
}
class _FavoriteList extends State<FavoriteList> {
static final Map<String, int> hasNews = {};
static final List<Book> all = [], //
inWeek = [], // 7
other = []; //
static bool showTip = false;
static bool showTip = true;
static final loadFailTextSpan = TextSpan(
text: '读取失败,下拉刷新', style: TextStyle(color: Colors.redAccent)),
waitToCheck =
TextSpan(text: '等待检查更新', style: TextStyle(color: Colors.grey)),
unCheck =
TextSpan(text: '请下拉列表检查更新', style: TextStyle(color: Colors.grey)),
noUpdate = TextSpan(text: '没有更新', style: TextStyle(color: Colors.grey));
static void getBooks() {
all.clear();
inWeek.clear();
other.clear();
all.addAll(Data.getFavorites().values);
if (all.isNotEmpty) {
final now = DateTime.now().millisecondsSinceEpoch;
all.forEach((book) {
if (book.history != null && (now - book.history.time) < weekTime) {
inWeek.add(book);
} else {
other.add(book);
}
@override
initState() {
super.initState();
if (showTip) {
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
showToast(
'下拉收藏列表检查更新\n分组和藏书左滑显示更多操作',
textPadding: EdgeInsets.all(10),
duration: Duration(seconds: 4),
);
showTip = false;
});
}
}
@override
void initState() {
super.initState();
getBooks();
if (all.isNotEmpty) {
if (showTip == false) {
showTip = true;
showToast(
'下拉列表可以检查漫画更新',
backgroundColor: Colors.black.withOpacity(0.5),
);
}
}
}
void _openBook(book) {
openBook(context, book, 'fb ${book.aid}');
}
static Future<void> checkNews() async {
hasNews.clear();
Book currentBook, newBook;
int different;
for (var i = 0; i < all.length; i++) {
currentBook = all[i];
try {
newBook = await UserAgentClient.instance
.getBook(aid: currentBook.aid)
.timeout(Duration(seconds: 2));
different = newBook.chapterCount - currentBook.chapterCount;
hasNews[currentBook.aid] = different;
} catch (e) {
hasNews[currentBook.aid] = -1;
}
}
}
Widget bookBuilder(Book book) {
TextSpan state;
if (hasNews.isEmpty) {
state = unCheck;
} else {
if (hasNews.containsKey(book.aid)) {
if (hasNews[book.aid] > 0) {
state = TextSpan(
text: '${hasNews[book.aid]} 章更新',
style: TextStyle(color: Colors.green));
} else if (hasNews[book.aid] == -1) {
state = loadFailTextSpan;
} else if (hasNews[book.aid] == 0) {
state = noUpdate;
}
} else {
state = waitToCheck;
}
}
return FBookItem(
book: book,
subtitle: state,
onTap: _openBook,
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);
}),
],
),
);
print('删书 $sure');
if (sure != true) return;
await Provider.of<FavoriteData>(context, listen: false).deleteBook(book);
}
Future<void> deleteGroup(Group group) async {
await showDeleteGroupDialog(context, group);
setState(() {});
}
Future<void> groupRename(Group group) async {
await showGroupFormDialog(context, group);
setState(() {});
}
@override
Widget build(BuildContext context) {
List<Book> inWeekUpdated = [],
inWeekUnUpdated = [],
otherUpdated = [],
otherUnUpdated = [];
inWeek.forEach((book) {
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0)
inWeekUpdated.add(book);
else
inWeekUnUpdated.add(book);
return Consumer<FavoriteData>(builder: (_, favorite, __) {
if (favorite.all.isEmpty && favorite.groups.keys.isEmpty)
return Center(child: Text('没有收藏'));
return ClipRect(
child: RefreshIndicator(
onRefresh: favorite.checkUpdate,
child: SafeArea(
child: CustomScrollView(
slivers: [
...favorite.groups.keys.map((group) {
final list = favorite.groups[group];
return BookGroupHeader(
group: group,
count: list.length,
builder: (ctx, i) => bookBuilder(favorite.groups[group][i]),
slideActions: [
IconSlideAction(
caption: '重命名',
color: Colors.blue,
icon: Icons.edit,
onTap: () => groupRename(group),
),
IconSlideAction(
caption: '删除',
color: Colors.red,
icon: Icons.delete,
onTap: () => deleteGroup(group),
),
],
);
}),
SliverExpandableGroup(
title: Text('没有分组的藏书(${favorite.others.length})'),
expanded: false,
count: favorite.others.length,
builder: (ctx, i) => bookBuilder(favorite.others[i]),
),
],
),
),
),
);
});
other.forEach((book) {
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0)
otherUpdated.add(book);
else
otherUnUpdated.add(book);
});
return Drawer(
child: RefreshIndicator(
onRefresh: () async {
await checkNews();
setState(() {});
},
child: all.isEmpty
? Center(child: Text('没有收藏'))
: SafeArea(
child: CustomScrollView(
slivers: [
SliverExpandableGroup(
title: Text('7天内看过并且有更新的藏书(${inWeekUpdated.length})'),
expanded: true,
count: inWeekUpdated.length,
builder: (ctx, i) => bookBuilder(inWeekUpdated[i]),
),
SliverExpandableGroup(
title: Text('7天内看过的藏书(${inWeekUnUpdated.length})'),
count: inWeekUnUpdated.length,
builder: (ctx, i) => bookBuilder(inWeekUnUpdated[i]),
),
SliverExpandableGroup(
title: Text('有更新的藏书(${otherUpdated.length})'),
count: otherUpdated.length,
builder: (ctx, i) => bookBuilder(otherUpdated[i]),
),
SliverExpandableGroup(
title: Text('没有更新的藏书(${otherUnUpdated.length})'),
count: otherUnUpdated.length,
builder: (ctx, i) => bookBuilder(otherUnUpdated[i]),
),
],
)),
),
);
}
}
class FBookItem extends StatelessWidget {
final bookStatusWidgets = {
BookUpdateStatus.loading:
TextSpan(text: '正在读取网络数据', style: TextStyle(color: Colors.blue)),
BookUpdateStatus.not:
TextSpan(text: '该藏书设置为不更新', style: TextStyle(color: Colors.grey)),
BookUpdateStatus.no:
TextSpan(text: '该藏书没有新章节', style: TextStyle(color: Colors.grey)),
BookUpdateStatus.wait:
TextSpan(text: '处于更新队列,等待更新', style: TextStyle(color: Colors.grey)),
BookUpdateStatus.old:
TextSpan(text: '旧藏书不检查更新', style: TextStyle(color: Colors.redAccent)),
BookUpdateStatus.fail:
TextSpan(text: '网络问题,检查更新失败', style: TextStyle(color: Colors.redAccent)),
};
class FBookItem extends StatefulWidget {
final Book book;
final TextSpan subtitle;
final void Function(Book book) onTap;
final void Function(Book book) onDelete;
const FBookItem({
Key key,
@required this.book,
@required this.subtitle,
@required this.onTap,
@required this.onDelete,
}) : super(key: key);
@override
_FBookItem createState() => _FBookItem();
}
class _FBookItem extends State<FBookItem> {
@override
Widget build(BuildContext context) {
return ListTile(
onTap: () => onTap(book),
leading: Hero(
tag: 'fb ${book.aid}',
child: Image(image: NetworkImageSSL(book.avatar))),
title: Text(book.name, style: Theme.of(context).textTheme.body1),
subtitle: RichText(text: subtitle),
TextSpan subtitle =
bookStatusWidgets[widget.book.status ?? BookUpdateStatus.no];
if (widget.book.status == BookUpdateStatus.had) {
final _subtitle = '${widget.book.newChapterCount} 章更新';
subtitle = TextSpan(
text: _subtitle,
style: TextStyle(
color: widget.book.look ? Colors.grey : Colors.green,
),
);
}
return Slidable(
actionPane: SlidableDrawerActionPane(),
closeOnScroll: true,
actionExtentRatio: 0.25,
secondaryActions: [
IconSlideAction(
caption: '设置',
color: Colors.blue,
icon: Icons.settings,
onTap: () async {
final before = widget.book.needUpdate;
await showBookSettingDialog(context, widget.book);
if (before != widget.book.needUpdate) {
widget.book.status = widget.book.needUpdate
? BookUpdateStatus.no
: BookUpdateStatus.not;
}
if (mounted) setState(() {});
},
),
if (widget.book.status == BookUpdateStatus.had &&
widget.book.look == false)
IconSlideAction(
caption: '已读',
color: Colors.greenAccent,
iconWidget: SizedBox(
width: 24,
height: 24,
child: Icon(
FontAwesomeIcons.bellSlash,
size: 20,
color: Colors.white,
),
),
foregroundColor: Colors.white,
onTap: () async {
widget.book.chapterCount += widget.book.newChapterCount;
widget.book.look = true;
await widget.book.save();
setState(() {});
},
),
IconSlideAction(
caption: '删除',
color: Colors.red,
icon: Icons.delete,
onTap: () => widget.onDelete(widget.book),
),
],
child: ListTile(
onTap: () async {
await openBook(context, widget.book, 'fb ${widget.book.aid}');
setState(() {});
},
// onLongPress: () => onDelete(book),
leading: Hero(
tag: 'fb ${widget.book.aid}',
child: widget.book.http == null
? oldBookAvatar(text: '旧书', width: 50.0, height: 80.0)
: ExtendedImage(
image: NetworkImageSSL(widget.book.http, widget.book.avatar),
width: 50.0,
height: 80.0),
),
title: Text(widget.book.name),
subtitle: RichText(text: subtitle),
),
);
}
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:weiman/db/group.dart';
Future<Group> showGroupFormDialog(BuildContext context, [Group group]) {
return showDialog(
context: context,
builder: (_) {
return GroupFormDialog(group: group);
},
);
}
class GroupFormDialog extends StatefulWidget {
final Group group;
const GroupFormDialog({Key key, this.group}) : super(key: key);
@override
_GroupFormDialog createState() => _GroupFormDialog();
}
class _GroupFormDialog extends State<GroupFormDialog> {
final _form = GlobalKey<FormState>();
TextEditingController _nameController;
Group group;
@override
void initState() {
group = widget.group;
_nameController = TextEditingController(text: widget.group?.name ?? '');
super.initState();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.group == null ? '创建分组' : '分组重命名'),
content: Form(
key: _form,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
autofocus: true,
controller: _nameController,
decoration: InputDecoration.collapsed(
hintText: group == null ? '输入分组名称' : '原名 ${group.name}',
),
validator: (value) {
value = value.trim();
if (value.isEmpty) {
return '分组名称不能为空';
}
final sameName =
Group.groupBox.values.firstWhere((Group group) {
return group.name == value && group.key != this.group?.key;
}, orElse: () => null);
if (sameName != null) {
return '已经存在同名的分组';
}
return null;
},
)
],
),
),
actions: [
FlatButton(
child: Text('确认'),
onPressed: () async {
if (group == null) {
group = Group(_nameController.text);
} else {
group.name = _nameController.text;
}
await group.save();
Navigator.pop(context, group);
},
),
RaisedButton(
child: Text('取消'),
textColor: Colors.white,
color: Colors.blue,
onPressed: () {
Navigator.pop(context, group);
},
),
],
);
}
}

View File

@ -1,4 +1,14 @@
part of '../main.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:oktoast/oktoast.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/utils.dart';
import 'package:weiman/widgets/sliverExpandableGroup.dart';
import 'package:weiman/widgets/utils.dart';
class Histories extends StatefulWidget {
@override
@ -6,22 +16,33 @@ class Histories extends StatefulWidget {
}
class _Histories extends State<Histories> {
static bool _showTips = true;
final List<Book> inWeek = [], other = [];
@override
void initState() {
super.initState();
loadBook();
if (_showTips)
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
_showTips = false;
showToast(
'阅读记录和时间分组\n往左滑显示更多操作',
textPadding: EdgeInsets.all(10),
duration: Duration(seconds: 4),
);
});
}
void loadBook() {
inWeek.clear();
other.clear();
final list = Data.getHistories().values.toList();
final list =
Book.bookBox.values.where((book) => book.history != null).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) {
if ((now - book.history.time.millisecondsSinceEpoch) < weekTime) {
inWeek.add(book);
} else {
other.add(book);
@ -50,47 +71,78 @@ class _Histories extends State<Histories> {
print('清理历史 $inWeek $res');
if (res == false) return;
List<Book> list = inWeek ? this.inWeek : this.other;
list.forEach((book) => Data.removeHistoryFromBook(book));
await Future.wait(list.map((book) => book.setHistory(null)));
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: () async {
await book.setHistory(null);
setState(() {
array.remove(book);
});
},
),
],
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: CustomScrollView(
slivers: [
SliverExpandableGroup(
title: Text('7天内的浏览历史 (${inWeek.length})'),
expanded: true,
actions: [
FlatButton(
child: Text('清空'),
onPressed: inWeek.length == 0 ? null : () => clear(true),
),
],
count: inWeek.length,
builder: (ctx, i) => WidgetBook(
inWeek[i],
subtitle: inWeek[i].history.cname,
child: ClipRect(
child: CustomScrollView(
slivers: [
SliverExpandableGroup(
title: Text('7天内的浏览历史 (${inWeek.length})'),
expanded: true,
count: inWeek.length,
builder: (ctx, i) => book(inWeek, i),
slideActions: [
IconSlideAction(
caption: '清空',
color: Colors.red,
icon: Icons.delete,
onTap: () => clear(true),
),
],
),
),
SliverExpandableGroup(
title: Text('更早的浏览历史 (${other.length})'),
actions: [
FlatButton(
child: Text('清空'),
onPressed: other.length == 0 ? null : () => clear(false),
),
],
count: other.length,
builder: (ctx, i) => WidgetBook(
other[i],
subtitle: other[i].history.cname,
SliverExpandableGroup(
title: Text('更早的浏览历史 (${other.length})'),
count: other.length,
builder: (ctx, i) => book(other, i),
slideActions: [
IconSlideAction(
caption: '清空',
color: Colors.red,
icon: Icons.delete,
onTap: () => clear(false),
),
],
),
),
],
],
),
),
);
}

View File

@ -1,4 +1,6 @@
part of '../main.dart';
import 'package:flutter/material.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
import 'package:weiman/widgets/animatedLogo.dart';
class SliverPullToRefreshHeader extends StatelessWidget {
static final double height = kToolbarHeight * 2;
@ -17,56 +19,43 @@ class SliverPullToRefreshHeader extends StatelessWidget {
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),
),
),
]);
Widget widget;
if (info.mode == RefreshIndicatorMode.error) {
text.children.addAll([
TextSpan(
text: '读取失败\n当失败次数太多可能是网络出现问题\n',
style: TextStyle(
color: Colors.red,
widget = Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('读取网络数据失败\n你可能需要梯子'),
RaisedButton.icon(
icon: Icon(Icons.refresh),
onPressed: onTap,
label: Text('再次尝试'),
),
),
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: '读取中,请稍候'),
]);
widget = Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedLogoWidget(width: 20, height: 30),
SizedBox(width: 5),
Text('读取中,请稍候'),
],
);
} else if ([
RefreshIndicatorMode.drag,
RefreshIndicatorMode.armed,
RefreshIndicatorMode.snap
].contains(info.mode)) {
text.children.add(TextSpan(text: '重新读取'));
widget = Text('下拉刷新');
} else {
text.children.add(TextSpan(text: 'Bye~'));
widget = SizedBox();
}
return SliverToBoxAdapter(
child: Container(
height: dragOffset,
child: Center(
child: Text.rich(
text,
textAlign: TextAlign.center,
),
),
alignment: Alignment.center,
child: widget,
),
);
}

View File

@ -1,4 +1,11 @@
part of '../main.dart';
import 'package:draggable_container/draggable_container.dart';
import 'package:flutter/material.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/utils.dart';
import 'selectFavoriteBooks.dart';
import 'utils.dart';
class QuickBook extends DraggableItem {
static const heroTag = 'quickBookAvatar';
@ -11,27 +18,23 @@ class QuickBook extends DraggableItem {
{@required this.book, @required this.context}) {
child = GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (ctx) => ActivityBook(
book: book,
heroTag: '$heroTag ${book.aid}',
)));
openBook(context, book, '$heroTag ${book.aid}');
},
child: Stack(
children: <Widget>[
SizedBox(
width: width,
height: height,
child: Hero(
tag: '$heroTag ${book.aid}',
child: Image(
image: NetworkImageSSL(book.avatar),
fit: BoxFit.cover,
),
),
),
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,
@ -53,10 +56,6 @@ class QuickBook extends DraggableItem {
),
);
}
checkUpdate() {
UserAgentClient.instance.getBook(aid: book.aid);
}
}
class Quick extends StatefulWidget {
@ -75,46 +74,14 @@ class QuickState extends State<Quick> {
final int count = 8;
final List<DraggableItem> _draggableItems = [];
DraggableItem _addButton;
GlobalKey<DraggableContainerState> _key = GlobalKey();
final List<String> id = [];
GlobalKey<DraggableContainerState> _key =
GlobalKey<DraggableContainerState>();
double width = 0, height = 0;
void exit() {
_key.currentState.draggableMode = false;
}
_showSelectBookDialog() async {
final books = Data.getFavorites();
final list = books.values
.where((book) => !id.contains(book.aid))
.map((book) => ListTile(
title: Text(book.name),
leading: Image(image: NetworkImageSSL(book.avatar)),
onTap: () {
Navigator.pop(context, book);
},
));
return showDialog<Book>(
context: context,
builder: (_) {
return AlertDialog(
title: Text('将收藏的漫画添加到快速导航'),
content: Container(
width: double.maxFinite,
height: 300,
child: list.isNotEmpty
? ListView(
children: ListTile.divideTiles(
context: context,
tiles: list,
).toList(),
)
: Center(child: Text('没有了')),
),
);
});
}
QuickState() {
_addButton = DraggableItem(
deletable: false,
@ -141,9 +108,12 @@ class QuickState extends State<Quick> {
final buttonIndex = items.indexOf(_addButton);
print('add $buttonIndex');
if (buttonIndex > -1) {
final book = await _showSelectBookDialog();
final book = await showFavoriteBooksDialog(context);
print('选择了 $book');
if (book == null) return;
book
..quick = buttonIndex
..save();
_key.currentState.insteadOfIndex(buttonIndex,
QuickBook(width, height, book: book, context: context),
force: true);
@ -163,8 +133,18 @@ class QuickState extends State<Quick> {
width = widget.width / 4 - 10;
height = (width / 0.7).roundToDouble();
_draggableItems.addAll(Data.quickList().map((book) {
id.add(book.aid);
final list = <Book>[];
Book.bookBox.values.forEach((book) {
if (book.quick != null && list.length < count) {
list.add(book);
} else {
book.quick = null;
book.save();
}
});
print('quick book length ${list.length}');
list.sort((a, b) => a.quick.compareTo(b.quick));
_draggableItems.addAll(list.map((book) {
return QuickBook(width, height, book: book, context: context);
}));
if (_draggableItems.length < count) _draggableItems.add(_addButton);
@ -175,6 +155,7 @@ class QuickState extends State<Quick> {
@override
Widget build(BuildContext context) {
print('quick build');
return Column(
children: <Widget>[
Container(
@ -195,18 +176,25 @@ class QuickState extends State<Quick> {
boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]),
items: _draggableItems,
onDraggableModeChanged: widget.draggableModeChanged,
onBeforeDelete: (index, item) async {
if (item is QuickBook) {
print('on before delete ${item.book.name}');
item.book.quick = null;
item.book.save();
}
return true;
},
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);
print('显示添加按钮 1');
_key.currentState.insteadOfIndex(
nullIndex, _addButton,
triggerEvent: false, force: true);
print('显示添加按钮 2');
setState(() {});
} else if (nullIndex > -1 &&
buttonIndex > -1 &&
nullIndex < buttonIndex) {
@ -214,6 +202,15 @@ class QuickState extends State<Quick> {
_key.currentState
.insteadOfIndex(nullIndex, _addButton, triggerEvent: false);
}
var quick = 0;
items.forEach((item) {
if (item is QuickBook) {
item.book
..quick = quick
..save();
quick++;
}
});
},
),
],

View File

@ -0,0 +1,51 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weiman/classes/networkImageSSL.dart';
import 'package:weiman/db/book.dart';
import 'package:weiman/provider/favoriteData.dart';
Future<Book> showFavoriteBooksDialog(BuildContext context) {
return showDialog<Book>(
context: context,
builder: (_) => FavoriteBooksDialog(title: '将藏书添加到快速导航'),
);
}
class FavoriteBooksDialog extends StatelessWidget {
final String title;
const FavoriteBooksDialog({
Key key,
@required this.title,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final fav = Provider.of<FavoriteData>(context, listen: false);
return AlertDialog(
title: Text(title),
scrollable: true,
content: Column(
children: ListTile.divideTiles(
context: context,
tiles: fav.all
.where((book) => book.quick == null)
.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),
),
)
.toList(),
).toList(),
),
);
}
}

View File

@ -1,4 +1,8 @@
part of '../main.dart';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
class SliverExpandableBuilder {
final int count;
@ -15,6 +19,7 @@ class SliverExpandableGroup extends StatefulWidget {
final double height;
final int count;
final IndexedWidgetBuilder builder;
final List<Widget> slideActions;
const SliverExpandableGroup({
Key key,
@ -25,6 +30,7 @@ class SliverExpandableGroup extends StatefulWidget {
this.actions = const [],
this.divideColor = Colors.grey,
this.height = kToolbarHeight,
this.slideActions,
}) : assert(title != null),
assert(builder != null),
super(key: key);
@ -35,6 +41,7 @@ class SliverExpandableGroup extends StatefulWidget {
class _SliverExpandableGroup extends State<SliverExpandableGroup> {
bool _expanded;
@override
initState() {
super.initState();
@ -48,36 +55,44 @@ class _SliverExpandableGroup extends State<SliverExpandableGroup> {
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,
]),
Widget header = InkWell(
child: Container(
height: widget.height,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
),
onTap: () {
setState(() {
_expanded = !_expanded;
});
},
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;
});
},
);
if (widget.slideActions != null && widget.slideActions.length > 0) {
header = Slidable(
child: header,
actionPane: SlidableDrawerActionPane(),
secondaryActions: widget.slideActions,
);
}
return SliverStickyHeader(
header: header,
sliver: _expanded
? SliverList(
delegate: SliverChildBuilderDelegate((ctx, i) {
if (i < widget.count-1) {
if (i < widget.count - 1) {
return DecoratedBox(
decoration: _decoration,
child: widget.builder(context, i),

View File

@ -1,4 +1,5 @@
part of '../main.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class TextDivider extends StatelessWidget {
final String text;
@ -28,3 +29,17 @@ class TextDivider extends StatelessWidget {
);
}
}
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),
);
}

View File

@ -1,5 +1,9 @@
name: weiman
description: 微漫
description: 微漫App
# The following line prevents the package from being accidentally published to
# pub.dev using `pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
@ -11,40 +15,66 @@ description: 微漫
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.4
version: 1.1.4+2007
environment:
sdk: ">=2.3.0 <3.0.0"
sdk: ">=2.9.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dio: any
dio_http_cache: any
image: any
intl: any
async: any
cupertino_icons: any
http: any
encrypt: any
html: any
hive: any
sa_anicoto: any
hive_flutter: any
shared_preferences: any
fluttertoast: 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
loading_more_list: any
webview_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
e2e: any
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.3
dev_dependencies:
flutter_test:
sdk: flutter
hive_generator: any
build_runner: any
#dependency_overrides:
# analyzer: '0.39.14'
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@ -58,11 +88,12 @@ flutter:
uses-material-design: true
assets:
- images/logo.png
- 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
# - 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.