mirror of
https://github.com/nrop19/weiman_app.git
synced 2025-08-02 23:05:48 +08:00
Compare commits
No commits in common. "v1.0.3" and "master" have entirely different histories.
13
README.md
13
README.md
@ -1,2 +1,11 @@
|
|||||||
# weiman_app
|
# 微漫 v1.1.4 [宣传页面](https://nrop19.github.io/weiman_app)
|
||||||
微漫
|
|
||||||
|
### 微漫脱敏后的开源代码
|
||||||
|
|
||||||
|
#### 不解答任何代码上的问题
|
||||||
|
|
||||||
|
#### App的问题请到 [Telegram群](https://t.me/boring_programer) 讨论
|
||||||
|
|
||||||
|
- 删除了android端文件夹,涉及到apk签名等敏感文件
|
||||||
|
- 删除了ios端文件夹
|
||||||
|
- 删除了lib/crawler/里的其它文件,保护被爬网站的同时防止被爬网站加大防爬难度。
|
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
136
lib/activities/checkData.dart
Normal file
136
lib/activities/checkData.dart
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:oktoast/oktoast.dart';
|
||||||
|
|
||||||
|
import 'package:weiman/classes/data.dart';
|
||||||
|
|
||||||
|
class ActivityCheckData extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_State createState() => _State();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CheckState {
|
||||||
|
Uncheck,
|
||||||
|
Pass,
|
||||||
|
Fail,
|
||||||
|
}
|
||||||
|
|
||||||
|
final titleTextStyle = TextStyle(fontSize: 14, color: Colors.blue),
|
||||||
|
passStyle = TextStyle(color: Colors.green),
|
||||||
|
failStyle = TextStyle(color: Colors.red);
|
||||||
|
|
||||||
|
class _State extends State<ActivityCheckData> {
|
||||||
|
CheckState firstState;
|
||||||
|
int firstLength = 0;
|
||||||
|
final TextSpan secondResults = TextSpan();
|
||||||
|
TextEditingController _outputController, _inputController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_outputController = TextEditingController();
|
||||||
|
_inputController = TextEditingController();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSpan first() {
|
||||||
|
String text;
|
||||||
|
switch (firstState) {
|
||||||
|
case CheckState.Pass:
|
||||||
|
text = '有数据, 一共 $firstLength 本收藏';
|
||||||
|
break;
|
||||||
|
case CheckState.Fail:
|
||||||
|
text = '没有收藏数据';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text = '未检查';
|
||||||
|
}
|
||||||
|
return TextSpan(
|
||||||
|
text: text,
|
||||||
|
style: firstState == CheckState.Pass ? passStyle : failStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final firstChildren = [
|
||||||
|
Text('检查漫画收藏列表'),
|
||||||
|
RaisedButton(
|
||||||
|
child: Text('检查'),
|
||||||
|
color: Colors.blue,
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () {
|
||||||
|
final has = Data.has(Data.favoriteBooksKey);
|
||||||
|
if (has) {
|
||||||
|
final String str = Data.instance.getString(Data.favoriteBooksKey);
|
||||||
|
final Map<String, Object> map = jsonDecode(str);
|
||||||
|
firstLength = map.keys.length;
|
||||||
|
_outputController.text = str;
|
||||||
|
}
|
||||||
|
firstState = firstLength > 0 ? CheckState.Pass : CheckState.Fail;
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: '结果:',
|
||||||
|
children: [first()],
|
||||||
|
style: TextStyle(color: Colors.black)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if (firstState == CheckState.Pass) {
|
||||||
|
firstChildren.add(Text('点击复制'));
|
||||||
|
firstChildren.add(TextField(
|
||||||
|
maxLines: 8,
|
||||||
|
controller: _outputController,
|
||||||
|
onTap: () {
|
||||||
|
showToast('已经复制');
|
||||||
|
Clipboard.setData(ClipboardData(text: _outputController.text));
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('收藏数据检修'),
|
||||||
|
),
|
||||||
|
body: ListView(children: [
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(5),
|
||||||
|
child: Container(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: firstChildren,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('导入收藏数据'),
|
||||||
|
TextField(
|
||||||
|
controller: _inputController,
|
||||||
|
maxLines: 8,
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
child: Text('导入'),
|
||||||
|
onPressed: () {
|
||||||
|
if (_inputController.text.length > 0) {
|
||||||
|
Data.instance.setString(
|
||||||
|
Data.favoriteBooksKey, _inputController.text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
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),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
380
lib/activities/home.dart
Normal file
380
lib/activities/home.dart
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
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 {
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => HomeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeState extends State<ActivityHome> {
|
||||||
|
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
final List<Widget> histories = [];
|
||||||
|
final List<Book> quick = [];
|
||||||
|
final GlobalKey<QuickState> _quickState = GlobalKey();
|
||||||
|
|
||||||
|
bool showFavorite = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
analytics.setCurrentScreen(screenName: '/activity_home');
|
||||||
|
|
||||||
|
/// 提前检查一次藏书的更新情况
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
autoSwitchTheme();
|
||||||
|
FavoriteData favData = Provider.of<FavoriteData>(context, listen: false);
|
||||||
|
await favData.loadBooksList();
|
||||||
|
final updated = await favData.checkUpdate();
|
||||||
|
if (updated > 0)
|
||||||
|
showToast(
|
||||||
|
'$updated 本藏书有更新',
|
||||||
|
textPadding: EdgeInsets.all(10),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void autoSwitchTheme() async {}
|
||||||
|
|
||||||
|
void gotoSearch() {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: '/activity_search'),
|
||||||
|
builder: (context) => ActivitySearch()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void gotoRecommend() {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: '/activity_recommend'),
|
||||||
|
builder: (_) => ActivityRank(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void gotoPatreon() {
|
||||||
|
launch('https://www.patreon.com/nrop19');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEdit = false;
|
||||||
|
|
||||||
|
void _draggableModeChanged(bool mode) {
|
||||||
|
print('mode changed $mode');
|
||||||
|
isEdit = mode;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
final width = (media.size.width * 0.8).roundToDouble();
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('微漫 v' + packageInfo.version),
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
leading: isEdit
|
||||||
|
? IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back_ios),
|
||||||
|
onPressed: () {
|
||||||
|
_quickState.currentState.exit();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
actions: <Widget>[
|
||||||
|
/// 黑白样式切换
|
||||||
|
themeButton(),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
|
||||||
|
/// 设置界面
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: '/activity_setting'),
|
||||||
|
builder: (_) => ActivitySetting()));
|
||||||
|
},
|
||||||
|
icon: Icon(FontAwesomeIcons.cog),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 收藏列表
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showFavorite = true;
|
||||||
|
_scaffoldKey.currentState.openEndDrawer();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.favorite,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 浏览历史列表
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showFavorite = false;
|
||||||
|
// getHistory();
|
||||||
|
_scaffoldKey.currentState.openEndDrawer();
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.history),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
drawerEnableOpenDragGesture: false,
|
||||||
|
endDrawerEnableOpenDragGesture: false,
|
||||||
|
endDrawer: Drawer(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (_, constraints) {
|
||||||
|
if (showFavorite) {
|
||||||
|
return FavoriteList();
|
||||||
|
} else {
|
||||||
|
return Histories();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.only(left: 40, right: 40),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
child: OutlineButton(
|
||||||
|
onPressed: gotoSearch,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'搜索漫画',
|
||||||
|
style: TextStyle(color: Colors.blue),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(color: Colors.blue, width: 2),
|
||||||
|
shape: StadiumBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 7,
|
||||||
|
child: OutlineButton(
|
||||||
|
onPressed: gotoRecommend,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.whatshot,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'热门漫画',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(color: Colors.red, width: 2),
|
||||||
|
shape: StadiumBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Quick(
|
||||||
|
key: _quickState,
|
||||||
|
width: width,
|
||||||
|
draggableModeChanged: _draggableModeChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CheckConnectWidget(),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
launch('https://bbs.level-plus.net/');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'魂+论坛首发',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue[200],
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
if (await canLaunch('tg://resolve?domain=weiman_app'))
|
||||||
|
launch('tg://resolve?domain=weiman_app');
|
||||||
|
else
|
||||||
|
launch('https://t.me/weiman_app');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Telegram 广播频道',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue[200],
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Visibility(
|
||||||
|
visible: isDevMode,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(context,
|
||||||
|
MaterialPageRoute(builder: (_) => ActivityCheckData()));
|
||||||
|
},
|
||||||
|
child: Text('操作 收藏列表数据'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
139
lib/classes/book.dart
Normal file
139
lib/classes/book.dart
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:weiman/crawler/http.dart';
|
||||||
|
import 'data.dart';
|
||||||
|
class Book {
|
||||||
|
final String http;
|
||||||
|
final String aid; // 漫画的数据库ID
|
||||||
|
final String name; // 书本名称
|
||||||
|
final String avatar; // 书本封面
|
||||||
|
final String author; // 画家
|
||||||
|
final String description; // 描述
|
||||||
|
final List<Chapter> chapters;
|
||||||
|
final int chapterCount;
|
||||||
|
final int version;
|
||||||
|
|
||||||
|
History history;
|
||||||
|
|
||||||
|
Book({
|
||||||
|
@required this.http,
|
||||||
|
@required this.name,
|
||||||
|
@required this.aid,
|
||||||
|
@required this.avatar,
|
||||||
|
this.author,
|
||||||
|
this.description,
|
||||||
|
this.chapters: const [],
|
||||||
|
this.chapterCount: 0,
|
||||||
|
this.history,
|
||||||
|
this.version: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return jsonEncode(toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isFavorite() {
|
||||||
|
var books = Data.getFavorites();
|
||||||
|
return books.containsKey(aid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
print('book toJson');
|
||||||
|
final Map<String, dynamic> data = {
|
||||||
|
'http': http,
|
||||||
|
'aid': aid,
|
||||||
|
'name': name,
|
||||||
|
'avatar': avatar,
|
||||||
|
'author': author,
|
||||||
|
'chapterCount': chapterCount,
|
||||||
|
'version': version,
|
||||||
|
};
|
||||||
|
if (history != null) data['history'] = history.toJson();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Book.fromJson(Map<String, dynamic> json) {
|
||||||
|
final book = Book(
|
||||||
|
http: json['http'],
|
||||||
|
aid: json['aid'],
|
||||||
|
name: json['name'],
|
||||||
|
avatar: json['avatar'],
|
||||||
|
author: json['author'],
|
||||||
|
description: json['description'],
|
||||||
|
chapterCount: json['chapterCount'] ?? 0,
|
||||||
|
version: json['version'] ?? 0);
|
||||||
|
if (json.containsKey('history'))
|
||||||
|
book.history = History.fromJson(json['history']);
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Chapter {
|
||||||
|
final HttpBook http;
|
||||||
|
final String cid; // 章节cid
|
||||||
|
final String cname; // 章节名称
|
||||||
|
final String avatar; // 章节封面
|
||||||
|
|
||||||
|
Chapter({
|
||||||
|
@required this.http,
|
||||||
|
@required this.cid,
|
||||||
|
@required this.cname,
|
||||||
|
@required this.avatar,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final Map<String, String> data = {
|
||||||
|
'cid': cid,
|
||||||
|
'cname': cname,
|
||||||
|
'avatar': avatar,
|
||||||
|
};
|
||||||
|
return jsonEncode(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class History {
|
||||||
|
final String cid;
|
||||||
|
final String cname;
|
||||||
|
final int time;
|
||||||
|
final int image;
|
||||||
|
|
||||||
|
History({
|
||||||
|
@required this.cid,
|
||||||
|
@required this.cname,
|
||||||
|
@required this.time,
|
||||||
|
this.image = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => jsonEncode(toJson());
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'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'],
|
||||||
|
image: json['image'] ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static History fromChapter(Chapter chapter) {
|
||||||
|
return History(
|
||||||
|
cid: chapter.cid,
|
||||||
|
cname: chapter.cname,
|
||||||
|
time: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
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';
|
||||||
|
}
|
||||||
|
}
|
160
lib/classes/data.dart
Normal file
160
lib/classes/data.dart
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'book.dart';
|
||||||
|
|
||||||
|
class Data {
|
||||||
|
static SharedPreferences instance;
|
||||||
|
static final favoriteBooksKey = 'favorite_books';
|
||||||
|
static final viewHistoryKey = 'view_history';
|
||||||
|
static final quickKey = 'quick_list';
|
||||||
|
|
||||||
|
static Future init() async {
|
||||||
|
instance = await SharedPreferences.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
static set<T>(String key, T value) {
|
||||||
|
if (value is String) {
|
||||||
|
instance.setString(key, value);
|
||||||
|
} else if (value is int) {
|
||||||
|
instance.setInt(key, value);
|
||||||
|
} else if (value is bool) {
|
||||||
|
instance.setBool(key, value);
|
||||||
|
} else if (value is List<String>) {
|
||||||
|
instance.setStringList(key, value);
|
||||||
|
} else if (value is double) {
|
||||||
|
instance.setDouble(key, value);
|
||||||
|
} else if (value is Map) {
|
||||||
|
instance.setString(key, json.encode(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static dynamic get(String key) {
|
||||||
|
return instance.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hasData() {
|
||||||
|
return instance.containsKey(favoriteBooksKey) ||
|
||||||
|
instance.containsKey(viewHistoryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Book> getFavorites() {
|
||||||
|
if (has(favoriteBooksKey)) {
|
||||||
|
final String str = instance.getString(favoriteBooksKey);
|
||||||
|
Map<String, Object> data = jsonDecode(str);
|
||||||
|
Map<String, Book> res = {};
|
||||||
|
data.keys.forEach((key) {
|
||||||
|
res[key] = Book.fromJson(data[key]);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addFavorite(Book book) {
|
||||||
|
var books = getFavorites();
|
||||||
|
books[book.aid] = book;
|
||||||
|
set<Map>(favoriteBooksKey, books);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void removeFavorite(Book book) {
|
||||||
|
var books = getFavorites();
|
||||||
|
if (books.containsKey(book.aid)) {
|
||||||
|
books.remove(book.aid);
|
||||||
|
set<Map>(favoriteBooksKey, books);
|
||||||
|
reQuick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static clear() {
|
||||||
|
instance.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool has(String key) {
|
||||||
|
return instance.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static remove(String key) {
|
||||||
|
instance.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Book> getHistories() {
|
||||||
|
if (has(viewHistoryKey)) {
|
||||||
|
var data =
|
||||||
|
jsonDecode(instance.getString(viewHistoryKey)) as Map<String, Object>;
|
||||||
|
final Map<String, Book> histories = {};
|
||||||
|
data.forEach((key, value) {
|
||||||
|
histories[key] = Book.fromJson(value);
|
||||||
|
});
|
||||||
|
return histories;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static addHistory(Book book, Chapter chapter) {
|
||||||
|
book.history = History(
|
||||||
|
cid: chapter.cid,
|
||||||
|
cname: chapter.cname,
|
||||||
|
time: DateTime.now().millisecondsSinceEpoch);
|
||||||
|
final books = getHistories();
|
||||||
|
books[book.aid] = book;
|
||||||
|
set(viewHistoryKey, books);
|
||||||
|
// print('保存历史\n' + books.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeHistory(bool Function(Book book) isDelete) {
|
||||||
|
var books = getHistories();
|
||||||
|
books.keys
|
||||||
|
.where((key) => isDelete(books[key]))
|
||||||
|
.toList()
|
||||||
|
.forEach(books.remove);
|
||||||
|
set(viewHistoryKey, books);
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeHistoryFromBook(Book book) {
|
||||||
|
final books = getHistories();
|
||||||
|
books.remove(book.aid);
|
||||||
|
set(viewHistoryKey, books);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 快速导航 id 列表,内部方法
|
||||||
|
static List<String> quickIdList() {
|
||||||
|
if (instance.containsKey(quickKey)) {
|
||||||
|
return instance.getStringList(quickKey);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 快速导航列表
|
||||||
|
static List<Book> quickList() {
|
||||||
|
final books = getFavorites();
|
||||||
|
final ids = books.keys;
|
||||||
|
final List<String> quickIds = quickIdList();
|
||||||
|
print('快捷 $quickIds');
|
||||||
|
return quickIds
|
||||||
|
.where((id) => ids.contains(id))
|
||||||
|
.map((id) => books[id])
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 增加快速导航
|
||||||
|
static addQuick(Book book) {
|
||||||
|
final list = quickIdList();
|
||||||
|
list.add(book.aid);
|
||||||
|
instance.setStringList(quickKey, list.toSet().toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
static addQuickAll(List<String> id) {
|
||||||
|
print('保存qid $id');
|
||||||
|
instance.setStringList(quickKey, id.toSet().toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重新整理Quick的id列表
|
||||||
|
static reQuick() {
|
||||||
|
final books = getFavorites();
|
||||||
|
final quickIds = quickIdList();
|
||||||
|
instance.setStringList(
|
||||||
|
quickKey, quickIds.where(books.keys.contains).toSet().toList());
|
||||||
|
}
|
||||||
|
}
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
95
lib/classes/networkImageSSL.dart
Normal file
95
lib/classes/networkImageSSL.dart
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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 ImageProvider<NetworkImage>
|
||||||
|
implements NetworkImage {
|
||||||
|
/// Creates an object that fetches the image at the given URL.
|
||||||
|
///
|
||||||
|
/// The arguments [url] and [scale] must not be null.
|
||||||
|
const NetworkImageSSL(
|
||||||
|
this.http,
|
||||||
|
this.url, {
|
||||||
|
this.scale = 1.0,
|
||||||
|
this.headers,
|
||||||
|
this.timeout = 8,
|
||||||
|
this.reSort = false,
|
||||||
|
}) : assert(url != null),
|
||||||
|
assert(scale != null);
|
||||||
|
|
||||||
|
final HttpBook http;
|
||||||
|
|
||||||
|
final int timeout;
|
||||||
|
@override
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final double scale;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Map<String, String> headers;
|
||||||
|
|
||||||
|
final bool reSort;
|
||||||
|
|
||||||
|
static void init(ByteData data) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<NetworkImageSSL> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture<NetworkImageSSL>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
|
||||||
|
// Ownership of this controller is handed off to [_loadAsync]; it is that
|
||||||
|
// method's responsibility to close the controller's stream when the image
|
||||||
|
// has been loaded or an error is thrown.
|
||||||
|
final StreamController<ImageChunkEvent> chunkEvents =
|
||||||
|
StreamController<ImageChunkEvent>();
|
||||||
|
|
||||||
|
return MultiFrameImageStreamCompleter(
|
||||||
|
codec: _loadAsync(key, chunkEvents, decode),
|
||||||
|
chunkEvents: chunkEvents.stream,
|
||||||
|
scale: key.scale,
|
||||||
|
informationCollector: () {
|
||||||
|
return <DiagnosticsNode>[
|
||||||
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
|
DiagnosticsProperty<NetworkImage>('Image key', key),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Codec> _loadAsync(
|
||||||
|
NetworkImageSSL key,
|
||||||
|
StreamController<ImageChunkEvent> chunkEvents,
|
||||||
|
DecoderCallback decode,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
assert(key == this);
|
||||||
|
final Uint8List bytes = await http.getImage(url, reSort: reSort);
|
||||||
|
if (bytes.lengthInBytes == 0)
|
||||||
|
throw Exception('NetworkImage is an empty file: $url');
|
||||||
|
return decode(bytes);
|
||||||
|
} finally {
|
||||||
|
chunkEvents.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
final NetworkImageSSL typedOther = other;
|
||||||
|
return url == typedOther.url && scale == typedOther.scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues(url, scale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '$runtimeType("$url", scale: $scale)';
|
||||||
|
}
|
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();
|
||||||
|
}
|
||||||
|
}
|
134
lib/main.dart
Normal file
134
lib/main.dart
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
|
import 'package:firebase_analytics/observer.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:hive/hive.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:oktoast/oktoast.dart';
|
||||||
|
import 'package:package_info/package_info.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '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();
|
||||||
|
observer = FirebaseAnalyticsObserver(analytics: analytics);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
Hive.initFlutter(),
|
||||||
|
Data.init(),
|
||||||
|
SystemChrome.setPreferredOrientations(
|
||||||
|
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
||||||
|
]);
|
||||||
|
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 {
|
||||||
|
@override
|
||||||
|
_Main createState() => _Main();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Main extends State<Main> with WidgetsBindingObserver {
|
||||||
|
@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) {
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
50
lib/utils.dart
Normal file
50
lib/utils.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import '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 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}'),
|
||||||
|
builder: (_) => ActivityBook(book: book, heroTag: heroTag),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> openChapter(BuildContext context, Book book, Chapter chapter) {
|
||||||
|
return Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: '/activity_chapter/${book.name}/${chapter.cname}'),
|
||||||
|
builder: (_) => ActivityChapter(book, chapter),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showStatusBar() {
|
||||||
|
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hideStatusBar() {
|
||||||
|
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||||
|
}
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
192
lib/widgets/book.dart
Normal file
192
lib/widgets/book.dart
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
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;
|
||||||
|
final String subtitle;
|
||||||
|
final Function(Book) onTap;
|
||||||
|
|
||||||
|
const WidgetBook(
|
||||||
|
this.book, {
|
||||||
|
Key key,
|
||||||
|
@required this.subtitle,
|
||||||
|
this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isLiked = book.favorite;
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
book.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
subtitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
leading: Hero(
|
||||||
|
tag: 'bookAvatar${book.aid}',
|
||||||
|
child: ExtendedImage(image: NetworkImageSSL(book.http, book.avatar)),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
isLiked ? Icons.favorite : Icons.favorite_border,
|
||||||
|
color: isLiked ? Colors.red : Colors.grey,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) return onTap(book);
|
||||||
|
openBook(context, book, 'bookAvatar${book.aid}');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final dateFormat = DateFormat('yyyy-MM-dd');
|
||||||
|
|
||||||
|
class WidgetChapter extends StatelessWidget {
|
||||||
|
static final double height = kToolbarHeight;
|
||||||
|
final Chapter chapter;
|
||||||
|
final Function(Chapter) onTap;
|
||||||
|
final bool read;
|
||||||
|
|
||||||
|
WidgetChapter({
|
||||||
|
Key key,
|
||||||
|
this.chapter,
|
||||||
|
this.onTap,
|
||||||
|
this.read = false,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final children = <InlineSpan>[TextSpan(text: chapter.cname)];
|
||||||
|
if (read) {
|
||||||
|
children.insert(
|
||||||
|
0,
|
||||||
|
TextSpan(
|
||||||
|
text: '[已看]',
|
||||||
|
style: TextStyle(color: Colors.orange),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return ListTile(
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) onTap(chapter);
|
||||||
|
},
|
||||||
|
title: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: children,
|
||||||
|
style: Theme.of(context).textTheme.bodyText2,
|
||||||
|
),
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
subtitle: chapter.time == null
|
||||||
|
? null
|
||||||
|
: Text('更新时间 ${dateFormat.format(chapter.time)}'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WidgetHistory extends StatelessWidget {
|
||||||
|
final Book book;
|
||||||
|
final Function(Book book) onTap;
|
||||||
|
|
||||||
|
WidgetHistory(this.book, this.onTap);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) onTap(book);
|
||||||
|
},
|
||||||
|
title: Text(book.name),
|
||||||
|
leading: Image(
|
||||||
|
image: ExtendedNetworkImageProvider(book.avatar, cache: true),
|
||||||
|
fit: BoxFit.fitHeight,
|
||||||
|
),
|
||||||
|
subtitle: Text(book.history.cname),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WidgetBookCheckNew extends StatefulWidget {
|
||||||
|
final Book book;
|
||||||
|
|
||||||
|
const WidgetBookCheckNew({Key key, this.book}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_WidgetBookCheckNew createState() => _WidgetBookCheckNew();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WidgetBookCheckNew extends State<WidgetBookCheckNew> {
|
||||||
|
bool loading = true, hasError = false;
|
||||||
|
int news;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
void load() async {
|
||||||
|
// loading = true;
|
||||||
|
// try {
|
||||||
|
// final book = await Http18Comic.instance
|
||||||
|
// .getBook(widget.book.aid)
|
||||||
|
// .timeout(Duration(seconds: 2));
|
||||||
|
// news = book.chapterCount - widget.book.chapterCount;
|
||||||
|
// hasError = false;
|
||||||
|
// } catch (e) {
|
||||||
|
// hasError = true;
|
||||||
|
// }
|
||||||
|
// loading = false;
|
||||||
|
// setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final children = <Widget>[];
|
||||||
|
if (widget.book.history != null)
|
||||||
|
children.add(Text(
|
||||||
|
widget.book.history.cname,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
children.add(Text('检查更新中'));
|
||||||
|
else if (hasError)
|
||||||
|
children.add(Text('网络错误'));
|
||||||
|
else if (news > 0)
|
||||||
|
children.add(Text('有 $news 章更新'));
|
||||||
|
else
|
||||||
|
children.add(Text('没有更新'));
|
||||||
|
return ListTile(
|
||||||
|
onTap: () =>
|
||||||
|
openBook(context, widget.book, 'checkBook${widget.book.aid}'),
|
||||||
|
leading: Hero(
|
||||||
|
tag: 'checkBook${widget.book.aid}',
|
||||||
|
child: Image(
|
||||||
|
image:
|
||||||
|
ExtendedNetworkImageProvider(widget.book.avatar, cache: true)),
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
isThreeLine: true,
|
||||||
|
title: Text(widget.book.name),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
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)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
240
lib/widgets/favorites.dart
Normal file
240
lib/widgets/favorites.dart
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
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
|
||||||
|
_FavoriteList createState() => _FavoriteList();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavoriteList extends State<FavoriteList> {
|
||||||
|
static bool showTip = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
if (showTip) {
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
showToast(
|
||||||
|
'下拉收藏列表检查更新\n分组和藏书左滑显示更多操作',
|
||||||
|
textPadding: EdgeInsets.all(10),
|
||||||
|
duration: Duration(seconds: 4),
|
||||||
|
);
|
||||||
|
showTip = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget bookBuilder(Book book) {
|
||||||
|
return FBookItem(
|
||||||
|
book: book,
|
||||||
|
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) {
|
||||||
|
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]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 void Function(Book book) onDelete;
|
||||||
|
|
||||||
|
const FBookItem({
|
||||||
|
Key key,
|
||||||
|
@required this.book,
|
||||||
|
@required this.onDelete,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_FBookItem createState() => _FBookItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FBookItem extends State<FBookItem> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
149
lib/widgets/histories.dart
Normal file
149
lib/widgets/histories.dart
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
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
|
||||||
|
_Histories createState() => _Histories();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 =
|
||||||
|
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.millisecondsSinceEpoch) < weekTime) {
|
||||||
|
inWeek.add(book);
|
||||||
|
} else {
|
||||||
|
other.add(book);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear(bool inWeek) async {
|
||||||
|
final title = '确认清空 ' + (inWeek ? '7天内的' : '更早的') + '浏览记录 ?';
|
||||||
|
final res = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
textColor: Colors.grey,
|
||||||
|
child: Text('取消'),
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('确认'),
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
print('清理历史 $inWeek $res');
|
||||||
|
if (res == false) return;
|
||||||
|
List<Book> list = inWeek ? this.inWeek : this.other;
|
||||||
|
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: 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})'),
|
||||||
|
count: other.length,
|
||||||
|
builder: (ctx, i) => book(other, i),
|
||||||
|
slideActions: [
|
||||||
|
IconSlideAction(
|
||||||
|
caption: '清空',
|
||||||
|
color: Colors.red,
|
||||||
|
icon: Icons.delete,
|
||||||
|
onTap: () => clear(false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
62
lib/widgets/pullToRefreshHeader.dart
Normal file
62
lib/widgets/pullToRefreshHeader.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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;
|
||||||
|
final PullToRefreshScrollNotificationInfo info;
|
||||||
|
final void Function() onTap;
|
||||||
|
final double fontSize;
|
||||||
|
|
||||||
|
const SliverPullToRefreshHeader({
|
||||||
|
Key key,
|
||||||
|
@required this.info,
|
||||||
|
this.onTap,
|
||||||
|
this.fontSize = 16,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (info == null) return SliverToBoxAdapter(child: SizedBox());
|
||||||
|
double dragOffset = info?.dragOffset ?? 0.0;
|
||||||
|
Widget widget;
|
||||||
|
if (info.mode == RefreshIndicatorMode.error) {
|
||||||
|
widget = Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('读取网络数据失败\n你可能需要梯子'),
|
||||||
|
RaisedButton.icon(
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
onPressed: onTap,
|
||||||
|
label: Text('再次尝试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else if (info.mode == RefreshIndicatorMode.refresh ||
|
||||||
|
info.mode == RefreshIndicatorMode.snap) {
|
||||||
|
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)) {
|
||||||
|
widget = Text('下拉刷新');
|
||||||
|
} else {
|
||||||
|
widget = SizedBox();
|
||||||
|
}
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
height: dragOffset,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: widget,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
219
lib/widgets/quick.dart
Normal file
219
lib/widgets/quick.dart
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
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';
|
||||||
|
Widget child;
|
||||||
|
final BuildContext context;
|
||||||
|
final Book book;
|
||||||
|
final double width, height;
|
||||||
|
|
||||||
|
QuickBook(this.width, this.height,
|
||||||
|
{@required this.book, @required this.context}) {
|
||||||
|
child = GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
openBook(context, book, '$heroTag ${book.aid}');
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
book.http == null
|
||||||
|
? oldBookAvatar(width: width, height: height)
|
||||||
|
: SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: Hero(
|
||||||
|
tag: '$heroTag ${book.aid}',
|
||||||
|
child: Image(
|
||||||
|
image: NetworkImageSSL(book.http, book.avatar),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(left: 2, right: 2, top: 2, bottom: 2),
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
child: Text(
|
||||||
|
book.name,
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: 1,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 10),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Quick extends StatefulWidget {
|
||||||
|
final double width, height;
|
||||||
|
final Function(bool mode) draggableModeChanged;
|
||||||
|
|
||||||
|
const Quick(
|
||||||
|
{Key key, this.width, this.height, @required this.draggableModeChanged})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
QuickState createState() => QuickState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuickState extends State<Quick> {
|
||||||
|
final int count = 8;
|
||||||
|
final List<DraggableItem> _draggableItems = [];
|
||||||
|
DraggableItem _addButton;
|
||||||
|
GlobalKey<DraggableContainerState> _key =
|
||||||
|
GlobalKey<DraggableContainerState>();
|
||||||
|
double width = 0, height = 0;
|
||||||
|
|
||||||
|
void exit() {
|
||||||
|
_key.currentState.draggableMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QuickState() {
|
||||||
|
_addButton = DraggableItem(
|
||||||
|
deletable: false,
|
||||||
|
fixed: true,
|
||||||
|
child: FlatButton(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'添加',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
final items = _key.currentState.items;
|
||||||
|
final buttonIndex = items.indexOf(_addButton);
|
||||||
|
print('add $buttonIndex');
|
||||||
|
if (buttonIndex > -1) {
|
||||||
|
final book = await 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int length() {
|
||||||
|
return _key.currentState.items.where((item) => item is QuickBook).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
width = widget.width / 4 - 10;
|
||||||
|
height = (width / 0.7).roundToDouble();
|
||||||
|
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);
|
||||||
|
for (var i = count - _draggableItems.length; i > 0; i--) {
|
||||||
|
_draggableItems.add(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
print('quick build');
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 8, bottom: 4, left: 8),
|
||||||
|
width: widget.width,
|
||||||
|
child: Text(
|
||||||
|
'快速导航(长按编辑)',
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DraggableContainer(
|
||||||
|
key: _key,
|
||||||
|
slotMargin: EdgeInsets.only(bottom: 8, left: 6, right: 6),
|
||||||
|
slotSize: Size(width, height),
|
||||||
|
slotDecoration: BoxDecoration(color: Colors.grey.withOpacity(0.3)),
|
||||||
|
dragDecoration: BoxDecoration(
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]),
|
||||||
|
items: _draggableItems,
|
||||||
|
onDraggableModeChanged: widget.draggableModeChanged,
|
||||||
|
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) {
|
||||||
|
final nullIndex = items.indexOf(null);
|
||||||
|
final buttonIndex = items.indexOf(_addButton);
|
||||||
|
print('null $nullIndex, button $buttonIndex');
|
||||||
|
if (nullIndex > -1 && buttonIndex == -1) {
|
||||||
|
print('显示添加按钮 1');
|
||||||
|
_key.currentState.insteadOfIndex(
|
||||||
|
nullIndex, _addButton,
|
||||||
|
triggerEvent: false, force: true);
|
||||||
|
print('显示添加按钮 2');
|
||||||
|
setState(() {});
|
||||||
|
} else if (nullIndex > -1 &&
|
||||||
|
buttonIndex > -1 &&
|
||||||
|
nullIndex < buttonIndex) {
|
||||||
|
_key.currentState.removeItem(_addButton);
|
||||||
|
_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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
106
lib/widgets/sliverExpandableGroup.dart
Normal file
106
lib/widgets/sliverExpandableGroup.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
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;
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
|
const SliverExpandableBuilder(this.count, this.builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SliverExpandableGroup extends StatefulWidget {
|
||||||
|
final Widget title;
|
||||||
|
final bool expanded;
|
||||||
|
final List<Widget> actions;
|
||||||
|
final Color divideColor;
|
||||||
|
final double height;
|
||||||
|
final int count;
|
||||||
|
final IndexedWidgetBuilder builder;
|
||||||
|
final List<Widget> slideActions;
|
||||||
|
|
||||||
|
const SliverExpandableGroup({
|
||||||
|
Key key,
|
||||||
|
@required this.title,
|
||||||
|
@required this.count,
|
||||||
|
@required this.builder,
|
||||||
|
this.expanded = false,
|
||||||
|
this.actions = const [],
|
||||||
|
this.divideColor = Colors.grey,
|
||||||
|
this.height = kToolbarHeight,
|
||||||
|
this.slideActions,
|
||||||
|
}) : assert(title != null),
|
||||||
|
assert(builder != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SliverExpandableGroup createState() => _SliverExpandableGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliverExpandableGroup extends State<SliverExpandableGroup> {
|
||||||
|
bool _expanded;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_expanded = widget.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Decoration _decoration = BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: Divider.createBorderSide(context, color: widget.divideColor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Widget header = InkWell(
|
||||||
|
child: Container(
|
||||||
|
height: widget.height,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).backgroundColor,
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Transform.rotate(
|
||||||
|
angle: _expanded ? 0 : math.pi,
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: widget.title),
|
||||||
|
...widget.actions,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_expanded = !_expanded;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (widget.slideActions != null && widget.slideActions.length > 0) {
|
||||||
|
header = Slidable(
|
||||||
|
child: header,
|
||||||
|
actionPane: SlidableDrawerActionPane(),
|
||||||
|
secondaryActions: widget.slideActions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SliverStickyHeader(
|
||||||
|
header: header,
|
||||||
|
sliver: _expanded
|
||||||
|
? SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate((ctx, i) {
|
||||||
|
if (i < widget.count - 1) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: _decoration,
|
||||||
|
child: widget.builder(context, i),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return widget.builder(context, i);
|
||||||
|
}, childCount: widget.count))
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
45
lib/widgets/utils.dart
Normal file
45
lib/widgets/utils.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class TextDivider extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final double leftPadding, padding;
|
||||||
|
final List<Widget> actions;
|
||||||
|
|
||||||
|
const TextDivider({
|
||||||
|
Key key,
|
||||||
|
@required this.text,
|
||||||
|
this.padding = 5,
|
||||||
|
this.leftPadding = 15,
|
||||||
|
this.actions = const [],
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding:
|
||||||
|
EdgeInsets.only(left: leftPadding, top: padding, bottom: padding),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(text, style: TextStyle(color: Colors.grey))),
|
||||||
|
...actions,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget oldBookAvatar({
|
||||||
|
String text = '旧\n藏\n书',
|
||||||
|
width = double.infinity,
|
||||||
|
height = double.infinity,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: Colors.greenAccent,
|
||||||
|
child: Text(text),
|
||||||
|
);
|
||||||
|
}
|
122
pubspec.yaml
Normal file
122
pubspec.yaml
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
name: weiman
|
||||||
|
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
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
version: 1.1.4+2007
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.9.0 <3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
dio: any
|
||||||
|
dio_http_cache: any
|
||||||
|
image: any
|
||||||
|
intl: any
|
||||||
|
async: any
|
||||||
|
http: any
|
||||||
|
encrypt: any
|
||||||
|
html: any
|
||||||
|
hive: any
|
||||||
|
sa_anicoto: any
|
||||||
|
hive_flutter: any
|
||||||
|
shared_preferences: any
|
||||||
|
random_string: any
|
||||||
|
filesize: any
|
||||||
|
oktoast: any
|
||||||
|
path_provider: any
|
||||||
|
draggable_container: any
|
||||||
|
sticky_headers: any
|
||||||
|
flutter_sticky_header: any
|
||||||
|
extended_nested_scroll_view: any
|
||||||
|
package_info: any
|
||||||
|
url_launcher: any
|
||||||
|
font_awesome_flutter: any
|
||||||
|
webview_flutter: any
|
||||||
|
loadmore: any
|
||||||
|
pull_to_refresh_notification: any
|
||||||
|
http_client_helper: any
|
||||||
|
extended_image: any
|
||||||
|
screenshot: any
|
||||||
|
focus_widget: any
|
||||||
|
provider: any
|
||||||
|
loading_more_list: any
|
||||||
|
flutter_slidable: any
|
||||||
|
|
||||||
|
firebase_core: any
|
||||||
|
firebase_analytics: 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
|
||||||
|
|
||||||
|
# The following section is specific to Flutter.
|
||||||
|
flutter:
|
||||||
|
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- assets/logo.png
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||||
|
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/custom-fonts/#from-packages
|
Loading…
x
Reference in New Issue
Block a user