mirror of
https://github.com/nrop19/weiman_app.git
synced 2025-08-03 23:32:47 +08:00
Compare commits
No commits in common. "master" and "v1.0.7" have entirely different histories.
73
.gitignore
vendored
Normal file
73
.gitignore
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Android related
|
||||||
|
**/android/**/gradle-wrapper.jar
|
||||||
|
**/android/.gradle
|
||||||
|
**/android/captures/
|
||||||
|
**/android/gradlew
|
||||||
|
**/android/gradlew.bat
|
||||||
|
**/android/local.properties
|
||||||
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
|
|
||||||
|
# iOS/XCode related
|
||||||
|
**/ios/**/*.mode1v3
|
||||||
|
**/ios/**/*.mode2v3
|
||||||
|
**/ios/**/*.moved-aside
|
||||||
|
**/ios/**/*.pbxuser
|
||||||
|
**/ios/**/*.perspectivev3
|
||||||
|
**/ios/**/*sync/
|
||||||
|
**/ios/**/.sconsign.dblite
|
||||||
|
**/ios/**/.tags*
|
||||||
|
**/ios/**/.vagrant/
|
||||||
|
**/ios/**/DerivedData/
|
||||||
|
**/ios/**/Icon?
|
||||||
|
**/ios/**/Pods/
|
||||||
|
**/ios/**/.symlinks/
|
||||||
|
**/ios/**/profile
|
||||||
|
**/ios/**/xcuserdata
|
||||||
|
**/ios/.generated/
|
||||||
|
**/ios/Flutter/App.framework
|
||||||
|
**/ios/Flutter/Flutter.framework
|
||||||
|
**/ios/Flutter/Generated.xcconfig
|
||||||
|
**/ios/Flutter/app.flx
|
||||||
|
**/ios/Flutter/app.zip
|
||||||
|
**/ios/Flutter/flutter_assets/
|
||||||
|
**/ios/Flutter/flutter_export_environment.sh
|
||||||
|
**/ios/ServiceDefinitions.json
|
||||||
|
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!**/ios/**/default.mode1v3
|
||||||
|
!**/ios/**/default.mode2v3
|
||||||
|
!**/ios/**/default.pbxuser
|
||||||
|
!**/ios/**/default.perspectivev3
|
||||||
|
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
@ -1,11 +1,11 @@
|
|||||||
# 微漫 v1.1.4 [宣传页面](https://nrop19.github.io/weiman_app)
|
# weiman v1.0.6
|
||||||
|
|
||||||
### 微漫脱敏后的开源代码
|
### 微漫脱敏后的开源代码
|
||||||
|
|
||||||
#### 不解答任何代码上的问题
|
#### 不解答任何代码上的问题
|
||||||
|
|
||||||
#### App的问题请到 [Telegram群](https://t.me/boring_programer) 讨论
|
#### App的问题请到 [Telegram群讨论](https://t.me/boring_programer)
|
||||||
|
|
||||||
- 删除了android端文件夹,涉及到apk签名等敏感文件
|
- 删除了android端文件夹,涉及到apk签名等敏感文件
|
||||||
- 删除了ios端文件夹
|
- 删除了ios端文件夹
|
||||||
- 删除了lib/crawler/里的其它文件,保护被爬网站的同时防止被爬网站加大防爬难度。
|
- 删除了lib/classes/http.dart文件里的网站域名和爬虫逻辑,保护被爬网站的同时防止被爬网站加大防爬难度。
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
225
lib/activities/book.dart
Normal file
225
lib/activities/book.dart
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,274 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
380
lib/activities/chapter.dart
Normal file
380
lib/activities/chapter.dart
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
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);
|
||||||
|
}
|
@ -1,92 +0,0 @@
|
|||||||
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],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,261 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,110 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
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(() {});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
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();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,4 @@
|
|||||||
import 'dart:convert';
|
part of '../main.dart';
|
||||||
|
|
||||||
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 {
|
class ActivityCheckData extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -23,7 +17,7 @@ final titleTextStyle = TextStyle(fontSize: 14, color: Colors.blue),
|
|||||||
|
|
||||||
class _State extends State<ActivityCheckData> {
|
class _State extends State<ActivityCheckData> {
|
||||||
CheckState firstState;
|
CheckState firstState;
|
||||||
int firstLength = 0;
|
int firstLength;
|
||||||
final TextSpan secondResults = TextSpan();
|
final TextSpan secondResults = TextSpan();
|
||||||
TextEditingController _outputController, _inputController;
|
TextEditingController _outputController, _inputController;
|
||||||
|
|
||||||
@ -63,7 +57,7 @@ class _State extends State<ActivityCheckData> {
|
|||||||
final has = Data.has(Data.favoriteBooksKey);
|
final has = Data.has(Data.favoriteBooksKey);
|
||||||
if (has) {
|
if (has) {
|
||||||
final String str = Data.instance.getString(Data.favoriteBooksKey);
|
final String str = Data.instance.getString(Data.favoriteBooksKey);
|
||||||
final Map<String, Object> map = jsonDecode(str);
|
final Map<String, Object> map = json.decode(str);
|
||||||
firstLength = map.keys.length;
|
firstLength = map.keys.length;
|
||||||
_outputController.text = str;
|
_outputController.text = str;
|
||||||
}
|
}
|
||||||
|
@ -1,134 +0,0 @@
|
|||||||
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),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
part of '../main.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 {
|
class ActivityHome extends StatefulWidget {
|
||||||
|
final PackageInfo packageInfo;
|
||||||
|
|
||||||
|
const ActivityHome(this.packageInfo, {Key key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => HomeState();
|
State<StatefulWidget> createState() => HomeState();
|
||||||
}
|
}
|
||||||
@ -32,6 +14,7 @@ class HomeState extends State<ActivityHome> {
|
|||||||
final List<Widget> histories = [];
|
final List<Widget> histories = [];
|
||||||
final List<Book> quick = [];
|
final List<Book> quick = [];
|
||||||
final GlobalKey<QuickState> _quickState = GlobalKey();
|
final GlobalKey<QuickState> _quickState = GlobalKey();
|
||||||
|
static final weekTime = 7 * 24 * 3600000;
|
||||||
|
|
||||||
bool showFavorite = true;
|
bool showFavorite = true;
|
||||||
|
|
||||||
@ -43,18 +26,26 @@ class HomeState extends State<ActivityHome> {
|
|||||||
/// 提前检查一次藏书的更新情况
|
/// 提前检查一次藏书的更新情况
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||||
autoSwitchTheme();
|
autoSwitchTheme();
|
||||||
FavoriteData favData = Provider.of<FavoriteData>(context, listen: false);
|
_FavoriteList.getBooks();
|
||||||
await favData.loadBooksList();
|
await _FavoriteList.checkNews();
|
||||||
final updated = await favData.checkUpdate();
|
final updated = _FavoriteList.hasNews.values
|
||||||
|
.where((int updatedChapters) => updatedChapters > 0)
|
||||||
|
.length;
|
||||||
if (updated > 0)
|
if (updated > 0)
|
||||||
showToast(
|
showToast(
|
||||||
'$updated 本藏书有更新',
|
'$updated 本藏书有更新',
|
||||||
textPadding: EdgeInsets.all(10),
|
backgroundColor: Colors.black.withOpacity(0.5),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void autoSwitchTheme() async {}
|
void autoSwitchTheme() async {
|
||||||
|
final isDark = await DynamicTheme.of(context).loadBrightness();
|
||||||
|
final nowIsDark = DynamicTheme.of(context).brightness == Brightness.dark;
|
||||||
|
if (isDark != nowIsDark)
|
||||||
|
DynamicTheme.of(context)
|
||||||
|
.setBrightness(isDark ? Brightness.dark : Brightness.light);
|
||||||
|
}
|
||||||
|
|
||||||
void gotoSearch() {
|
void gotoSearch() {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
@ -68,15 +59,11 @@ class HomeState extends State<ActivityHome> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
settings: RouteSettings(name: '/activity_recommend'),
|
settings: RouteSettings(name: '/activity_recommend/'),
|
||||||
builder: (_) => ActivityRank(),
|
builder: (_) => ActivityRank(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void gotoPatreon() {
|
|
||||||
launch('https://www.patreon.com/nrop19');
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isEdit = false;
|
bool isEdit = false;
|
||||||
|
|
||||||
void _draggableModeChanged(bool mode) {
|
void _draggableModeChanged(bool mode) {
|
||||||
@ -85,85 +72,6 @@ class HomeState extends State<ActivityHome> {
|
|||||||
setState(() {});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final media = MediaQuery.of(context);
|
final media = MediaQuery.of(context);
|
||||||
@ -171,7 +79,7 @@ class HomeState extends State<ActivityHome> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('微漫 v' + packageInfo.version),
|
title: Text('微漫 v' + widget.packageInfo.version),
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
leading: isEdit
|
leading: isEdit
|
||||||
? IconButton(
|
? IconButton(
|
||||||
@ -183,20 +91,18 @@ class HomeState extends State<ActivityHome> {
|
|||||||
: null,
|
: null,
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
/// 黑白样式切换
|
/// 黑白样式切换
|
||||||
themeButton(),
|
|
||||||
SizedBox(width: 20),
|
|
||||||
|
|
||||||
/// 设置界面
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
DynamicTheme.of(context).setBrightness(
|
||||||
context,
|
Theme.of(context).brightness == Brightness.dark
|
||||||
MaterialPageRoute(
|
? Brightness.light
|
||||||
settings: RouteSettings(name: '/activity_setting'),
|
: Brightness.dark);
|
||||||
builder: (_) => ActivitySetting()));
|
|
||||||
},
|
},
|
||||||
icon: Icon(FontAwesomeIcons.cog),
|
icon: Icon(Theme.of(context).brightness == Brightness.light
|
||||||
|
? FontAwesomeIcons.lightbulb
|
||||||
|
: FontAwesomeIcons.solidLightbulb),
|
||||||
),
|
),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
|
||||||
/// 收藏列表
|
/// 收藏列表
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -221,11 +127,9 @@ class HomeState extends State<ActivityHome> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawerEnableOpenDragGesture: false,
|
|
||||||
endDrawerEnableOpenDragGesture: false,
|
|
||||||
endDrawer: Drawer(
|
endDrawer: Drawer(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (_, constraints) {
|
builder: (_, __) {
|
||||||
if (showFavorite) {
|
if (showFavorite) {
|
||||||
return FavoriteList();
|
return FavoriteList();
|
||||||
} else {
|
} else {
|
||||||
@ -234,147 +138,112 @@ class HomeState extends State<ActivityHome> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Container(
|
||||||
child: SingleChildScrollView(
|
alignment: Alignment.center,
|
||||||
padding: EdgeInsets.only(left: 40, right: 40),
|
padding: EdgeInsets.only(left: 40, right: 40),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
child: OutlineButton(
|
child: OutlineButton(
|
||||||
onPressed: gotoSearch,
|
onPressed: gotoSearch,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(
|
Icon(
|
||||||
Icons.search,
|
Icons.search,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'搜索漫画',
|
|
||||||
style: TextStyle(color: Colors.blue),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderSide: BorderSide(color: Colors.blue, width: 2),
|
|
||||||
shape: StadiumBorder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 7,
|
|
||||||
child: OutlineButton(
|
|
||||||
onPressed: gotoRecommend,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Icon(
|
|
||||||
Icons.whatshot,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'热门漫画',
|
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderSide: BorderSide(color: Colors.red, width: 2),
|
|
||||||
shape: StadiumBorder(),
|
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
],
|
'搜索漫画',
|
||||||
),
|
style: TextStyle(color: Colors.blue),
|
||||||
Center(
|
)
|
||||||
child: Quick(
|
],
|
||||||
key: _quickState,
|
|
||||||
width: width,
|
|
||||||
draggableModeChanged: _draggableModeChanged,
|
|
||||||
),
|
),
|
||||||
|
borderSide: BorderSide(color: Colors.blue, width: 2),
|
||||||
|
shape: StadiumBorder(),
|
||||||
),
|
),
|
||||||
CheckConnectWidget(),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
GestureDetector(
|
child: OutlineButton(
|
||||||
onTap: () async {
|
onPressed: gotoRecommend,
|
||||||
launch('https://bbs.level-plus.net/');
|
child: Row(
|
||||||
},
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: Text(
|
children: <Widget>[
|
||||||
'魂+论坛首发',
|
Icon(
|
||||||
textAlign: TextAlign.center,
|
Icons.whatshot,
|
||||||
style: TextStyle(
|
color: Colors.red,
|
||||||
color: Colors.blue[200],
|
),
|
||||||
decoration: TextDecoration.underline,
|
Text(
|
||||||
),
|
'月排行榜',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
borderSide: BorderSide(color: Colors.red, width: 2),
|
||||||
|
shape: StadiumBorder(),
|
||||||
),
|
),
|
||||||
SizedBox(width: 20),
|
),
|
||||||
GestureDetector(
|
],
|
||||||
onTap: () async {
|
),
|
||||||
if (await canLaunch('tg://resolve?domain=weiman_app'))
|
Center(
|
||||||
launch('tg://resolve?domain=weiman_app');
|
child: Quick(
|
||||||
else
|
key: _quickState,
|
||||||
launch('https://t.me/weiman_app');
|
width: width,
|
||||||
},
|
draggableModeChanged: _draggableModeChanged,
|
||||||
child: Text(
|
|
||||||
'Telegram 广播频道',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.blue[200],
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
Visibility(
|
),
|
||||||
visible: isDevMode,
|
Container(
|
||||||
child: FlatButton(
|
margin: EdgeInsets.only(bottom: 10),
|
||||||
onPressed: () {
|
child: Text(
|
||||||
Navigator.push(context,
|
'在 level-plus.net 论坛首发',
|
||||||
MaterialPageRoute(builder: (_) => ActivityCheckData()));
|
textAlign: TextAlign.center,
|
||||||
},
|
style: TextStyle(color: Colors.grey[500]),
|
||||||
child: Text('操作 收藏列表数据'),
|
),
|
||||||
|
),
|
||||||
|
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,
|
Visibility(
|
||||||
child: FlatButton(
|
visible: isDevMode,
|
||||||
onPressed: () {
|
child: FlatButton(
|
||||||
Navigator.push(context,
|
onPressed: () {
|
||||||
MaterialPageRoute(builder: (_) => ActivityCheckDB()));
|
Navigator.push(context,
|
||||||
},
|
MaterialPageRoute(builder: (_) => ActivityTest()));
|
||||||
child: Text('操作 DB数据'),
|
},
|
||||||
),
|
child: Text('测试界面'),
|
||||||
),
|
),
|
||||||
Visibility(
|
),
|
||||||
visible: isDevMode,
|
Visibility(
|
||||||
child: FlatButton(
|
visible: isDevMode,
|
||||||
onPressed: () {
|
child: FlatButton(
|
||||||
Navigator.push(
|
onPressed: () {
|
||||||
context,
|
Navigator.push(context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(builder: (_) => ActivityCheckData()));
|
||||||
builder: (_) => ActivityDataConvert()));
|
},
|
||||||
},
|
child: Text('操作 收藏列表数据'),
|
||||||
child: Text('进入旧数据处理功能'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: isDevMode
|
|
||||||
? FloatingActionButton(
|
|
||||||
child: Text('测试'),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context, MaterialPageRoute(builder: (_) => ActivityTest()));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,238 +0,0 @@
|
|||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
123
lib/activities/search.dart
Normal file
123
lib/activities/search.dart
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,103 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,183 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,192 +0,0 @@
|
|||||||
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(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
46
lib/activities/test.dart
Normal file
46
lib/activities/test.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ActivityTest extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('asd'),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
onPressed: save,
|
||||||
|
child: Text('保存'),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: read,
|
||||||
|
child: Text('读取'),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: clear,
|
||||||
|
child: Text('清空数据'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void save() {
|
||||||
|
Data.addFavorite(Book(
|
||||||
|
aid: '123',
|
||||||
|
name: 'name',
|
||||||
|
avatar: 'avatar',
|
||||||
|
description: '',
|
||||||
|
author: ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
void read() {
|
||||||
|
var books = Data.getFavorites();
|
||||||
|
print(jsonEncode(books));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
Data.clear();
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,17 @@
|
|||||||
import 'dart:convert';
|
part of '../main.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:weiman/crawler/http.dart';
|
|
||||||
import 'data.dart';
|
|
||||||
class Book {
|
class Book {
|
||||||
final String http;
|
final String aid; // 书本ID
|
||||||
final String aid; // 漫画的数据库ID
|
|
||||||
final String name; // 书本名称
|
final String name; // 书本名称
|
||||||
final String avatar; // 书本封面
|
final String avatar; // 书本封面
|
||||||
final String author; // 画家
|
final String author; // 画家
|
||||||
final String description; // 描述
|
final String description; // 描述
|
||||||
final List<Chapter> chapters;
|
final List<Chapter> chapters;
|
||||||
final int chapterCount;
|
final int chapterCount;
|
||||||
final int version;
|
|
||||||
|
|
||||||
History history;
|
History history;
|
||||||
|
|
||||||
Book({
|
Book({
|
||||||
@required this.http,
|
|
||||||
@required this.name,
|
@required this.name,
|
||||||
@required this.aid,
|
@required this.aid,
|
||||||
@required this.avatar,
|
@required this.avatar,
|
||||||
@ -26,8 +19,6 @@ class Book {
|
|||||||
this.description,
|
this.description,
|
||||||
this.chapters: const [],
|
this.chapters: const [],
|
||||||
this.chapterCount: 0,
|
this.chapterCount: 0,
|
||||||
this.history,
|
|
||||||
this.version: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -40,31 +31,34 @@ class Book {
|
|||||||
return books.containsKey(aid);
|
return books.containsKey(aid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
favorite() {
|
||||||
|
if (isFavorite())
|
||||||
|
Data.removeFavorite(this);
|
||||||
|
else
|
||||||
|
Data.addFavorite(this);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
print('book toJson');
|
|
||||||
final Map<String, dynamic> data = {
|
final Map<String, dynamic> data = {
|
||||||
'http': http,
|
|
||||||
'aid': aid,
|
'aid': aid,
|
||||||
'name': name,
|
'name': name,
|
||||||
'avatar': avatar,
|
'avatar': avatar,
|
||||||
'author': author,
|
'author': author,
|
||||||
'chapterCount': chapterCount,
|
'chapterCount': chapterCount,
|
||||||
'version': version,
|
|
||||||
};
|
};
|
||||||
if (history != null) data['history'] = history.toJson();
|
if (history != null) data['history'] = history.toJson();
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Book.fromJson(Map<String, dynamic> json) {
|
static Book fromJson(Map<String, dynamic> json) {
|
||||||
final book = Book(
|
final book = Book(
|
||||||
http: json['http'],
|
aid: json['aid'],
|
||||||
aid: json['aid'],
|
name: json['name'],
|
||||||
name: json['name'],
|
avatar: json['avatar'],
|
||||||
avatar: json['avatar'],
|
author: json['author'],
|
||||||
author: json['author'],
|
description: json['description'],
|
||||||
description: json['description'],
|
chapterCount: json['chapterCount'] ?? 0,
|
||||||
chapterCount: json['chapterCount'] ?? 0,
|
);
|
||||||
version: json['version'] ?? 0);
|
|
||||||
if (json.containsKey('history'))
|
if (json.containsKey('history'))
|
||||||
book.history = History.fromJson(json['history']);
|
book.history = History.fromJson(json['history']);
|
||||||
return book;
|
return book;
|
||||||
@ -72,26 +66,15 @@ class Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Chapter {
|
class Chapter {
|
||||||
final HttpBook http;
|
|
||||||
final String cid; // 章节cid
|
final String cid; // 章节cid
|
||||||
final String cname; // 章节名称
|
final String cname; // 章节名称
|
||||||
final String avatar; // 章节封面
|
final String avatar; // 章节封面
|
||||||
|
|
||||||
Chapter({
|
Chapter({@required this.cid, @required this.cname, @required this.avatar});
|
||||||
@required this.http,
|
|
||||||
@required this.cid,
|
|
||||||
@required this.cname,
|
|
||||||
@required this.avatar,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
final Map<String, String> data = {
|
return jsonEncode({cid: cid, cname: cname, avatar: avatar});
|
||||||
'cid': cid,
|
|
||||||
'cname': cname,
|
|
||||||
'avatar': avatar,
|
|
||||||
};
|
|
||||||
return jsonEncode(data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,14 +82,8 @@ class History {
|
|||||||
final String cid;
|
final String cid;
|
||||||
final String cname;
|
final String cname;
|
||||||
final int time;
|
final int time;
|
||||||
final int image;
|
|
||||||
|
|
||||||
History({
|
History({@required this.cid, @required this.cname, @required this.time});
|
||||||
@required this.cid,
|
|
||||||
@required this.cname,
|
|
||||||
@required this.time,
|
|
||||||
this.image = 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => jsonEncode(toJson());
|
String toString() => jsonEncode(toJson());
|
||||||
@ -116,17 +93,11 @@ class History {
|
|||||||
'cid': cid,
|
'cid': cid,
|
||||||
'cname': cname,
|
'cname': cname,
|
||||||
'time': time,
|
'time': time,
|
||||||
'image': image,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static History fromJson(Map<String, dynamic> json) {
|
static History fromJson(Map<String, dynamic> json) {
|
||||||
return History(
|
return History(cid: json['cid'], cname: json['cname'], time: json['time']);
|
||||||
cid: json['cid'],
|
|
||||||
cname: json['cname'],
|
|
||||||
time: json['time'],
|
|
||||||
image: json['image'] ?? 0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static History fromChapter(Chapter chapter) {
|
static History fromChapter(Chapter chapter) {
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
class ChapterContent {
|
|
||||||
final List<String> images;
|
|
||||||
final bool hasNextPage;
|
|
||||||
|
|
||||||
ChapterContent(this.images, this.hasNextPage);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'ChapterContent images:${images.length} nexPage:$hasNextPage';
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,4 @@
|
|||||||
import 'dart:convert';
|
part of '../main.dart';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
import 'book.dart';
|
|
||||||
|
|
||||||
class Data {
|
class Data {
|
||||||
static SharedPreferences instance;
|
static SharedPreferences instance;
|
||||||
@ -26,7 +22,7 @@ class Data {
|
|||||||
} else if (value is double) {
|
} else if (value is double) {
|
||||||
instance.setDouble(key, value);
|
instance.setDouble(key, value);
|
||||||
} else if (value is Map) {
|
} else if (value is Map) {
|
||||||
instance.setString(key, json.encode(value));
|
instance.setString(key, jsonEncode(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,11 +30,6 @@ class Data {
|
|||||||
return instance.get(key);
|
return instance.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool hasData() {
|
|
||||||
return instance.containsKey(favoriteBooksKey) ||
|
|
||||||
instance.containsKey(viewHistoryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, Book> getFavorites() {
|
static Map<String, Book> getFavorites() {
|
||||||
if (has(favoriteBooksKey)) {
|
if (has(favoriteBooksKey)) {
|
||||||
final String str = instance.getString(favoriteBooksKey);
|
final String str = instance.getString(favoriteBooksKey);
|
||||||
@ -119,7 +110,7 @@ class Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 快速导航 id 列表,内部方法
|
/// 快速导航 id 列表,内部方法
|
||||||
static List<String> quickIdList() {
|
static List<String> _quickIdList() {
|
||||||
if (instance.containsKey(quickKey)) {
|
if (instance.containsKey(quickKey)) {
|
||||||
return instance.getStringList(quickKey);
|
return instance.getStringList(quickKey);
|
||||||
}
|
}
|
||||||
@ -130,7 +121,7 @@ class Data {
|
|||||||
static List<Book> quickList() {
|
static List<Book> quickList() {
|
||||||
final books = getFavorites();
|
final books = getFavorites();
|
||||||
final ids = books.keys;
|
final ids = books.keys;
|
||||||
final List<String> quickIds = quickIdList();
|
final List<String> quickIds = _quickIdList();
|
||||||
print('快捷 $quickIds');
|
print('快捷 $quickIds');
|
||||||
return quickIds
|
return quickIds
|
||||||
.where((id) => ids.contains(id))
|
.where((id) => ids.contains(id))
|
||||||
@ -140,7 +131,7 @@ class Data {
|
|||||||
|
|
||||||
/// 增加快速导航
|
/// 增加快速导航
|
||||||
static addQuick(Book book) {
|
static addQuick(Book book) {
|
||||||
final list = quickIdList();
|
final list = _quickIdList();
|
||||||
list.add(book.aid);
|
list.add(book.aid);
|
||||||
instance.setStringList(quickKey, list.toSet().toList());
|
instance.setStringList(quickKey, list.toSet().toList());
|
||||||
}
|
}
|
||||||
@ -153,7 +144,7 @@ class Data {
|
|||||||
/// 重新整理Quick的id列表
|
/// 重新整理Quick的id列表
|
||||||
static reQuick() {
|
static reQuick() {
|
||||||
final books = getFavorites();
|
final books = getFavorites();
|
||||||
final quickIds = quickIdList();
|
final quickIds = _quickIdList();
|
||||||
instance.setStringList(
|
instance.setStringList(
|
||||||
quickKey, quickIds.where(books.keys.contains).toSet().toList());
|
quickKey, quickIds.where(books.keys.contains).toSet().toList());
|
||||||
}
|
}
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
48
lib/classes/http.dart
Normal file
48
lib/classes/http.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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 {
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +1,16 @@
|
|||||||
import 'dart:async';
|
part of '../main.dart';
|
||||||
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].
|
/// The dart:io implementation of [image_provider.NetworkImage].
|
||||||
class NetworkImageSSL extends ImageProvider<NetworkImage>
|
class NetworkImageSSL
|
||||||
implements NetworkImage {
|
extends image_provider.ImageProvider<image_provider.NetworkImage>
|
||||||
|
implements image_provider.NetworkImage {
|
||||||
/// Creates an object that fetches the image at the given URL.
|
/// Creates an object that fetches the image at the given URL.
|
||||||
///
|
///
|
||||||
/// The arguments [url] and [scale] must not be null.
|
/// The arguments [url] and [scale] must not be null.
|
||||||
const NetworkImageSSL(
|
const NetworkImageSSL(this.url, {this.scale = 1.0, this.headers})
|
||||||
this.http,
|
: assert(url != null),
|
||||||
this.url, {
|
|
||||||
this.scale = 1.0,
|
|
||||||
this.headers,
|
|
||||||
this.timeout = 8,
|
|
||||||
this.reSort = false,
|
|
||||||
}) : assert(url != null),
|
|
||||||
assert(scale != null);
|
assert(scale != null);
|
||||||
|
|
||||||
final HttpBook http;
|
|
||||||
|
|
||||||
final int timeout;
|
|
||||||
@override
|
@override
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
@ -34,46 +20,80 @@ class NetworkImageSSL extends ImageProvider<NetworkImage>
|
|||||||
@override
|
@override
|
||||||
final Map<String, String> headers;
|
final Map<String, String> headers;
|
||||||
|
|
||||||
final bool reSort;
|
|
||||||
|
|
||||||
static void init(ByteData data) {}
|
static void init(ByteData data) {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<NetworkImageSSL> obtainKey(ImageConfiguration configuration) {
|
Future<NetworkImageSSL> obtainKey(
|
||||||
|
image_provider.ImageConfiguration configuration) {
|
||||||
return SynchronousFuture<NetworkImageSSL>(this);
|
return SynchronousFuture<NetworkImageSSL>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
|
image_provider.ImageStreamCompleter load(
|
||||||
|
image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
|
||||||
// Ownership of this controller is handed off to [_loadAsync]; it is that
|
// Ownership of this controller is handed off to [_loadAsync]; it is that
|
||||||
// method's responsibility to close the controller's stream when the image
|
// method's responsibility to close the controller's stream when the image
|
||||||
// has been loaded or an error is thrown.
|
// has been loaded or an error is thrown.
|
||||||
final StreamController<ImageChunkEvent> chunkEvents =
|
final StreamController<image_provider.ImageChunkEvent> chunkEvents =
|
||||||
StreamController<ImageChunkEvent>();
|
StreamController<image_provider.ImageChunkEvent>();
|
||||||
|
|
||||||
return MultiFrameImageStreamCompleter(
|
return image_provider.MultiFrameImageStreamCompleter(
|
||||||
codec: _loadAsync(key, chunkEvents, decode),
|
codec: _loadAsync(key, chunkEvents, decode),
|
||||||
chunkEvents: chunkEvents.stream,
|
chunkEvents: chunkEvents.stream,
|
||||||
scale: key.scale,
|
scale: key.scale,
|
||||||
informationCollector: () {
|
informationCollector: () {
|
||||||
return <DiagnosticsNode>[
|
return <DiagnosticsNode>[
|
||||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
DiagnosticsProperty<image_provider.ImageProvider>(
|
||||||
DiagnosticsProperty<NetworkImage>('Image key', key),
|
'Image provider', this),
|
||||||
|
DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Codec> _loadAsync(
|
// 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(
|
||||||
NetworkImageSSL key,
|
NetworkImageSSL key,
|
||||||
StreamController<ImageChunkEvent> chunkEvents,
|
StreamController<image_provider.ImageChunkEvent> chunkEvents,
|
||||||
DecoderCallback decode,
|
image_provider.DecoderCallback decode,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
assert(key == this);
|
assert(key == this);
|
||||||
final Uint8List bytes = await http.getImage(url, reSort: reSort);
|
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
if (bytes.lengthInBytes == 0)
|
if (bytes.lengthInBytes == 0)
|
||||||
throw Exception('NetworkImage is an empty file: $url');
|
throw Exception('NetworkImage is an empty file: $resolved');
|
||||||
|
|
||||||
return decode(bytes);
|
return decode(bytes);
|
||||||
} finally {
|
} finally {
|
||||||
chunkEvents.close();
|
chunkEvents.close();
|
||||||
@ -88,7 +108,7 @@ class NetworkImageSSL extends ImageProvider<NetworkImage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => hashValues(url, scale);
|
int get hashCode => ui.hashValues(url, scale);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType("$url", scale: $scale)';
|
String toString() => '$runtimeType("$url", scale: $scale)';
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
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
178
lib/db/book.dart
@ -1,178 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
183
lib/main.dart
183
lib/main.dart
@ -1,53 +1,85 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
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/firebase_analytics.dart';
|
||||||
import 'package:firebase_analytics/observer.dart';
|
import 'package:firebase_analytics/observer.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:flutter/material.dart' hide NestedScrollView;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.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:oktoast/oktoast.dart';
|
import 'package:oktoast/oktoast.dart';
|
||||||
import 'package:package_info/package_info.dart';
|
import 'package:package_info/package_info.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'
|
||||||
import 'package:path_provider/path_provider.dart';
|
hide CircularProgressIndicator;
|
||||||
import 'package:provider/provider.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:weiman/activities/dataConvert.dart';
|
import 'package:sticky_headers/sticky_headers/widget.dart';
|
||||||
import 'package:weiman/activities/home.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:weiman/classes/data.dart';
|
import 'dart:async';
|
||||||
import 'package:weiman/db/book.dart';
|
import 'dart:io';
|
||||||
import 'package:weiman/db/group.dart';
|
import 'dart:typed_data';
|
||||||
import 'package:weiman/db/historyOffset.dart';
|
import 'dart:ui' as ui;
|
||||||
import 'package:weiman/db/setting.dart';
|
|
||||||
import 'package:weiman/provider/favoriteData.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:weiman/provider/theme.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';
|
||||||
|
|
||||||
FirebaseAnalytics analytics;
|
FirebaseAnalytics analytics;
|
||||||
FirebaseAnalyticsObserver observer;
|
FirebaseAnalyticsObserver observer;
|
||||||
|
|
||||||
const bool isDevMode = !bool.fromEnvironment('dart.vm.product');
|
const bool isDevMode = !bool.fromEnvironment('dart.vm.product');
|
||||||
|
|
||||||
int version;
|
|
||||||
BoxDecoration border;
|
|
||||||
|
|
||||||
Directory imageCacheDir;
|
|
||||||
String imageCacheDirPath;
|
|
||||||
PackageInfo packageInfo;
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
print("开发模式 $isDevMode");
|
|
||||||
FlutterError.onError = (FlutterErrorDetails details) {};
|
FlutterError.onError = (FlutterErrorDetails details) {};
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
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 {
|
try {
|
||||||
analytics = FirebaseAnalytics();
|
analytics = FirebaseAnalytics();
|
||||||
@ -55,80 +87,47 @@ void main() async {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
Hive.initFlutter(),
|
|
||||||
Data.init(),
|
Data.init(),
|
||||||
SystemChrome.setPreferredOrientations(
|
SystemChrome.setPreferredOrientations(
|
||||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
||||||
]);
|
]);
|
||||||
Hive.registerAdapter<Group>(GroupAdapter());
|
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
Hive.registerAdapter<Book>(BookAdapter());
|
|
||||||
await Future.wait([
|
UserAgentClient.init();
|
||||||
Hive.openBox<Group>(GroupName).then((value) => Group.groupBox = value),
|
runApp(Main(packageInfo: packageInfo));
|
||||||
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 {
|
class Main extends StatefulWidget {
|
||||||
|
final PackageInfo packageInfo;
|
||||||
|
|
||||||
|
const Main({Key key, this.packageInfo}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_Main createState() => _Main();
|
_Main createState() => _Main();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Main extends State<Main> with WidgetsBindingObserver {
|
class _Main extends State<Main> with WidgetsBindingObserver {
|
||||||
@override
|
static BoxDecoration _border;
|
||||||
void initState() {
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangePlatformBrightness() {
|
|
||||||
super.didChangePlatformBrightness();
|
|
||||||
Provider.of<ThemeProvider>(context, listen: false).update(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
border = BoxDecoration(
|
if (_border == null)
|
||||||
border: Border(
|
_border = BoxDecoration(
|
||||||
bottom: Divider.createBorderSide(context, color: Colors.grey)));
|
border: Border(
|
||||||
return OKToast(
|
bottom: Divider.createBorderSide(context, color: Colors.grey)));
|
||||||
child: MultiProvider(
|
return DynamicTheme(
|
||||||
providers: [
|
defaultBrightness: Brightness.dark,
|
||||||
ChangeNotifierProvider<Setting>(
|
data: (brightness) => new ThemeData(
|
||||||
lazy: false,
|
brightness: brightness,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
home: Data.hasData() ? ActivityDataConvert() : ActivityHome(),
|
themedWidgetBuilder: (context, theme) {
|
||||||
// home: ActivityHome(),
|
return OKToast(
|
||||||
debugShowCheckedModeBanner: isDevMode,
|
child: MaterialApp(
|
||||||
navigatorObservers: <NavigatorObserver>[observer],
|
title: 'Flutter Demo',
|
||||||
),
|
theme: theme,
|
||||||
),
|
home: ActivityHome(widget.packageInfo),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,133 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
part of 'main.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;
|
final weekTime = Duration.millisecondsPerDay * 7;
|
||||||
|
|
||||||
void openSearch(BuildContext context, String word) {}
|
void openBook(BuildContext context, Book book, String heroTag) {
|
||||||
|
Navigator.push(
|
||||||
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,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
settings: RouteSettings(name: '/activity_book/${book.name}'),
|
settings: RouteSettings(name: '/activity_book/${book.name}'),
|
||||||
@ -30,8 +12,8 @@ Future openBook(BuildContext context, Book book, String heroTag) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openChapter(BuildContext context, Book book, Chapter chapter) {
|
void openChapter(BuildContext context, Book book, Chapter chapter) {
|
||||||
return Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
settings: RouteSettings(
|
settings: RouteSettings(
|
||||||
@ -40,11 +22,3 @@ Future<void> openChapter(BuildContext context, Book book, Chapter chapter) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showStatusBar() {
|
|
||||||
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
|
||||||
}
|
|
||||||
|
|
||||||
void hideStatusBar() {
|
|
||||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
|
||||||
}
|
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,4 @@
|
|||||||
import 'package:extended_image/extended_image.dart';
|
part of '../main.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 {
|
class WidgetBook extends StatelessWidget {
|
||||||
final Book book;
|
final Book book;
|
||||||
@ -20,7 +14,7 @@ class WidgetBook extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isLiked = book.favorite;
|
var isLiked = book.isFavorite();
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
book.name,
|
book.name,
|
||||||
@ -34,24 +28,25 @@ class WidgetBook extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: Hero(
|
leading: Hero(
|
||||||
tag: 'bookAvatar${book.aid}',
|
tag: 'bookAvatar${book.aid}',
|
||||||
child: ExtendedImage(image: NetworkImageSSL(book.http, book.avatar)),
|
child: Image(image:NetworkImageSSL(
|
||||||
),
|
book.avatar),
|
||||||
|
height: 200,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
)),
|
||||||
trailing: Icon(
|
trailing: Icon(
|
||||||
isLiked ? Icons.favorite : Icons.favorite_border,
|
isLiked ? Icons.favorite : Icons.favorite_border,
|
||||||
color: isLiked ? Colors.red : Colors.grey,
|
color: isLiked ? Colors.red : Colors.grey,
|
||||||
size: 12,
|
size: 12,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (onTap != null) return onTap(book);
|
if (onTap != null) onTap(book);
|
||||||
openBook(context, book, 'bookAvatar${book.aid}');
|
openBook(context, book, 'bookAvatar${book.aid}');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final dateFormat = DateFormat('yyyy-MM-dd');
|
|
||||||
|
|
||||||
class WidgetChapter extends StatelessWidget {
|
class WidgetChapter extends StatelessWidget {
|
||||||
static final double height = kToolbarHeight;
|
static final double height = kToolbarHeight;
|
||||||
final Chapter chapter;
|
final Chapter chapter;
|
||||||
@ -62,7 +57,7 @@ class WidgetChapter extends StatelessWidget {
|
|||||||
Key key,
|
Key key,
|
||||||
this.chapter,
|
this.chapter,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.read = false,
|
this.read,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -83,14 +78,16 @@ class WidgetChapter extends StatelessWidget {
|
|||||||
title: RichText(
|
title: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
children: children,
|
children: children,
|
||||||
style: Theme.of(context).textTheme.bodyText2,
|
style: Theme.of(context).textTheme.body1,
|
||||||
),
|
),
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
),
|
),
|
||||||
subtitle: chapter.time == null
|
leading: Image(image:NetworkImageSSL(
|
||||||
? null
|
chapter.avatar),
|
||||||
: Text('更新时间 ${dateFormat.format(chapter.time)}'),
|
fit: BoxFit.fitWidth,
|
||||||
|
width: 100,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,8 +106,8 @@ class WidgetHistory extends StatelessWidget {
|
|||||||
if (onTap != null) onTap(book);
|
if (onTap != null) onTap(book);
|
||||||
},
|
},
|
||||||
title: Text(book.name),
|
title: Text(book.name),
|
||||||
leading: Image(
|
leading: Image(image:NetworkImageSSL(
|
||||||
image: ExtendedNetworkImageProvider(book.avatar, cache: true),
|
book.avatar),
|
||||||
fit: BoxFit.fitHeight,
|
fit: BoxFit.fitHeight,
|
||||||
),
|
),
|
||||||
subtitle: Text(book.history.cname),
|
subtitle: Text(book.history.cname),
|
||||||
@ -139,18 +136,18 @@ class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void load() async {
|
void load() async {
|
||||||
// loading = true;
|
loading = true;
|
||||||
// try {
|
try {
|
||||||
// final book = await Http18Comic.instance
|
final book = await UserAgentClient.instance
|
||||||
// .getBook(widget.book.aid)
|
.getBook(aid: widget.book.aid)
|
||||||
// .timeout(Duration(seconds: 2));
|
.timeout(Duration(seconds: 2));
|
||||||
// news = book.chapterCount - widget.book.chapterCount;
|
news = book.chapterCount - widget.book.chapterCount;
|
||||||
// hasError = false;
|
hasError = false;
|
||||||
// } catch (e) {
|
} catch (e) {
|
||||||
// hasError = true;
|
hasError = true;
|
||||||
// }
|
}
|
||||||
// loading = false;
|
loading = false;
|
||||||
// setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -176,9 +173,7 @@ class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
|
|||||||
openBook(context, widget.book, 'checkBook${widget.book.aid}'),
|
openBook(context, widget.book, 'checkBook${widget.book.aid}'),
|
||||||
leading: Hero(
|
leading: Hero(
|
||||||
tag: 'checkBook${widget.book.aid}',
|
tag: 'checkBook${widget.book.aid}',
|
||||||
child: Image(
|
child: Image(image:NetworkImageSSL(widget.book.avatar)),
|
||||||
image:
|
|
||||||
ExtendedNetworkImageProvider(widget.book.avatar, cache: true)),
|
|
||||||
),
|
),
|
||||||
dense: true,
|
dense: true,
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
|
@ -1,103 +0,0 @@
|
|||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,314 +0,0 @@
|
|||||||
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('还没有开始网络请求'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
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: []);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
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)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +1,4 @@
|
|||||||
import 'package:extended_image/extended_image.dart';
|
part of '../main.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 {
|
class FavoriteList extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -23,218 +6,178 @@ class FavoriteList extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FavoriteList extends State<FavoriteList> {
|
class _FavoriteList extends State<FavoriteList> {
|
||||||
static bool showTip = true;
|
static final Map<String, int> hasNews = {};
|
||||||
|
static final List<Book> all = [], // 所有收藏
|
||||||
|
inWeek = [], // 7天看过的收藏
|
||||||
|
other = []; // 其他收藏
|
||||||
|
static bool showTip = false;
|
||||||
|
|
||||||
@override
|
static final loadFailTextSpan = TextSpan(
|
||||||
initState() {
|
text: '读取失败,下拉刷新', style: TextStyle(color: Colors.redAccent)),
|
||||||
super.initState();
|
waitToCheck =
|
||||||
if (showTip) {
|
TextSpan(text: '等待检查更新', style: TextStyle(color: Colors.grey)),
|
||||||
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
unCheck =
|
||||||
showToast(
|
TextSpan(text: '请下拉列表检查更新', style: TextStyle(color: Colors.grey)),
|
||||||
'下拉收藏列表检查更新\n分组和藏书左滑显示更多操作',
|
noUpdate = TextSpan(text: '没有更新', style: TextStyle(color: Colors.grey));
|
||||||
textPadding: EdgeInsets.all(10),
|
|
||||||
duration: Duration(seconds: 4),
|
static void getBooks() {
|
||||||
);
|
all.clear();
|
||||||
showTip = false;
|
inWeek.clear();
|
||||||
|
other.clear();
|
||||||
|
all.addAll(Data.getFavorites().values);
|
||||||
|
if (all.isNotEmpty) {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
all.forEach((book) {
|
||||||
|
if (book.history != null && (now - book.history.time) < weekTime) {
|
||||||
|
inWeek.add(book);
|
||||||
|
} else {
|
||||||
|
other.add(book);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
getBooks();
|
||||||
|
if (all.isNotEmpty) {
|
||||||
|
if (showTip == false) {
|
||||||
|
showTip = true;
|
||||||
|
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) {
|
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(
|
return FBookItem(
|
||||||
book: book,
|
book: book,
|
||||||
onDelete: deleteBook,
|
subtitle: state,
|
||||||
|
onTap: _openBook,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<FavoriteData>(builder: (_, favorite, __) {
|
List<Book> inWeekUpdated = [],
|
||||||
if (favorite.all.isEmpty && favorite.groups.keys.isEmpty)
|
inWeekUnUpdated = [],
|
||||||
return Center(child: Text('没有收藏'));
|
otherUpdated = [],
|
||||||
return ClipRect(
|
otherUnUpdated = [];
|
||||||
child: RefreshIndicator(
|
inWeek.forEach((book) {
|
||||||
onRefresh: favorite.checkUpdate,
|
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0)
|
||||||
child: SafeArea(
|
inWeekUpdated.add(book);
|
||||||
child: CustomScrollView(
|
else
|
||||||
slivers: [
|
inWeekUnUpdated.add(book);
|
||||||
...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]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final bookStatusWidgets = {
|
class FBookItem extends StatelessWidget {
|
||||||
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 Book book;
|
||||||
final void Function(Book book) onDelete;
|
final TextSpan subtitle;
|
||||||
|
final void Function(Book book) onTap;
|
||||||
|
|
||||||
const FBookItem({
|
const FBookItem({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.book,
|
@required this.book,
|
||||||
@required this.onDelete,
|
@required this.subtitle,
|
||||||
|
@required this.onTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
|
||||||
_FBookItem createState() => _FBookItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FBookItem extends State<FBookItem> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
TextSpan subtitle =
|
return ListTile(
|
||||||
bookStatusWidgets[widget.book.status ?? BookUpdateStatus.no];
|
onTap: () => onTap(book),
|
||||||
if (widget.book.status == BookUpdateStatus.had) {
|
leading: Hero(
|
||||||
final _subtitle = '有 ${widget.book.newChapterCount} 章更新';
|
tag: 'fb ${book.aid}',
|
||||||
subtitle = TextSpan(
|
child: Image(image: NetworkImageSSL(book.avatar))),
|
||||||
text: _subtitle,
|
title: Text(book.name, style: Theme.of(context).textTheme.body1),
|
||||||
style: TextStyle(
|
subtitle: RichText(text: subtitle),
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +1,4 @@
|
|||||||
import 'package:extended_image/extended_image.dart';
|
part of '../main.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 {
|
class Histories extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -16,33 +6,22 @@ class Histories extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _Histories extends State<Histories> {
|
class _Histories extends State<Histories> {
|
||||||
static bool _showTips = true;
|
|
||||||
final List<Book> inWeek = [], other = [];
|
final List<Book> inWeek = [], other = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
loadBook();
|
loadBook();
|
||||||
if (_showTips)
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
|
||||||
_showTips = false;
|
|
||||||
showToast(
|
|
||||||
'阅读记录和时间分组\n往左滑显示更多操作',
|
|
||||||
textPadding: EdgeInsets.all(10),
|
|
||||||
duration: Duration(seconds: 4),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadBook() {
|
void loadBook() {
|
||||||
inWeek.clear();
|
inWeek.clear();
|
||||||
other.clear();
|
other.clear();
|
||||||
final list =
|
final list = Data.getHistories().values.toList();
|
||||||
Book.bookBox.values.where((book) => book.history != null).toList();
|
|
||||||
final now = DateTime.now().millisecondsSinceEpoch;
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
list.sort((a, b) => b.history.time.compareTo(a.history.time));
|
list.sort((a, b) => b.history.time.compareTo(a.history.time));
|
||||||
list.forEach((book) {
|
list.forEach((book) {
|
||||||
if ((now - book.history.time.millisecondsSinceEpoch) < weekTime) {
|
if ((now - book.history.time) < weekTime) {
|
||||||
inWeek.add(book);
|
inWeek.add(book);
|
||||||
} else {
|
} else {
|
||||||
other.add(book);
|
other.add(book);
|
||||||
@ -71,78 +50,47 @@ class _Histories extends State<Histories> {
|
|||||||
print('清理历史 $inWeek $res');
|
print('清理历史 $inWeek $res');
|
||||||
if (res == false) return;
|
if (res == false) return;
|
||||||
List<Book> list = inWeek ? this.inWeek : this.other;
|
List<Book> list = inWeek ? this.inWeek : this.other;
|
||||||
await Future.wait(list.map((book) => book.setHistory(null)));
|
list.forEach((book) => Data.removeHistoryFromBook(book));
|
||||||
setState(() {
|
setState(() {
|
||||||
loadBook();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: ClipRect(
|
child: CustomScrollView(
|
||||||
child: CustomScrollView(
|
slivers: [
|
||||||
slivers: [
|
SliverExpandableGroup(
|
||||||
SliverExpandableGroup(
|
title: Text('7天内的浏览历史 (${inWeek.length})'),
|
||||||
title: Text('7天内的浏览历史 (${inWeek.length})'),
|
expanded: true,
|
||||||
expanded: true,
|
actions: [
|
||||||
count: inWeek.length,
|
FlatButton(
|
||||||
builder: (ctx, i) => book(inWeek, i),
|
child: Text('清空'),
|
||||||
slideActions: [
|
onPressed: inWeek.length == 0 ? null : () => clear(true),
|
||||||
IconSlideAction(
|
),
|
||||||
caption: '清空',
|
],
|
||||||
color: Colors.red,
|
count: inWeek.length,
|
||||||
icon: Icons.delete,
|
builder: (ctx, i) => WidgetBook(
|
||||||
onTap: () => clear(true),
|
inWeek[i],
|
||||||
),
|
subtitle: inWeek[i].history.cname,
|
||||||
],
|
|
||||||
),
|
),
|
||||||
SliverExpandableGroup(
|
),
|
||||||
title: Text('更早的浏览历史 (${other.length})'),
|
SliverExpandableGroup(
|
||||||
count: other.length,
|
title: Text('更早的浏览历史 (${other.length})'),
|
||||||
builder: (ctx, i) => book(other, i),
|
actions: [
|
||||||
slideActions: [
|
FlatButton(
|
||||||
IconSlideAction(
|
child: Text('清空'),
|
||||||
caption: '清空',
|
onPressed: other.length == 0 ? null : () => clear(false),
|
||||||
color: Colors.red,
|
),
|
||||||
icon: Icons.delete,
|
],
|
||||||
onTap: () => clear(false),
|
count: other.length,
|
||||||
),
|
builder: (ctx, i) => WidgetBook(
|
||||||
],
|
other[i],
|
||||||
|
subtitle: other[i].history.cname,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
part of '../main.dart';
|
||||||
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
|
||||||
import 'package:weiman/widgets/animatedLogo.dart';
|
|
||||||
|
|
||||||
class SliverPullToRefreshHeader extends StatelessWidget {
|
class SliverPullToRefreshHeader extends StatelessWidget {
|
||||||
static final double height = kToolbarHeight * 2;
|
static final double height = kToolbarHeight * 2;
|
||||||
@ -19,43 +17,56 @@ class SliverPullToRefreshHeader extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (info == null) return SliverToBoxAdapter(child: SizedBox());
|
if (info == null) return SliverToBoxAdapter(child: SizedBox());
|
||||||
double dragOffset = info?.dragOffset ?? 0.0;
|
double dragOffset = info?.dragOffset ?? 0.0;
|
||||||
Widget widget;
|
TextSpan text = TextSpan(
|
||||||
if (info.mode == RefreshIndicatorMode.error) {
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
widget = Column(
|
fontSize: fontSize,
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
children: [
|
children: [
|
||||||
Text('读取网络数据失败\n你可能需要梯子'),
|
WidgetSpan(
|
||||||
RaisedButton.icon(
|
baseline: TextBaseline.alphabetic,
|
||||||
icon: Icon(Icons.refresh),
|
child: Padding(
|
||||||
onPressed: onTap,
|
child: Image.asset("assets/logo.png", height: 20),
|
||||||
label: Text('再次尝试'),
|
padding: EdgeInsets.only(right: 5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
]);
|
||||||
);
|
if (info.mode == RefreshIndicatorMode.error) {
|
||||||
|
text.children.addAll([
|
||||||
|
TextSpan(
|
||||||
|
text: '读取失败\n当失败次数太多可能是网络出现问题\n',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
WidgetSpan(
|
||||||
|
child: RaisedButton.icon(
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
onPressed: onTap,
|
||||||
|
label: Text('再次尝试'))),
|
||||||
|
]);
|
||||||
} else if (info.mode == RefreshIndicatorMode.refresh ||
|
} else if (info.mode == RefreshIndicatorMode.refresh ||
|
||||||
info.mode == RefreshIndicatorMode.snap) {
|
info.mode == RefreshIndicatorMode.snap) {
|
||||||
widget = Row(
|
text.children.addAll([
|
||||||
mainAxisSize: MainAxisSize.min,
|
TextSpan(text: '读取中,请稍候'),
|
||||||
children: [
|
]);
|
||||||
AnimatedLogoWidget(width: 20, height: 30),
|
|
||||||
SizedBox(width: 5),
|
|
||||||
Text('读取中,请稍候'),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else if ([
|
} else if ([
|
||||||
RefreshIndicatorMode.drag,
|
RefreshIndicatorMode.drag,
|
||||||
RefreshIndicatorMode.armed,
|
RefreshIndicatorMode.armed,
|
||||||
RefreshIndicatorMode.snap
|
RefreshIndicatorMode.snap
|
||||||
].contains(info.mode)) {
|
].contains(info.mode)) {
|
||||||
widget = Text('下拉刷新');
|
text.children.add(TextSpan(text: '重新读取'));
|
||||||
} else {
|
} else {
|
||||||
widget = SizedBox();
|
text.children.add(TextSpan(text: 'Bye~'));
|
||||||
}
|
}
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: dragOffset,
|
height: dragOffset,
|
||||||
alignment: Alignment.center,
|
child: Center(
|
||||||
child: widget,
|
child: Text.rich(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
import 'package:draggable_container/draggable_container.dart';
|
part of '../main.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 {
|
class QuickBook extends DraggableItem {
|
||||||
static const heroTag = 'quickBookAvatar';
|
static const heroTag = 'quickBookAvatar';
|
||||||
@ -18,23 +11,27 @@ class QuickBook extends DraggableItem {
|
|||||||
{@required this.book, @required this.context}) {
|
{@required this.book, @required this.context}) {
|
||||||
child = GestureDetector(
|
child = GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
openBook(context, book, '$heroTag ${book.aid}');
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (ctx) => ActivityBook(
|
||||||
|
book: book,
|
||||||
|
heroTag: '$heroTag ${book.aid}',
|
||||||
|
)));
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
book.http == null
|
SizedBox(
|
||||||
? oldBookAvatar(width: width, height: height)
|
width: width,
|
||||||
: SizedBox(
|
height: height,
|
||||||
width: width,
|
child: Hero(
|
||||||
height: height,
|
tag: '$heroTag ${book.aid}',
|
||||||
child: Hero(
|
child: Image(
|
||||||
tag: '$heroTag ${book.aid}',
|
image: NetworkImageSSL(book.avatar),
|
||||||
child: Image(
|
fit: BoxFit.cover,
|
||||||
image: NetworkImageSSL(book.http, book.avatar),
|
),
|
||||||
fit: BoxFit.cover,
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
@ -56,6 +53,10 @@ class QuickBook extends DraggableItem {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkUpdate() {
|
||||||
|
UserAgentClient.instance.getBook(aid: book.aid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Quick extends StatefulWidget {
|
class Quick extends StatefulWidget {
|
||||||
@ -74,14 +75,46 @@ class QuickState extends State<Quick> {
|
|||||||
final int count = 8;
|
final int count = 8;
|
||||||
final List<DraggableItem> _draggableItems = [];
|
final List<DraggableItem> _draggableItems = [];
|
||||||
DraggableItem _addButton;
|
DraggableItem _addButton;
|
||||||
GlobalKey<DraggableContainerState> _key =
|
GlobalKey<DraggableContainerState> _key = GlobalKey();
|
||||||
GlobalKey<DraggableContainerState>();
|
final List<String> id = [];
|
||||||
double width = 0, height = 0;
|
double width = 0, height = 0;
|
||||||
|
|
||||||
void exit() {
|
void exit() {
|
||||||
_key.currentState.draggableMode = false;
|
_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() {
|
QuickState() {
|
||||||
_addButton = DraggableItem(
|
_addButton = DraggableItem(
|
||||||
deletable: false,
|
deletable: false,
|
||||||
@ -108,12 +141,9 @@ class QuickState extends State<Quick> {
|
|||||||
final buttonIndex = items.indexOf(_addButton);
|
final buttonIndex = items.indexOf(_addButton);
|
||||||
print('add $buttonIndex');
|
print('add $buttonIndex');
|
||||||
if (buttonIndex > -1) {
|
if (buttonIndex > -1) {
|
||||||
final book = await showFavoriteBooksDialog(context);
|
final book = await _showSelectBookDialog();
|
||||||
print('选择了 $book');
|
print('选择了 $book');
|
||||||
if (book == null) return;
|
if (book == null) return;
|
||||||
book
|
|
||||||
..quick = buttonIndex
|
|
||||||
..save();
|
|
||||||
_key.currentState.insteadOfIndex(buttonIndex,
|
_key.currentState.insteadOfIndex(buttonIndex,
|
||||||
QuickBook(width, height, book: book, context: context),
|
QuickBook(width, height, book: book, context: context),
|
||||||
force: true);
|
force: true);
|
||||||
@ -133,18 +163,8 @@ class QuickState extends State<Quick> {
|
|||||||
|
|
||||||
width = widget.width / 4 - 10;
|
width = widget.width / 4 - 10;
|
||||||
height = (width / 0.7).roundToDouble();
|
height = (width / 0.7).roundToDouble();
|
||||||
final list = <Book>[];
|
_draggableItems.addAll(Data.quickList().map((book) {
|
||||||
Book.bookBox.values.forEach((book) {
|
id.add(book.aid);
|
||||||
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);
|
return QuickBook(width, height, book: book, context: context);
|
||||||
}));
|
}));
|
||||||
if (_draggableItems.length < count) _draggableItems.add(_addButton);
|
if (_draggableItems.length < count) _draggableItems.add(_addButton);
|
||||||
@ -155,7 +175,6 @@ class QuickState extends State<Quick> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
print('quick build');
|
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
@ -176,25 +195,18 @@ class QuickState extends State<Quick> {
|
|||||||
boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]),
|
boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]),
|
||||||
items: _draggableItems,
|
items: _draggableItems,
|
||||||
onDraggableModeChanged: widget.draggableModeChanged,
|
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) {
|
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 nullIndex = items.indexOf(null);
|
||||||
final buttonIndex = items.indexOf(_addButton);
|
final buttonIndex = items.indexOf(_addButton);
|
||||||
print('null $nullIndex, button $buttonIndex');
|
print('null $nullIndex, button $buttonIndex');
|
||||||
if (nullIndex > -1 && buttonIndex == -1) {
|
if (nullIndex > -1 && buttonIndex == -1) {
|
||||||
print('显示添加按钮 1');
|
_key.currentState
|
||||||
_key.currentState.insteadOfIndex(
|
.insteadOfIndex(nullIndex, _addButton, triggerEvent: false);
|
||||||
nullIndex, _addButton,
|
|
||||||
triggerEvent: false, force: true);
|
|
||||||
print('显示添加按钮 2');
|
|
||||||
setState(() {});
|
|
||||||
} else if (nullIndex > -1 &&
|
} else if (nullIndex > -1 &&
|
||||||
buttonIndex > -1 &&
|
buttonIndex > -1 &&
|
||||||
nullIndex < buttonIndex) {
|
nullIndex < buttonIndex) {
|
||||||
@ -202,15 +214,6 @@ class QuickState extends State<Quick> {
|
|||||||
_key.currentState
|
_key.currentState
|
||||||
.insteadOfIndex(nullIndex, _addButton, triggerEvent: false);
|
.insteadOfIndex(nullIndex, _addButton, triggerEvent: false);
|
||||||
}
|
}
|
||||||
var quick = 0;
|
|
||||||
items.forEach((item) {
|
|
||||||
if (item is QuickBook) {
|
|
||||||
item.book
|
|
||||||
..quick = quick
|
|
||||||
..save();
|
|
||||||
quick++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
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(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,4 @@
|
|||||||
import 'dart:math' as math;
|
part of '../main.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
|
||||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
|
||||||
|
|
||||||
class SliverExpandableBuilder {
|
class SliverExpandableBuilder {
|
||||||
final int count;
|
final int count;
|
||||||
@ -19,7 +15,6 @@ class SliverExpandableGroup extends StatefulWidget {
|
|||||||
final double height;
|
final double height;
|
||||||
final int count;
|
final int count;
|
||||||
final IndexedWidgetBuilder builder;
|
final IndexedWidgetBuilder builder;
|
||||||
final List<Widget> slideActions;
|
|
||||||
|
|
||||||
const SliverExpandableGroup({
|
const SliverExpandableGroup({
|
||||||
Key key,
|
Key key,
|
||||||
@ -30,7 +25,6 @@ class SliverExpandableGroup extends StatefulWidget {
|
|||||||
this.actions = const [],
|
this.actions = const [],
|
||||||
this.divideColor = Colors.grey,
|
this.divideColor = Colors.grey,
|
||||||
this.height = kToolbarHeight,
|
this.height = kToolbarHeight,
|
||||||
this.slideActions,
|
|
||||||
}) : assert(title != null),
|
}) : assert(title != null),
|
||||||
assert(builder != null),
|
assert(builder != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@ -41,7 +35,6 @@ class SliverExpandableGroup extends StatefulWidget {
|
|||||||
|
|
||||||
class _SliverExpandableGroup extends State<SliverExpandableGroup> {
|
class _SliverExpandableGroup extends State<SliverExpandableGroup> {
|
||||||
bool _expanded;
|
bool _expanded;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -55,44 +48,36 @@ class _SliverExpandableGroup extends State<SliverExpandableGroup> {
|
|||||||
bottom: Divider.createBorderSide(context, color: widget.divideColor),
|
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: _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(
|
return SliverStickyHeader(
|
||||||
header: header,
|
header: InkWell(
|
||||||
|
child: Container(
|
||||||
|
height: widget.height,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).dialogBackgroundColor,
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Transform.rotate(
|
||||||
|
angle: _expanded ? 0 : math.pi,
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: widget.title),
|
||||||
|
...widget.actions,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_expanded = !_expanded;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
sliver: _expanded
|
sliver: _expanded
|
||||||
? SliverList(
|
? SliverList(
|
||||||
delegate: SliverChildBuilderDelegate((ctx, i) {
|
delegate: SliverChildBuilderDelegate((ctx, i) {
|
||||||
if (i < widget.count - 1) {
|
if (i < widget.count-1) {
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: _decoration,
|
decoration: _decoration,
|
||||||
child: widget.builder(context, i),
|
child: widget.builder(context, i),
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
part of '../main.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
|
|
||||||
class TextDivider extends StatelessWidget {
|
class TextDivider extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
@ -29,17 +28,3 @@ 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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
53
pubspec.yaml
53
pubspec.yaml
@ -1,9 +1,5 @@
|
|||||||
name: weiman
|
name: weiman
|
||||||
description: 微漫App
|
description: 微漫
|
||||||
|
|
||||||
# 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.
|
# The following defines the version and build number for your application.
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
@ -15,66 +11,40 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.1.4+2007
|
version: 1.0.4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.9.0 <3.0.0"
|
sdk: ">=2.3.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
dio: any
|
|
||||||
dio_http_cache: any
|
|
||||||
image: any
|
|
||||||
intl: any
|
|
||||||
async: any
|
async: any
|
||||||
|
cupertino_icons: any
|
||||||
http: any
|
http: any
|
||||||
encrypt: any
|
encrypt: any
|
||||||
html: any
|
html: any
|
||||||
hive: any
|
|
||||||
sa_anicoto: any
|
|
||||||
hive_flutter: any
|
|
||||||
shared_preferences: any
|
shared_preferences: any
|
||||||
random_string: any
|
fluttertoast: any
|
||||||
filesize: any
|
|
||||||
oktoast: any
|
|
||||||
path_provider: any
|
|
||||||
draggable_container: any
|
draggable_container: any
|
||||||
sticky_headers: any
|
|
||||||
flutter_sticky_header: any
|
flutter_sticky_header: any
|
||||||
extended_nested_scroll_view: any
|
extended_nested_scroll_view: any
|
||||||
|
dynamic_theme: any
|
||||||
package_info: any
|
package_info: any
|
||||||
url_launcher: any
|
url_launcher: any
|
||||||
font_awesome_flutter: any
|
font_awesome_flutter: 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
|
loading_more_list: any
|
||||||
flutter_slidable: any
|
pull_to_refresh_notification: any
|
||||||
|
|
||||||
firebase_core: any
|
firebase_core: any
|
||||||
firebase_analytics: 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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
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
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
@ -88,12 +58,11 @@ flutter:
|
|||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
assets:
|
assets:
|
||||||
- assets/logo.png
|
- images/logo.png
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
# assets:
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user