mirror of
https://github.com/nrop19/weiman_app.git
synced 2025-08-02 06:52:36 +08:00
Compare commits
No commits in common. "v1.0.7" and "master" have entirely different histories.
73
.gitignore
vendored
73
.gitignore
vendored
@ -1,73 +0,0 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Android related
|
||||
**/android/**/gradle-wrapper.jar
|
||||
**/android/.gradle
|
||||
**/android/captures/
|
||||
**/android/gradlew
|
||||
**/android/gradlew.bat
|
||||
**/android/local.properties
|
||||
**/android/**/GeneratedPluginRegistrant.java
|
||||
|
||||
# iOS/XCode related
|
||||
**/ios/**/*.mode1v3
|
||||
**/ios/**/*.mode2v3
|
||||
**/ios/**/*.moved-aside
|
||||
**/ios/**/*.pbxuser
|
||||
**/ios/**/*.perspectivev3
|
||||
**/ios/**/*sync/
|
||||
**/ios/**/.sconsign.dblite
|
||||
**/ios/**/.tags*
|
||||
**/ios/**/.vagrant/
|
||||
**/ios/**/DerivedData/
|
||||
**/ios/**/Icon?
|
||||
**/ios/**/Pods/
|
||||
**/ios/**/.symlinks/
|
||||
**/ios/**/profile
|
||||
**/ios/**/xcuserdata
|
||||
**/ios/.generated/
|
||||
**/ios/Flutter/App.framework
|
||||
**/ios/Flutter/Flutter.framework
|
||||
**/ios/Flutter/Generated.xcconfig
|
||||
**/ios/Flutter/app.flx
|
||||
**/ios/Flutter/app.zip
|
||||
**/ios/Flutter/flutter_assets/
|
||||
**/ios/Flutter/flutter_export_environment.sh
|
||||
**/ios/ServiceDefinitions.json
|
||||
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!**/ios/**/default.mode1v3
|
||||
!**/ios/**/default.mode2v3
|
||||
!**/ios/**/default.pbxuser
|
||||
!**/ios/**/default.perspectivev3
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
@ -1,11 +1,11 @@
|
||||
# weiman v1.0.6
|
||||
# 微漫 v1.1.4 [宣传页面](https://nrop19.github.io/weiman_app)
|
||||
|
||||
### 微漫脱敏后的开源代码
|
||||
|
||||
#### 不解答任何代码上的问题
|
||||
|
||||
#### App的问题请到 [Telegram群讨论](https://t.me/boring_programer)
|
||||
#### App的问题请到 [Telegram群](https://t.me/boring_programer) 讨论
|
||||
|
||||
- 删除了android端文件夹,涉及到apk签名等敏感文件
|
||||
- 删除了ios端文件夹
|
||||
- 删除了lib/classes/http.dart文件里的网站域名和爬虫逻辑,保护被爬网站的同时防止被爬网站加大防爬难度。
|
||||
- 删除了lib/crawler/里的其它文件,保护被爬网站的同时防止被爬网站加大防爬难度。
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@ -1,225 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ActivityBook extends StatefulWidget {
|
||||
final Book book;
|
||||
final String heroTag;
|
||||
|
||||
ActivityBook({@required this.book, @required this.heroTag});
|
||||
|
||||
@override
|
||||
BookState createState() => BookState();
|
||||
}
|
||||
|
||||
class BookState extends State<ActivityBook> {
|
||||
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
|
||||
ScrollController _scrollController;
|
||||
|
||||
bool _reverse = false;
|
||||
bool isFavorite = false;
|
||||
bool isLoading = true, isSuccess = false;
|
||||
Book book;
|
||||
List<Chapter> chapters = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isFavorite = widget.book.isFavorite();
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
_refresh.currentState
|
||||
.show(notificationDragOffset: SliverPullToRefreshHeader.height);
|
||||
});
|
||||
_scrollController = ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<bool> loadBook() async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
isSuccess = false;
|
||||
});
|
||||
try {
|
||||
book = await UserAgentClient.instance
|
||||
.getBook(aid: widget.book.aid)
|
||||
.timeout(Duration(seconds: 5));
|
||||
book.history = Data.getHistories()[book.aid]?.history;
|
||||
chapters
|
||||
..clear()
|
||||
..addAll(book.chapters);
|
||||
if (_reverse) chapters = chapters.reversed.toList();
|
||||
|
||||
/// 更新收藏列表里的漫画数据
|
||||
if (isFavorite) Data.addFavorite(book);
|
||||
|
||||
_scrollToRead();
|
||||
isLoading = false;
|
||||
isSuccess = true;
|
||||
} catch (e) {
|
||||
isLoading = false;
|
||||
isSuccess = false;
|
||||
return false;
|
||||
}
|
||||
print('刷新 $book');
|
||||
setState(() {});
|
||||
return true;
|
||||
}
|
||||
|
||||
void _scrollToRead() {
|
||||
if (book.history != null) {
|
||||
final history = book.chapters
|
||||
.firstWhere((chapter) => chapter.cid == book.history.cid);
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollController.animateTo(
|
||||
WidgetChapter.height * chapters.indexOf(history).toDouble(),
|
||||
duration: Duration(milliseconds: 500),
|
||||
curve: Curves.linear);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_openChapter(Chapter chapter) {
|
||||
setState(() {
|
||||
book.history = History(cid: chapter.cid, cname: chapter.cname, time: 0);
|
||||
openChapter(context, book, chapter);
|
||||
});
|
||||
}
|
||||
|
||||
favoriteBook() {
|
||||
widget.book.favorite();
|
||||
isFavorite = !isFavorite;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _sort() {
|
||||
setState(() {
|
||||
_reverse = !_reverse;
|
||||
chapters = chapters.reversed.toList();
|
||||
_scrollToRead();
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> chapterWidgets() {
|
||||
final book = this.book ?? widget.book;
|
||||
List<Widget> list = [];
|
||||
chapters.forEach((chapter) {
|
||||
final isRead = chapter.cid == book.history?.cid;
|
||||
list.add(WidgetChapter(
|
||||
chapter: chapter,
|
||||
onTap: _openChapter,
|
||||
read: isRead,
|
||||
));
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
Widget buildChapter(BuildContext context, int index) {
|
||||
final book = this.book ?? widget.book;
|
||||
final chapter = chapters[index];
|
||||
final isRead = chapter.cid == book.history?.cid;
|
||||
if (index < chapters.length - 1) {
|
||||
return DecoratedBox(
|
||||
decoration: _Main._border,
|
||||
child: WidgetChapter(
|
||||
chapter: chapter,
|
||||
onTap: _openChapter,
|
||||
read: isRead,
|
||||
),
|
||||
);
|
||||
}
|
||||
return WidgetChapter(
|
||||
chapter: chapter,
|
||||
onTap: _openChapter,
|
||||
read: isRead,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color color = isFavorite ? Colors.red : Colors.white;
|
||||
IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border;
|
||||
final book = this.book ?? widget.book;
|
||||
return Scaffold(
|
||||
body: PullToRefreshNotification(
|
||||
key: _refresh,
|
||||
onRefresh: loadBook,
|
||||
maxDragOffset: kToolbarHeight * 2,
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
floating: true,
|
||||
pinned: true,
|
||||
title: Text(widget.book.name),
|
||||
expandedHeight: 200,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
onPressed: _sort,
|
||||
icon: Icon(_reverse
|
||||
? FontAwesomeIcons.sortNumericDown
|
||||
: FontAwesomeIcons.sortNumericDownAlt)),
|
||||
IconButton(
|
||||
onPressed: favoriteBook, icon: Icon(icon, color: color))
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: SafeArea(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
top: 50, left: 20, right: 10, bottom: 20),
|
||||
height: 160,
|
||||
child: Hero(
|
||||
tag: widget.heroTag,
|
||||
child:
|
||||
Image(image: NetworkImageSSL(widget.book.avatar)),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 50, right: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'作者:' + (book.author ?? ''),
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 10),
|
||||
),
|
||||
Text(
|
||||
'简介:\n' + (book.description ?? ''),
|
||||
softWrap: true,
|
||||
style:
|
||||
TextStyle(color: Colors.white, height: 1.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PullToRefreshContainer((info) => SliverPullToRefreshHeader(
|
||||
info: info,
|
||||
onTap: () => _refresh.currentState.show(
|
||||
notificationDragOffset: SliverPullToRefreshHeader.height),
|
||||
)),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
buildChapter,
|
||||
childCount: book.chapters.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
274
lib/activities/book/book.dart
Normal file
274
lib/activities/book/book.dart
Normal file
@ -0,0 +1,274 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||
import 'package:weiman/activities/book/tapToSearch.dart';
|
||||
import 'package:weiman/classes/chapter.dart';
|
||||
import 'package:weiman/classes/networkImageSSL.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/main.dart';
|
||||
import 'package:weiman/provider/favoriteData.dart';
|
||||
import 'package:weiman/utils.dart';
|
||||
import 'package:weiman/widgets/book.dart';
|
||||
import 'package:weiman/widgets/bookSettingDialog.dart';
|
||||
import 'package:weiman/widgets/pullToRefreshHeader.dart';
|
||||
|
||||
class ActivityBook extends StatefulWidget {
|
||||
final Book book;
|
||||
final String heroTag;
|
||||
|
||||
ActivityBook({@required this.book, @required this.heroTag});
|
||||
|
||||
@override
|
||||
_ActivityBook createState() => _ActivityBook();
|
||||
}
|
||||
|
||||
class _ActivityBook extends State<ActivityBook> {
|
||||
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
|
||||
ScrollController _scrollController;
|
||||
|
||||
bool _reverse = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.book.look = true;
|
||||
_scrollController = ScrollController();
|
||||
print('${widget.book}');
|
||||
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_refresh.currentState
|
||||
.show(notificationDragOffset: SliverPullToRefreshHeader.height);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<bool> loadBook() async {
|
||||
try {
|
||||
final res = await widget.book.load();
|
||||
if (mounted && widget.book.needToSave()) {
|
||||
await widget.book.save();
|
||||
// Provider.of<FavoriteData>(context, listen: false).loadBooksList(true);
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
return res;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_openChapter(Chapter chapter) async {
|
||||
await openChapter(context, widget.book, chapter);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
favoriteBook() async {
|
||||
final fav = Provider.of<FavoriteData>(context, listen: false);
|
||||
if (widget.book.favorite) {
|
||||
final sure = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text('确认取消收藏?'),
|
||||
// content: Text('删除这本藏书后,首页的快速导航也会删除这本藏书'),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('确认'),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
),
|
||||
RaisedButton(
|
||||
child: Text('取消'),
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
),
|
||||
],
|
||||
));
|
||||
if (sure == true) {
|
||||
fav.deleteBook(widget.book);
|
||||
}
|
||||
} else {
|
||||
await fav.addBook(widget.book);
|
||||
await showBookSettingDialog(context, widget.book);
|
||||
if (widget.book.needUpdate == true) {
|
||||
widget.book.status = BookUpdateStatus.no;
|
||||
} else {
|
||||
widget.book.status = BookUpdateStatus.not;
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
List<Chapter> _sort() {
|
||||
final List<Chapter> list = List.from(widget.book.chapters);
|
||||
// print('sort ${list.length}');
|
||||
if (_reverse) return list.reversed.toList();
|
||||
return list;
|
||||
}
|
||||
|
||||
IndexedWidgetBuilder buildChapters(List<Chapter> chapters) {
|
||||
IndexedWidgetBuilder builder = (BuildContext context, int index) {
|
||||
final chapter = chapters[index];
|
||||
Widget child = WidgetChapter(
|
||||
chapter: chapter,
|
||||
onTap: _openChapter,
|
||||
read: chapter.cid == widget.book.history?.cid,
|
||||
);
|
||||
if (index < chapters.length - 1)
|
||||
child = DecoratedBox(
|
||||
decoration: border,
|
||||
child: child,
|
||||
);
|
||||
return child;
|
||||
};
|
||||
return builder;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color color = widget.book.favorite ? Colors.red : Colors.white;
|
||||
IconData icon =
|
||||
widget.book.favorite ? Icons.favorite : Icons.favorite_border;
|
||||
final List<Chapter> chapters = _sort();
|
||||
final history = <Widget>[];
|
||||
if (widget.book.history != null && widget.book.chapters.length > 0) {
|
||||
final chapter = widget.book.chapters.firstWhere(
|
||||
(chapter) => chapter.cid == widget.book.history.cid,
|
||||
orElse: () => null,
|
||||
);
|
||||
if(chapter != null){
|
||||
history.add(ListTile(title: Text('阅读历史')));
|
||||
history.add(WidgetChapter(
|
||||
chapter: chapter,
|
||||
onTap: _openChapter,
|
||||
read: true,
|
||||
));
|
||||
history.add(ListTile(title: Text('下一章')));
|
||||
final nextIndex = widget.book.chapters.indexOf(chapter) + 1;
|
||||
if (nextIndex < widget.book.chapterCount) {
|
||||
history.add(WidgetChapter(
|
||||
chapter: widget.book.chapters[nextIndex],
|
||||
onTap: _openChapter,
|
||||
read: false,
|
||||
));
|
||||
} else {
|
||||
history.add(ListTile(subtitle: Text('没有了')));
|
||||
}
|
||||
}
|
||||
history.add(SizedBox(height: 20));
|
||||
}
|
||||
history.add(
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Text('章节列表'),
|
||||
SizedBox(width: 10),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_reverse = !_reverse;
|
||||
setState(() {});
|
||||
},
|
||||
child: Text('倒序'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: PullToRefreshNotification(
|
||||
key: _refresh,
|
||||
onRefresh: loadBook,
|
||||
maxDragOffset: kToolbarHeight * 2,
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
/// 标题栏
|
||||
SliverAppBar(
|
||||
floating: true,
|
||||
pinned: true,
|
||||
title: Text(widget.book.name),
|
||||
expandedHeight: 200,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
onPressed: favoriteBook, icon: Icon(icon, color: color))
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: SafeArea(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
/// 漫画封面
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
top: 50, left: 20, right: 10, bottom: 20),
|
||||
height: 160,
|
||||
child: Hero(
|
||||
tag: widget.heroTag,
|
||||
child: ExtendedImage(
|
||||
width: 100,
|
||||
image: NetworkImageSSL(
|
||||
widget.book.http,
|
||||
widget.book.avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// 作者、标签、简介内容
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 50, right: 20),
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
TapToSearchWidget(
|
||||
leading: '作者', items: widget.book.authors),
|
||||
TapToSearchWidget(
|
||||
leading: '标签', items: widget.book.tags),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 10),
|
||||
),
|
||||
Text(
|
||||
widget.book.description ?? '',
|
||||
softWrap: true,
|
||||
style:
|
||||
TextStyle(color: Colors.white, height: 1.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
PullToRefreshContainer((info) => SliverPullToRefreshHeader(
|
||||
info: info,
|
||||
onTap: () => _refresh.currentState.show(
|
||||
notificationDragOffset: SliverPullToRefreshHeader.height),
|
||||
)),
|
||||
|
||||
/// 观看历史
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: history,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
),
|
||||
),
|
||||
|
||||
/// 章节列表
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
buildChapters(chapters),
|
||||
childCount: chapters.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
98
lib/activities/book/tapToSearch.dart
Normal file
98
lib/activities/book/tapToSearch.dart
Normal file
@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:weiman/activities/search/search.dart';
|
||||
|
||||
class TapToSearchWidget extends StatelessWidget {
|
||||
final String leading;
|
||||
final List<String> items;
|
||||
|
||||
const TapToSearchWidget({
|
||||
Key key,
|
||||
this.leading,
|
||||
this.items,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text('$leading:'),
|
||||
onPressed: null,
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
|
||||
overlayColor:
|
||||
MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.3)),
|
||||
visualDensity: VisualDensity.comfortable,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 10,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: items.map((e) => _Item(string: e)).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Item extends StatelessWidget {
|
||||
final String string;
|
||||
|
||||
const _Item({Key key, @required this.string})
|
||||
: assert(string != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ActivitySearch(
|
||||
search: string,
|
||||
)));
|
||||
},
|
||||
icon: Icon(Icons.search, size: 14),
|
||||
label: Text(string),
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
|
||||
overlayColor:
|
||||
MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.3)),
|
||||
visualDensity: VisualDensity.comfortable,
|
||||
),
|
||||
);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ActivitySearch(
|
||||
search: string,
|
||||
)));
|
||||
},
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: string,
|
||||
style: TextStyle(decoration: TextDecoration.underline)),
|
||||
WidgetSpan(
|
||||
child: Icon(
|
||||
Icons.search,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
)),
|
||||
],
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,380 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ActivityChapter extends StatefulWidget {
|
||||
final Book book;
|
||||
final Chapter chapter;
|
||||
|
||||
ActivityChapter(this.book, this.chapter);
|
||||
|
||||
@override
|
||||
ChapterState createState() => ChapterState();
|
||||
}
|
||||
|
||||
class ChapterState extends State<ActivityChapter> {
|
||||
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
PageController _pageController;
|
||||
int showIndex = 0;
|
||||
bool hasNextImage = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController(
|
||||
keepPage: false,
|
||||
initialPage: widget.book.chapters.indexOf(widget.chapter));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
endDrawer: ChapterDrawer(
|
||||
book: widget.book,
|
||||
onTap: (chapter) {
|
||||
_pageController.jumpToPage(widget.book.chapters.indexOf(chapter));
|
||||
},
|
||||
),
|
||||
body: PageView.builder(
|
||||
physics: AlwaysScrollableClampingScrollPhysics(),
|
||||
controller: _pageController,
|
||||
itemCount: widget.book.chapters.length,
|
||||
itemBuilder: (ctx, index) {
|
||||
return ChapterContentView(
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.openEndDrawer();
|
||||
},
|
||||
),
|
||||
],
|
||||
book: widget.book,
|
||||
chapter: widget.book.chapters[index],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChapterDrawer extends StatefulWidget {
|
||||
final Book book;
|
||||
final void Function(Chapter chapter) onTap;
|
||||
|
||||
const ChapterDrawer({
|
||||
Key key,
|
||||
@required this.book,
|
||||
@required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ChapterDrawer createState() => _ChapterDrawer();
|
||||
}
|
||||
|
||||
class _ChapterDrawer extends State<ChapterDrawer> {
|
||||
ScrollController _controller;
|
||||
int read;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
updateRead();
|
||||
_controller =
|
||||
ScrollController(initialScrollOffset: WidgetChapter.height * read);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void updateRead() {
|
||||
final readChapter = widget.book.chapters
|
||||
.firstWhere((chapter) => widget.book.history?.cid == chapter.cid);
|
||||
read = widget.book.chapters.indexOf(readChapter);
|
||||
}
|
||||
|
||||
void scrollToRead() {
|
||||
setState(() {
|
||||
updateRead();
|
||||
});
|
||||
_controller.animateTo(
|
||||
WidgetChapter.height * read,
|
||||
duration: Duration(milliseconds: 200),
|
||||
curve: Curves.linear,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: SafeArea(
|
||||
child: ListView(
|
||||
controller: _controller,
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: widget.book.chapters.map((chapter) {
|
||||
final isRead = widget.book.history?.cid == chapter.cid;
|
||||
return WidgetChapter(
|
||||
chapter: chapter,
|
||||
onTap: (chapter) {
|
||||
if (widget.onTap != null) widget.onTap(chapter);
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
scrollToRead();
|
||||
});
|
||||
},
|
||||
read: isRead,
|
||||
);
|
||||
}),
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChapterContentView extends StatefulWidget {
|
||||
final Book book;
|
||||
final Chapter chapter;
|
||||
final List<Widget> actions;
|
||||
|
||||
const ChapterContentView({Key key, this.book, this.chapter, this.actions})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_ChapterContentView createState() => _ChapterContentView();
|
||||
}
|
||||
|
||||
class _ChapterContentView extends State<ChapterContentView> {
|
||||
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
|
||||
final List<String> images = [];
|
||||
TextStyle _style = TextStyle(color: Colors.white);
|
||||
BoxDecoration _decoration =
|
||||
BoxDecoration(color: Colors.black.withOpacity(0.4));
|
||||
|
||||
bool loading = true;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
Data.addHistory(widget.book, widget.chapter);
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) => _refresh?.currentState
|
||||
?.show(notificationDragOffset: SliverPullToRefreshHeader.height));
|
||||
}
|
||||
|
||||
Future<bool> fetchImages() async {
|
||||
print('fetchImages');
|
||||
if (mounted) setState(() {});
|
||||
loading = true;
|
||||
images.clear();
|
||||
try {
|
||||
images.addAll(await UserAgentClient.instance
|
||||
.getImages(aid: widget.book.aid, cid: widget.chapter.cid)
|
||||
.timeout(Duration(seconds: 5)));
|
||||
if (images.length < 5) {
|
||||
// print('图片 前:' + images.toString());
|
||||
final list =
|
||||
await checkImage(images.last).timeout(Duration(seconds: 15));
|
||||
images.addAll(list);
|
||||
}
|
||||
} catch (e) {
|
||||
print('错误 $e');
|
||||
showToastWidget(
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
child: Text('读取章节内容出现错误\n点击复制错误内容'),
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
padding: EdgeInsets.all(10),
|
||||
),
|
||||
onTap: () async {
|
||||
await Clipboard.setData(ClipboardData(text: e.toString()));
|
||||
final content = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
print('粘贴板 ${content.text}');
|
||||
},
|
||||
),
|
||||
duration: Duration(seconds: 5),
|
||||
handleTouch: true,
|
||||
);
|
||||
return false;
|
||||
// throw(e);
|
||||
}
|
||||
loading = false;
|
||||
// print('所有图片:' + images.toString());
|
||||
if (mounted) setState(() {});
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final list = <Widget>[];
|
||||
if (!loading && images.length < 20) {
|
||||
list.add(SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Text('只读取到少于20张图片,友情提示:\n'
|
||||
'由于能力有限,可能没有办法识别出本章的所有图片,\n'
|
||||
'敬请谅解。'))));
|
||||
}
|
||||
return PullToRefreshNotification(
|
||||
key: _refresh,
|
||||
onRefresh: fetchImages,
|
||||
maxDragOffset: kToolbarHeight * 2,
|
||||
child: CustomScrollView(
|
||||
physics: AlwaysScrollableClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
title: Text(widget.chapter.cname),
|
||||
pinned: false,
|
||||
floating: true,
|
||||
actions: widget.actions,
|
||||
),
|
||||
PullToRefreshContainer(
|
||||
(info) => SliverPullToRefreshHeader(
|
||||
info: info,
|
||||
onTap: () => _refresh.currentState.show(
|
||||
notificationDragOffset: SliverPullToRefreshHeader.height),
|
||||
),
|
||||
),
|
||||
...list,
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(ctx, i) {
|
||||
print('item $i');
|
||||
return StickyHeader(
|
||||
overlapHeaders: true,
|
||||
header: SafeArea(
|
||||
top: true,
|
||||
bottom: false,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(5),
|
||||
decoration: _decoration,
|
||||
child: Text(
|
||||
'${i + 1} / ${images.length}',
|
||||
style: _style,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
content: ExtendedImage(
|
||||
image: NetworkImageSSL(images[i]),
|
||||
enableLoadState: true,
|
||||
enableMemoryCache: true,
|
||||
fit: BoxFit.fitWidth,
|
||||
loadStateChanged: (state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
return SizedBox(
|
||||
height: 300,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case LoadState.failed:
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 300,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('图片读取失败'),
|
||||
RaisedButton(
|
||||
child: Text('重试'),
|
||||
onPressed: state.reLoadImage,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return ExtendedRawImage(
|
||||
image: state.extendedImageInfo?.image,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
// content: Image(
|
||||
// image: NetworkImageSSL(images[i]),
|
||||
// loadingBuilder: (_, child, loadingProgress) {
|
||||
// if (loadingProgress == null) return child;
|
||||
// return SizedBox(
|
||||
// height: 400,
|
||||
// child: Center(
|
||||
// child: CircularProgressIndicator(
|
||||
// value: loadingProgress.expectedTotalBytes != null
|
||||
// ? loadingProgress.cumulativeBytesLoaded /
|
||||
// loadingProgress.expectedTotalBytes
|
||||
// : null,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
);
|
||||
},
|
||||
childCount: images.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> checkImage(String last) async {
|
||||
final uri = Uri.parse(last);
|
||||
// print({'scheme': uri.scheme, 'host': uri.host, 'path': uri.path});
|
||||
final a = uri.scheme + '://' + uri.host;
|
||||
final b = uri.pathSegments.take(uri.pathSegments.length - 1).join('/');
|
||||
// print({'a': a, 'b': b});
|
||||
//网址最后的图片文件名
|
||||
final file = uri.pathSegments.last.split('.');
|
||||
final fileName = file[0];
|
||||
// 图片格式
|
||||
final fileFormat = file[1];
|
||||
final List<String> list = [];
|
||||
int plus = 1;
|
||||
//print('最后的图片:' + last);
|
||||
while (true) {
|
||||
final String file1 = getFileName(name: fileName, divider: '_', plus: plus),
|
||||
file2 = getFileName(name: fileName, divider: '_', plus: plus + 1);
|
||||
var url1 = '$a/$b/$file1.$fileFormat', url2 = '$a/$b/$file2.$fileFormat';
|
||||
// print('正在测试:\n' + url1 + '\n' + url2);
|
||||
final res = await Future.wait([
|
||||
UserAgentClient.instance.head(url1),
|
||||
UserAgentClient.instance.head(url2)
|
||||
]);
|
||||
if (res[0].statusCode != 200) break;
|
||||
list.add(url1);
|
||||
if (res[1].statusCode != 200) {
|
||||
break;
|
||||
}
|
||||
list.add(url2);
|
||||
plus += 2;
|
||||
}
|
||||
// print('最后的图片数量: ' + number.toString());
|
||||
return list;
|
||||
}
|
||||
|
||||
String getFileName(
|
||||
{@required String name, @required String divider, @required int plus}) {
|
||||
List<String> data = name.split(divider), newName = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
try {
|
||||
int number = int.parse(data[i]) + plus;
|
||||
newName.add(number.toString());
|
||||
} catch (e) {
|
||||
newName.add(data[i]);
|
||||
}
|
||||
}
|
||||
return newName.join(divider);
|
||||
}
|
92
lib/activities/chapter/activity.dart
Normal file
92
lib/activities/chapter/activity.dart
Normal file
@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||
import 'package:weiman/activities/chapter/chapterTab.dart';
|
||||
import 'package:weiman/activities/chapter/drawer.dart';
|
||||
import 'package:weiman/classes/chapter.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/db/setting.dart';
|
||||
import 'package:weiman/utils.dart';
|
||||
|
||||
class ActivityChapter extends StatefulWidget {
|
||||
final Book book;
|
||||
final Chapter chapter;
|
||||
|
||||
ActivityChapter(this.book, this.chapter);
|
||||
|
||||
@override
|
||||
_ActivityChapter createState() => _ActivityChapter();
|
||||
}
|
||||
|
||||
class _ActivityChapter extends State<ActivityChapter> {
|
||||
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
PageController _pageController;
|
||||
int showIndex = 0;
|
||||
bool hasNextImage = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pageController = PageController(
|
||||
keepPage: false,
|
||||
initialPage: widget.book.chapters.indexOf(widget.chapter));
|
||||
super.initState();
|
||||
saveHistory(widget.chapter);
|
||||
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final hide = Provider.of<Setting>(context, listen: false).getHideOption();
|
||||
if (hide == HideOption.always) {
|
||||
hideStatusBar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController?.dispose();
|
||||
showStatusBar();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void pageChanged(int page) {
|
||||
saveHistory(widget.book.chapters[page]);
|
||||
}
|
||||
|
||||
void saveHistory(Chapter chapter) async {
|
||||
await widget.book.setHistory(chapter);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<Setting>(builder: (_, data, __) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
endDrawer: ChapterDrawer(
|
||||
book: widget.book,
|
||||
onTap: (chapter) {
|
||||
_pageController.jumpToPage(widget.book.chapters.indexOf(chapter));
|
||||
},
|
||||
),
|
||||
body: PageView.builder(
|
||||
physics: AlwaysScrollableClampingScrollPhysics(),
|
||||
controller: _pageController,
|
||||
itemCount: widget.book.chapters.length,
|
||||
onPageChanged: pageChanged,
|
||||
itemBuilder: (ctx, index) {
|
||||
return ChapterTab(
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.openEndDrawer();
|
||||
},
|
||||
),
|
||||
],
|
||||
book: widget.book,
|
||||
chapter: widget.book.chapters[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
261
lib/activities/chapter/chapterTab.dart
Normal file
261
lib/activities/chapter/chapterTab.dart
Normal file
@ -0,0 +1,261 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:loading_more_list/loading_more_list.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:weiman/activities/chapter/image.dart';
|
||||
import 'package:weiman/activities/chapter/viewerSwitcherWidget.dart';
|
||||
import 'package:weiman/classes/chapter.dart';
|
||||
import 'package:weiman/crawler/http18Comic.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/db/setting.dart';
|
||||
import 'package:weiman/utils.dart';
|
||||
import 'package:weiman/widgets/animatedLogo.dart';
|
||||
|
||||
class ChapterSourceList extends LoadingMoreBase<String> {
|
||||
final Book book;
|
||||
final Chapter chapter;
|
||||
final Function onFirstLoaded;
|
||||
|
||||
bool firstLoad = true;
|
||||
bool hasMore = true;
|
||||
bool isMultiPage = false;
|
||||
int page = 1;
|
||||
|
||||
ChapterSourceList({
|
||||
this.book,
|
||||
this.chapter,
|
||||
this.onFirstLoaded,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<bool> loadData([bool isloadMoreAction = false]) async {
|
||||
final chapterContent = await Http18Comic.instance.getChapterContent(
|
||||
book,
|
||||
chapter,
|
||||
page: page,
|
||||
);
|
||||
print(chapterContent.toString());
|
||||
hasMore = chapterContent.hasNextPage;
|
||||
this.addAll(chapterContent.images);
|
||||
if (firstLoad) {
|
||||
firstLoad = false;
|
||||
isMultiPage = hasMore;
|
||||
}
|
||||
page++;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> refresh([bool notifyStateChanged = false]) {
|
||||
firstLoad = true;
|
||||
hasMore = true;
|
||||
page = 1;
|
||||
return super.refresh(notifyStateChanged);
|
||||
}
|
||||
}
|
||||
|
||||
class ChapterTab extends StatefulWidget {
|
||||
final Book book;
|
||||
final Chapter chapter;
|
||||
final List<Widget> actions;
|
||||
|
||||
const ChapterTab({Key key, this.book, this.chapter, this.actions})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_State createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<ChapterTab> {
|
||||
ChapterSourceList sourceList;
|
||||
ScrollController scrollController;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
scrollController = ScrollController();
|
||||
sourceList = ChapterSourceList(
|
||||
book: widget.book,
|
||||
chapter: widget.chapter,
|
||||
);
|
||||
widget.book.setHistory(widget.chapter);
|
||||
super.initState();
|
||||
|
||||
// 隐藏/显示 状态栏
|
||||
final setting = Provider.of<Setting>(context, listen: false);
|
||||
final hide = setting.getHideOption();
|
||||
if (hide == HideOption.auto) {
|
||||
scrollController.addListener(() {
|
||||
final isUp = scrollController.position.userScrollDirection ==
|
||||
ScrollDirection.forward;
|
||||
if (isUp)
|
||||
showStatusBar();
|
||||
else
|
||||
hideStatusBar();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
widget.book.setHistory(widget.chapter);
|
||||
scrollController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget imageBuilder(ctx, String image, int index) {
|
||||
index += 1;
|
||||
bool reDraw = false;
|
||||
try {
|
||||
int cid = int.parse(widget.chapter.cid);
|
||||
reDraw = cid >= 220980;
|
||||
// print('创建图片 cid $cid, reDraw $reDraw');
|
||||
} catch (e) {}
|
||||
return ImageWidget(
|
||||
image: image,
|
||||
index: index,
|
||||
total: sourceList.length,
|
||||
reSort: reDraw,
|
||||
);
|
||||
}
|
||||
|
||||
Widget indicatorBuilder(context, IndicatorStatus status) {
|
||||
print('indicatorBuilder $status');
|
||||
bool isSliver = true;
|
||||
Widget widget;
|
||||
switch (status) {
|
||||
case IndicatorStatus.none:
|
||||
widget = SizedBox();
|
||||
break;
|
||||
case IndicatorStatus.loadingMoreBusying:
|
||||
widget = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
AnimatedLogoWidget(width: 20, height: 30),
|
||||
SizedBox(width: 10),
|
||||
Text("正在读取")
|
||||
],
|
||||
);
|
||||
widget = Container(
|
||||
width: double.infinity,
|
||||
height: kToolbarHeight,
|
||||
child: widget,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
break;
|
||||
case IndicatorStatus.fullScreenBusying:
|
||||
widget = Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedLogoWidget(width: 25, height: 30),
|
||||
Text('读取中'),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (isSliver) {
|
||||
widget = SliverFillRemaining(
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case IndicatorStatus.error:
|
||||
case IndicatorStatus.fullScreenError:
|
||||
widget = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'读取失败\n你可能需要用梯子',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
RaisedButton(
|
||||
child: Text('再次重试'),
|
||||
onPressed: sourceList.errorRefresh,
|
||||
)
|
||||
],
|
||||
);
|
||||
widget = Container(
|
||||
width: double.infinity,
|
||||
height: kToolbarHeight,
|
||||
child: widget,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
if (status == IndicatorStatus.fullScreenError) {
|
||||
if (isSliver) {
|
||||
widget = SliverFillRemaining(
|
||||
child: widget,
|
||||
);
|
||||
} else {
|
||||
widget = CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverFillRemaining(
|
||||
child: widget,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IndicatorStatus.noMoreLoad:
|
||||
widget = SizedBox();
|
||||
break;
|
||||
case IndicatorStatus.empty:
|
||||
widget = Text(
|
||||
'没有图片',
|
||||
);
|
||||
widget = Container(
|
||||
width: double.infinity,
|
||||
height: kToolbarHeight,
|
||||
child: widget,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
if (isSliver) {
|
||||
widget = SliverToBoxAdapter(
|
||||
child: widget,
|
||||
);
|
||||
} else {
|
||||
widget = CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverFillRemaining(
|
||||
child: widget,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
snap: true,
|
||||
floating: true,
|
||||
title: Text(widget.chapter.cname),
|
||||
actions: [
|
||||
ViewerSwitcherWidget(),
|
||||
IconButton(
|
||||
icon: Icon(Icons.vertical_align_top),
|
||||
onPressed: () => scrollController.jumpTo(0.0),
|
||||
),
|
||||
...widget.actions,
|
||||
],
|
||||
),
|
||||
LoadingMoreSliverList(
|
||||
SliverListConfig(
|
||||
sourceList: sourceList,
|
||||
itemBuilder: imageBuilder,
|
||||
addSemanticIndexes: true,
|
||||
semanticIndexOffset: 10,
|
||||
autoLoadMore: true,
|
||||
indicatorBuilder: indicatorBuilder,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
82
lib/activities/chapter/drawer.dart
Normal file
82
lib/activities/chapter/drawer.dart
Normal file
@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:weiman/classes/chapter.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/widgets/book.dart';
|
||||
|
||||
class ChapterDrawer extends StatefulWidget {
|
||||
final Book book;
|
||||
final void Function(Chapter chapter) onTap;
|
||||
|
||||
const ChapterDrawer({
|
||||
Key key,
|
||||
@required this.book,
|
||||
@required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ChapterDrawer createState() => _ChapterDrawer();
|
||||
}
|
||||
|
||||
class _ChapterDrawer extends State<ChapterDrawer> {
|
||||
ScrollController _controller;
|
||||
int read;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
updateRead();
|
||||
_controller =
|
||||
ScrollController(initialScrollOffset: WidgetChapter.height * read);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void updateRead() {
|
||||
final readChapter = widget.book.chapters
|
||||
.firstWhere((chapter) => widget.book.history?.cid == chapter.cid);
|
||||
read = widget.book.chapters.indexOf(readChapter);
|
||||
}
|
||||
|
||||
void scrollToRead() {
|
||||
setState(() {
|
||||
updateRead();
|
||||
});
|
||||
_controller.animateTo(
|
||||
WidgetChapter.height * read,
|
||||
duration: Duration(milliseconds: 200),
|
||||
curve: Curves.linear,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: SafeArea(
|
||||
child: ListView(
|
||||
controller: _controller,
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: widget.book.chapters.map((chapter) {
|
||||
final isRead = widget.book.history?.cid == chapter.cid;
|
||||
return WidgetChapter(
|
||||
chapter: chapter,
|
||||
onTap: (chapter) {
|
||||
if (widget.onTap != null) widget.onTap(chapter);
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
scrollToRead();
|
||||
});
|
||||
},
|
||||
read: isRead,
|
||||
);
|
||||
}),
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
110
lib/activities/chapter/image.dart
Normal file
110
lib/activities/chapter/image.dart
Normal file
@ -0,0 +1,110 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sticky_headers/sticky_headers/widget.dart';
|
||||
import 'package:weiman/activities/chapter/viewer.dart';
|
||||
import 'package:weiman/classes/networkImageSSL.dart';
|
||||
import 'package:weiman/crawler/http18Comic.dart';
|
||||
import 'package:weiman/db/setting.dart';
|
||||
|
||||
class ImageWidget extends StatefulWidget {
|
||||
final int index;
|
||||
final int total;
|
||||
final String image;
|
||||
final bool reSort;
|
||||
|
||||
const ImageWidget({
|
||||
Key key,
|
||||
this.image,
|
||||
this.index,
|
||||
this.total,
|
||||
this.reSort = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<ImageWidget> {
|
||||
static TextStyle _style = TextStyle(color: Colors.white);
|
||||
static BoxDecoration _decoration =
|
||||
BoxDecoration(color: Colors.black.withOpacity(0.4));
|
||||
|
||||
String get tag {
|
||||
return 'image_viewer_${widget.index}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StickyHeader(
|
||||
overlapHeaders: true,
|
||||
header: SafeArea(
|
||||
top: true,
|
||||
bottom: false,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(5),
|
||||
decoration: _decoration,
|
||||
child: Text(
|
||||
'${widget.index} / ${widget.total}',
|
||||
style: _style,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
content: ExtendedImage(
|
||||
image: NetworkImageSSL(
|
||||
Http18Comic.instance,
|
||||
widget.image,
|
||||
reSort: widget.reSort,
|
||||
),
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
Widget widget;
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
widget = SizedBox(
|
||||
height: 300,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case LoadState.completed:
|
||||
widget = GestureDetector(
|
||||
child: Hero(
|
||||
child:
|
||||
ExtendedRawImage(image: state.extendedImageInfo?.image),
|
||||
tag: tag,
|
||||
),
|
||||
onTap: () => onTap(context),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return widget;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
onTap(BuildContext context) {
|
||||
final viewerSwitch =
|
||||
Provider.of<Setting>(context, listen: false).getViewerSwitch();
|
||||
// print('viewer $viewerSwitch');
|
||||
if (!viewerSwitch) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
TransparentMaterialPageRoute(
|
||||
builder: (_) => ActivityImageViewer(
|
||||
url: this.widget.image,
|
||||
heroTag: tag,
|
||||
reSort: widget.reSort,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
65
lib/activities/chapter/viewer.dart
Normal file
65
lib/activities/chapter/viewer.dart
Normal file
@ -0,0 +1,65 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:weiman/classes/networkImageSSL.dart';
|
||||
import 'package:weiman/crawler/http18Comic.dart';
|
||||
|
||||
class ActivityImageViewer extends StatefulWidget {
|
||||
final String url;
|
||||
final String heroTag;
|
||||
final bool reSort;
|
||||
|
||||
const ActivityImageViewer({
|
||||
Key key,
|
||||
this.url,
|
||||
this.heroTag,
|
||||
this.reSort = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_State createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<ActivityImageViewer> {
|
||||
double currentScale = 1.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExtendedImageSlidePage(
|
||||
slideAxis: SlideAxis.both,
|
||||
slideType: SlideType.onlyImage,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: ExtendedImage(
|
||||
image: NetworkImageSSL(
|
||||
Http18Comic.instance,
|
||||
widget.url,
|
||||
reSort: widget.reSort,
|
||||
),
|
||||
enableSlideOutPage: true,
|
||||
mode: ExtendedImageMode.gesture,
|
||||
onDoubleTap: (status) {
|
||||
currentScale = currentScale == 1 ? 3 : 1;
|
||||
status.handleDoubleTap(scale: currentScale);
|
||||
},
|
||||
heroBuilderForSlidingPage: (child) {
|
||||
return Hero(
|
||||
child: child,
|
||||
tag: widget.heroTag,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
34
lib/activities/chapter/viewerSwitcherWidget.dart
Normal file
34
lib/activities/chapter/viewerSwitcherWidget.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:weiman/db/setting.dart';
|
||||
|
||||
class ViewerSwitcherWidget extends StatefulWidget {
|
||||
@override
|
||||
ViewerSwitcherState createState() => ViewerSwitcherState();
|
||||
}
|
||||
|
||||
class ViewerSwitcherState extends State<ViewerSwitcherWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<Setting>(builder: (_, data, __) {
|
||||
final icon = data.getViewerSwitch()
|
||||
? Icons.check_box_outlined
|
||||
: Icons.check_box_outline_blank;
|
||||
return
|
||||
TextButton.icon(
|
||||
icon: Icon(icon),
|
||||
label: Text('看图'),
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
|
||||
overlayColor:
|
||||
MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.3)),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onPressed: () {
|
||||
data.setViewerSwitch(!data.getViewerSwitch());
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
42
lib/activities/checkDB.dart
Normal file
42
lib/activities/checkDB.dart
Normal file
@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
|
||||
class ActivityCheckDB extends StatefulWidget {
|
||||
@override
|
||||
_State createState() => _State();
|
||||
}
|
||||
|
||||
enum CheckState {
|
||||
Uncheck,
|
||||
Pass,
|
||||
Fail,
|
||||
}
|
||||
|
||||
class _State extends State<ActivityCheckDB> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('收藏数据检修'),
|
||||
),
|
||||
body: ListView(children: [
|
||||
ListTile(
|
||||
title: Text('所有藏书章节数量归零'),
|
||||
onTap: () async {
|
||||
for (final book in Book.bookBox.values) {
|
||||
book.chapterCount = 0;
|
||||
await book.save();
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('清空漫画数据'),
|
||||
subtitle: Text('有 ${Book.bookBox.length} 本'),
|
||||
onTap: () async {
|
||||
await Book.bookBox.clear();
|
||||
},
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,10 @@
|
||||
part of '../main.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:oktoast/oktoast.dart';
|
||||
|
||||
import 'package:weiman/classes/data.dart';
|
||||
|
||||
class ActivityCheckData extends StatefulWidget {
|
||||
@override
|
||||
@ -17,7 +23,7 @@ final titleTextStyle = TextStyle(fontSize: 14, color: Colors.blue),
|
||||
|
||||
class _State extends State<ActivityCheckData> {
|
||||
CheckState firstState;
|
||||
int firstLength;
|
||||
int firstLength = 0;
|
||||
final TextSpan secondResults = TextSpan();
|
||||
TextEditingController _outputController, _inputController;
|
||||
|
||||
@ -57,7 +63,7 @@ class _State extends State<ActivityCheckData> {
|
||||
final has = Data.has(Data.favoriteBooksKey);
|
||||
if (has) {
|
||||
final String str = Data.instance.getString(Data.favoriteBooksKey);
|
||||
final Map<String, Object> map = json.decode(str);
|
||||
final Map<String, Object> map = jsonDecode(str);
|
||||
firstLength = map.keys.length;
|
||||
_outputController.text = str;
|
||||
}
|
||||
|
134
lib/activities/dataConvert.dart
Normal file
134
lib/activities/dataConvert.dart
Normal file
@ -0,0 +1,134 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:oktoast/oktoast.dart';
|
||||
|
||||
import 'package:weiman/classes/book.dart';
|
||||
import 'package:weiman/classes/data.dart';
|
||||
import 'package:weiman/db/book.dart' as newBook;
|
||||
import 'package:weiman/main.dart';
|
||||
import 'home.dart';
|
||||
|
||||
class ActivityDataConvert extends StatefulWidget {
|
||||
@override
|
||||
_State createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<ActivityDataConvert> {
|
||||
List<Book> quick;
|
||||
Map<String, Book> favorites;
|
||||
bool selectQ = true, selectH = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
analytics.setCurrentScreen(screenName: '/activity_data_convert');
|
||||
favorites = Data.getFavorites();
|
||||
quick = Data.quickList();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future convert() async {
|
||||
int quickIndex = 0;
|
||||
int skip = 0;
|
||||
final awaitList = <Future>[];
|
||||
favorites.keys.forEach((id) {
|
||||
if (newBook.Book.bookBox.containsKey(id)) return;
|
||||
final oldBook = favorites[id];
|
||||
final isQuick = selectQ && quick.contains(oldBook.aid);
|
||||
final book = new newBook.Book(
|
||||
httpId: null,
|
||||
aid: oldBook.aid,
|
||||
name: oldBook.name,
|
||||
avatar: oldBook.avatar,
|
||||
description: oldBook.description,
|
||||
authors: [oldBook.author],
|
||||
chapterCount: oldBook.chapterCount,
|
||||
quick: isQuick ? quickIndex : null,
|
||||
needUpdate: true,
|
||||
favorite: true,
|
||||
history: null,
|
||||
);
|
||||
if (isQuick) quickIndex++;
|
||||
awaitList.add(book.save());
|
||||
});
|
||||
await Future.wait(awaitList);
|
||||
showToast(
|
||||
'成功转存 ${awaitList.length} 本小说\n跳过了 $skip 本',
|
||||
textPadding: EdgeInsets.all(10),
|
||||
);
|
||||
}
|
||||
|
||||
Future clean() async {
|
||||
await Data.instance.remove(Data.favoriteBooksKey);
|
||||
await Data.instance.remove(Data.quickKey);
|
||||
await Data.instance.remove(Data.viewHistoryKey);
|
||||
}
|
||||
|
||||
void gotoHome() {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ActivityHome(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('旧数据转存'),
|
||||
),
|
||||
body: ListView(children: [
|
||||
ListTile(
|
||||
title: Text('从v1.1.2开始,为了实现藏书分组功能,使用了新的数据存储方式'
|
||||
'\n【旧书】打开后直接搜索同名漫画。'
|
||||
'\n清空旧数据后这个界面不会再次出现。'
|
||||
'\n需要将旧的藏书数据转存为新数据吗?'
|
||||
'\n旧藏书不多的话,我个人建议直接清空,可以防止产生数据干扰')),
|
||||
ListTile(
|
||||
title: Text('收藏列表'),
|
||||
subtitle: Text('一共有 ${favorites.length} 本'),
|
||||
trailing: Checkbox(
|
||||
value: true,
|
||||
onChanged: null,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('快速导航'),
|
||||
subtitle: Text('一共有 ${quick.length} 本'),
|
||||
trailing: Checkbox(
|
||||
value: selectQ,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectQ = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
]),
|
||||
bottomNavigationBar: Row(children: [
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: OutlineButton(
|
||||
child: Text('直接清空旧数据'),
|
||||
onPressed: () async {
|
||||
await clean();
|
||||
gotoHome();
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: OutlineButton(
|
||||
child: Text('转存并清空旧数据'),
|
||||
onPressed: () async {
|
||||
await convert();
|
||||
await clean();
|
||||
gotoHome();
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,28 @@
|
||||
part of '../main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:oktoast/oktoast.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:weiman/activities/dataConvert.dart';
|
||||
import 'package:weiman/db/setting.dart';
|
||||
import 'package:weiman/provider/theme.dart';
|
||||
|
||||
import 'package:weiman/activities/checkData.dart';
|
||||
import 'package:weiman/activities/hot.dart';
|
||||
import 'package:weiman/activities/search/search.dart';
|
||||
import 'package:weiman/activities/test2.dart';
|
||||
import 'package:weiman/classes/book.dart';
|
||||
import 'package:weiman/main.dart';
|
||||
import 'package:weiman/provider/favoriteData.dart';
|
||||
import 'package:weiman/widgets/checkConnect/checkConnect.dart';
|
||||
import 'package:weiman/widgets/favorites.dart';
|
||||
import 'package:weiman/widgets/histories.dart';
|
||||
import 'package:weiman/widgets/quick.dart';
|
||||
import 'checkDB.dart';
|
||||
import 'setting/setting.dart';
|
||||
|
||||
class ActivityHome extends StatefulWidget {
|
||||
final PackageInfo packageInfo;
|
||||
|
||||
const ActivityHome(this.packageInfo, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => HomeState();
|
||||
}
|
||||
@ -14,7 +32,6 @@ class HomeState extends State<ActivityHome> {
|
||||
final List<Widget> histories = [];
|
||||
final List<Book> quick = [];
|
||||
final GlobalKey<QuickState> _quickState = GlobalKey();
|
||||
static final weekTime = 7 * 24 * 3600000;
|
||||
|
||||
bool showFavorite = true;
|
||||
|
||||
@ -26,26 +43,18 @@ class HomeState extends State<ActivityHome> {
|
||||
/// 提前检查一次藏书的更新情况
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||
autoSwitchTheme();
|
||||
_FavoriteList.getBooks();
|
||||
await _FavoriteList.checkNews();
|
||||
final updated = _FavoriteList.hasNews.values
|
||||
.where((int updatedChapters) => updatedChapters > 0)
|
||||
.length;
|
||||
FavoriteData favData = Provider.of<FavoriteData>(context, listen: false);
|
||||
await favData.loadBooksList();
|
||||
final updated = await favData.checkUpdate();
|
||||
if (updated > 0)
|
||||
showToast(
|
||||
'$updated 本藏书有更新',
|
||||
backgroundColor: Colors.black.withOpacity(0.5),
|
||||
textPadding: EdgeInsets.all(10),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void autoSwitchTheme() async {
|
||||
final isDark = await DynamicTheme.of(context).loadBrightness();
|
||||
final nowIsDark = DynamicTheme.of(context).brightness == Brightness.dark;
|
||||
if (isDark != nowIsDark)
|
||||
DynamicTheme.of(context)
|
||||
.setBrightness(isDark ? Brightness.dark : Brightness.light);
|
||||
}
|
||||
void autoSwitchTheme() async {}
|
||||
|
||||
void gotoSearch() {
|
||||
Navigator.push(
|
||||
@ -59,11 +68,15 @@ class HomeState extends State<ActivityHome> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: '/activity_recommend/'),
|
||||
settings: RouteSettings(name: '/activity_recommend'),
|
||||
builder: (_) => ActivityRank(),
|
||||
));
|
||||
}
|
||||
|
||||
void gotoPatreon() {
|
||||
launch('https://www.patreon.com/nrop19');
|
||||
}
|
||||
|
||||
bool isEdit = false;
|
||||
|
||||
void _draggableModeChanged(bool mode) {
|
||||
@ -72,6 +85,85 @@ class HomeState extends State<ActivityHome> {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Widget themeButton() {
|
||||
final system = FontAwesomeIcons.cloudSun,
|
||||
light = FontAwesomeIcons.solidSun,
|
||||
dark = FontAwesomeIcons.solidMoon;
|
||||
final theme = Provider.of<ThemeProvider>(context, listen: false);
|
||||
Widget themeIcon;
|
||||
switch (theme.themeMode) {
|
||||
case ThemeMode.light:
|
||||
themeIcon = Icon(light);
|
||||
break;
|
||||
case ThemeMode.dark:
|
||||
themeIcon = Icon(dark);
|
||||
break;
|
||||
default:
|
||||
themeIcon = Icon(system);
|
||||
break;
|
||||
}
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
switch (theme.themeMode) {
|
||||
case ThemeMode.light:
|
||||
theme.changeTheme(ThemeMode.dark);
|
||||
break;
|
||||
case ThemeMode.dark:
|
||||
theme.changeTheme(ThemeMode.system);
|
||||
break;
|
||||
default:
|
||||
theme.changeTheme(ThemeMode.light);
|
||||
}
|
||||
Provider.of<Setting>(context, listen: false)
|
||||
.setThemeMode(theme.themeMode);
|
||||
showToastWidget(
|
||||
Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(
|
||||
system,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('跟随系统,自动切换明暗模式\n如果系统不支持,默认为明亮模式'),
|
||||
]),
|
||||
SizedBox(height: 10),
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(
|
||||
light,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('为明亮模式'),
|
||||
]),
|
||||
SizedBox(height: 10),
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(
|
||||
dark,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('为暗黑模式'),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
dismissOtherToast: true,
|
||||
duration: Duration(seconds: 4),
|
||||
);
|
||||
},
|
||||
icon: themeIcon,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final media = MediaQuery.of(context);
|
||||
@ -79,7 +171,7 @@ class HomeState extends State<ActivityHome> {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: Text('微漫 v' + widget.packageInfo.version),
|
||||
title: Text('微漫 v' + packageInfo.version),
|
||||
automaticallyImplyLeading: false,
|
||||
leading: isEdit
|
||||
? IconButton(
|
||||
@ -91,18 +183,20 @@ class HomeState extends State<ActivityHome> {
|
||||
: null,
|
||||
actions: <Widget>[
|
||||
/// 黑白样式切换
|
||||
themeButton(),
|
||||
SizedBox(width: 20),
|
||||
|
||||
/// 设置界面
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
DynamicTheme.of(context).setBrightness(
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Brightness.light
|
||||
: Brightness.dark);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: '/activity_setting'),
|
||||
builder: (_) => ActivitySetting()));
|
||||
},
|
||||
icon: Icon(Theme.of(context).brightness == Brightness.light
|
||||
? FontAwesomeIcons.lightbulb
|
||||
: FontAwesomeIcons.solidLightbulb),
|
||||
icon: Icon(FontAwesomeIcons.cog),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
|
||||
/// 收藏列表
|
||||
IconButton(
|
||||
@ -127,9 +221,11 @@ class HomeState extends State<ActivityHome> {
|
||||
),
|
||||
],
|
||||
),
|
||||
drawerEnableOpenDragGesture: false,
|
||||
endDrawerEnableOpenDragGesture: false,
|
||||
endDrawer: Drawer(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, __) {
|
||||
builder: (_, constraints) {
|
||||
if (showFavorite) {
|
||||
return FavoriteList();
|
||||
} else {
|
||||
@ -138,112 +234,147 @@ class HomeState extends State<ActivityHome> {
|
||||
},
|
||||
),
|
||||
),
|
||||
body: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.only(left: 40, right: 40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
child: OutlineButton(
|
||||
onPressed: gotoSearch,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.search,
|
||||
color: Colors.blue,
|
||||
),
|
||||
Text(
|
||||
'搜索漫画',
|
||||
style: TextStyle(color: Colors.blue),
|
||||
)
|
||||
],
|
||||
),
|
||||
borderSide: BorderSide(color: Colors.blue, width: 2),
|
||||
shape: StadiumBorder(),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlineButton(
|
||||
onPressed: gotoRecommend,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.whatshot,
|
||||
color: Colors.red,
|
||||
),
|
||||
Text(
|
||||
'月排行榜',
|
||||
style: TextStyle(color: Colors.red),
|
||||
)
|
||||
],
|
||||
),
|
||||
borderSide: BorderSide(color: Colors.red, width: 2),
|
||||
shape: StadiumBorder(),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(left: 40, right: 40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
child: OutlineButton(
|
||||
onPressed: gotoSearch,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.search,
|
||||
color: Colors.blue,
|
||||
),
|
||||
Text(
|
||||
'搜索漫画',
|
||||
style: TextStyle(color: Colors.blue),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: Quick(
|
||||
key: _quickState,
|
||||
width: width,
|
||||
draggableModeChanged: _draggableModeChanged,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 10),
|
||||
child: Text(
|
||||
'在 level-plus.net 论坛首发',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey[500]),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (await canLaunch('tg://resolve?domain=weiman_app'))
|
||||
launch('tg://resolve?domain=weiman_app');
|
||||
else
|
||||
launch('https://t.me/weiman_app');
|
||||
},
|
||||
child: Text(
|
||||
'Telegram广播频道',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.blue[200],
|
||||
decoration: TextDecoration.underline,
|
||||
borderSide: BorderSide(color: Colors.blue, width: 2),
|
||||
shape: StadiumBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: isDevMode,
|
||||
child: FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (_) => ActivityTest()));
|
||||
},
|
||||
child: Text('测试界面'),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: OutlineButton(
|
||||
onPressed: gotoRecommend,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.whatshot,
|
||||
color: Colors.red,
|
||||
),
|
||||
Text(
|
||||
'热门漫画',
|
||||
style: TextStyle(color: Colors.red),
|
||||
)
|
||||
],
|
||||
),
|
||||
borderSide: BorderSide(color: Colors.red, width: 2),
|
||||
shape: StadiumBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: isDevMode,
|
||||
child: FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (_) => ActivityCheckData()));
|
||||
},
|
||||
child: Text('操作 收藏列表数据'),
|
||||
Center(
|
||||
child: Quick(
|
||||
key: _quickState,
|
||||
width: width,
|
||||
draggableModeChanged: _draggableModeChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
CheckConnectWidget(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
launch('https://bbs.level-plus.net/');
|
||||
},
|
||||
child: Text(
|
||||
'魂+论坛首发',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.blue[200],
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (await canLaunch('tg://resolve?domain=weiman_app'))
|
||||
launch('tg://resolve?domain=weiman_app');
|
||||
else
|
||||
launch('https://t.me/weiman_app');
|
||||
},
|
||||
child: Text(
|
||||
'Telegram 广播频道',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.blue[200],
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Visibility(
|
||||
visible: isDevMode,
|
||||
child: FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (_) => ActivityCheckData()));
|
||||
},
|
||||
child: Text('操作 收藏列表数据'),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: isDevMode,
|
||||
child: FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (_) => ActivityCheckDB()));
|
||||
},
|
||||
child: Text('操作 DB数据'),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: isDevMode,
|
||||
child: FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ActivityDataConvert()));
|
||||
},
|
||||
child: Text('进入旧数据处理功能'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: isDevMode
|
||||
? FloatingActionButton(
|
||||
child: Text('测试'),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context, MaterialPageRoute(builder: (_) => ActivityTest()));
|
||||
},
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
238
lib/activities/hot.dart
Normal file
238
lib/activities/hot.dart
Normal file
@ -0,0 +1,238 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_more_list/loading_more_list.dart';
|
||||
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||
import 'package:weiman/widgets/animatedLogo.dart';
|
||||
|
||||
import 'package:weiman/crawler/http.dart';
|
||||
import 'package:weiman/crawler/http18Comic.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/widgets/book.dart';
|
||||
import 'package:weiman/widgets/pullToRefreshHeader.dart';
|
||||
|
||||
class ActivityRank extends StatefulWidget {
|
||||
@override
|
||||
_ActivityRank createState() => _ActivityRank();
|
||||
}
|
||||
|
||||
class _ActivityRank extends State<ActivityRank>
|
||||
with SingleTickerProviderStateMixin {
|
||||
TabController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
controller = TabController(length: 2, vsync: this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('热门漫画'),
|
||||
bottom: TabBar(controller: controller, tabs: [
|
||||
Tab(text: '韩漫'),
|
||||
Tab(text: '全部'),
|
||||
]),
|
||||
),
|
||||
body: TabBarView(controller: controller, children: [
|
||||
HotTab(http: Http18Comic.instance, type: '/hanman'),
|
||||
HotTab(http: Http18Comic.instance, type: ''),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SourceList extends LoadingMoreBase<Book> {
|
||||
final String type;
|
||||
final HttpBook http;
|
||||
int page = 1;
|
||||
String firstBookId;
|
||||
|
||||
bool hasMore = true;
|
||||
|
||||
SourceList({this.type, this.http});
|
||||
|
||||
@override
|
||||
Future<bool> loadData([bool isloadMoreAction = false]) async {
|
||||
try {
|
||||
final books = await http.hotBooks(type, page);
|
||||
if (books.isEmpty) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
if (firstBookId == books[0].aid) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
firstBookId = books[0].aid;
|
||||
page++;
|
||||
this.addAll(books);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> refresh([bool notifyStateChanged = false]) {
|
||||
hasMore = true;
|
||||
page = 1;
|
||||
return super.refresh(notifyStateChanged);
|
||||
}
|
||||
}
|
||||
|
||||
class HotTab extends StatefulWidget {
|
||||
final String type;
|
||||
final HttpBook http;
|
||||
|
||||
const HotTab({Key key, this.type, this.http}) : super(key: key);
|
||||
|
||||
@override
|
||||
_HotTab createState() => _HotTab();
|
||||
}
|
||||
|
||||
class _HotTab extends State<HotTab> {
|
||||
SourceList sourceList;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
sourceList = SourceList(type: widget.type, http: widget.http);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
PullToRefreshContainer(
|
||||
(info) => SliverPullToRefreshHeader(info: info),
|
||||
),
|
||||
LoadingMoreSliverList(SliverListConfig<Book>(
|
||||
sourceList: sourceList,
|
||||
indicatorBuilder: indicatorBuilder,
|
||||
itemBuilder: (_, book, __) => WidgetBook(
|
||||
book,
|
||||
subtitle: book.authors?.join('/'),
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget book(Book book) {
|
||||
return WidgetBook(book, subtitle: book.authors?.join('/'));
|
||||
}
|
||||
|
||||
Widget indicatorBuilder(context, IndicatorStatus status) {
|
||||
print('indicatorBuilder $status');
|
||||
bool isSliver = true;
|
||||
Widget widget;
|
||||
switch (status) {
|
||||
case IndicatorStatus.none:
|
||||
widget = SizedBox();
|
||||
break;
|
||||
case IndicatorStatus.loadingMoreBusying:
|
||||
widget = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
AnimatedLogoWidget(width: 20, height: 30),
|
||||
SizedBox(width: 10),
|
||||
Text("正在读取")
|
||||
],
|
||||
);
|
||||
widget = _setbackground(false, widget, 35.0);
|
||||
break;
|
||||
case IndicatorStatus.fullScreenBusying:
|
||||
widget = Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedLogoWidget(width: 25, height: 30),
|
||||
Text('读取中'),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (isSliver) {
|
||||
widget = SliverFillRemaining(
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case IndicatorStatus.error:
|
||||
case IndicatorStatus.fullScreenError:
|
||||
widget = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'读取失败\n你可能需要用梯子',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
RaisedButton(
|
||||
child: Text('再次重试'),
|
||||
onPressed: sourceList.errorRefresh,
|
||||
)
|
||||
],
|
||||
);
|
||||
final height = status == IndicatorStatus.error ? 35.0 : double.infinity;
|
||||
widget = _setbackground(false, widget, height);
|
||||
if (status == IndicatorStatus.fullScreenError) {
|
||||
if (isSliver) {
|
||||
widget = SliverFillRemaining(
|
||||
child: widget,
|
||||
);
|
||||
} else {
|
||||
widget = CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverFillRemaining(
|
||||
child: widget,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IndicatorStatus.noMoreLoad:
|
||||
widget = Text("已经显示全部搜索结果");
|
||||
widget = _setbackground(false, widget, 35.0);
|
||||
break;
|
||||
case IndicatorStatus.empty:
|
||||
widget = Text(
|
||||
'没有内容',
|
||||
);
|
||||
widget = _setbackground(true, widget, double.infinity);
|
||||
if (isSliver) {
|
||||
widget = SliverToBoxAdapter(
|
||||
child: widget,
|
||||
);
|
||||
} else {
|
||||
widget = CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverFillRemaining(
|
||||
child: widget,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
Widget _setbackground(bool full, Widget widget, double height) {
|
||||
widget = Container(
|
||||
width: double.infinity,
|
||||
height: kToolbarHeight,
|
||||
child: widget,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
return widget;
|
||||
}
|
||||
|
||||
Widget getIndicator(BuildContext context) {
|
||||
return CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ActivitySearch extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Search();
|
||||
}
|
||||
}
|
||||
|
||||
class Search extends StatefulWidget {
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return SearchState();
|
||||
}
|
||||
}
|
||||
|
||||
class SearchState extends State<Search> {
|
||||
TextEditingController _controller = TextEditingController();
|
||||
GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
|
||||
final List<Book> _books = [];
|
||||
bool loading;
|
||||
|
||||
void submit() {
|
||||
_refresh.currentState
|
||||
.show(notificationDragOffset: SliverPullToRefreshHeader.height);
|
||||
}
|
||||
|
||||
Future<bool> startSearch() async {
|
||||
print('搜索漫画: ' + _controller.text);
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
_books.clear();
|
||||
try {
|
||||
final List<Book> books = await UserAgentClient.instance
|
||||
.searchBook(_controller.text)
|
||||
.timeout(Duration(seconds: 5));
|
||||
_books.addAll(books);
|
||||
loading = false;
|
||||
} catch (e) {
|
||||
loading = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: PullToRefreshNotification(
|
||||
key: _refresh,
|
||||
onRefresh: startSearch,
|
||||
child: CustomScrollView(slivers: [
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
title: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: (RawKeyEvent event) {
|
||||
print(
|
||||
'is enter: ${LogicalKeyboardKey.enter == event.logicalKey}');
|
||||
if (_controller.text.isEmpty) return;
|
||||
if (event.runtimeType == RawKeyUpEvent &&
|
||||
LogicalKeyboardKey.enter == event.logicalKey) {
|
||||
print('回车键搜索');
|
||||
submit();
|
||||
}
|
||||
},
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索书名',
|
||||
prefixIcon: IconButton(
|
||||
onPressed: () {
|
||||
_refresh.currentState.show(
|
||||
notificationDragOffset:
|
||||
SliverPullToRefreshHeader.height);
|
||||
},
|
||||
icon: Icon(Icons.search),
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
controller: _controller,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (String name) {
|
||||
print('onSubmitted');
|
||||
submit();
|
||||
},
|
||||
keyboardType: TextInputType.text,
|
||||
onEditingComplete: () {
|
||||
print('onEditingComplete');
|
||||
submit();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
PullToRefreshContainer((info) => SliverPullToRefreshHeader(
|
||||
info: info,
|
||||
onTap: submit,
|
||||
)),
|
||||
SliverLayoutBuilder(
|
||||
builder: (_, __) {
|
||||
if (loading == null)
|
||||
return SliverFillRemaining(
|
||||
child: Center(child: Text('输入关键词搜索')));
|
||||
if (loading) return SliverToBoxAdapter();
|
||||
if (_books.length == 0) {
|
||||
return SliverFillRemaining(child: Center(child: Text('一本也没有')));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((_, i) {
|
||||
return WidgetBook(
|
||||
_books[i],
|
||||
subtitle: _books[i].author,
|
||||
);
|
||||
}, childCount: _books.length),
|
||||
);
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
103
lib/activities/search/search.dart
Normal file
103
lib/activities/search/search.dart
Normal file
@ -0,0 +1,103 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:focus_widget/focus_widget.dart';
|
||||
|
||||
import 'package:weiman/crawler/http18Comic.dart';
|
||||
import 'tab.dart';
|
||||
|
||||
class ActivitySearch extends StatefulWidget {
|
||||
final String search;
|
||||
|
||||
const ActivitySearch({Key key, this.search = ''}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return SearchState();
|
||||
}
|
||||
}
|
||||
|
||||
class SearchState extends State<ActivitySearch>
|
||||
with SingleTickerProviderStateMixin {
|
||||
TextEditingController _controller;
|
||||
GlobalKey<SearchTabState> key = GlobalKey<SearchTabState>();
|
||||
|
||||
@override
|
||||
initState() {
|
||||
_controller = TextEditingController(text: widget.search);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void search() {
|
||||
key.currentState.search = _controller.text;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: (RawKeyEvent event) {
|
||||
print('is enter: ${LogicalKeyboardKey.enter == event.logicalKey}');
|
||||
if (_controller.text.isEmpty) return;
|
||||
if (event.runtimeType == RawKeyUpEvent &&
|
||||
LogicalKeyboardKey.enter == event.logicalKey) {
|
||||
print('回车键搜索');
|
||||
search();
|
||||
}
|
||||
},
|
||||
child: FocusWidget.builder(
|
||||
context,
|
||||
builder: (_, focusNode) => TextField(
|
||||
focusNode: focusNode,
|
||||
style: TextStyle(color: Colors.white),
|
||||
cursorColor: Colors.white,
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索书名',
|
||||
prefixIcon: IconButton(
|
||||
onPressed: search,
|
||||
icon: Icon(Icons.search, color: Colors.white),
|
||||
),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white),
|
||||
),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white),
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
controller: _controller,
|
||||
autofocus: widget.search.isEmpty,
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (String name) {
|
||||
focusNode.unfocus();
|
||||
print('onSubmitted');
|
||||
search();
|
||||
},
|
||||
keyboardType: TextInputType.text,
|
||||
onEditingComplete: () {
|
||||
focusNode.unfocus();
|
||||
print('onEditingComplete');
|
||||
search();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SearchTab(
|
||||
http: Http18Comic.instance,
|
||||
search: _controller.text,
|
||||
key: key,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
46
lib/activities/search/source.dart
Normal file
46
lib/activities/search/source.dart
Normal file
@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_more_list/loading_more_list.dart';
|
||||
|
||||
import '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);
|
||||
}
|
||||
}
|
183
lib/activities/search/tab.dart
Normal file
183
lib/activities/search/tab.dart
Normal file
@ -0,0 +1,183 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_more_list/loading_more_list.dart';
|
||||
import 'package:weiman/activities/search/source.dart';
|
||||
import 'package:weiman/crawler/http.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/widgets/book.dart';
|
||||
|
||||
class SearchTab extends StatefulWidget {
|
||||
final HttpBook http;
|
||||
final String search;
|
||||
|
||||
const SearchTab({
|
||||
Key key,
|
||||
@required this.http,
|
||||
this.search,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
SearchTabState createState() => SearchTabState();
|
||||
}
|
||||
|
||||
class SearchTabState extends State<SearchTab>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
SearchSourceList sourceList;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
sourceList = SearchSourceList(http: widget.http, search: widget.search);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget book(Book book) {
|
||||
return WidgetBook(book, subtitle: book.authors.join('/'));
|
||||
}
|
||||
|
||||
Future<bool> refresh() async {
|
||||
return sourceList.refresh(true);
|
||||
}
|
||||
|
||||
get search => sourceList.search;
|
||||
|
||||
set search(String value) {
|
||||
print('tab search $value');
|
||||
sourceList.search = value;
|
||||
sourceList.refresh(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return LoadingMoreList(
|
||||
ListConfig(
|
||||
sourceList: sourceList,
|
||||
itemBuilder: (_, item, index) => book(item),
|
||||
autoLoadMore: true,
|
||||
indicatorBuilder: indicatorBuilder,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget indicatorBuilder(context, IndicatorStatus status) {
|
||||
bool isSliver = false;
|
||||
Widget widget;
|
||||
switch (status) {
|
||||
case IndicatorStatus.none:
|
||||
widget = SizedBox();
|
||||
break;
|
||||
case IndicatorStatus.loadingMoreBusying:
|
||||
widget = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(right: 5.0),
|
||||
height: 15.0,
|
||||
width: 15.0,
|
||||
child: getIndicator(context),
|
||||
),
|
||||
Text("正在读取")
|
||||
],
|
||||
);
|
||||
widget = _setbackground(false, widget, 35.0);
|
||||
break;
|
||||
case IndicatorStatus.fullScreenBusying:
|
||||
widget = widget = _setbackground(
|
||||
false,
|
||||
Text(
|
||||
'正在读取',
|
||||
),
|
||||
35.0);
|
||||
if (isSliver) {
|
||||
widget = SliverFillRemaining(
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case IndicatorStatus.error:
|
||||
widget = _setbackground(
|
||||
false,
|
||||
Text(
|
||||
'网络错误\n点击重试',
|
||||
),
|
||||
35.0);
|
||||
|
||||
widget = GestureDetector(
|
||||
onTap: () {
|
||||
sourceList.errorRefresh();
|
||||
},
|
||||
child: widget,
|
||||
);
|
||||
break;
|
||||
case IndicatorStatus.fullScreenError:
|
||||
widget = Text(
|
||||
'读取失败,如果失败的次数太多可能需要用梯子',
|
||||
);
|
||||
widget = _setbackground(true, widget, double.infinity);
|
||||
widget = GestureDetector(
|
||||
onTap: () {
|
||||
sourceList.errorRefresh();
|
||||
},
|
||||
child: widget,
|
||||
);
|
||||
if (isSliver) {
|
||||
widget = SliverFillRemaining(
|
||||
child: widget,
|
||||
);
|
||||
} else {
|
||||
widget = CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverFillRemaining(
|
||||
child: widget,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
break;
|
||||
case IndicatorStatus.noMoreLoad:
|
||||
widget = Text("已经显示全部搜索结果");
|
||||
widget = _setbackground(false, widget, 35.0);
|
||||
break;
|
||||
case IndicatorStatus.empty:
|
||||
widget = Text(
|
||||
sourceList.search.isEmpty ? '请输入搜索内容' : '搜索不到任何内容',
|
||||
);
|
||||
widget = _setbackground(true, widget, double.infinity);
|
||||
if (isSliver) {
|
||||
widget = SliverToBoxAdapter(
|
||||
child: widget,
|
||||
);
|
||||
} else {
|
||||
widget = CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverFillRemaining(
|
||||
child: widget,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
Widget _setbackground(bool full, Widget widget, double height) {
|
||||
widget = Container(
|
||||
width: double.infinity,
|
||||
height: kToolbarHeight,
|
||||
child: widget,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
return widget;
|
||||
}
|
||||
|
||||
Widget getIndicator(BuildContext context) {
|
||||
return CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
35
lib/activities/setting/hideStatusBar.dart
Normal file
35
lib/activities/setting/hideStatusBar.dart
Normal file
@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:weiman/db/setting.dart';
|
||||
|
||||
class HideStatusBar extends StatelessWidget {
|
||||
final options = {
|
||||
'自动': HideOption.auto,
|
||||
'全程隐藏': HideOption.always,
|
||||
'不隐藏': HideOption.none,
|
||||
};
|
||||
final Function(HideOption option) onChanged;
|
||||
final HideOption option;
|
||||
|
||||
HideStatusBar({Key key, @required this.onChanged, @required this.option})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text('看漫画时隐藏状态栏'),
|
||||
subtitle: Text('自动:随着图片列表的上下滚动而自动显示或隐藏状态栏\n'
|
||||
'全程隐藏:进入看图界面就隐藏状态栏,退出就显示状态栏\n'
|
||||
'不隐藏:就是不隐藏状态栏咯'),
|
||||
trailing: DropdownButton<HideOption>(
|
||||
value: option,
|
||||
items: options.keys
|
||||
.map((key) => DropdownMenuItem(
|
||||
child: Text(key),
|
||||
value: options[key],
|
||||
))
|
||||
.toList(),
|
||||
onChanged: onChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
192
lib/activities/setting/setting.dart
Normal file
192
lib/activities/setting/setting.dart
Normal file
@ -0,0 +1,192 @@
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:oktoast/oktoast.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:weiman/activities/setting/hideStatusBar.dart';
|
||||
import 'package:weiman/activities/setting/web.dart';
|
||||
import 'package:weiman/db/setting.dart';
|
||||
import 'package:weiman/main.dart';
|
||||
|
||||
class ActivitySetting extends StatefulWidget {
|
||||
@override
|
||||
_ActivitySetting createState() => _ActivitySetting();
|
||||
}
|
||||
|
||||
class _ActivitySetting extends State<ActivitySetting> {
|
||||
int imagesCount, sizeCount;
|
||||
bool isClearing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
imageCaches();
|
||||
}
|
||||
|
||||
Future<void> imageCaches() async {
|
||||
final files = imageCacheDir.listSync();
|
||||
imagesCount = files.length;
|
||||
sizeCount = 0;
|
||||
files.forEach((file) => sizeCount += file.statSync().size);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> clearDiskCachedImages() async {
|
||||
await imageCacheDir.delete(recursive: true);
|
||||
await imageCacheDir.create();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('设置')),
|
||||
body: Consumer<Setting>(builder: (_, data, __) {
|
||||
print('代理 ${data.getProxy()}');
|
||||
return ListView(
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: [
|
||||
/// 隐藏状态栏设置
|
||||
HideStatusBar(
|
||||
option: data.getHideOption(),
|
||||
onChanged: (option) => data.setHideOption(option),
|
||||
),
|
||||
|
||||
/// 设置代理
|
||||
ListTile(
|
||||
title: Text('设置代理'),
|
||||
subtitle: Text(data.getProxy() ?? '无'),
|
||||
onTap: () async {
|
||||
var proxy = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
final _c = TextEditingController(text: data.getProxy());
|
||||
return WillPopScope(
|
||||
child: AlertDialog(
|
||||
title: Text('设置网络代理'),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'只支持http代理\nSS,SSR,V2Ray,Trojan(Clash)\n这些梯子App都有提供Http代理功能'),
|
||||
TextField(
|
||||
controller: _c,
|
||||
decoration: InputDecoration(
|
||||
hintText: '例如Clash提供的127.0.0.1:7890'),
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('清空'),
|
||||
onPressed: () {
|
||||
_c.clear();
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('确定'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, _c.text);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onWillPop: () {
|
||||
Navigator.pop(context, '-1');
|
||||
return Future.value(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
print('用户输入 $proxy');
|
||||
if (proxy == '-1') return;
|
||||
// 在前
|
||||
if (proxy != null) {
|
||||
proxy = proxy
|
||||
.trim()
|
||||
.replaceFirst('http://', '')
|
||||
.replaceFirst('https://', '');
|
||||
}
|
||||
// 在后
|
||||
if (proxy == null || proxy.isEmpty) {
|
||||
proxy = null;
|
||||
}
|
||||
print('设置代理 $proxy');
|
||||
await data.setProxy(proxy);
|
||||
},
|
||||
),
|
||||
|
||||
/// 清空图片缓存
|
||||
ListTile(
|
||||
title: Text('清除所有图片缓存'),
|
||||
subtitle: isClearing
|
||||
? Text('清理中')
|
||||
: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: '图片数量:'),
|
||||
TextSpan(
|
||||
text: imagesCount == null
|
||||
? '读取中'
|
||||
: '$imagesCount 张'),
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(text: '存储容量:'),
|
||||
TextSpan(
|
||||
text: sizeCount == null
|
||||
? '读取中'
|
||||
: '${filesize(sizeCount)}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
if (isClearing == true) return;
|
||||
final sure = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text('确认清除所有图片缓存?'),
|
||||
actions: [
|
||||
RaisedButton(
|
||||
child: Text('确认'),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (sure == true) {
|
||||
showToast('正在清理图片缓存');
|
||||
isClearing = true;
|
||||
setState(() {});
|
||||
await clearDiskCachedImages();
|
||||
isClearing = false;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
await imageCaches();
|
||||
}
|
||||
showToast('成功清理图片缓存');
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: Text('查看最新版'),
|
||||
subtitle: Text('当前版本为 ${packageInfo.version}'),
|
||||
onTap: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (_) => ActivityWeb()));
|
||||
},
|
||||
),
|
||||
|
||||
/// 清空数据缓存
|
||||
/* ListTile(
|
||||
title: Text('清空漫画数据缓存'),
|
||||
subtitle: Text('正常情况是不需要清空的'),
|
||||
onTap: () async {
|
||||
await HttpBook.dataCache.clearAll();
|
||||
showToast('成功清空漫画数据缓存', textPadding: EdgeInsets.all(10));
|
||||
},
|
||||
),*/
|
||||
],
|
||||
).toList(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
50
lib/activities/setting/web.dart
Normal file
50
lib/activities/setting/web.dart
Normal file
@ -0,0 +1,50 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:weiman/main.dart';
|
||||
|
||||
class ActivityWeb extends StatefulWidget {
|
||||
@override
|
||||
_State createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<ActivityWeb> {
|
||||
LoadState state = LoadState.loading;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
analytics.setCurrentScreen(screenName: '/activity_update_web');
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('最新版本'),
|
||||
),
|
||||
body: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
WebView(
|
||||
initialUrl: 'https://nrop19.github.io/weiman_app',
|
||||
onWebViewCreated: (controller) {
|
||||
state = LoadState.loading;
|
||||
setState(() {});
|
||||
},
|
||||
onPageFinished: (_) {
|
||||
state = LoadState.completed;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (state == LoadState.loading)
|
||||
Container(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ActivityTest extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('asd'),
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: save,
|
||||
child: Text('保存'),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: read,
|
||||
child: Text('读取'),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: clear,
|
||||
child: Text('清空数据'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void save() {
|
||||
Data.addFavorite(Book(
|
||||
aid: '123',
|
||||
name: 'name',
|
||||
avatar: 'avatar',
|
||||
description: '',
|
||||
author: ''));
|
||||
}
|
||||
|
||||
void read() {
|
||||
var books = Data.getFavorites();
|
||||
print(jsonEncode(books));
|
||||
}
|
||||
|
||||
void clear() {
|
||||
Data.clear();
|
||||
}
|
||||
}
|
@ -1,17 +1,24 @@
|
||||
part of '../main.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:weiman/crawler/http.dart';
|
||||
import 'data.dart';
|
||||
class Book {
|
||||
final String aid; // 书本ID
|
||||
final String http;
|
||||
final String aid; // 漫画的数据库ID
|
||||
final String name; // 书本名称
|
||||
final String avatar; // 书本封面
|
||||
final String author; // 画家
|
||||
final String description; // 描述
|
||||
final List<Chapter> chapters;
|
||||
final int chapterCount;
|
||||
final int version;
|
||||
|
||||
History history;
|
||||
|
||||
Book({
|
||||
@required this.http,
|
||||
@required this.name,
|
||||
@required this.aid,
|
||||
@required this.avatar,
|
||||
@ -19,6 +26,8 @@ class Book {
|
||||
this.description,
|
||||
this.chapters: const [],
|
||||
this.chapterCount: 0,
|
||||
this.history,
|
||||
this.version: 0,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -31,34 +40,31 @@ class Book {
|
||||
return books.containsKey(aid);
|
||||
}
|
||||
|
||||
favorite() {
|
||||
if (isFavorite())
|
||||
Data.removeFavorite(this);
|
||||
else
|
||||
Data.addFavorite(this);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
print('book toJson');
|
||||
final Map<String, dynamic> data = {
|
||||
'http': http,
|
||||
'aid': aid,
|
||||
'name': name,
|
||||
'avatar': avatar,
|
||||
'author': author,
|
||||
'chapterCount': chapterCount,
|
||||
'version': version,
|
||||
};
|
||||
if (history != null) data['history'] = history.toJson();
|
||||
return data;
|
||||
}
|
||||
|
||||
static Book fromJson(Map<String, dynamic> json) {
|
||||
factory Book.fromJson(Map<String, dynamic> json) {
|
||||
final book = Book(
|
||||
aid: json['aid'],
|
||||
name: json['name'],
|
||||
avatar: json['avatar'],
|
||||
author: json['author'],
|
||||
description: json['description'],
|
||||
chapterCount: json['chapterCount'] ?? 0,
|
||||
);
|
||||
http: json['http'],
|
||||
aid: json['aid'],
|
||||
name: json['name'],
|
||||
avatar: json['avatar'],
|
||||
author: json['author'],
|
||||
description: json['description'],
|
||||
chapterCount: json['chapterCount'] ?? 0,
|
||||
version: json['version'] ?? 0);
|
||||
if (json.containsKey('history'))
|
||||
book.history = History.fromJson(json['history']);
|
||||
return book;
|
||||
@ -66,15 +72,26 @@ class Book {
|
||||
}
|
||||
|
||||
class Chapter {
|
||||
final HttpBook http;
|
||||
final String cid; // 章节cid
|
||||
final String cname; // 章节名称
|
||||
final String avatar; // 章节封面
|
||||
|
||||
Chapter({@required this.cid, @required this.cname, @required this.avatar});
|
||||
Chapter({
|
||||
@required this.http,
|
||||
@required this.cid,
|
||||
@required this.cname,
|
||||
@required this.avatar,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return jsonEncode({cid: cid, cname: cname, avatar: avatar});
|
||||
final Map<String, String> data = {
|
||||
'cid': cid,
|
||||
'cname': cname,
|
||||
'avatar': avatar,
|
||||
};
|
||||
return jsonEncode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,8 +99,14 @@ class History {
|
||||
final String cid;
|
||||
final String cname;
|
||||
final int time;
|
||||
final int image;
|
||||
|
||||
History({@required this.cid, @required this.cname, @required this.time});
|
||||
History({
|
||||
@required this.cid,
|
||||
@required this.cname,
|
||||
@required this.time,
|
||||
this.image = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => jsonEncode(toJson());
|
||||
@ -93,11 +116,17 @@ class History {
|
||||
'cid': cid,
|
||||
'cname': cname,
|
||||
'time': time,
|
||||
'image': image,
|
||||
};
|
||||
}
|
||||
|
||||
static History fromJson(Map<String, dynamic> json) {
|
||||
return History(cid: json['cid'], cname: json['cname'], time: json['time']);
|
||||
return History(
|
||||
cid: json['cid'],
|
||||
cname: json['cname'],
|
||||
time: json['time'],
|
||||
image: json['image'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
static History fromChapter(Chapter chapter) {
|
||||
|
24
lib/classes/chapter.dart
Normal file
24
lib/classes/chapter.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Chapter {
|
||||
final String cid; // 章节cid
|
||||
final String cname; // 章节名称
|
||||
final DateTime time; // 章节更新时间
|
||||
|
||||
Chapter({
|
||||
@required this.cid,
|
||||
@required this.cname,
|
||||
this.time,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final Map<String, String> data = {
|
||||
'cid': cid,
|
||||
'cname': cname,
|
||||
};
|
||||
return jsonEncode(data);
|
||||
}
|
||||
}
|
11
lib/classes/chapterContent.dart
Normal file
11
lib/classes/chapterContent.dart
Normal file
@ -0,0 +1,11 @@
|
||||
class ChapterContent {
|
||||
final List<String> images;
|
||||
final bool hasNextPage;
|
||||
|
||||
ChapterContent(this.images, this.hasNextPage);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChapterContent images:${images.length} nexPage:$hasNextPage';
|
||||
}
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
part of '../main.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'book.dart';
|
||||
|
||||
class Data {
|
||||
static SharedPreferences instance;
|
||||
@ -22,7 +26,7 @@ class Data {
|
||||
} else if (value is double) {
|
||||
instance.setDouble(key, value);
|
||||
} else if (value is Map) {
|
||||
instance.setString(key, jsonEncode(value));
|
||||
instance.setString(key, json.encode(value));
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +34,11 @@ class Data {
|
||||
return instance.get(key);
|
||||
}
|
||||
|
||||
static bool hasData() {
|
||||
return instance.containsKey(favoriteBooksKey) ||
|
||||
instance.containsKey(viewHistoryKey);
|
||||
}
|
||||
|
||||
static Map<String, Book> getFavorites() {
|
||||
if (has(favoriteBooksKey)) {
|
||||
final String str = instance.getString(favoriteBooksKey);
|
||||
@ -110,7 +119,7 @@ class Data {
|
||||
}
|
||||
|
||||
/// 快速导航 id 列表,内部方法
|
||||
static List<String> _quickIdList() {
|
||||
static List<String> quickIdList() {
|
||||
if (instance.containsKey(quickKey)) {
|
||||
return instance.getStringList(quickKey);
|
||||
}
|
||||
@ -121,7 +130,7 @@ class Data {
|
||||
static List<Book> quickList() {
|
||||
final books = getFavorites();
|
||||
final ids = books.keys;
|
||||
final List<String> quickIds = _quickIdList();
|
||||
final List<String> quickIds = quickIdList();
|
||||
print('快捷 $quickIds');
|
||||
return quickIds
|
||||
.where((id) => ids.contains(id))
|
||||
@ -131,7 +140,7 @@ class Data {
|
||||
|
||||
/// 增加快速导航
|
||||
static addQuick(Book book) {
|
||||
final list = _quickIdList();
|
||||
final list = quickIdList();
|
||||
list.add(book.aid);
|
||||
instance.setStringList(quickKey, list.toSet().toList());
|
||||
}
|
||||
@ -144,7 +153,7 @@ class Data {
|
||||
/// 重新整理Quick的id列表
|
||||
static reQuick() {
|
||||
final books = getFavorites();
|
||||
final quickIds = _quickIdList();
|
||||
final quickIds = quickIdList();
|
||||
instance.setStringList(
|
||||
quickKey, quickIds.where(books.keys.contains).toSet().toList());
|
||||
}
|
||||
|
34
lib/classes/history.dart
Normal file
34
lib/classes/history.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:weiman/classes/chapter.dart';
|
||||
|
||||
class History extends Chapter {
|
||||
DateTime time; // 历史时间
|
||||
|
||||
History({
|
||||
@required cid,
|
||||
@required cname,
|
||||
@required this.time,
|
||||
}) : super(cid: cid, cname: cname);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'cid': cid, 'cname': cname, 'time': time};
|
||||
}
|
||||
|
||||
factory History.fromJson(Map<String, dynamic> map) {
|
||||
if (map == null) return null;
|
||||
return History(
|
||||
cid: map['cid'],
|
||||
cname: map['cname'],
|
||||
time: map['time'],
|
||||
);
|
||||
}
|
||||
|
||||
factory History.fromChapter(Chapter chapter) {
|
||||
return History(
|
||||
cid: chapter.cid,
|
||||
cname: chapter.cname,
|
||||
time: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
const domain = '';
|
||||
|
||||
class UserAgentClient extends http.BaseClient {
|
||||
http.Client _inner = http.Client();
|
||||
String lastKey;
|
||||
int lastKeyTime = 0;
|
||||
|
||||
static UserAgentClient instance;
|
||||
|
||||
// UserAgentClient(this.userAgent);
|
||||
|
||||
UserAgentClient(String userAgent, ByteData data) {
|
||||
}
|
||||
|
||||
Future<String> getKey() async {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
||||
}
|
||||
|
||||
Future<List<String>> getImages(
|
||||
{@required String aid, @required String cid}) async {
|
||||
}
|
||||
|
||||
Future<Book> getBook({String aid}) async {
|
||||
}
|
||||
|
||||
static String _decrypt({String key, String content}) {
|
||||
}
|
||||
|
||||
Future<List<Book>> searchBook(String name) async {
|
||||
}
|
||||
|
||||
static void init() async {
|
||||
}
|
||||
|
||||
Future<http.Response> _get(url, {Map<String, String> headers}) async {
|
||||
}
|
||||
|
||||
Future<List<Book>> getMonthList() async {
|
||||
}
|
||||
|
||||
Future<List<Book>> getIndexRandomBooks() async {
|
||||
}
|
||||
}
|
@ -1,16 +1,30 @@
|
||||
part of '../main.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:weiman/crawler/http.dart';
|
||||
|
||||
/// The dart:io implementation of [image_provider.NetworkImage].
|
||||
class NetworkImageSSL
|
||||
extends image_provider.ImageProvider<image_provider.NetworkImage>
|
||||
implements image_provider.NetworkImage {
|
||||
class NetworkImageSSL extends ImageProvider<NetworkImage>
|
||||
implements NetworkImage {
|
||||
/// Creates an object that fetches the image at the given URL.
|
||||
///
|
||||
/// The arguments [url] and [scale] must not be null.
|
||||
const NetworkImageSSL(this.url, {this.scale = 1.0, this.headers})
|
||||
: assert(url != null),
|
||||
const NetworkImageSSL(
|
||||
this.http,
|
||||
this.url, {
|
||||
this.scale = 1.0,
|
||||
this.headers,
|
||||
this.timeout = 8,
|
||||
this.reSort = false,
|
||||
}) : assert(url != null),
|
||||
assert(scale != null);
|
||||
|
||||
final HttpBook http;
|
||||
|
||||
final int timeout;
|
||||
@override
|
||||
final String url;
|
||||
|
||||
@ -20,80 +34,46 @@ class NetworkImageSSL
|
||||
@override
|
||||
final Map<String, String> headers;
|
||||
|
||||
final bool reSort;
|
||||
|
||||
static void init(ByteData data) {}
|
||||
|
||||
@override
|
||||
Future<NetworkImageSSL> obtainKey(
|
||||
image_provider.ImageConfiguration configuration) {
|
||||
Future<NetworkImageSSL> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<NetworkImageSSL>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
image_provider.ImageStreamCompleter load(
|
||||
image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
|
||||
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
|
||||
// Ownership of this controller is handed off to [_loadAsync]; it is that
|
||||
// method's responsibility to close the controller's stream when the image
|
||||
// has been loaded or an error is thrown.
|
||||
final StreamController<image_provider.ImageChunkEvent> chunkEvents =
|
||||
StreamController<image_provider.ImageChunkEvent>();
|
||||
final StreamController<ImageChunkEvent> chunkEvents =
|
||||
StreamController<ImageChunkEvent>();
|
||||
|
||||
return image_provider.MultiFrameImageStreamCompleter(
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, chunkEvents, decode),
|
||||
chunkEvents: chunkEvents.stream,
|
||||
scale: key.scale,
|
||||
informationCollector: () {
|
||||
return <DiagnosticsNode>[
|
||||
DiagnosticsProperty<image_provider.ImageProvider>(
|
||||
'Image provider', this),
|
||||
DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<NetworkImage>('Image key', key),
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Do not access this field directly; use [_httpClient] instead.
|
||||
// We set `autoUncompress` to false to ensure that we can trust the value of
|
||||
// the `Content-Length` HTTP header. We automatically uncompress the content
|
||||
// in our call to [consolidateHttpClientResponseBytes].
|
||||
static HttpClient _sharedHttpClient = HttpClient()
|
||||
..autoUncompress = false
|
||||
..badCertificateCallback = (_, __, ___) => true;
|
||||
|
||||
static HttpClient get _httpClient {
|
||||
return _sharedHttpClient;
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(
|
||||
Future<Codec> _loadAsync(
|
||||
NetworkImageSSL key,
|
||||
StreamController<image_provider.ImageChunkEvent> chunkEvents,
|
||||
image_provider.DecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
DecoderCallback decode,
|
||||
) async {
|
||||
try {
|
||||
assert(key == this);
|
||||
|
||||
final Uri resolved = Uri.base.resolve(key.url);
|
||||
final HttpClientRequest request =
|
||||
await _httpClient.getUrl(resolved).timeout(Duration(seconds: 5));
|
||||
headers?.forEach((String name, String value) {
|
||||
request.headers.add(name, value);
|
||||
});
|
||||
final HttpClientResponse response = await request.close();
|
||||
if (response.statusCode != HttpStatus.ok)
|
||||
throw image_provider.NetworkImageLoadException(
|
||||
statusCode: response.statusCode, uri: resolved);
|
||||
|
||||
final Uint8List bytes = await consolidateHttpClientResponseBytes(
|
||||
response,
|
||||
onBytesReceived: (int cumulative, int total) {
|
||||
chunkEvents.add(image_provider.ImageChunkEvent(
|
||||
cumulativeBytesLoaded: cumulative,
|
||||
expectedTotalBytes: total,
|
||||
));
|
||||
},
|
||||
);
|
||||
final Uint8List bytes = await http.getImage(url, reSort: reSort);
|
||||
if (bytes.lengthInBytes == 0)
|
||||
throw Exception('NetworkImage is an empty file: $resolved');
|
||||
|
||||
throw Exception('NetworkImage is an empty file: $url');
|
||||
return decode(bytes);
|
||||
} finally {
|
||||
chunkEvents.close();
|
||||
@ -108,7 +88,7 @@ class NetworkImageSSL
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => ui.hashValues(url, scale);
|
||||
int get hashCode => hashValues(url, scale);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType("$url", scale: $scale)';
|
||||
|
80
lib/crawler/http.dart
Normal file
80
lib/crawler/http.dart
Normal file
@ -0,0 +1,80 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:weiman/classes/chapter.dart';
|
||||
import 'package:weiman/classes/chapterContent.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
|
||||
import 'http18Comic.dart';
|
||||
|
||||
final headers = {
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
|
||||
'accept':
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,zh-HK;q=0.7',
|
||||
'cache-control': 'no-cache',
|
||||
'pragma': 'no-cache',
|
||||
};
|
||||
|
||||
class MyHttpClient {
|
||||
static Map<String, HttpBook> clients = {};
|
||||
|
||||
static init(String proxy, int timeout) {
|
||||
Http18Comic.instance = Http18Comic(
|
||||
baseUrls.values.first,
|
||||
name: baseUrls.keys.first,
|
||||
headers: headers,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
clients[Http18Comic.instance.id] = Http18Comic.instance;
|
||||
|
||||
setGlobalProxy(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class HttpBook {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
final Dio dio;
|
||||
|
||||
HttpBook(this.id, this.name, this.dio);
|
||||
|
||||
Future<List<Book>> searchBook(String name, [int page]);
|
||||
|
||||
Future<Book> getBook(String aid);
|
||||
|
||||
Future<List<String>> getChapterImages(Book book, Chapter chapter);
|
||||
|
||||
Future<ChapterContent> getChapterContent(Book book, Chapter chapter);
|
||||
|
||||
Future<List<int>> getImage(String url, {bool reSort = false});
|
||||
|
||||
Future<List<Book>> hotBooks([String type = '', int page]);
|
||||
}
|
||||
|
||||
class MyProxyHttpOverride extends HttpOverrides {
|
||||
final String proxy;
|
||||
|
||||
MyProxyHttpOverride(this.proxy);
|
||||
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext context) {
|
||||
return super.createHttpClient(context)
|
||||
..findProxy = (uri) {
|
||||
return 'PROXY $proxy;';
|
||||
}
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
}
|
||||
}
|
||||
|
||||
void setGlobalProxy(String proxy) {
|
||||
print('setGlobalProxy $proxy');
|
||||
if (proxy != null)
|
||||
HttpOverrides.global = MyProxyHttpOverride(proxy);
|
||||
else
|
||||
HttpOverrides.global = null;
|
||||
}
|
178
lib/db/book.dart
Normal file
178
lib/db/book.dart
Normal file
@ -0,0 +1,178 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
import 'package:weiman/classes/chapter.dart';
|
||||
import 'package:weiman/classes/history.dart';
|
||||
import 'package:weiman/crawler/http.dart';
|
||||
import 'package:weiman/db/group.dart';
|
||||
|
||||
part 'book.g.dart';
|
||||
|
||||
const BookName = 'book';
|
||||
enum BookUpdateStatus {
|
||||
not, // 不检查更新
|
||||
no, // 没有更新
|
||||
had, // 有更新
|
||||
fail, // 检查更新失败
|
||||
wait, // 检查更新的队列中
|
||||
loading, // 正在检查更新
|
||||
old, // 旧藏书,不检查更新
|
||||
}
|
||||
|
||||
@HiveType(typeId: 1)
|
||||
class Book extends HiveObject {
|
||||
static Box<Book> bookBox;
|
||||
|
||||
@HiveField(0)
|
||||
String aid;
|
||||
|
||||
@HiveField(1)
|
||||
String name;
|
||||
|
||||
@HiveField(2)
|
||||
String avatar;
|
||||
|
||||
@HiveField(3)
|
||||
List<String> authors;
|
||||
|
||||
@HiveField(4)
|
||||
String description;
|
||||
|
||||
@HiveField(5)
|
||||
int chapterCount;
|
||||
|
||||
// [新章节数量]减[旧章节数量]得到的差值
|
||||
int newChapterCount;
|
||||
|
||||
BookUpdateStatus status;
|
||||
|
||||
List<Chapter> chapters;
|
||||
|
||||
List<String> tags;
|
||||
|
||||
@HiveField(6)
|
||||
bool favorite;
|
||||
|
||||
@HiveField(7)
|
||||
bool needUpdate;
|
||||
|
||||
@HiveField(8)
|
||||
bool hasUpdate;
|
||||
|
||||
@HiveField(9)
|
||||
DateTime updatedAt;
|
||||
|
||||
// 首页快速导航
|
||||
@HiveField(10)
|
||||
int quick;
|
||||
|
||||
@HiveField(11)
|
||||
Map<String, dynamic> _history;
|
||||
|
||||
@HiveField(12)
|
||||
int groupId;
|
||||
|
||||
@HiveField(13)
|
||||
String httpId;
|
||||
|
||||
bool look = false;
|
||||
|
||||
Group get group =>
|
||||
groupId == null ? null : Group.groupBox.get(groupId, defaultValue: null);
|
||||
|
||||
HttpBook get http => MyHttpClient.clients[httpId];
|
||||
|
||||
History get history => History.fromJson(_history);
|
||||
|
||||
Future setFavorite(bool value) {
|
||||
favorite = value;
|
||||
return save();
|
||||
}
|
||||
|
||||
Future setHistory(Chapter value) {
|
||||
if (value == null) {
|
||||
_history = null;
|
||||
} else {
|
||||
_history = History.fromChapter(value).toJson();
|
||||
}
|
||||
return save();
|
||||
}
|
||||
|
||||
Book({
|
||||
this.httpId,
|
||||
this.aid,
|
||||
this.name,
|
||||
this.groupId,
|
||||
this.avatar,
|
||||
this.authors,
|
||||
this.description,
|
||||
this.chapterCount,
|
||||
this.favorite = false,
|
||||
this.needUpdate = false,
|
||||
this.quick,
|
||||
this.chapters = const [],
|
||||
this.tags = const [],
|
||||
Map<String, dynamic> history,
|
||||
}) : _history = history;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Book:${toJson()}';
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
'key': key,
|
||||
'aid': aid,
|
||||
'name': name,
|
||||
'httpId': httpId,
|
||||
'groupId': groupId,
|
||||
'favorite': favorite,
|
||||
'history': _history,
|
||||
'status': status,
|
||||
'chapterCount': chapterCount,
|
||||
};
|
||||
}
|
||||
|
||||
bool needToSave() {
|
||||
return favorite == true || _history != null || quick != null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save() {
|
||||
if (needToSave()) {
|
||||
return bookBox.put(aid, this);
|
||||
}
|
||||
return bookBox.delete(aid);
|
||||
}
|
||||
|
||||
Future<bool> load() async {
|
||||
if (httpId == null) return false;
|
||||
final newBook = await this.http.getBook(aid);
|
||||
print('load newBook:${newBook.httpId}');
|
||||
chapters = newBook.chapters;
|
||||
chapterCount = newBook.chapterCount;
|
||||
authors = newBook.authors;
|
||||
description = newBook.description;
|
||||
httpId = newBook.httpId;
|
||||
tags = newBook.tags;
|
||||
print('book httpId $httpId');
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<List<String>> loadChapter(Chapter chapter) async {
|
||||
if (httpId == null) return null;
|
||||
return this.http.getChapterImages(this, chapter);
|
||||
}
|
||||
|
||||
Future<void> update() async {
|
||||
try {
|
||||
final newBook = await this.http.getBook(aid);
|
||||
print('$name 旧$chapterCount 新${newBook.chapterCount}');
|
||||
newChapterCount = newBook.chapterCount - chapterCount;
|
||||
status = newChapterCount > 0 ? BookUpdateStatus.had : BookUpdateStatus.no;
|
||||
} catch (e) {
|
||||
status = BookUpdateStatus.fail;
|
||||
}
|
||||
print('book update $status');
|
||||
}
|
||||
}
|
80
lib/db/book.g.dart
Normal file
80
lib/db/book.g.dart
Normal file
@ -0,0 +1,80 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'book.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class BookAdapter extends TypeAdapter<Book> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
Book read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return Book(
|
||||
httpId: fields[13] as String,
|
||||
aid: fields[0] as String,
|
||||
name: fields[1] as String,
|
||||
groupId: fields[12] as int,
|
||||
avatar: fields[2] as String,
|
||||
authors: (fields[3] as List)?.cast<String>(),
|
||||
description: fields[4] as String,
|
||||
chapterCount: fields[5] as int,
|
||||
favorite: fields[6] as bool,
|
||||
needUpdate: fields[7] as bool,
|
||||
quick: fields[10] as int,
|
||||
)
|
||||
..hasUpdate = fields[8] as bool
|
||||
..updatedAt = fields[9] as DateTime
|
||||
.._history = (fields[11] as Map)?.cast<String, dynamic>();
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Book obj) {
|
||||
writer
|
||||
..writeByte(14)
|
||||
..writeByte(0)
|
||||
..write(obj.aid)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.avatar)
|
||||
..writeByte(3)
|
||||
..write(obj.authors)
|
||||
..writeByte(4)
|
||||
..write(obj.description)
|
||||
..writeByte(5)
|
||||
..write(obj.chapterCount)
|
||||
..writeByte(6)
|
||||
..write(obj.favorite)
|
||||
..writeByte(7)
|
||||
..write(obj.needUpdate)
|
||||
..writeByte(8)
|
||||
..write(obj.hasUpdate)
|
||||
..writeByte(9)
|
||||
..write(obj.updatedAt)
|
||||
..writeByte(10)
|
||||
..write(obj.quick)
|
||||
..writeByte(11)
|
||||
..write(obj._history)
|
||||
..writeByte(12)
|
||||
..write(obj.groupId)
|
||||
..writeByte(13)
|
||||
..write(obj.httpId);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BookAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
36
lib/db/group.dart
Normal file
36
lib/db/group.dart
Normal file
@ -0,0 +1,36 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
import 'book.dart';
|
||||
|
||||
part 'group.g.dart';
|
||||
|
||||
const GroupName = 'group';
|
||||
|
||||
@HiveType(typeId: 0)
|
||||
class Group extends HiveObject {
|
||||
static Box<Group> groupBox;
|
||||
static Box<Book> bookBox;
|
||||
|
||||
@HiveField(0)
|
||||
String name;
|
||||
|
||||
@HiveField(1)
|
||||
bool expended;
|
||||
|
||||
Group(this.name, [this.expended = false]);
|
||||
|
||||
List<Book> get books => bookBox.values
|
||||
.where((book) => book.favorite && book.groupId == this.key)
|
||||
.toList();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Group:${{'key': key, 'name': name, 'books': books.length}}';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save() {
|
||||
if (!isInBox) return groupBox.add(this);
|
||||
return super.save();
|
||||
}
|
||||
}
|
44
lib/db/group.g.dart
Normal file
44
lib/db/group.g.dart
Normal file
@ -0,0 +1,44 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'group.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class GroupAdapter extends TypeAdapter<Group> {
|
||||
@override
|
||||
final int typeId = 0;
|
||||
|
||||
@override
|
||||
Group read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return Group(
|
||||
fields[0] as String,
|
||||
fields[1] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Group obj) {
|
||||
writer
|
||||
..writeByte(2)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.expended);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is GroupAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
17
lib/db/historyOffset.dart
Normal file
17
lib/db/historyOffset.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
const HistoryOffsetName = 'history';
|
||||
|
||||
class HistoryOffset {
|
||||
static Box box;
|
||||
|
||||
static double get(String cid) {
|
||||
print('get $cid');
|
||||
return box.get(cid) ?? 0.0;
|
||||
}
|
||||
|
||||
static Future<void> save(String cid, double offset) {
|
||||
print('save $cid $offset');
|
||||
return box.put(cid, offset);
|
||||
}
|
||||
}
|
79
lib/db/setting.dart
Normal file
79
lib/db/setting.dart
Normal file
@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:weiman/crawler/http.dart';
|
||||
import 'package:weiman/crawler/http18Comic.dart';
|
||||
|
||||
enum HideOption {
|
||||
none,
|
||||
auto,
|
||||
always,
|
||||
}
|
||||
|
||||
class Setting with ChangeNotifier {
|
||||
static final String name = 'setting';
|
||||
static Box settingBox;
|
||||
Http18Comic http;
|
||||
|
||||
Setting() {
|
||||
MyHttpClient.init(getProxy(), 10000);
|
||||
}
|
||||
|
||||
HideOption getHideOption() {
|
||||
final index =
|
||||
settingBox.get('hideOption', defaultValue: HideOption.auto.index);
|
||||
return HideOption.values[index];
|
||||
}
|
||||
|
||||
Future setHideOption(HideOption option) async {
|
||||
await settingBox.put('hideOption', option.index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String getProxy() {
|
||||
print('getProxy');
|
||||
return settingBox.get('proxy', defaultValue: null);
|
||||
}
|
||||
|
||||
Future setProxy(String proxy) async {
|
||||
print('db/setting.setProxy $proxy');
|
||||
await settingBox.put('proxy', proxy);
|
||||
MyHttpClient.init(proxy, 10000);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
ThemeMode getThemeMode() {
|
||||
final int index = settingBox.get('theme', defaultValue: -1);
|
||||
if (index == -1) return ThemeMode.system;
|
||||
return ThemeMode.values[index];
|
||||
}
|
||||
|
||||
Future setThemeMode(ThemeMode mode) {
|
||||
return settingBox.put('theme', mode.index);
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Http18Comic getHttp() {
|
||||
final String name =
|
||||
settingBox.get('http', defaultValue: baseUrls.keys.first);
|
||||
final http = Http18Comic(baseUrls[name], name: name, headers: headers);
|
||||
setProxy(getProxy());
|
||||
return http;
|
||||
}
|
||||
|
||||
Future setHttp(HttpBook http) async {
|
||||
await settingBox.put('http', http.name);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool getViewerSwitch() {
|
||||
return settingBox.get('viewerSwitch', defaultValue: true);
|
||||
}
|
||||
|
||||
Future setViewerSwitch(bool value) async {
|
||||
await settingBox.put('viewerSwitch', value);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
183
lib/main.dart
183
lib/main.dart
@ -1,85 +1,53 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:draggable_container/draggable_container.dart';
|
||||
import 'package:dynamic_theme/dynamic_theme.dart';
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import 'package:firebase_analytics/observer.dart';
|
||||
import 'package:flutter/material.dart' hide NestedScrollView;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:html/parser.dart' as html;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:oktoast/oktoast.dart';
|
||||
import 'package:package_info/package_info.dart';
|
||||
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'
|
||||
hide CircularProgressIndicator;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sticky_headers/sticky_headers/widget.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart' as image_provider;
|
||||
|
||||
part './activities/book.dart';
|
||||
|
||||
part './activities/chapter.dart';
|
||||
|
||||
part './activities/checkData.dart';
|
||||
|
||||
part './activities/home.dart';
|
||||
|
||||
part './activities/rank.dart';
|
||||
|
||||
part './activities/search.dart';
|
||||
|
||||
part './activities/test.dart';
|
||||
|
||||
part './classes/book.dart';
|
||||
|
||||
part './classes/data.dart';
|
||||
|
||||
part './classes/http.dart';
|
||||
|
||||
part './classes/networkImageSSL.dart';
|
||||
|
||||
part './widgets/book.dart';
|
||||
|
||||
part './widgets/favorites.dart';
|
||||
|
||||
part './widgets/histories.dart';
|
||||
|
||||
part './widgets/pullToRefreshHeader.dart';
|
||||
|
||||
part './widgets/quick.dart';
|
||||
|
||||
part './widgets/sliverExpandableGroup.dart';
|
||||
|
||||
part './widgets/utils.dart';
|
||||
|
||||
part 'utils.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:weiman/activities/dataConvert.dart';
|
||||
import 'package:weiman/activities/home.dart';
|
||||
import 'package:weiman/classes/data.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/db/group.dart';
|
||||
import 'package:weiman/db/historyOffset.dart';
|
||||
import 'package:weiman/db/setting.dart';
|
||||
import 'package:weiman/provider/favoriteData.dart';
|
||||
import 'package:weiman/provider/theme.dart';
|
||||
|
||||
FirebaseAnalytics analytics;
|
||||
FirebaseAnalyticsObserver observer;
|
||||
|
||||
const bool isDevMode = !bool.fromEnvironment('dart.vm.product');
|
||||
|
||||
int version;
|
||||
BoxDecoration border;
|
||||
|
||||
Directory imageCacheDir;
|
||||
String imageCacheDirPath;
|
||||
PackageInfo packageInfo;
|
||||
|
||||
void main() async {
|
||||
print("开发模式 $isDevMode");
|
||||
FlutterError.onError = (FlutterErrorDetails details) {};
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp();
|
||||
|
||||
getTemporaryDirectory().then((dir) {
|
||||
imageCacheDir = Directory(path.join(dir.path, 'images'));
|
||||
imageCacheDirPath = imageCacheDir.path;
|
||||
if (imageCacheDir.existsSync() == false) imageCacheDir.createSync();
|
||||
print('图片缓存目录 $imageCacheDirPath');
|
||||
});
|
||||
|
||||
try {
|
||||
analytics = FirebaseAnalytics();
|
||||
@ -87,47 +55,80 @@ void main() async {
|
||||
} catch (e) {}
|
||||
|
||||
await Future.wait([
|
||||
Hive.initFlutter(),
|
||||
Data.init(),
|
||||
SystemChrome.setPreferredOrientations(
|
||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
||||
]);
|
||||
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
UserAgentClient.init();
|
||||
runApp(Main(packageInfo: packageInfo));
|
||||
Hive.registerAdapter<Group>(GroupAdapter());
|
||||
Hive.registerAdapter<Book>(BookAdapter());
|
||||
await Future.wait([
|
||||
Hive.openBox<Group>(GroupName).then((value) => Group.groupBox = value),
|
||||
Hive.openBox<Book>(BookName)
|
||||
.then((value) => Book.bookBox = Group.bookBox = value),
|
||||
Hive.openBox(HistoryOffsetName).then((value) => HistoryOffset.box = value),
|
||||
Hive.openBox(Setting.name).then((value) => Setting.settingBox = value),
|
||||
]);
|
||||
packageInfo = await PackageInfo.fromPlatform();
|
||||
version = int.parse(packageInfo.buildNumber);
|
||||
runApp(Main());
|
||||
}
|
||||
|
||||
class Main extends StatefulWidget {
|
||||
final PackageInfo packageInfo;
|
||||
|
||||
const Main({Key key, this.packageInfo}) : super(key: key);
|
||||
|
||||
@override
|
||||
_Main createState() => _Main();
|
||||
}
|
||||
|
||||
class _Main extends State<Main> with WidgetsBindingObserver {
|
||||
static BoxDecoration _border;
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangePlatformBrightness() {
|
||||
super.didChangePlatformBrightness();
|
||||
Provider.of<ThemeProvider>(context, listen: false).update(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_border == null)
|
||||
_border = BoxDecoration(
|
||||
border: Border(
|
||||
bottom: Divider.createBorderSide(context, color: Colors.grey)));
|
||||
return DynamicTheme(
|
||||
defaultBrightness: Brightness.dark,
|
||||
data: (brightness) => new ThemeData(
|
||||
brightness: brightness,
|
||||
border = BoxDecoration(
|
||||
border: Border(
|
||||
bottom: Divider.createBorderSide(context, color: Colors.grey)));
|
||||
return OKToast(
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<Setting>(
|
||||
lazy: false,
|
||||
create: (_) => Setting(),
|
||||
),
|
||||
ChangeNotifierProvider<FavoriteData>(
|
||||
lazy: false,
|
||||
create: (_) => FavoriteData(),
|
||||
),
|
||||
ChangeNotifierProvider<ThemeProvider>(
|
||||
lazy: true,
|
||||
create: (_) => ThemeProvider(_),
|
||||
),
|
||||
],
|
||||
child: Consumer<ThemeProvider>(
|
||||
builder: (_, theme, __) => MaterialApp(
|
||||
title: '微漫 v${packageInfo.version}',
|
||||
themeMode: theme.themeMode,
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
accentColor: Colors.redAccent,
|
||||
),
|
||||
themedWidgetBuilder: (context, theme) {
|
||||
return OKToast(
|
||||
child: MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: theme,
|
||||
home: ActivityHome(widget.packageInfo),
|
||||
),
|
||||
);
|
||||
});
|
||||
home: Data.hasData() ? ActivityDataConvert() : ActivityHome(),
|
||||
// home: ActivityHome(),
|
||||
debugShowCheckedModeBanner: isDevMode,
|
||||
navigatorObservers: <NavigatorObserver>[observer],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
133
lib/provider/favoriteData.dart
Normal file
133
lib/provider/favoriteData.dart
Normal file
@ -0,0 +1,133 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/db/group.dart';
|
||||
|
||||
class FavoriteData extends ChangeNotifier {
|
||||
final List<Book> all = [], others = [];
|
||||
final Map<Group, List<Book>> groups = {};
|
||||
|
||||
FavoriteData() {
|
||||
loadBooksList();
|
||||
}
|
||||
|
||||
Future<void> loadBooksList([notify = false]) async {
|
||||
final groupList = Group.groupBox.values.toList();
|
||||
final groupMap = {for (final group in groupList) group.key: group};
|
||||
groups.clear();
|
||||
groupList.forEach((group) {
|
||||
groups[group] = [];
|
||||
});
|
||||
|
||||
all.clear();
|
||||
others.clear();
|
||||
|
||||
// if(isDevMode){
|
||||
// final temp = [
|
||||
// Book(
|
||||
// aid: '180454',
|
||||
// name: '朋友,女朋友',
|
||||
// avatar:
|
||||
// 'https://cdn-msp.18comic.org/media/albums/206567.jpg',
|
||||
// chapterCount: 0,
|
||||
// httpId: '18',
|
||||
// needUpdate: false,
|
||||
// authors: [],
|
||||
// ),
|
||||
// Book(
|
||||
// aid: '206567',
|
||||
// name: '抑欲人妻',
|
||||
// avatar:
|
||||
// 'https://cdn-msp.18comic.org/media/albums/206567.jpg',
|
||||
// chapterCount: 0,
|
||||
// httpId: '18',
|
||||
// needUpdate: true,
|
||||
// authors: [],
|
||||
// ),
|
||||
// Book(
|
||||
// aid: '147335',
|
||||
// name: '亲爱的大叔',
|
||||
// avatar:
|
||||
// 'https://cdn-msp.msp-comic.xyz/media/albums/147335.jpg',
|
||||
// chapterCount: 0,
|
||||
// httpId: '18',
|
||||
// needUpdate: true,
|
||||
// authors: [],
|
||||
// ),
|
||||
// ];
|
||||
// all.addAll(temp);
|
||||
// others.addAll(temp);
|
||||
// }
|
||||
|
||||
Book.bookBox.values.forEach((book) {
|
||||
if (book.favorite != true) return;
|
||||
all.add(book);
|
||||
if (groupMap.containsKey(book.groupId)) {
|
||||
//有分组的藏书
|
||||
groups[groupMap[book.groupId]].add(book);
|
||||
} else {
|
||||
//没有分组的藏书
|
||||
others.add(book);
|
||||
}
|
||||
});
|
||||
|
||||
print({'all': all.length, 'other': others.length});
|
||||
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
Future<int> checkUpdate() async {
|
||||
final groupList = [others, ...groups.values];
|
||||
for (final array in groupList) {
|
||||
for (final book in array) {
|
||||
if (book.httpId == null) {
|
||||
book.status = BookUpdateStatus.old;
|
||||
} else if (book.needUpdate != true) {
|
||||
book.status = BookUpdateStatus.not;
|
||||
} else {
|
||||
book.status = BookUpdateStatus.wait;
|
||||
}
|
||||
notifyListeners();
|
||||
if (book.status != BookUpdateStatus.wait) continue;
|
||||
book.status = BookUpdateStatus.loading;
|
||||
notifyListeners();
|
||||
await book.update();
|
||||
if (book.status == BookUpdateStatus.had) sort(array, book);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
return all.where((book) => book.status == BookUpdateStatus.had).length;
|
||||
}
|
||||
|
||||
/// 显示在前排
|
||||
void sort(List<Book> array, Book book) {
|
||||
print('sort ${book.name}');
|
||||
array.remove(book);
|
||||
array.insert(0, book);
|
||||
}
|
||||
|
||||
Future<void> deleteBook(Book book) async {
|
||||
book.favorite = false;
|
||||
await book.save();
|
||||
// print('删书 ${book.name} 成功');
|
||||
loadBooksList(true);
|
||||
}
|
||||
|
||||
Future<void> deleteGroup(Group group, [bool deleteBooks = false]) async {
|
||||
if (deleteBooks && groups.containsKey(group)) {
|
||||
await Future.wait(groups[group].map((book) => book.setFavorite(false)));
|
||||
}
|
||||
await Group.groupBox.delete(group.key);
|
||||
await loadBooksList(true);
|
||||
}
|
||||
|
||||
Future<void> addGroup(Group group) async {
|
||||
group.save();
|
||||
await loadBooksList(true);
|
||||
}
|
||||
|
||||
Future<void> addBook(Book book) async {
|
||||
book.favorite = true;
|
||||
await book.save();
|
||||
loadBooksList(true);
|
||||
}
|
||||
}
|
29
lib/provider/theme.dart
Normal file
29
lib/provider/theme.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:weiman/db/setting.dart';
|
||||
|
||||
class ThemeProvider extends ChangeNotifier {
|
||||
ThemeMode themeMode = ThemeMode.system; // 主题模式
|
||||
|
||||
ThemeProvider(BuildContext context) {
|
||||
themeMode = Provider.of<Setting>(context, listen: false).getThemeMode();
|
||||
}
|
||||
|
||||
void changeTheme(ThemeMode mode) {
|
||||
print('改变主题 $mode');
|
||||
themeMode = mode;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void update(BuildContext context) {
|
||||
final bright = MediaQuery.platformBrightnessOf(context);
|
||||
switch (bright) {
|
||||
case Brightness.light:
|
||||
changeTheme(ThemeMode.light);
|
||||
break;
|
||||
case Brightness.dark:
|
||||
changeTheme(ThemeMode.dark);
|
||||
}
|
||||
print('update $bright');
|
||||
}
|
||||
}
|
@ -1,9 +1,27 @@
|
||||
part of 'main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:weiman/activities/book/book.dart';
|
||||
import 'package:weiman/activities/chapter/activity.dart';
|
||||
import 'package:weiman/activities/search/search.dart';
|
||||
import 'package:weiman/classes/chapter.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
|
||||
final weekTime = Duration.millisecondsPerDay * 7;
|
||||
|
||||
void openBook(BuildContext context, Book book, String heroTag) {
|
||||
Navigator.push(
|
||||
void openSearch(BuildContext context, String word) {}
|
||||
|
||||
Future openBook(BuildContext context, Book book, String heroTag) {
|
||||
print('openBook $book');
|
||||
if (book.http == null) {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: '/activity_search/${book.name}'),
|
||||
builder: (_) => ActivitySearch(search: book.name),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: '/activity_book/${book.name}'),
|
||||
@ -12,8 +30,8 @@ void openBook(BuildContext context, Book book, String heroTag) {
|
||||
);
|
||||
}
|
||||
|
||||
void openChapter(BuildContext context, Book book, Chapter chapter) {
|
||||
Navigator.push(
|
||||
Future<void> openChapter(BuildContext context, Book book, Chapter chapter) {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(
|
||||
@ -22,3 +40,11 @@ void openChapter(BuildContext context, Book book, Chapter chapter) {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showStatusBar() {
|
||||
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||
}
|
||||
|
||||
void hideStatusBar() {
|
||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||
}
|
||||
|
49
lib/widgets/animatedLogo.dart
Normal file
49
lib/widgets/animatedLogo.dart
Normal file
@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sa_anicoto/sa_anicoto.dart';
|
||||
|
||||
class AnimatedLogoWidget extends StatefulWidget {
|
||||
final double width, height;
|
||||
|
||||
const AnimatedLogoWidget({
|
||||
Key key,
|
||||
@required this.width,
|
||||
@required this.height,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AnimatedLogoWidget createState() => _AnimatedLogoWidget();
|
||||
}
|
||||
|
||||
class _AnimatedLogoWidget extends State<AnimatedLogoWidget>
|
||||
with AnimationMixin {
|
||||
Animation<double> size; // Declare animation variable
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
size = Tween<double>(begin: 0, end: widget.height - 20).animate(controller);
|
||||
controller.mirror(
|
||||
duration: Duration(seconds: 1)); // Start the animation playback
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
top: size.value,
|
||||
child: Image.asset(
|
||||
'assets/logo.png',
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,10 @@
|
||||
part of '../main.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:weiman/classes/chapter.dart';
|
||||
import 'package:weiman/classes/networkImageSSL.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/utils.dart';
|
||||
|
||||
class WidgetBook extends StatelessWidget {
|
||||
final Book book;
|
||||
@ -14,7 +20,7 @@ class WidgetBook extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var isLiked = book.isFavorite();
|
||||
final isLiked = book.favorite;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
book.name,
|
||||
@ -28,25 +34,24 @@ class WidgetBook extends StatelessWidget {
|
||||
),
|
||||
dense: true,
|
||||
leading: Hero(
|
||||
tag: 'bookAvatar${book.aid}',
|
||||
child: Image(image:NetworkImageSSL(
|
||||
book.avatar),
|
||||
height: 200,
|
||||
fit: BoxFit.scaleDown,
|
||||
)),
|
||||
tag: 'bookAvatar${book.aid}',
|
||||
child: ExtendedImage(image: NetworkImageSSL(book.http, book.avatar)),
|
||||
),
|
||||
trailing: Icon(
|
||||
isLiked ? Icons.favorite : Icons.favorite_border,
|
||||
color: isLiked ? Colors.red : Colors.grey,
|
||||
size: 12,
|
||||
),
|
||||
onTap: () {
|
||||
if (onTap != null) onTap(book);
|
||||
if (onTap != null) return onTap(book);
|
||||
openBook(context, book, 'bookAvatar${book.aid}');
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final dateFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
class WidgetChapter extends StatelessWidget {
|
||||
static final double height = kToolbarHeight;
|
||||
final Chapter chapter;
|
||||
@ -57,7 +62,7 @@ class WidgetChapter extends StatelessWidget {
|
||||
Key key,
|
||||
this.chapter,
|
||||
this.onTap,
|
||||
this.read,
|
||||
this.read = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -78,16 +83,14 @@ class WidgetChapter extends StatelessWidget {
|
||||
title: RichText(
|
||||
text: TextSpan(
|
||||
children: children,
|
||||
style: Theme.of(context).textTheme.body1,
|
||||
style: Theme.of(context).textTheme.bodyText2,
|
||||
),
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
),
|
||||
leading: Image(image:NetworkImageSSL(
|
||||
chapter.avatar),
|
||||
fit: BoxFit.fitWidth,
|
||||
width: 100,
|
||||
),
|
||||
subtitle: chapter.time == null
|
||||
? null
|
||||
: Text('更新时间 ${dateFormat.format(chapter.time)}'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -106,8 +109,8 @@ class WidgetHistory extends StatelessWidget {
|
||||
if (onTap != null) onTap(book);
|
||||
},
|
||||
title: Text(book.name),
|
||||
leading: Image(image:NetworkImageSSL(
|
||||
book.avatar),
|
||||
leading: Image(
|
||||
image: ExtendedNetworkImageProvider(book.avatar, cache: true),
|
||||
fit: BoxFit.fitHeight,
|
||||
),
|
||||
subtitle: Text(book.history.cname),
|
||||
@ -136,18 +139,18 @@ class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
|
||||
}
|
||||
|
||||
void load() async {
|
||||
loading = true;
|
||||
try {
|
||||
final book = await UserAgentClient.instance
|
||||
.getBook(aid: widget.book.aid)
|
||||
.timeout(Duration(seconds: 2));
|
||||
news = book.chapterCount - widget.book.chapterCount;
|
||||
hasError = false;
|
||||
} catch (e) {
|
||||
hasError = true;
|
||||
}
|
||||
loading = false;
|
||||
setState(() {});
|
||||
// loading = true;
|
||||
// try {
|
||||
// final book = await Http18Comic.instance
|
||||
// .getBook(widget.book.aid)
|
||||
// .timeout(Duration(seconds: 2));
|
||||
// news = book.chapterCount - widget.book.chapterCount;
|
||||
// hasError = false;
|
||||
// } catch (e) {
|
||||
// hasError = true;
|
||||
// }
|
||||
// loading = false;
|
||||
// setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -173,7 +176,9 @@ class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
|
||||
openBook(context, widget.book, 'checkBook${widget.book.aid}'),
|
||||
leading: Hero(
|
||||
tag: 'checkBook${widget.book.aid}',
|
||||
child: Image(image:NetworkImageSSL(widget.book.avatar)),
|
||||
child: Image(
|
||||
image:
|
||||
ExtendedNetworkImageProvider(widget.book.avatar, cache: true)),
|
||||
),
|
||||
dense: true,
|
||||
isThreeLine: true,
|
||||
|
103
lib/widgets/bookGroup.dart
Normal file
103
lib/widgets/bookGroup.dart
Normal file
@ -0,0 +1,103 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||
import 'package:weiman/db/group.dart';
|
||||
|
||||
class BookGroupHeader extends StatefulWidget {
|
||||
final Group group;
|
||||
final int count;
|
||||
final List<Widget> actions;
|
||||
final Color divideColor;
|
||||
final double height;
|
||||
final IndexedWidgetBuilder builder;
|
||||
final List<Widget> slideActions;
|
||||
|
||||
const BookGroupHeader({
|
||||
Key key,
|
||||
@required this.group,
|
||||
@required this.count,
|
||||
@required this.builder,
|
||||
this.actions = const [],
|
||||
this.divideColor = Colors.grey,
|
||||
this.height = kToolbarHeight,
|
||||
this.slideActions,
|
||||
}) : assert(group != null),
|
||||
assert(builder != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_State createState() => _State();
|
||||
}
|
||||
|
||||
class _State extends State<BookGroupHeader> {
|
||||
bool expended;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
expended = widget.group.expended ?? false;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Decoration _decoration = BoxDecoration(
|
||||
border: Border(
|
||||
bottom: Divider.createBorderSide(context, color: widget.divideColor),
|
||||
),
|
||||
);
|
||||
Widget header = InkWell(
|
||||
child: Container(
|
||||
height: widget.height,
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
child: Row(children: [
|
||||
Transform.rotate(
|
||||
angle: expended ? 0 : math.pi,
|
||||
child: Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
Expanded(child: Text('${widget.group.name}(${widget.count})')),
|
||||
...widget.actions,
|
||||
]),
|
||||
),
|
||||
onTap: () {
|
||||
expended = !expended;
|
||||
widget.group
|
||||
..expended = expended
|
||||
..save();
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
if (widget.slideActions != null && widget.slideActions.length > 0) {
|
||||
header = Slidable(
|
||||
child: header,
|
||||
actionPane: SlidableDrawerActionPane(),
|
||||
secondaryActions: widget.slideActions,
|
||||
);
|
||||
}
|
||||
return SliverStickyHeader(
|
||||
header: header,
|
||||
sliver: expended
|
||||
? SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(ctx, i) {
|
||||
if (i < widget.count - 1) {
|
||||
return DecoratedBox(
|
||||
decoration: _decoration,
|
||||
child: widget.builder(context, i),
|
||||
);
|
||||
}
|
||||
return widget.builder(context, i);
|
||||
},
|
||||
childCount: widget.count,
|
||||
))
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
100
lib/widgets/bookSettingDialog.dart
Normal file
100
lib/widgets/bookSettingDialog.dart
Normal file
@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/db/group.dart';
|
||||
import 'package:weiman/provider/favoriteData.dart';
|
||||
import 'package:weiman/widgets/groupFormDialog.dart';
|
||||
|
||||
Future showBookSettingDialog(BuildContext context, Book book) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text('藏书《${book.name}》的设置'),
|
||||
scrollable: true,
|
||||
content: WidgetSetting(book: book),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class WidgetSetting extends StatefulWidget {
|
||||
final Book book;
|
||||
|
||||
const WidgetSetting({Key key, this.book}) : super(key: key);
|
||||
|
||||
@override
|
||||
_WidgetSetting createState() => _WidgetSetting();
|
||||
}
|
||||
|
||||
class _WidgetSetting extends State<WidgetSetting> {
|
||||
static final updateMenus = {true: '自动', false: '不检查'};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: ListTile.divideTiles(context: context, tiles: [
|
||||
ListTile(
|
||||
title: Text('检查更新'),
|
||||
trailing: DropdownButton<bool>(
|
||||
value: widget.book.needUpdate,
|
||||
items: updateMenus.keys
|
||||
.map((key) =>
|
||||
DropdownMenuItem(value: key, child: Text(updateMenus[key])))
|
||||
.toList(),
|
||||
onChanged: changeUpdate,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('分组'),
|
||||
trailing: DropdownButton<Group>(
|
||||
hint: Text('没有分组'),
|
||||
value: widget.book.group,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
child: Text('新建'),
|
||||
value: null,
|
||||
),
|
||||
...Group.groupBox.values
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e.name)))
|
||||
.toList(),
|
||||
],
|
||||
onChanged: changeGroup,
|
||||
),
|
||||
),
|
||||
]).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
changeUpdate(bool needUpdate) async {
|
||||
widget.book.needUpdate = needUpdate;
|
||||
await widget.book.save();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
changeGroup(Group group) async {
|
||||
if (group == null) {
|
||||
group = await showGroupFormDialog(context);
|
||||
}
|
||||
widget.book.groupId = group == null ? widget.book.groupId : group.key;
|
||||
await widget.book.save();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
changeFavorite() async {
|
||||
await widget.book.setFavorite(!widget.book.favorite);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
removeHistory() async {
|
||||
if (widget.book.history != null) await widget.book.setHistory(null);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(fn) {
|
||||
final fav = Provider.of<FavoriteData>(context, listen: false);
|
||||
fav.loadBooksList(true);
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
314
lib/widgets/checkConnect/checkConnect.dart
Normal file
314
lib/widgets/checkConnect/checkConnect.dart
Normal file
@ -0,0 +1,314 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:weiman/crawler/http.dart';
|
||||
import 'package:weiman/crawler/http18Comic.dart';
|
||||
import 'package:weiman/db/setting.dart';
|
||||
|
||||
class CheckConnectWidget extends StatefulWidget {
|
||||
@override
|
||||
_CheckConnectWidget createState() => _CheckConnectWidget();
|
||||
}
|
||||
|
||||
class _CheckConnectWidget extends State<CheckConnectWidget> {
|
||||
LoadState state = LoadState.loading;
|
||||
final List<_Check> https = [];
|
||||
String lastProxy;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final setting = Provider.of<Setting>(context, listen: false);
|
||||
lastProxy = setting.getProxy();
|
||||
createHttps();
|
||||
super.initState();
|
||||
setting.addListener(() {
|
||||
final proxy = setting.getProxy();
|
||||
if (lastProxy != proxy) {
|
||||
lastProxy = proxy;
|
||||
createHttps();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void createHttps() {
|
||||
print('重建http池 proxy:$lastProxy');
|
||||
https.clear();
|
||||
https.addAll(
|
||||
baseUrls.keys.map(
|
||||
(key) => _Check(
|
||||
name: key,
|
||||
url: baseUrls[key],
|
||||
proxy: lastProxy,
|
||||
),
|
||||
),
|
||||
);
|
||||
check();
|
||||
}
|
||||
|
||||
void check() async {
|
||||
setState(() {
|
||||
state = LoadState.loading;
|
||||
});
|
||||
https.forEach((http) => http.load());
|
||||
await Future.wait(https.map((http) => http.load()));
|
||||
final bool hasCompleted =
|
||||
https.where((http) => http.state == LoadState.completed).isNotEmpty;
|
||||
state = hasCompleted ? LoadState.completed : LoadState.failed;
|
||||
if (hasCompleted) {
|
||||
final sort = https.toList()..sort((a, b) => a.time.compareTo(b.time));
|
||||
Http18Comic.instance = sort.first.http;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _showDialog(String title) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
title: title,
|
||||
https: https,
|
||||
retry: check,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget row;
|
||||
switch (state) {
|
||||
case LoadState.loading:
|
||||
row = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('正在尝试连接漫画网站'),
|
||||
],
|
||||
);
|
||||
break;
|
||||
case LoadState.failed:
|
||||
row = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Icon(Icons.error, color: Colors.red),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('连接不上漫画网站,点击查看错误'),
|
||||
],
|
||||
);
|
||||
break;
|
||||
default:
|
||||
row = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Icon(Icons.check_circle, color: Colors.green),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text('成功连接到漫画网站,点击查看结果'),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 10, bottom: 15),
|
||||
child: GestureDetector(
|
||||
child: row,
|
||||
onTap: () => _showDialog('测试结果,选择源'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Dialog extends StatefulWidget {
|
||||
final String title;
|
||||
final List<_Check> https;
|
||||
final Function retry;
|
||||
|
||||
const Dialog({Key key, this.title, this.https, this.retry}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _Dialog();
|
||||
}
|
||||
|
||||
class _Dialog extends State<Dialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final proxy = widget.https[0].proxy;
|
||||
return AlertDialog(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.title),
|
||||
if (proxy != null)
|
||||
Text('正在使用代理:$proxy', style: TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
content: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
child: ListView(
|
||||
physics: ClampingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: widget.https.map(
|
||||
(e) => e.build(onTap: () => setState(() {})),
|
||||
)).toList(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('再次测试'),
|
||||
onPressed: () {
|
||||
widget.retry();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Check {
|
||||
final String name;
|
||||
final String proxy;
|
||||
Http18Comic http;
|
||||
Future future;
|
||||
Duration time;
|
||||
String error;
|
||||
LoadState state;
|
||||
|
||||
_Check({
|
||||
String url,
|
||||
@required this.name,
|
||||
@required this.proxy,
|
||||
}) {
|
||||
http = Http18Comic(
|
||||
url,
|
||||
name: name,
|
||||
headers: headers,
|
||||
proxy: proxy,
|
||||
);
|
||||
}
|
||||
|
||||
Future load() {
|
||||
future = this._load();
|
||||
return future;
|
||||
}
|
||||
|
||||
Future _load() async {
|
||||
state = LoadState.loading;
|
||||
final now = DateTime.now();
|
||||
try {
|
||||
final Response<String> res = await http.dio.get<String>('/');
|
||||
final $ = parse(res.data);
|
||||
final $title = $.querySelector('title');
|
||||
if (res.data.contains('Restricted') ||
|
||||
$title == null ||
|
||||
$title.text.indexOf('禁漫天堂') == -1) {
|
||||
throw DioError(
|
||||
request: res.request,
|
||||
response: res,
|
||||
error: '你使用的IP被漫画网站禁止访问,请更换网络IP\n不要使用日本IP。',
|
||||
);
|
||||
}
|
||||
state = LoadState.completed;
|
||||
} catch (e) {
|
||||
print(e);
|
||||
if (e.runtimeType == DioError) {
|
||||
final DioError error = e as DioError;
|
||||
switch (error.type) {
|
||||
case DioErrorType.CONNECT_TIMEOUT:
|
||||
case DioErrorType.RECEIVE_TIMEOUT:
|
||||
case DioErrorType.SEND_TIMEOUT:
|
||||
this.error = '连接超时';
|
||||
break;
|
||||
default:
|
||||
this.error = error.error.toString();
|
||||
}
|
||||
if (error.response?.data != null) {
|
||||
this.error += '\n接收到的内容:\n' + error.response.data;
|
||||
}
|
||||
} else {
|
||||
this.error = e.toString();
|
||||
}
|
||||
state = LoadState.failed;
|
||||
print('$name 结果 $state');
|
||||
}
|
||||
time = DateTime.now().difference(now);
|
||||
}
|
||||
|
||||
Widget build({Function onTap}) {
|
||||
return FutureBuilder(
|
||||
future: future,
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
final Widget title = Text(name);
|
||||
switch (snapshot.connectionState) {
|
||||
case ConnectionState.active:
|
||||
case ConnectionState.waiting:
|
||||
return ListTile(
|
||||
title: title,
|
||||
subtitle: Row(children: [
|
||||
SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Text('读取中'),
|
||||
]),
|
||||
);
|
||||
break;
|
||||
case ConnectionState.done:
|
||||
if (state == LoadState.failed) {
|
||||
return ListTile(
|
||||
title: title,
|
||||
subtitle: Text('连接失败,点击查看原因'),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
title: Text('$name 错误内容'),
|
||||
content: Text(error),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
final _time = time.inMilliseconds;
|
||||
final timeString = _time > 1000
|
||||
? '${(time.inMilliseconds / 1000).toStringAsFixed(2)} 秒'
|
||||
: '${time.inMilliseconds} 毫秒';
|
||||
return CheckboxListTile(
|
||||
title: title,
|
||||
subtitle: Text('连接成功\n耗时:$timeString'),
|
||||
isThreeLine: true,
|
||||
value: Http18Comic.instance?.name == name,
|
||||
onChanged: (name) {
|
||||
Http18Comic.instance = http;
|
||||
MyHttpClient.clients[http.id] = http;
|
||||
onTap();
|
||||
},
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return ListTile(title: title, subtitle: Text('还没有开始网络请求'));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
13
lib/widgets/dbSourceListWidget.dart
Normal file
13
lib/widgets/dbSourceListWidget.dart
Normal file
@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DBSourceListWidget extends StatefulWidget {
|
||||
@override
|
||||
_DBSourceListWidget createState() => _DBSourceListWidget();
|
||||
}
|
||||
|
||||
class _DBSourceListWidget extends State<DBSourceListWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(children: []);
|
||||
}
|
||||
}
|
58
lib/widgets/deleteGroupDialog.dart
Normal file
58
lib/widgets/deleteGroupDialog.dart
Normal file
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:weiman/db/group.dart';
|
||||
import 'package:weiman/provider/favoriteData.dart';
|
||||
|
||||
Future showDeleteGroupDialog(BuildContext context, Group group) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (_) => DeleteGroupWidget(group: group),
|
||||
);
|
||||
}
|
||||
|
||||
class DeleteGroupWidget extends StatefulWidget {
|
||||
final Group group;
|
||||
|
||||
const DeleteGroupWidget({Key key, this.group}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DeleteGroupWidget createState() => _DeleteGroupWidget();
|
||||
}
|
||||
|
||||
class _DeleteGroupWidget extends State<DeleteGroupWidget> {
|
||||
bool deleteBooks = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final length = widget.group.books.length;
|
||||
return AlertDialog(
|
||||
title: Text('删除分组 ${widget.group.name}'),
|
||||
scrollable: true,
|
||||
content: Column(
|
||||
children: ListTile.divideTiles(context: context, tiles: [
|
||||
if (length > 0)
|
||||
ListTile(
|
||||
title: Text('删除藏书'),
|
||||
subtitle: Text('有 $length 本藏书'),
|
||||
trailing: Checkbox(
|
||||
value: deleteBooks,
|
||||
onChanged: (v) => setState(() => deleteBooks = v),
|
||||
),
|
||||
)
|
||||
]).toList(),
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('确认'),
|
||||
onPressed: () async {
|
||||
await Provider.of<FavoriteData>(context, listen: false)
|
||||
.deleteGroup(widget.group, deleteBooks);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
RaisedButton(
|
||||
child: Text('取消'), onPressed: () => Navigator.pop(context)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,21 @@
|
||||
part of '../main.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:oktoast/oktoast.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:weiman/classes/networkImageSSL.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/db/group.dart';
|
||||
import 'package:weiman/provider/favoriteData.dart';
|
||||
import 'package:weiman/utils.dart';
|
||||
import 'package:weiman/widgets/bookGroup.dart';
|
||||
import 'package:weiman/widgets/bookSettingDialog.dart';
|
||||
import 'package:weiman/widgets/deleteGroupDialog.dart';
|
||||
import 'package:weiman/widgets/groupFormDialog.dart';
|
||||
import 'package:weiman/widgets/sliverExpandableGroup.dart';
|
||||
import 'package:weiman/widgets/utils.dart';
|
||||
|
||||
class FavoriteList extends StatefulWidget {
|
||||
@override
|
||||
@ -6,178 +23,218 @@ class FavoriteList extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FavoriteList extends State<FavoriteList> {
|
||||
static final Map<String, int> hasNews = {};
|
||||
static final List<Book> all = [], // 所有收藏
|
||||
inWeek = [], // 7天看过的收藏
|
||||
other = []; // 其他收藏
|
||||
static bool showTip = false;
|
||||
static bool showTip = true;
|
||||
|
||||
static final loadFailTextSpan = TextSpan(
|
||||
text: '读取失败,下拉刷新', style: TextStyle(color: Colors.redAccent)),
|
||||
waitToCheck =
|
||||
TextSpan(text: '等待检查更新', style: TextStyle(color: Colors.grey)),
|
||||
unCheck =
|
||||
TextSpan(text: '请下拉列表检查更新', style: TextStyle(color: Colors.grey)),
|
||||
noUpdate = TextSpan(text: '没有更新', style: TextStyle(color: Colors.grey));
|
||||
|
||||
static void getBooks() {
|
||||
all.clear();
|
||||
inWeek.clear();
|
||||
other.clear();
|
||||
all.addAll(Data.getFavorites().values);
|
||||
if (all.isNotEmpty) {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
all.forEach((book) {
|
||||
if (book.history != null && (now - book.history.time) < weekTime) {
|
||||
inWeek.add(book);
|
||||
} else {
|
||||
other.add(book);
|
||||
}
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
if (showTip) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
showToast(
|
||||
'下拉收藏列表检查更新\n分组和藏书左滑显示更多操作',
|
||||
textPadding: EdgeInsets.all(10),
|
||||
duration: Duration(seconds: 4),
|
||||
);
|
||||
showTip = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getBooks();
|
||||
if (all.isNotEmpty) {
|
||||
if (showTip == false) {
|
||||
showTip = true;
|
||||
showToast(
|
||||
'下拉列表可以检查漫画更新',
|
||||
backgroundColor: Colors.black.withOpacity(0.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _openBook(book) {
|
||||
openBook(context, book, 'fb ${book.aid}');
|
||||
}
|
||||
|
||||
static Future<void> checkNews() async {
|
||||
hasNews.clear();
|
||||
Book currentBook, newBook;
|
||||
int different;
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
currentBook = all[i];
|
||||
try {
|
||||
newBook = await UserAgentClient.instance
|
||||
.getBook(aid: currentBook.aid)
|
||||
.timeout(Duration(seconds: 2));
|
||||
different = newBook.chapterCount - currentBook.chapterCount;
|
||||
hasNews[currentBook.aid] = different;
|
||||
} catch (e) {
|
||||
hasNews[currentBook.aid] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget bookBuilder(Book book) {
|
||||
TextSpan state;
|
||||
if (hasNews.isEmpty) {
|
||||
state = unCheck;
|
||||
} else {
|
||||
if (hasNews.containsKey(book.aid)) {
|
||||
if (hasNews[book.aid] > 0) {
|
||||
state = TextSpan(
|
||||
text: '有 ${hasNews[book.aid]} 章更新',
|
||||
style: TextStyle(color: Colors.green));
|
||||
} else if (hasNews[book.aid] == -1) {
|
||||
state = loadFailTextSpan;
|
||||
} else if (hasNews[book.aid] == 0) {
|
||||
state = noUpdate;
|
||||
}
|
||||
} else {
|
||||
state = waitToCheck;
|
||||
}
|
||||
}
|
||||
return FBookItem(
|
||||
book: book,
|
||||
subtitle: state,
|
||||
onTap: _openBook,
|
||||
onDelete: deleteBook,
|
||||
);
|
||||
}
|
||||
|
||||
deleteBook(Book book) async {
|
||||
final sure = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text('删除藏书 ${book.name} ?'),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('确认'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
print('删书 $sure');
|
||||
if (sure != true) return;
|
||||
|
||||
await Provider.of<FavoriteData>(context, listen: false).deleteBook(book);
|
||||
}
|
||||
|
||||
Future<void> deleteGroup(Group group) async {
|
||||
await showDeleteGroupDialog(context, group);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> groupRename(Group group) async {
|
||||
await showGroupFormDialog(context, group);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Book> inWeekUpdated = [],
|
||||
inWeekUnUpdated = [],
|
||||
otherUpdated = [],
|
||||
otherUnUpdated = [];
|
||||
inWeek.forEach((book) {
|
||||
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0)
|
||||
inWeekUpdated.add(book);
|
||||
else
|
||||
inWeekUnUpdated.add(book);
|
||||
return Consumer<FavoriteData>(builder: (_, favorite, __) {
|
||||
if (favorite.all.isEmpty && favorite.groups.keys.isEmpty)
|
||||
return Center(child: Text('没有收藏'));
|
||||
return ClipRect(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: favorite.checkUpdate,
|
||||
child: SafeArea(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
...favorite.groups.keys.map((group) {
|
||||
final list = favorite.groups[group];
|
||||
return BookGroupHeader(
|
||||
group: group,
|
||||
count: list.length,
|
||||
builder: (ctx, i) => bookBuilder(favorite.groups[group][i]),
|
||||
slideActions: [
|
||||
IconSlideAction(
|
||||
caption: '重命名',
|
||||
color: Colors.blue,
|
||||
icon: Icons.edit,
|
||||
onTap: () => groupRename(group),
|
||||
),
|
||||
IconSlideAction(
|
||||
caption: '删除',
|
||||
color: Colors.red,
|
||||
icon: Icons.delete,
|
||||
onTap: () => deleteGroup(group),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
SliverExpandableGroup(
|
||||
title: Text('没有分组的藏书(${favorite.others.length})'),
|
||||
expanded: false,
|
||||
count: favorite.others.length,
|
||||
builder: (ctx, i) => bookBuilder(favorite.others[i]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
other.forEach((book) {
|
||||
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0)
|
||||
otherUpdated.add(book);
|
||||
else
|
||||
otherUnUpdated.add(book);
|
||||
});
|
||||
return Drawer(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await checkNews();
|
||||
setState(() {});
|
||||
},
|
||||
child: all.isEmpty
|
||||
? Center(child: Text('没有收藏'))
|
||||
: SafeArea(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverExpandableGroup(
|
||||
title: Text('7天内看过并且有更新的藏书(${inWeekUpdated.length})'),
|
||||
expanded: true,
|
||||
count: inWeekUpdated.length,
|
||||
builder: (ctx, i) => bookBuilder(inWeekUpdated[i]),
|
||||
),
|
||||
SliverExpandableGroup(
|
||||
title: Text('7天内看过的藏书(${inWeekUnUpdated.length})'),
|
||||
count: inWeekUnUpdated.length,
|
||||
builder: (ctx, i) => bookBuilder(inWeekUnUpdated[i]),
|
||||
),
|
||||
SliverExpandableGroup(
|
||||
title: Text('有更新的藏书(${otherUpdated.length})'),
|
||||
count: otherUpdated.length,
|
||||
builder: (ctx, i) => bookBuilder(otherUpdated[i]),
|
||||
),
|
||||
SliverExpandableGroup(
|
||||
title: Text('没有更新的藏书(${otherUnUpdated.length})'),
|
||||
count: otherUnUpdated.length,
|
||||
builder: (ctx, i) => bookBuilder(otherUnUpdated[i]),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FBookItem extends StatelessWidget {
|
||||
final bookStatusWidgets = {
|
||||
BookUpdateStatus.loading:
|
||||
TextSpan(text: '正在读取网络数据', style: TextStyle(color: Colors.blue)),
|
||||
BookUpdateStatus.not:
|
||||
TextSpan(text: '该藏书设置为不更新', style: TextStyle(color: Colors.grey)),
|
||||
BookUpdateStatus.no:
|
||||
TextSpan(text: '该藏书没有新章节', style: TextStyle(color: Colors.grey)),
|
||||
BookUpdateStatus.wait:
|
||||
TextSpan(text: '处于更新队列,等待更新', style: TextStyle(color: Colors.grey)),
|
||||
BookUpdateStatus.old:
|
||||
TextSpan(text: '旧藏书不检查更新', style: TextStyle(color: Colors.redAccent)),
|
||||
BookUpdateStatus.fail:
|
||||
TextSpan(text: '网络问题,检查更新失败', style: TextStyle(color: Colors.redAccent)),
|
||||
};
|
||||
|
||||
class FBookItem extends StatefulWidget {
|
||||
final Book book;
|
||||
final TextSpan subtitle;
|
||||
final void Function(Book book) onTap;
|
||||
final void Function(Book book) onDelete;
|
||||
|
||||
const FBookItem({
|
||||
Key key,
|
||||
@required this.book,
|
||||
@required this.subtitle,
|
||||
@required this.onTap,
|
||||
@required this.onDelete,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FBookItem createState() => _FBookItem();
|
||||
}
|
||||
|
||||
class _FBookItem extends State<FBookItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
onTap: () => onTap(book),
|
||||
leading: Hero(
|
||||
tag: 'fb ${book.aid}',
|
||||
child: Image(image: NetworkImageSSL(book.avatar))),
|
||||
title: Text(book.name, style: Theme.of(context).textTheme.body1),
|
||||
subtitle: RichText(text: subtitle),
|
||||
TextSpan subtitle =
|
||||
bookStatusWidgets[widget.book.status ?? BookUpdateStatus.no];
|
||||
if (widget.book.status == BookUpdateStatus.had) {
|
||||
final _subtitle = '有 ${widget.book.newChapterCount} 章更新';
|
||||
subtitle = TextSpan(
|
||||
text: _subtitle,
|
||||
style: TextStyle(
|
||||
color: widget.book.look ? Colors.grey : Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Slidable(
|
||||
actionPane: SlidableDrawerActionPane(),
|
||||
closeOnScroll: true,
|
||||
actionExtentRatio: 0.25,
|
||||
secondaryActions: [
|
||||
IconSlideAction(
|
||||
caption: '设置',
|
||||
color: Colors.blue,
|
||||
icon: Icons.settings,
|
||||
onTap: () async {
|
||||
final before = widget.book.needUpdate;
|
||||
await showBookSettingDialog(context, widget.book);
|
||||
if (before != widget.book.needUpdate) {
|
||||
widget.book.status = widget.book.needUpdate
|
||||
? BookUpdateStatus.no
|
||||
: BookUpdateStatus.not;
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
if (widget.book.status == BookUpdateStatus.had &&
|
||||
widget.book.look == false)
|
||||
IconSlideAction(
|
||||
caption: '已读',
|
||||
color: Colors.greenAccent,
|
||||
iconWidget: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Icon(
|
||||
FontAwesomeIcons.bellSlash,
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
foregroundColor: Colors.white,
|
||||
onTap: () async {
|
||||
widget.book.chapterCount += widget.book.newChapterCount;
|
||||
widget.book.look = true;
|
||||
await widget.book.save();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
IconSlideAction(
|
||||
caption: '删除',
|
||||
color: Colors.red,
|
||||
icon: Icons.delete,
|
||||
onTap: () => widget.onDelete(widget.book),
|
||||
),
|
||||
],
|
||||
child: ListTile(
|
||||
onTap: () async {
|
||||
await openBook(context, widget.book, 'fb ${widget.book.aid}');
|
||||
setState(() {});
|
||||
},
|
||||
// onLongPress: () => onDelete(book),
|
||||
leading: Hero(
|
||||
tag: 'fb ${widget.book.aid}',
|
||||
child: widget.book.http == null
|
||||
? oldBookAvatar(text: '旧书', width: 50.0, height: 80.0)
|
||||
: ExtendedImage(
|
||||
image: NetworkImageSSL(widget.book.http, widget.book.avatar),
|
||||
width: 50.0,
|
||||
height: 80.0),
|
||||
),
|
||||
title: Text(widget.book.name),
|
||||
subtitle: RichText(text: subtitle),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
92
lib/widgets/groupFormDialog.dart
Normal file
92
lib/widgets/groupFormDialog.dart
Normal file
@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:weiman/db/group.dart';
|
||||
|
||||
Future<Group> showGroupFormDialog(BuildContext context, [Group group]) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return GroupFormDialog(group: group);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class GroupFormDialog extends StatefulWidget {
|
||||
final Group group;
|
||||
|
||||
const GroupFormDialog({Key key, this.group}) : super(key: key);
|
||||
|
||||
@override
|
||||
_GroupFormDialog createState() => _GroupFormDialog();
|
||||
}
|
||||
|
||||
class _GroupFormDialog extends State<GroupFormDialog> {
|
||||
final _form = GlobalKey<FormState>();
|
||||
TextEditingController _nameController;
|
||||
Group group;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
group = widget.group;
|
||||
_nameController = TextEditingController(text: widget.group?.name ?? '');
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.group == null ? '创建分组' : '分组重命名'),
|
||||
content: Form(
|
||||
key: _form,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: group == null ? '输入分组名称' : '原名 ${group.name}',
|
||||
),
|
||||
validator: (value) {
|
||||
value = value.trim();
|
||||
if (value.isEmpty) {
|
||||
return '分组名称不能为空';
|
||||
}
|
||||
final sameName =
|
||||
Group.groupBox.values.firstWhere((Group group) {
|
||||
return group.name == value && group.key != this.group?.key;
|
||||
}, orElse: () => null);
|
||||
if (sameName != null) {
|
||||
return '已经存在同名的分组';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('确认'),
|
||||
onPressed: () async {
|
||||
if (group == null) {
|
||||
group = Group(_nameController.text);
|
||||
} else {
|
||||
group.name = _nameController.text;
|
||||
}
|
||||
await group.save();
|
||||
Navigator.pop(context, group);
|
||||
},
|
||||
),
|
||||
RaisedButton(
|
||||
child: Text('取消'),
|
||||
textColor: Colors.white,
|
||||
color: Colors.blue,
|
||||
onPressed: () {
|
||||
Navigator.pop(context, group);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,14 @@
|
||||
part of '../main.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:oktoast/oktoast.dart';
|
||||
|
||||
import 'package:weiman/classes/networkImageSSL.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/utils.dart';
|
||||
import 'package:weiman/widgets/sliverExpandableGroup.dart';
|
||||
import 'package:weiman/widgets/utils.dart';
|
||||
|
||||
class Histories extends StatefulWidget {
|
||||
@override
|
||||
@ -6,22 +16,33 @@ class Histories extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _Histories extends State<Histories> {
|
||||
static bool _showTips = true;
|
||||
final List<Book> inWeek = [], other = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadBook();
|
||||
if (_showTips)
|
||||
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_showTips = false;
|
||||
showToast(
|
||||
'阅读记录和时间分组\n往左滑显示更多操作',
|
||||
textPadding: EdgeInsets.all(10),
|
||||
duration: Duration(seconds: 4),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void loadBook() {
|
||||
inWeek.clear();
|
||||
other.clear();
|
||||
final list = Data.getHistories().values.toList();
|
||||
final list =
|
||||
Book.bookBox.values.where((book) => book.history != null).toList();
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
list.sort((a, b) => b.history.time.compareTo(a.history.time));
|
||||
list.forEach((book) {
|
||||
if ((now - book.history.time) < weekTime) {
|
||||
if ((now - book.history.time.millisecondsSinceEpoch) < weekTime) {
|
||||
inWeek.add(book);
|
||||
} else {
|
||||
other.add(book);
|
||||
@ -50,47 +71,78 @@ class _Histories extends State<Histories> {
|
||||
print('清理历史 $inWeek $res');
|
||||
if (res == false) return;
|
||||
List<Book> list = inWeek ? this.inWeek : this.other;
|
||||
list.forEach((book) => Data.removeHistoryFromBook(book));
|
||||
await Future.wait(list.map((book) => book.setHistory(null)));
|
||||
setState(() {
|
||||
loadBook();
|
||||
});
|
||||
}
|
||||
|
||||
Widget book(List array, int index) {
|
||||
final Book book = array[index];
|
||||
return Slidable(
|
||||
child: ListTile(
|
||||
leading: book.http == null
|
||||
? oldBookAvatar(text: '旧\n书', width: 50.0, height: 80.0)
|
||||
: ExtendedImage(
|
||||
image: NetworkImageSSL(book.http, book.avatar),
|
||||
width: 50.0,
|
||||
height: 80.0),
|
||||
title: Text(book.name),
|
||||
subtitle: Text(book.history.cname),
|
||||
onTap: () => openBook(context, book, 'fb ${book.aid}'),
|
||||
),
|
||||
actionPane: SlidableDrawerActionPane(),
|
||||
secondaryActions: [
|
||||
IconSlideAction(
|
||||
caption: '删除',
|
||||
color: Colors.red,
|
||||
icon: Icons.delete,
|
||||
onTap: () async {
|
||||
await book.setHistory(null);
|
||||
setState(() {
|
||||
array.remove(book);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverExpandableGroup(
|
||||
title: Text('7天内的浏览历史 (${inWeek.length})'),
|
||||
expanded: true,
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('清空'),
|
||||
onPressed: inWeek.length == 0 ? null : () => clear(true),
|
||||
),
|
||||
],
|
||||
count: inWeek.length,
|
||||
builder: (ctx, i) => WidgetBook(
|
||||
inWeek[i],
|
||||
subtitle: inWeek[i].history.cname,
|
||||
child: ClipRect(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverExpandableGroup(
|
||||
title: Text('7天内的浏览历史 (${inWeek.length})'),
|
||||
expanded: true,
|
||||
count: inWeek.length,
|
||||
builder: (ctx, i) => book(inWeek, i),
|
||||
slideActions: [
|
||||
IconSlideAction(
|
||||
caption: '清空',
|
||||
color: Colors.red,
|
||||
icon: Icons.delete,
|
||||
onTap: () => clear(true),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverExpandableGroup(
|
||||
title: Text('更早的浏览历史 (${other.length})'),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('清空'),
|
||||
onPressed: other.length == 0 ? null : () => clear(false),
|
||||
),
|
||||
],
|
||||
count: other.length,
|
||||
builder: (ctx, i) => WidgetBook(
|
||||
other[i],
|
||||
subtitle: other[i].history.cname,
|
||||
SliverExpandableGroup(
|
||||
title: Text('更早的浏览历史 (${other.length})'),
|
||||
count: other.length,
|
||||
builder: (ctx, i) => book(other, i),
|
||||
slideActions: [
|
||||
IconSlideAction(
|
||||
caption: '清空',
|
||||
color: Colors.red,
|
||||
icon: Icons.delete,
|
||||
onTap: () => clear(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
part of '../main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||
import 'package:weiman/widgets/animatedLogo.dart';
|
||||
|
||||
class SliverPullToRefreshHeader extends StatelessWidget {
|
||||
static final double height = kToolbarHeight * 2;
|
||||
@ -17,56 +19,43 @@ class SliverPullToRefreshHeader extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
if (info == null) return SliverToBoxAdapter(child: SizedBox());
|
||||
double dragOffset = info?.dragOffset ?? 0.0;
|
||||
TextSpan text = TextSpan(
|
||||
style: Theme.of(context).textTheme.body1.copyWith(
|
||||
fontSize: fontSize,
|
||||
),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Padding(
|
||||
child: Image.asset("assets/logo.png", height: 20),
|
||||
padding: EdgeInsets.only(right: 5),
|
||||
),
|
||||
),
|
||||
]);
|
||||
Widget widget;
|
||||
if (info.mode == RefreshIndicatorMode.error) {
|
||||
text.children.addAll([
|
||||
TextSpan(
|
||||
text: '读取失败\n当失败次数太多可能是网络出现问题\n',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
widget = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('读取网络数据失败\n你可能需要梯子'),
|
||||
RaisedButton.icon(
|
||||
icon: Icon(Icons.refresh),
|
||||
onPressed: onTap,
|
||||
label: Text('再次尝试'),
|
||||
),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: RaisedButton.icon(
|
||||
icon: Icon(Icons.refresh),
|
||||
onPressed: onTap,
|
||||
label: Text('再次尝试'))),
|
||||
]);
|
||||
],
|
||||
);
|
||||
} else if (info.mode == RefreshIndicatorMode.refresh ||
|
||||
info.mode == RefreshIndicatorMode.snap) {
|
||||
text.children.addAll([
|
||||
TextSpan(text: '读取中,请稍候'),
|
||||
]);
|
||||
widget = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedLogoWidget(width: 20, height: 30),
|
||||
SizedBox(width: 5),
|
||||
Text('读取中,请稍候'),
|
||||
],
|
||||
);
|
||||
} else if ([
|
||||
RefreshIndicatorMode.drag,
|
||||
RefreshIndicatorMode.armed,
|
||||
RefreshIndicatorMode.snap
|
||||
].contains(info.mode)) {
|
||||
text.children.add(TextSpan(text: '重新读取'));
|
||||
widget = Text('下拉刷新');
|
||||
} else {
|
||||
text.children.add(TextSpan(text: 'Bye~'));
|
||||
widget = SizedBox();
|
||||
}
|
||||
return SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: dragOffset,
|
||||
child: Center(
|
||||
child: Text.rich(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: widget,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,11 @@
|
||||
part of '../main.dart';
|
||||
import 'package:draggable_container/draggable_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:weiman/classes/networkImageSSL.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/utils.dart';
|
||||
import 'selectFavoriteBooks.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class QuickBook extends DraggableItem {
|
||||
static const heroTag = 'quickBookAvatar';
|
||||
@ -11,27 +18,23 @@ class QuickBook extends DraggableItem {
|
||||
{@required this.book, @required this.context}) {
|
||||
child = GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (ctx) => ActivityBook(
|
||||
book: book,
|
||||
heroTag: '$heroTag ${book.aid}',
|
||||
)));
|
||||
openBook(context, book, '$heroTag ${book.aid}');
|
||||
},
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Hero(
|
||||
tag: '$heroTag ${book.aid}',
|
||||
child: Image(
|
||||
image: NetworkImageSSL(book.avatar),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
book.http == null
|
||||
? oldBookAvatar(width: width, height: height)
|
||||
: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Hero(
|
||||
tag: '$heroTag ${book.aid}',
|
||||
child: Image(
|
||||
image: NetworkImageSSL(book.http, book.avatar),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@ -53,10 +56,6 @@ class QuickBook extends DraggableItem {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
checkUpdate() {
|
||||
UserAgentClient.instance.getBook(aid: book.aid);
|
||||
}
|
||||
}
|
||||
|
||||
class Quick extends StatefulWidget {
|
||||
@ -75,46 +74,14 @@ class QuickState extends State<Quick> {
|
||||
final int count = 8;
|
||||
final List<DraggableItem> _draggableItems = [];
|
||||
DraggableItem _addButton;
|
||||
GlobalKey<DraggableContainerState> _key = GlobalKey();
|
||||
final List<String> id = [];
|
||||
GlobalKey<DraggableContainerState> _key =
|
||||
GlobalKey<DraggableContainerState>();
|
||||
double width = 0, height = 0;
|
||||
|
||||
void exit() {
|
||||
_key.currentState.draggableMode = false;
|
||||
}
|
||||
|
||||
_showSelectBookDialog() async {
|
||||
final books = Data.getFavorites();
|
||||
final list = books.values
|
||||
.where((book) => !id.contains(book.aid))
|
||||
.map((book) => ListTile(
|
||||
title: Text(book.name),
|
||||
leading: Image(image: NetworkImageSSL(book.avatar)),
|
||||
onTap: () {
|
||||
Navigator.pop(context, book);
|
||||
},
|
||||
));
|
||||
return showDialog<Book>(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
title: Text('将收藏的漫画添加到快速导航'),
|
||||
content: Container(
|
||||
width: double.maxFinite,
|
||||
height: 300,
|
||||
child: list.isNotEmpty
|
||||
? ListView(
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: list,
|
||||
).toList(),
|
||||
)
|
||||
: Center(child: Text('没有了')),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QuickState() {
|
||||
_addButton = DraggableItem(
|
||||
deletable: false,
|
||||
@ -141,9 +108,12 @@ class QuickState extends State<Quick> {
|
||||
final buttonIndex = items.indexOf(_addButton);
|
||||
print('add $buttonIndex');
|
||||
if (buttonIndex > -1) {
|
||||
final book = await _showSelectBookDialog();
|
||||
final book = await showFavoriteBooksDialog(context);
|
||||
print('选择了 $book');
|
||||
if (book == null) return;
|
||||
book
|
||||
..quick = buttonIndex
|
||||
..save();
|
||||
_key.currentState.insteadOfIndex(buttonIndex,
|
||||
QuickBook(width, height, book: book, context: context),
|
||||
force: true);
|
||||
@ -163,8 +133,18 @@ class QuickState extends State<Quick> {
|
||||
|
||||
width = widget.width / 4 - 10;
|
||||
height = (width / 0.7).roundToDouble();
|
||||
_draggableItems.addAll(Data.quickList().map((book) {
|
||||
id.add(book.aid);
|
||||
final list = <Book>[];
|
||||
Book.bookBox.values.forEach((book) {
|
||||
if (book.quick != null && list.length < count) {
|
||||
list.add(book);
|
||||
} else {
|
||||
book.quick = null;
|
||||
book.save();
|
||||
}
|
||||
});
|
||||
print('quick book length ${list.length}');
|
||||
list.sort((a, b) => a.quick.compareTo(b.quick));
|
||||
_draggableItems.addAll(list.map((book) {
|
||||
return QuickBook(width, height, book: book, context: context);
|
||||
}));
|
||||
if (_draggableItems.length < count) _draggableItems.add(_addButton);
|
||||
@ -175,6 +155,7 @@ class QuickState extends State<Quick> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print('quick build');
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
@ -195,18 +176,25 @@ class QuickState extends State<Quick> {
|
||||
boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]),
|
||||
items: _draggableItems,
|
||||
onDraggableModeChanged: widget.draggableModeChanged,
|
||||
onBeforeDelete: (index, item) async {
|
||||
if (item is QuickBook) {
|
||||
print('on before delete ${item.book.name}');
|
||||
item.book.quick = null;
|
||||
item.book.save();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onChanged: (List<DraggableItem> items) {
|
||||
id.clear();
|
||||
items.forEach((item) {
|
||||
if (item is QuickBook) id.add(item.book.aid);
|
||||
});
|
||||
Data.addQuickAll(id);
|
||||
final nullIndex = items.indexOf(null);
|
||||
final buttonIndex = items.indexOf(_addButton);
|
||||
print('null $nullIndex, button $buttonIndex');
|
||||
if (nullIndex > -1 && buttonIndex == -1) {
|
||||
_key.currentState
|
||||
.insteadOfIndex(nullIndex, _addButton, triggerEvent: false);
|
||||
print('显示添加按钮 1');
|
||||
_key.currentState.insteadOfIndex(
|
||||
nullIndex, _addButton,
|
||||
triggerEvent: false, force: true);
|
||||
print('显示添加按钮 2');
|
||||
setState(() {});
|
||||
} else if (nullIndex > -1 &&
|
||||
buttonIndex > -1 &&
|
||||
nullIndex < buttonIndex) {
|
||||
@ -214,6 +202,15 @@ class QuickState extends State<Quick> {
|
||||
_key.currentState
|
||||
.insteadOfIndex(nullIndex, _addButton, triggerEvent: false);
|
||||
}
|
||||
var quick = 0;
|
||||
items.forEach((item) {
|
||||
if (item is QuickBook) {
|
||||
item.book
|
||||
..quick = quick
|
||||
..save();
|
||||
quick++;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
|
51
lib/widgets/selectFavoriteBooks.dart
Normal file
51
lib/widgets/selectFavoriteBooks.dart
Normal file
@ -0,0 +1,51 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:weiman/classes/networkImageSSL.dart';
|
||||
import 'package:weiman/db/book.dart';
|
||||
import 'package:weiman/provider/favoriteData.dart';
|
||||
|
||||
Future<Book> showFavoriteBooksDialog(BuildContext context) {
|
||||
return showDialog<Book>(
|
||||
context: context,
|
||||
builder: (_) => FavoriteBooksDialog(title: '将藏书添加到快速导航'),
|
||||
);
|
||||
}
|
||||
|
||||
class FavoriteBooksDialog extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const FavoriteBooksDialog({
|
||||
Key key,
|
||||
@required this.title,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fav = Provider.of<FavoriteData>(context, listen: false);
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
scrollable: true,
|
||||
content: Column(
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: fav.all
|
||||
.where((book) => book.quick == null)
|
||||
.map(
|
||||
(book) => ListTile(
|
||||
title: Text(book.name),
|
||||
leading: ExtendedImage(
|
||||
image: NetworkImageSSL(book.http, book.avatar),
|
||||
fit: BoxFit.cover,
|
||||
width: 40,
|
||||
),
|
||||
onTap: () => Navigator.pop(context, book),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
part of '../main.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||
|
||||
class SliverExpandableBuilder {
|
||||
final int count;
|
||||
@ -15,6 +19,7 @@ class SliverExpandableGroup extends StatefulWidget {
|
||||
final double height;
|
||||
final int count;
|
||||
final IndexedWidgetBuilder builder;
|
||||
final List<Widget> slideActions;
|
||||
|
||||
const SliverExpandableGroup({
|
||||
Key key,
|
||||
@ -25,6 +30,7 @@ class SliverExpandableGroup extends StatefulWidget {
|
||||
this.actions = const [],
|
||||
this.divideColor = Colors.grey,
|
||||
this.height = kToolbarHeight,
|
||||
this.slideActions,
|
||||
}) : assert(title != null),
|
||||
assert(builder != null),
|
||||
super(key: key);
|
||||
@ -35,6 +41,7 @@ class SliverExpandableGroup extends StatefulWidget {
|
||||
|
||||
class _SliverExpandableGroup extends State<SliverExpandableGroup> {
|
||||
bool _expanded;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
@ -48,36 +55,44 @@ class _SliverExpandableGroup extends State<SliverExpandableGroup> {
|
||||
bottom: Divider.createBorderSide(context, color: widget.divideColor),
|
||||
),
|
||||
);
|
||||
return SliverStickyHeader(
|
||||
header: InkWell(
|
||||
child: Container(
|
||||
height: widget.height,
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dialogBackgroundColor,
|
||||
),
|
||||
child: Row(children: [
|
||||
Transform.rotate(
|
||||
angle: _expanded ? 0 : math.pi,
|
||||
child: Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
Expanded(child: widget.title),
|
||||
...widget.actions,
|
||||
]),
|
||||
Widget header = InkWell(
|
||||
child: Container(
|
||||
height: widget.height,
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_expanded = !_expanded;
|
||||
});
|
||||
},
|
||||
child: Row(children: [
|
||||
Transform.rotate(
|
||||
angle: _expanded ? 0 : math.pi,
|
||||
child: Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
Expanded(child: widget.title),
|
||||
...widget.actions,
|
||||
]),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_expanded = !_expanded;
|
||||
});
|
||||
},
|
||||
);
|
||||
if (widget.slideActions != null && widget.slideActions.length > 0) {
|
||||
header = Slidable(
|
||||
child: header,
|
||||
actionPane: SlidableDrawerActionPane(),
|
||||
secondaryActions: widget.slideActions,
|
||||
);
|
||||
}
|
||||
return SliverStickyHeader(
|
||||
header: header,
|
||||
sliver: _expanded
|
||||
? SliverList(
|
||||
delegate: SliverChildBuilderDelegate((ctx, i) {
|
||||
if (i < widget.count-1) {
|
||||
if (i < widget.count - 1) {
|
||||
return DecoratedBox(
|
||||
decoration: _decoration,
|
||||
child: widget.builder(context, i),
|
||||
|
@ -1,4 +1,5 @@
|
||||
part of '../main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class TextDivider extends StatelessWidget {
|
||||
final String text;
|
||||
@ -28,3 +29,17 @@ class TextDivider extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget oldBookAvatar({
|
||||
String text = '旧\n藏\n书',
|
||||
width = double.infinity,
|
||||
height = double.infinity,
|
||||
}) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
alignment: Alignment.center,
|
||||
color: Colors.greenAccent,
|
||||
child: Text(text),
|
||||
);
|
||||
}
|
||||
|
53
pubspec.yaml
53
pubspec.yaml
@ -1,5 +1,9 @@
|
||||
name: weiman
|
||||
description: 微漫
|
||||
description: 微漫App
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
@ -11,40 +15,66 @@ description: 微漫
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.0.4
|
||||
version: 1.1.4+2007
|
||||
|
||||
environment:
|
||||
sdk: ">=2.3.0 <3.0.0"
|
||||
sdk: ">=2.9.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dio: any
|
||||
dio_http_cache: any
|
||||
image: any
|
||||
intl: any
|
||||
async: any
|
||||
cupertino_icons: any
|
||||
http: any
|
||||
encrypt: any
|
||||
html: any
|
||||
hive: any
|
||||
sa_anicoto: any
|
||||
hive_flutter: any
|
||||
shared_preferences: any
|
||||
fluttertoast: any
|
||||
random_string: any
|
||||
filesize: any
|
||||
oktoast: any
|
||||
path_provider: any
|
||||
draggable_container: any
|
||||
sticky_headers: any
|
||||
flutter_sticky_header: any
|
||||
extended_nested_scroll_view: any
|
||||
dynamic_theme: any
|
||||
package_info: any
|
||||
url_launcher: any
|
||||
font_awesome_flutter: any
|
||||
loading_more_list: any
|
||||
webview_flutter: any
|
||||
loadmore: any
|
||||
pull_to_refresh_notification: any
|
||||
http_client_helper: any
|
||||
extended_image: any
|
||||
screenshot: any
|
||||
focus_widget: any
|
||||
provider: any
|
||||
loading_more_list: any
|
||||
flutter_slidable: any
|
||||
|
||||
firebase_core: any
|
||||
firebase_analytics: any
|
||||
e2e: any
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^0.1.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
hive_generator: any
|
||||
build_runner: any
|
||||
|
||||
#dependency_overrides:
|
||||
# analyzer: '0.39.14'
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
@ -58,11 +88,12 @@ flutter:
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- images/logo.png
|
||||
- assets/logo.png
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
Loading…
x
Reference in New Issue
Block a user