mirror of
https://github.com/nrop19/weiman_app.git
synced 2025-08-02 23:05:48 +08:00
v1.0.4
This commit is contained in:
parent
87b0d9897b
commit
582d231063
73
.gitignore
vendored
Normal file
73
.gitignore
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Android related
|
||||||
|
**/android/**/gradle-wrapper.jar
|
||||||
|
**/android/.gradle
|
||||||
|
**/android/captures/
|
||||||
|
**/android/gradlew
|
||||||
|
**/android/gradlew.bat
|
||||||
|
**/android/local.properties
|
||||||
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
|
|
||||||
|
# iOS/XCode related
|
||||||
|
**/ios/**/*.mode1v3
|
||||||
|
**/ios/**/*.mode2v3
|
||||||
|
**/ios/**/*.moved-aside
|
||||||
|
**/ios/**/*.pbxuser
|
||||||
|
**/ios/**/*.perspectivev3
|
||||||
|
**/ios/**/*sync/
|
||||||
|
**/ios/**/.sconsign.dblite
|
||||||
|
**/ios/**/.tags*
|
||||||
|
**/ios/**/.vagrant/
|
||||||
|
**/ios/**/DerivedData/
|
||||||
|
**/ios/**/Icon?
|
||||||
|
**/ios/**/Pods/
|
||||||
|
**/ios/**/.symlinks/
|
||||||
|
**/ios/**/profile
|
||||||
|
**/ios/**/xcuserdata
|
||||||
|
**/ios/.generated/
|
||||||
|
**/ios/Flutter/App.framework
|
||||||
|
**/ios/Flutter/Flutter.framework
|
||||||
|
**/ios/Flutter/Generated.xcconfig
|
||||||
|
**/ios/Flutter/app.flx
|
||||||
|
**/ios/Flutter/app.zip
|
||||||
|
**/ios/Flutter/flutter_assets/
|
||||||
|
**/ios/Flutter/flutter_export_environment.sh
|
||||||
|
**/ios/ServiceDefinitions.json
|
||||||
|
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!**/ios/**/default.mode1v3
|
||||||
|
!**/ios/**/default.mode2v3
|
||||||
|
!**/ios/**/default.pbxuser
|
||||||
|
!**/ios/**/default.perspectivev3
|
||||||
|
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
13
README.md
13
README.md
@ -1,2 +1,11 @@
|
|||||||
# weiman_app
|
# weiman v1.0.4
|
||||||
微漫
|
|
||||||
|
### 微漫脱敏后的开源代码
|
||||||
|
|
||||||
|
#### 不解答任何代码上的问题
|
||||||
|
|
||||||
|
#### App的问题请到 [Telegram群讨论](https://t.me/boring_programer)
|
||||||
|
|
||||||
|
- 删除了android端文件夹,涉及到apk签名等敏感文件
|
||||||
|
- 删除了ios端文件夹
|
||||||
|
- 删除了lib/classes/http.dart文件里的网站域名和爬虫逻辑,保护被爬网站的同时防止被爬网站加大防爬难度。
|
BIN
images/logo.png
Normal file
BIN
images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
331
lib/activities/book.dart
Normal file
331
lib/activities/book.dart
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ActivityBook extends StatefulWidget {
|
||||||
|
final Book book;
|
||||||
|
final String heroTag;
|
||||||
|
|
||||||
|
ActivityBook({@required this.book, @required this.heroTag});
|
||||||
|
|
||||||
|
@override
|
||||||
|
BookState createState() => BookState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookState extends State<ActivityBook> {
|
||||||
|
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
|
||||||
|
GlobalKey<NestedScrollViewState> _key = GlobalKey<NestedScrollViewState>();
|
||||||
|
|
||||||
|
bool _reverse = false;
|
||||||
|
bool isFavorite = false;
|
||||||
|
bool isLoading = true, isSuccess = false;
|
||||||
|
Book book;
|
||||||
|
List<Chapter> chapters = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
isFavorite = widget.book.isFavorite();
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_refresh.currentState.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> loadBook() async {
|
||||||
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
isSuccess = false;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
book = await UserAgentClient.instance
|
||||||
|
.getBook(aid: widget.book.aid)
|
||||||
|
.timeout(Duration(seconds: 5));
|
||||||
|
book.history = Data.getHistories()[book.aid]?.history;
|
||||||
|
chapters
|
||||||
|
..clear()
|
||||||
|
..addAll(book.chapters);
|
||||||
|
if (_reverse) chapters = chapters.reversed.toList();
|
||||||
|
|
||||||
|
/// 更新收藏列表里的漫画数据
|
||||||
|
if (isFavorite) Data.addFavorite(book);
|
||||||
|
|
||||||
|
_scrollToRead();
|
||||||
|
isSuccess = true;
|
||||||
|
} catch (e) {
|
||||||
|
isSuccess = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
isLoading = false;
|
||||||
|
print('刷新 $book');
|
||||||
|
setState(() {});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollToRead() {
|
||||||
|
if (book.history != null) {
|
||||||
|
final history = book.chapters
|
||||||
|
.firstWhere((chapter) => chapter.cid == book.history.cid);
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_key.currentState.currentInnerPosition.animateTo(
|
||||||
|
WidgetChapter.height * chapters.indexOf(history).toDouble(),
|
||||||
|
duration: Duration(milliseconds: 500),
|
||||||
|
curve: Curves.linear);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_openChapter(Chapter chapter) {
|
||||||
|
setState(() {
|
||||||
|
book.history = History(cid: chapter.cid, cname: chapter.cname, time: 0);
|
||||||
|
openChapter(context, book, chapter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
favoriteBook() {
|
||||||
|
widget.book.favorite();
|
||||||
|
isFavorite = !isFavorite;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sort() {
|
||||||
|
setState(() {
|
||||||
|
_reverse = !_reverse;
|
||||||
|
chapters = chapters.reversed.toList();
|
||||||
|
_scrollToRead();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _headerBuilder(BuildContext context, bool innerBoxIsScrolled) {
|
||||||
|
Color color = isFavorite ? Colors.red : Colors.white;
|
||||||
|
IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border;
|
||||||
|
final book = this.book ?? widget.book;
|
||||||
|
return <Widget>[
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
pinned: true,
|
||||||
|
snap: false,
|
||||||
|
title: Text(widget.book.name),
|
||||||
|
expandedHeight: 200,
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: _sort,
|
||||||
|
icon: Icon(_reverse
|
||||||
|
? FontAwesomeIcons.sortNumericDown
|
||||||
|
: FontAwesomeIcons.sortNumericDownAlt)),
|
||||||
|
IconButton(onPressed: favoriteBook, icon: Icon(icon, color: color))
|
||||||
|
],
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
background: SafeArea(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin:
|
||||||
|
EdgeInsets.only(top: 50, left: 20, right: 10, bottom: 20),
|
||||||
|
height: 160,
|
||||||
|
child: Hero(
|
||||||
|
tag: widget.heroTag,
|
||||||
|
child: Image.network(
|
||||||
|
widget.book.avatar,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(top: 50, right: 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'作者:' + (book.author ?? ''),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'简介:\n' + (book.description ?? ''),
|
||||||
|
softWrap: true,
|
||||||
|
style: TextStyle(color: Colors.white, height: 1.2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> chapterWidgets() {
|
||||||
|
final book = this.book ?? widget.book;
|
||||||
|
List<Widget> list = [];
|
||||||
|
chapters.forEach((chapter) {
|
||||||
|
final isRead = chapter.cid == book.history?.cid;
|
||||||
|
list.add(WidgetChapter(
|
||||||
|
chapter: chapter,
|
||||||
|
onTap: _openChapter,
|
||||||
|
read: isRead,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildChapter(BuildContext context, int index) {
|
||||||
|
final book = this.book ?? widget.book;
|
||||||
|
final chapter = chapters[index];
|
||||||
|
final isRead = chapter.cid == book.history?.cid;
|
||||||
|
return WidgetChapter(
|
||||||
|
chapter: chapter,
|
||||||
|
onTap: _openChapter,
|
||||||
|
read: isRead,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Color color = isFavorite ? Colors.red : Colors.white;
|
||||||
|
IconData icon = isFavorite ? Icons.favorite : Icons.favorite_border;
|
||||||
|
final book = this.book ?? widget.book;
|
||||||
|
return Scaffold(
|
||||||
|
body: PullToRefreshNotification(
|
||||||
|
key: _refresh,
|
||||||
|
onRefresh: loadBook,
|
||||||
|
maxDragOffset: kToolbarHeight * 2,
|
||||||
|
child: NestedScrollView(
|
||||||
|
key: _key,
|
||||||
|
headerSliverBuilder: (_, __) => [],
|
||||||
|
physics: AlwaysScrollableClampingScrollPhysics(),
|
||||||
|
body: CustomScrollView(
|
||||||
|
physics: AlwaysScrollableClampingScrollPhysics(),
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
pinned: false,
|
||||||
|
title: Text(widget.book.name),
|
||||||
|
expandedHeight: 200,
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: _sort,
|
||||||
|
icon: Icon(_reverse
|
||||||
|
? FontAwesomeIcons.sortNumericDown
|
||||||
|
: FontAwesomeIcons.sortNumericDownAlt)),
|
||||||
|
IconButton(
|
||||||
|
onPressed: favoriteBook, icon: Icon(icon, color: color))
|
||||||
|
],
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
background: SafeArea(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
top: 50, left: 20, right: 10, bottom: 20),
|
||||||
|
height: 160,
|
||||||
|
child: Hero(
|
||||||
|
tag: widget.heroTag,
|
||||||
|
child: Image.network(
|
||||||
|
widget.book.avatar,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(top: 50, right: 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'作者:' + (book.author ?? ''),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'简介:\n' + (book.description ?? ''),
|
||||||
|
softWrap: true,
|
||||||
|
style:
|
||||||
|
TextStyle(color: Colors.white, height: 1.2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PullToRefreshContainer((info) => SliverPullToRefreshHeader(
|
||||||
|
info: info,
|
||||||
|
onTap: () => _refresh.currentState
|
||||||
|
.show(notificationDragOffset: kToolbarHeight * 2),
|
||||||
|
)),
|
||||||
|
NestedScrollViewInnerScrollPositionKeyWidget(
|
||||||
|
Key('0'),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
buildChapter,
|
||||||
|
childCount: book.chapters.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget build1(BuildContext context) {
|
||||||
|
final double statusBarHeight = MediaQuery.of(context).padding.top;
|
||||||
|
var pinnedHeaderHeight =
|
||||||
|
//statusBar height
|
||||||
|
statusBarHeight +
|
||||||
|
//pinned SliverAppBar height in header
|
||||||
|
kToolbarHeight;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: NestedScrollViewRefreshIndicator(
|
||||||
|
key: _refresh,
|
||||||
|
onRefresh: loadBook,
|
||||||
|
child: NestedScrollView(
|
||||||
|
key: _key,
|
||||||
|
pinnedHeaderSliverHeightBuilder: () => pinnedHeaderHeight,
|
||||||
|
headerSliverBuilder: _headerBuilder,
|
||||||
|
body: LayoutBuilder(
|
||||||
|
builder: (_, __) {
|
||||||
|
if (isLoading)
|
||||||
|
return Container();
|
||||||
|
else if (isSuccess) {
|
||||||
|
return ListView(
|
||||||
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
color: Colors.grey,
|
||||||
|
tiles: chapterWidgets())
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints.expand(),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'读取失败,下拉刷新\n如果多次失败,请检查网络',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
_key.currentState.currentInnerPosition.animateTo(0,
|
||||||
|
duration: Duration(milliseconds: 100), curve: Curves.linear);
|
||||||
|
},
|
||||||
|
child: Icon(FontAwesomeIcons.angleDoubleUp),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
323
lib/activities/chapter.dart
Normal file
323
lib/activities/chapter.dart
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
enum LoadState {
|
||||||
|
Loading,
|
||||||
|
Finish,
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadMoreListSource extends LoadingMoreBase<int> {
|
||||||
|
@override
|
||||||
|
Future<bool> loadData([bool isloadMoreAction = false]) {
|
||||||
|
return Future.delayed(Duration(seconds: 1), () {
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
this.add(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActivityChapter extends StatefulWidget {
|
||||||
|
final Book book;
|
||||||
|
final Chapter chapter;
|
||||||
|
|
||||||
|
ActivityChapter(this.book, this.chapter);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChapterState createState() => ChapterState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterState extends State<ActivityChapter> {
|
||||||
|
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
PageController _pageController;
|
||||||
|
int showIndex = 0;
|
||||||
|
bool hasNextImage = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_pageController = PageController(
|
||||||
|
keepPage: false,
|
||||||
|
initialPage: widget.book.chapters.indexOf(widget.chapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
endDrawer: ChapterDrawer(
|
||||||
|
book: widget.book,
|
||||||
|
onTap: (chapter) {
|
||||||
|
_pageController.jumpToPage(widget.book.chapters.indexOf(chapter));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
body: PageView.builder(
|
||||||
|
physics: AlwaysScrollableClampingScrollPhysics(),
|
||||||
|
controller: _pageController,
|
||||||
|
itemCount: widget.book.chapters.length,
|
||||||
|
itemBuilder: (ctx, index) {
|
||||||
|
return ChapterContentView(
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.menu),
|
||||||
|
onPressed: () {
|
||||||
|
_scaffoldKey.currentState.openEndDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
book: widget.book,
|
||||||
|
chapter: widget.book.chapters[index],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
// floatingActionButton: FloatingActionButton(
|
||||||
|
// child: Text('下一章'),
|
||||||
|
// onPressed: () {
|
||||||
|
// if (hasNextChapter)
|
||||||
|
// return openChapter(widget.book.chapters[chapterIndex + 1]);
|
||||||
|
// Fluttertoast.showToast(msg: '已经是最后一章了');
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
_controller = ScrollController();
|
||||||
|
updateRead();
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_controller.jumpTo(WidgetChapter.height * read);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Drawer(
|
||||||
|
child: SafeArea(
|
||||||
|
child: ListView(
|
||||||
|
controller: _controller,
|
||||||
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: widget.book.chapters.map((chapter) {
|
||||||
|
final isRead = widget.book.history?.cid == chapter.cid;
|
||||||
|
return WidgetChapter(
|
||||||
|
chapter: chapter,
|
||||||
|
onTap: (chapter) {
|
||||||
|
if (widget.onTap != null) widget.onTap(chapter);
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
scrollToRead();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
read: isRead,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterContentView extends StatefulWidget {
|
||||||
|
final Book book;
|
||||||
|
final Chapter chapter;
|
||||||
|
final List<Widget> actions;
|
||||||
|
|
||||||
|
const ChapterContentView({Key key, this.book, this.chapter, this.actions})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ChapterContentView createState() => _ChapterContentView();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChapterContentView extends State<ChapterContentView> {
|
||||||
|
final GlobalKey<PullToRefreshNotificationState> _refresh = GlobalKey();
|
||||||
|
final List<String> images = [];
|
||||||
|
|
||||||
|
int chapterIndex = -1;
|
||||||
|
bool hasNextChapter = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
chapterIndex = widget.book.chapters.indexOf(widget.chapter);
|
||||||
|
hasNextChapter = widget.book.chapters.last != widget.chapter;
|
||||||
|
Data.addHistory(widget.book, widget.chapter);
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) => _refresh?.currentState
|
||||||
|
?.show(notificationDragOffset: kToolbarHeight * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> fetchImages() async {
|
||||||
|
print('fetchImages');
|
||||||
|
setState(() {});
|
||||||
|
images.clear();
|
||||||
|
try {
|
||||||
|
images.addAll(await UserAgentClient.instance
|
||||||
|
.getImages(aid: widget.book.aid, cid: widget.chapter.cid)
|
||||||
|
.timeout(const Duration(seconds: 5)));
|
||||||
|
if (images.length < 5) {
|
||||||
|
// print('图片 前:' + images.toString());
|
||||||
|
var list = await checkImage(images.last);
|
||||||
|
images.addAll(list);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('错误');
|
||||||
|
return false;
|
||||||
|
// throw(e);
|
||||||
|
}
|
||||||
|
// print('所有图片:' + images.toString());
|
||||||
|
setState(() {});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PullToRefreshNotification(
|
||||||
|
key: _refresh,
|
||||||
|
onRefresh: fetchImages,
|
||||||
|
maxDragOffset: kToolbarHeight * 2,
|
||||||
|
child: CustomScrollView(
|
||||||
|
physics: AlwaysScrollableClampingScrollPhysics(),
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
title: Text(widget.chapter.cname),
|
||||||
|
pinned: false,
|
||||||
|
floating: true,
|
||||||
|
actions: widget.actions,
|
||||||
|
),
|
||||||
|
PullToRefreshContainer((info) => SliverPullToRefreshHeader(
|
||||||
|
info: info,
|
||||||
|
onTap: () => _refresh.currentState
|
||||||
|
.show(notificationDragOffset: kToolbarHeight * 2),
|
||||||
|
)),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(ctx, i) => Image.network(images[i]),
|
||||||
|
childCount: images.length),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> checkImage(String last) async {
|
||||||
|
final response = new ReceivePort();
|
||||||
|
await Isolate.spawn(_checkImage, response.sendPort);
|
||||||
|
final sendPort = await response.first as SendPort;
|
||||||
|
//接收消息的ReceivePort
|
||||||
|
final answer = new ReceivePort();
|
||||||
|
//发送数据
|
||||||
|
sendPort.send([answer.sendPort, last]);
|
||||||
|
return answer.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkImage(SendPort initialReplyTo) {
|
||||||
|
UserAgentClient.instance = UserAgentClient('chrome');
|
||||||
|
final port = new ReceivePort();
|
||||||
|
initialReplyTo.send(port.sendPort);
|
||||||
|
port.listen((message) async {
|
||||||
|
// 获取数据并解析
|
||||||
|
final send = message[0] as SendPort;
|
||||||
|
final last = message[1] as String;
|
||||||
|
// 返回结果
|
||||||
|
final uri = Uri.parse(last);
|
||||||
|
// print({'scheme': uri.scheme, 'host': uri.host, 'path': uri.path});
|
||||||
|
final a = uri.scheme + '://' + uri.host;
|
||||||
|
final b = uri.pathSegments.take(uri.pathSegments.length - 1).join('/');
|
||||||
|
// print({'a': a, 'b': b});
|
||||||
|
//网址最后的图片文件名
|
||||||
|
final file = uri.pathSegments.last.split('.');
|
||||||
|
final fileName = file[0];
|
||||||
|
// 图片格式
|
||||||
|
final fileFormat = file[1];
|
||||||
|
final list = <String>[];
|
||||||
|
int plus = 1;
|
||||||
|
//print('最后的图片:' + last);
|
||||||
|
while (true) {
|
||||||
|
final String file1 =
|
||||||
|
getFileName(name: fileName, divider: '_', plus: plus),
|
||||||
|
file2 = getFileName(name: fileName, divider: '_', plus: plus + 1);
|
||||||
|
var url1 = '$a/$b/$file1.$fileFormat', url2 = '$a/$b/$file2.$fileFormat';
|
||||||
|
// print('正在测试:\n' + url1 + '\n' + url2);
|
||||||
|
final res = await Future.wait([
|
||||||
|
UserAgentClient.instance.head(url1),
|
||||||
|
UserAgentClient.instance.head(url2)
|
||||||
|
]);
|
||||||
|
if (res[0].statusCode != 200) break;
|
||||||
|
list.add(url1);
|
||||||
|
if (res[1].statusCode != 200) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
list.add(url2);
|
||||||
|
plus += 2;
|
||||||
|
}
|
||||||
|
// print('最后的图片数量: ' + number.toString());
|
||||||
|
send.send(list);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String getFileName(
|
||||||
|
{@required String name, @required String divider, @required int plus}) {
|
||||||
|
List<String> data = name.split(divider), newName = [];
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
try {
|
||||||
|
int number = int.parse(data[i]) + plus;
|
||||||
|
newName.add(number.toString());
|
||||||
|
} catch (e) {
|
||||||
|
newName.add(data[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newName.join(divider);
|
||||||
|
}
|
135
lib/activities/checkData.dart
Normal file
135
lib/activities/checkData.dart
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
part of '../main.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;
|
||||||
|
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 = json.decode(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: () {
|
||||||
|
Fluttertoast.showToast(
|
||||||
|
msg: '已经复制',
|
||||||
|
toastLength: Toast.LENGTH_SHORT,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
timeInSecForIos: 1,
|
||||||
|
);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
258
lib/activities/home.dart
Normal file
258
lib/activities/home.dart
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ActivityHome extends StatefulWidget {
|
||||||
|
final PackageInfo packageInfo;
|
||||||
|
|
||||||
|
const ActivityHome(this.packageInfo, {Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => HomeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeState extends State<ActivityHome> {
|
||||||
|
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
final List<Widget> histories = [];
|
||||||
|
final List<Book> quick = [];
|
||||||
|
final GlobalKey<QuickState> _quickState = GlobalKey();
|
||||||
|
static final weekTime = 7 * 24 * 3600000;
|
||||||
|
|
||||||
|
bool showFavorite = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
analytics.setCurrentScreen(screenName: '/activity_home');
|
||||||
|
|
||||||
|
/// 提前检查一次藏书的更新情况
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
_FavoriteList.getBooks();
|
||||||
|
await _FavoriteList.checkNews();
|
||||||
|
final updated = _FavoriteList.hasNews.values
|
||||||
|
.where((int updatedChapters) => updatedChapters > 0)
|
||||||
|
.length;
|
||||||
|
if (updated > 0)
|
||||||
|
Fluttertoast.showToast(
|
||||||
|
msg: '$updated 本藏书有更新',
|
||||||
|
gravity: ToastGravity.CENTER,
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.5),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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: (_) => ActivityRecommend(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEdit = false;
|
||||||
|
|
||||||
|
void _draggableModeChanged(bool mode) {
|
||||||
|
print('mode changed $mode');
|
||||||
|
isEdit = mode;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var media = MediaQuery.of(context);
|
||||||
|
var width = media.size.width;
|
||||||
|
width = width * .8;
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('微漫 v' + widget.packageInfo.version),
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
leading: isEdit
|
||||||
|
? IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back_ios),
|
||||||
|
onPressed: () {
|
||||||
|
_quickState.currentState.exit();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
actions: <Widget>[
|
||||||
|
/// 黑白样式切换
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
DynamicTheme.of(context).setBrightness(
|
||||||
|
Theme.of(context).brightness == Brightness.dark
|
||||||
|
? Brightness.light
|
||||||
|
: Brightness.dark);
|
||||||
|
},
|
||||||
|
icon: Icon(Theme.of(context).brightness == Brightness.light
|
||||||
|
? FontAwesomeIcons.lightbulb
|
||||||
|
: FontAwesomeIcons.solidLightbulb),
|
||||||
|
),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
|
||||||
|
/// 收藏列表
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showFavorite = true;
|
||||||
|
_scaffoldKey.currentState.openEndDrawer();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.favorite,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 浏览历史列表
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showFavorite = false;
|
||||||
|
// getHistory();
|
||||||
|
_scaffoldKey.currentState.openEndDrawer();
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.history),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
endDrawer: Drawer(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (_, __) {
|
||||||
|
if (showFavorite) {
|
||||||
|
return FavoriteList();
|
||||||
|
} else {
|
||||||
|
return Histories();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
padding: EdgeInsets.only(left: 40, right: 40),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
child: OutlineButton(
|
||||||
|
onPressed: gotoSearch,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'搜索漫画',
|
||||||
|
style: TextStyle(color: Colors.blue),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(color: Colors.blue, width: 2),
|
||||||
|
shape: StadiumBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlineButton(
|
||||||
|
onPressed: gotoRecommend,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.whatshot,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'月排行榜',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderSide: BorderSide(color: Colors.red, width: 2),
|
||||||
|
shape: StadiumBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Quick(
|
||||||
|
key: _quickState,
|
||||||
|
width: width,
|
||||||
|
draggableModeChanged: _draggableModeChanged,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 10),
|
||||||
|
child: Text(
|
||||||
|
'在 level-plus.net 论坛首发',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.grey[500]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
if (await canLaunch('tg://resolve?domain=weiman_app'))
|
||||||
|
launch('tg://resolve?domain=weiman_app');
|
||||||
|
else
|
||||||
|
launch('https://t.me/weiman_app');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Telegram广播频道',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue[200],
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Visibility(
|
||||||
|
visible: isDevMode,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(context,
|
||||||
|
MaterialPageRoute(builder: (_) => ActivityTest()));
|
||||||
|
},
|
||||||
|
child: Text('测试界面'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Visibility(
|
||||||
|
visible: isDevMode,
|
||||||
|
child: FlatButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(context,
|
||||||
|
MaterialPageRoute(builder: (_) => ActivityCheckData()));
|
||||||
|
},
|
||||||
|
child: Text('操作 收藏列表数据'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Widget> favoriteTiles(context, Iterable<Book> books,
|
||||||
|
{void Function(Book book) onTap}) {
|
||||||
|
return books.map((book) => ListTile(
|
||||||
|
onTap: () {
|
||||||
|
onTap(book);
|
||||||
|
},
|
||||||
|
title: Text(book.name),
|
||||||
|
leading: Image.network(book.avatar),
|
||||||
|
subtitle: Text(
|
||||||
|
'作者:' + book.author,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
83
lib/activities/recommend.dart
Normal file
83
lib/activities/recommend.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ActivityRecommend extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_ActivityRecommend createState() => _ActivityRecommend();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActivityRecommend extends State<ActivityRecommend> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('月排行榜'),
|
||||||
|
),
|
||||||
|
body: BookList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookList extends StatefulWidget {
|
||||||
|
const BookList({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_BookList createState() => _BookList();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BookList extends State<BookList> {
|
||||||
|
final GlobalKey<RefreshIndicatorState> _refresh = GlobalKey();
|
||||||
|
final List<Book> books = [];
|
||||||
|
bool loadFail = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_refresh.currentState.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadBooks() async {
|
||||||
|
loadFail = false;
|
||||||
|
try {
|
||||||
|
final books = await UserAgentClient.instance
|
||||||
|
.getMonthList()
|
||||||
|
.timeout(Duration(seconds: 5));
|
||||||
|
this.books
|
||||||
|
..clear()
|
||||||
|
..addAll(books);
|
||||||
|
} catch (e) {
|
||||||
|
loadFail = true;
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RefreshIndicator(
|
||||||
|
key: _refresh,
|
||||||
|
onRefresh: loadBooks,
|
||||||
|
child: loadFail
|
||||||
|
? CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverFillRemaining(
|
||||||
|
child: Center(child: Text('读取失败,下拉刷新')),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: ListView(
|
||||||
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: books.map((book) => WidgetBook(
|
||||||
|
book,
|
||||||
|
subtitle: book.author,
|
||||||
|
))).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
130
lib/activities/search.dart
Normal file
130
lib/activities/search.dart
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ActivitySearch extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Search();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Search extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return SearchState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _SearchState {
|
||||||
|
None,
|
||||||
|
Searching,
|
||||||
|
Done,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchState extends State<Search> {
|
||||||
|
Future<List<Book>> search;
|
||||||
|
TextEditingController _controller = TextEditingController();
|
||||||
|
CancelableOperation _searcher;
|
||||||
|
_SearchState _state = _SearchState.None;
|
||||||
|
final List<Book> _books = [];
|
||||||
|
|
||||||
|
void startSearch() {
|
||||||
|
print('搜索漫画: ' + _controller.text);
|
||||||
|
if (_searcher != null) _searcher.cancel();
|
||||||
|
_books.clear();
|
||||||
|
setState(() {
|
||||||
|
_state = _SearchState.Searching;
|
||||||
|
});
|
||||||
|
_searcher = CancelableOperation.fromFuture(
|
||||||
|
UserAgentClient.instance.searchBook(_controller.text))
|
||||||
|
.then((books) {
|
||||||
|
setState(() {
|
||||||
|
print('搜索完成: ' + books.length.toString());
|
||||||
|
_books.addAll(books);
|
||||||
|
_state = _SearchState.Done;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
search = null;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('搜索漫画'),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: RawKeyboardListener(
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
onKey: (RawKeyEvent event) {
|
||||||
|
if (event.runtimeType == RawKeyUpEvent &&
|
||||||
|
event.logicalKey.debugName.toLowerCase() == 'enter') {
|
||||||
|
if (_controller.text.isEmpty) return;
|
||||||
|
startSearch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '搜索书名', prefixIcon: Icon(Icons.search)),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
controller: _controller,
|
||||||
|
autofocus: true,
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
|
onSubmitted: (String name) {
|
||||||
|
print('onSubmitted');
|
||||||
|
startSearch();
|
||||||
|
},
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
onEditingComplete: () {
|
||||||
|
print('onEditingComplete');
|
||||||
|
startSearch();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
switch (_state) {
|
||||||
|
case _SearchState.Searching:
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
case _SearchState.None:
|
||||||
|
return Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: Colors.grey,
|
||||||
|
));
|
||||||
|
default:
|
||||||
|
if (_books.length == 0)
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'一本也找不到',
|
||||||
|
style: TextStyle(color: Colors.blueGrey),
|
||||||
|
));
|
||||||
|
List<Widget> list = _books
|
||||||
|
.map((book) => WidgetBook(
|
||||||
|
book,
|
||||||
|
subtitle: book.author,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
return ListView(
|
||||||
|
children:
|
||||||
|
ListTile.divideTiles(context: context, tiles: list)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
lib/activities/test.dart
Normal file
46
lib/activities/test.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ActivityTest extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('asd'),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
onPressed: save,
|
||||||
|
child: Text('保存'),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: read,
|
||||||
|
child: Text('读取'),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: clear,
|
||||||
|
child: Text('清空数据'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void save() {
|
||||||
|
Data.addFavorite(Book(
|
||||||
|
aid: '123',
|
||||||
|
name: 'name',
|
||||||
|
avatar: 'avatar',
|
||||||
|
description: '',
|
||||||
|
author: ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
void read() {
|
||||||
|
var books = Data.getFavorites();
|
||||||
|
print(jsonEncode(books));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
Data.clear();
|
||||||
|
}
|
||||||
|
}
|
110
lib/classes/book.dart
Normal file
110
lib/classes/book.dart
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class Book {
|
||||||
|
final String aid; // 书本ID
|
||||||
|
final String name; // 书本名称
|
||||||
|
final String avatar; // 书本封面
|
||||||
|
final String author; // 画家
|
||||||
|
final String description; // 描述
|
||||||
|
final List<Chapter> chapters;
|
||||||
|
final int chapterCount;
|
||||||
|
|
||||||
|
History history;
|
||||||
|
|
||||||
|
Book({
|
||||||
|
@required this.name,
|
||||||
|
@required this.aid,
|
||||||
|
@required this.avatar,
|
||||||
|
this.author,
|
||||||
|
this.description,
|
||||||
|
this.chapters: const [],
|
||||||
|
this.chapterCount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return jsonEncode(toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isFavorite() {
|
||||||
|
var books = Data.getFavorites();
|
||||||
|
return books.containsKey(aid);
|
||||||
|
}
|
||||||
|
|
||||||
|
favorite() {
|
||||||
|
if (isFavorite())
|
||||||
|
Data.removeFavorite(this);
|
||||||
|
else
|
||||||
|
Data.addFavorite(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = {
|
||||||
|
'aid': aid,
|
||||||
|
'name': name,
|
||||||
|
'avatar': avatar,
|
||||||
|
'author': author,
|
||||||
|
'chapterCount': chapterCount,
|
||||||
|
};
|
||||||
|
if (history != null) data['history'] = history.toJson();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Book fromJson(Map<String, dynamic> json) {
|
||||||
|
final book = Book(
|
||||||
|
aid: json['aid'],
|
||||||
|
name: json['name'],
|
||||||
|
avatar: json['avatar'],
|
||||||
|
author: json['author'],
|
||||||
|
description: json['description'],
|
||||||
|
chapterCount: json['chapterCount'] ?? 0,
|
||||||
|
);
|
||||||
|
if (json.containsKey('history'))
|
||||||
|
book.history = History.fromJson(json['history']);
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Chapter {
|
||||||
|
final String cid; // 章节cid
|
||||||
|
final String cname; // 章节名称
|
||||||
|
final String avatar; // 章节封面
|
||||||
|
|
||||||
|
Chapter({@required this.cid, @required this.cname, @required this.avatar});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return jsonEncode({cid: cid, cname: cname, avatar: avatar});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class History {
|
||||||
|
final String cid;
|
||||||
|
final String cname;
|
||||||
|
final int time;
|
||||||
|
|
||||||
|
History({@required this.cid, @required this.cname, @required this.time});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => jsonEncode(toJson());
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'cid': cid,
|
||||||
|
'cname': cname,
|
||||||
|
'time': time,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static History fromJson(Map<String, dynamic> json) {
|
||||||
|
return History(cid: json['cid'], cname: json['cname'], time: json['time']);
|
||||||
|
}
|
||||||
|
|
||||||
|
static History fromChapter(Chapter chapter) {
|
||||||
|
return History(
|
||||||
|
cid: chapter.cid,
|
||||||
|
cname: chapter.cname,
|
||||||
|
time: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
151
lib/classes/data.dart
Normal file
151
lib/classes/data.dart
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
part of '../main.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, jsonEncode(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static dynamic get(String key) {
|
||||||
|
return instance.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Book> getFavorites() {
|
||||||
|
if (has(favoriteBooksKey)) {
|
||||||
|
final String str = instance.getString(favoriteBooksKey);
|
||||||
|
Map<String, Object> data = jsonDecode(str);
|
||||||
|
Map<String, Book> res = {};
|
||||||
|
data.keys.forEach((key) {
|
||||||
|
res[key] = Book.fromJson(data[key]);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addFavorite(Book book) {
|
||||||
|
var books = getFavorites();
|
||||||
|
books[book.aid] = book;
|
||||||
|
set<Map>(favoriteBooksKey, books);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void removeFavorite(Book book) {
|
||||||
|
var books = getFavorites();
|
||||||
|
if (books.containsKey(book.aid)) {
|
||||||
|
books.remove(book.aid);
|
||||||
|
set<Map>(favoriteBooksKey, books);
|
||||||
|
reQuick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static clear() {
|
||||||
|
instance.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool has(String key) {
|
||||||
|
return instance.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static remove(String key) {
|
||||||
|
instance.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Book> getHistories() {
|
||||||
|
if (has(viewHistoryKey)) {
|
||||||
|
var data =
|
||||||
|
jsonDecode(instance.getString(viewHistoryKey)) as Map<String, Object>;
|
||||||
|
final Map<String, Book> histories = {};
|
||||||
|
data.forEach((key, value) {
|
||||||
|
histories[key] = Book.fromJson(value);
|
||||||
|
});
|
||||||
|
return histories;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static addHistory(Book book, Chapter chapter) {
|
||||||
|
book.history = History(
|
||||||
|
cid: chapter.cid,
|
||||||
|
cname: chapter.cname,
|
||||||
|
time: DateTime.now().millisecondsSinceEpoch);
|
||||||
|
final books = getHistories();
|
||||||
|
books[book.aid] = book;
|
||||||
|
set(viewHistoryKey, books);
|
||||||
|
// print('保存历史\n' + books.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeHistory(bool Function(Book book) isDelete) {
|
||||||
|
var books = getHistories();
|
||||||
|
books.keys
|
||||||
|
.where((key) => isDelete(books[key]))
|
||||||
|
.toList()
|
||||||
|
.forEach(books.remove);
|
||||||
|
set(viewHistoryKey, books);
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeHistoryFromBook(Book book) {
|
||||||
|
final books = getHistories();
|
||||||
|
books.remove(book.aid);
|
||||||
|
set(viewHistoryKey, books);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 快速导航 id 列表,内部方法
|
||||||
|
static List<String> _quickIdList() {
|
||||||
|
if (instance.containsKey(quickKey)) {
|
||||||
|
return instance.getStringList(quickKey);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 快速导航列表
|
||||||
|
static List<Book> quickList() {
|
||||||
|
final books = getFavorites();
|
||||||
|
final ids = books.keys;
|
||||||
|
final List<String> quickIds = _quickIdList();
|
||||||
|
print('快捷 $quickIds');
|
||||||
|
return quickIds
|
||||||
|
.where((id) => ids.contains(id))
|
||||||
|
.map((id) => books[id])
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 增加快速导航
|
||||||
|
static addQuick(Book book) {
|
||||||
|
final list = _quickIdList();
|
||||||
|
list.add(book.aid);
|
||||||
|
instance.setStringList(quickKey, list.toSet().toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
static addQuickAll(List<String> id) {
|
||||||
|
print('保存qid $id');
|
||||||
|
instance.setStringList(quickKey, id.toSet().toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重新整理Quick的id列表
|
||||||
|
static reQuick() {
|
||||||
|
final books = getFavorites();
|
||||||
|
final quickIds = _quickIdList();
|
||||||
|
instance.setStringList(
|
||||||
|
quickKey, quickIds.where(books.keys.contains).toSet().toList());
|
||||||
|
}
|
||||||
|
}
|
47
lib/classes/http.dart
Normal file
47
lib/classes/http.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
const domain = '';
|
||||||
|
final host = Uri.parse(domain).host;
|
||||||
|
|
||||||
|
class UserAgentClient extends http.BaseClient {
|
||||||
|
final String userAgent;
|
||||||
|
http.Client _inner;
|
||||||
|
String lastKey;
|
||||||
|
int lastKeyTime = 0;
|
||||||
|
|
||||||
|
static UserAgentClient instance;
|
||||||
|
|
||||||
|
UserAgentClient(this.userAgent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getKey() async {
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getImages(
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Book> getBook({String aid}) async {
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _decrypt({String key, String content}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Book>> searchBook(String name) async {
|
||||||
|
}
|
||||||
|
|
||||||
|
static void init(String userAgent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> _get(url, {Map<String, String> headers}) async {
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Book>> getMonthList() async {
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Book>> getIndexRandomBooks() async {
|
||||||
|
}
|
||||||
|
}
|
157
lib/main.dart
Normal file
157
lib/main.dart
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
|
import 'package:draggable_container/draggable_container.dart';
|
||||||
|
import 'package:dynamic_theme/dynamic_theme.dart';
|
||||||
|
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||||
|
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||||
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
|
import 'package:firebase_analytics/observer.dart';
|
||||||
|
import 'package:flutter/material.dart' hide NestedScrollView;
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:html/parser.dart' as html;
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:http/io_client.dart';
|
||||||
|
import 'package:loading_more_list/loading_more_list.dart';
|
||||||
|
import 'package:package_info/package_info.dart';
|
||||||
|
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'
|
||||||
|
hide CircularProgressIndicator;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
part './activities/book.dart';
|
||||||
|
|
||||||
|
part './activities/chapter.dart';
|
||||||
|
|
||||||
|
part './activities/checkData.dart';
|
||||||
|
|
||||||
|
part './activities/home.dart';
|
||||||
|
|
||||||
|
part './activities/recommend.dart';
|
||||||
|
|
||||||
|
part './activities/search.dart';
|
||||||
|
|
||||||
|
part './activities/test.dart';
|
||||||
|
|
||||||
|
part './classes/book.dart';
|
||||||
|
|
||||||
|
part './classes/data.dart';
|
||||||
|
|
||||||
|
part './classes/http.dart';
|
||||||
|
|
||||||
|
part './widgets/book.dart';
|
||||||
|
|
||||||
|
part './widgets/favorites.dart';
|
||||||
|
|
||||||
|
part './widgets/histories.dart';
|
||||||
|
|
||||||
|
part './widgets/pullToRefreshHeader.dart';
|
||||||
|
|
||||||
|
part './widgets/quick.dart';
|
||||||
|
|
||||||
|
part './widgets/sliverExpandableGroup.dart';
|
||||||
|
|
||||||
|
part './widgets/utils.dart';
|
||||||
|
|
||||||
|
part 'utils.dart';
|
||||||
|
|
||||||
|
FirebaseAnalytics analytics;
|
||||||
|
FirebaseAnalyticsObserver observer;
|
||||||
|
|
||||||
|
const bool isDevMode = !bool.fromEnvironment('dart.vm.product');
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
try {
|
||||||
|
analytics = FirebaseAnalytics();
|
||||||
|
observer = FirebaseAnalyticsObserver(analytics: analytics);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
Data.init(),
|
||||||
|
SystemChrome.setPreferredOrientations(
|
||||||
|
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
||||||
|
]);
|
||||||
|
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
UserAgentClient.init(
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36');
|
||||||
|
runApp(Main(packageInfo: packageInfo));
|
||||||
|
// runApp(MaterialApp(
|
||||||
|
// title: '微漫',
|
||||||
|
// theme: ThemeData.light(),
|
||||||
|
// darkTheme: ThemeData.dark(),
|
||||||
|
// themeMode: ThemeMode.system,
|
||||||
|
// debugShowCheckedModeBanner: false,
|
||||||
|
// navigatorObservers: [observer],
|
||||||
|
// home: ActivityHome(packageInfo),
|
||||||
|
// ));
|
||||||
|
}
|
||||||
|
|
||||||
|
class Main extends StatefulWidget {
|
||||||
|
final PackageInfo packageInfo;
|
||||||
|
|
||||||
|
const Main({Key key, this.packageInfo}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_Main createState() => _Main();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Main extends State<Main> with WidgetsBindingObserver {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DynamicTheme(
|
||||||
|
defaultBrightness: ThemeMode.system == ThemeMode.light
|
||||||
|
? Brightness.light
|
||||||
|
: Brightness.dark,
|
||||||
|
data: (brightness) => new ThemeData(
|
||||||
|
brightness: brightness,
|
||||||
|
),
|
||||||
|
themedWidgetBuilder: (context, theme) {
|
||||||
|
return new MaterialApp(
|
||||||
|
title: 'Flutter Demo',
|
||||||
|
theme: theme,
|
||||||
|
home: ActivityHome(widget.packageInfo),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// return MaterialApp(
|
||||||
|
// title: '微漫',
|
||||||
|
// theme: ThemeData(
|
||||||
|
// brightness: Brightness.light,
|
||||||
|
// ),
|
||||||
|
// darkTheme: ThemeData(
|
||||||
|
// brightness: Brightness.dark,
|
||||||
|
// ),
|
||||||
|
// themeMode: ThemeMode.system,
|
||||||
|
// debugShowCheckedModeBanner: false,
|
||||||
|
// navigatorObservers: [observer],
|
||||||
|
// home: ActivityHome(widget.packageInfo),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangePlatformBrightness() {
|
||||||
|
print('改变亮度');
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
24
lib/utils.dart
Normal file
24
lib/utils.dart
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
part of 'main.dart';
|
||||||
|
|
||||||
|
final weekTime = Duration.millisecondsPerDay * 7;
|
||||||
|
|
||||||
|
void openBook(BuildContext context, Book book, String heroTag) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: '/activity_book/${book.name}'),
|
||||||
|
builder: (_) => ActivityBook(book: book, heroTag: heroTag),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void openChapter(BuildContext context, Book book, Chapter chapter) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: '/activity_chapter/${book.name}/${chapter.cname}'),
|
||||||
|
builder: (_) => ActivityChapter(book, chapter),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
187
lib/widgets/book.dart
Normal file
187
lib/widgets/book.dart
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class WidgetBook extends StatelessWidget {
|
||||||
|
final Book book;
|
||||||
|
final String subtitle;
|
||||||
|
final Function(Book) onTap;
|
||||||
|
|
||||||
|
const WidgetBook(
|
||||||
|
this.book, {
|
||||||
|
Key key,
|
||||||
|
@required this.subtitle,
|
||||||
|
this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var isLiked = book.isFavorite();
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
book.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
subtitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
leading: Hero(
|
||||||
|
tag: 'bookAvatar${book.aid}',
|
||||||
|
child: Image.network(
|
||||||
|
book.avatar,
|
||||||
|
height: 200,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
)),
|
||||||
|
trailing: Icon(
|
||||||
|
isLiked ? Icons.favorite : Icons.favorite_border,
|
||||||
|
color: isLiked ? Colors.red : Colors.grey,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) onTap(book);
|
||||||
|
openBook(context, book, 'bookAvatar${book.aid}');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WidgetChapter extends StatelessWidget {
|
||||||
|
static final double height = 56;
|
||||||
|
final Chapter chapter;
|
||||||
|
final Function(Chapter) onTap;
|
||||||
|
final bool read;
|
||||||
|
|
||||||
|
WidgetChapter({
|
||||||
|
Key key,
|
||||||
|
this.chapter,
|
||||||
|
this.onTap,
|
||||||
|
this.read,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final children = <InlineSpan>[TextSpan(text: chapter.cname)];
|
||||||
|
if (read) {
|
||||||
|
children.insert(
|
||||||
|
0,
|
||||||
|
TextSpan(
|
||||||
|
text: '[已看]',
|
||||||
|
style: TextStyle(color: Colors.orange),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return ListTile(
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) onTap(chapter);
|
||||||
|
},
|
||||||
|
title: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: children,
|
||||||
|
style: Theme.of(context).textTheme.body1,
|
||||||
|
),
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
leading: Image.network(
|
||||||
|
chapter.avatar,
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
width: 100,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WidgetHistory extends StatelessWidget {
|
||||||
|
final Book book;
|
||||||
|
final Function(Book book) onTap;
|
||||||
|
|
||||||
|
WidgetHistory(this.book, this.onTap);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) onTap(book);
|
||||||
|
},
|
||||||
|
title: Text(book.name),
|
||||||
|
leading: Image.network(
|
||||||
|
book.avatar,
|
||||||
|
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 UserAgentClient.instance
|
||||||
|
.getBook(aid: widget.book.aid)
|
||||||
|
.timeout(Duration(seconds: 2));
|
||||||
|
news = book.chapterCount - widget.book.chapterCount;
|
||||||
|
hasError = false;
|
||||||
|
} catch (e) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.network(widget.book.avatar),
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
isThreeLine: true,
|
||||||
|
title: Text(widget.book.name),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
182
lib/widgets/favorites.dart
Normal file
182
lib/widgets/favorites.dart
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class FavoriteList extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_FavoriteList createState() => _FavoriteList();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavoriteList extends State<FavoriteList> {
|
||||||
|
static final Map<String, int> hasNews = {};
|
||||||
|
static final List<Book> all = [], // 所有收藏
|
||||||
|
inWeek = [], // 7天看过的收藏
|
||||||
|
other = []; // 其他收藏
|
||||||
|
static bool showTip = false;
|
||||||
|
|
||||||
|
static final loadFailTextSpan = TextSpan(
|
||||||
|
text: '读取失败,下拉刷新', style: TextStyle(color: Colors.redAccent)),
|
||||||
|
waitToCheck =
|
||||||
|
TextSpan(text: '等待检查更新', style: TextStyle(color: Colors.grey)),
|
||||||
|
unCheck =
|
||||||
|
TextSpan(text: '请下拉列表检查更新', style: TextStyle(color: Colors.grey)),
|
||||||
|
noUpdate = TextSpan(text: '没有更新', style: TextStyle(color: Colors.grey));
|
||||||
|
|
||||||
|
static void getBooks() {
|
||||||
|
all.clear();
|
||||||
|
inWeek.clear();
|
||||||
|
other.clear();
|
||||||
|
all.addAll(Data.getFavorites().values);
|
||||||
|
if (all.isNotEmpty) {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
all.forEach((book) {
|
||||||
|
if (book.history != null && (now - book.history.time) < weekTime) {
|
||||||
|
inWeek.add(book);
|
||||||
|
} else {
|
||||||
|
other.add(book);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
getBooks();
|
||||||
|
if (all.isNotEmpty) {
|
||||||
|
if (showTip == false) {
|
||||||
|
showTip = true;
|
||||||
|
Fluttertoast.showToast(
|
||||||
|
msg: '下拉列表可以检查漫画更新',
|
||||||
|
gravity: ToastGravity.CENTER,
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.5),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openBook(book) {
|
||||||
|
openBook(context, book, 'fb ${book.aid}');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> checkNews() async {
|
||||||
|
hasNews.clear();
|
||||||
|
Book currentBook, newBook;
|
||||||
|
int different;
|
||||||
|
for (var i = 0; i < all.length; i++) {
|
||||||
|
currentBook = all[i];
|
||||||
|
try {
|
||||||
|
newBook = await UserAgentClient.instance
|
||||||
|
.getBook(aid: currentBook.aid)
|
||||||
|
.timeout(Duration(seconds: 2));
|
||||||
|
different = newBook.chapterCount - currentBook.chapterCount;
|
||||||
|
hasNews[currentBook.aid] = different;
|
||||||
|
} catch (e) {
|
||||||
|
hasNews[currentBook.aid] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget bookBuilder(Book book) {
|
||||||
|
TextSpan state;
|
||||||
|
if (hasNews.isEmpty) {
|
||||||
|
state = unCheck;
|
||||||
|
} else {
|
||||||
|
if (hasNews.containsKey(book.aid)) {
|
||||||
|
if (hasNews[book.aid] > 0) {
|
||||||
|
state = TextSpan(
|
||||||
|
text: '有 ${hasNews[book.aid]} 章更新',
|
||||||
|
style: TextStyle(color: Colors.green));
|
||||||
|
} else if (hasNews[book.aid] == -1) {
|
||||||
|
state = loadFailTextSpan;
|
||||||
|
} else if (hasNews[book.aid] == 0) {
|
||||||
|
state = noUpdate;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state = waitToCheck;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FBookItem(
|
||||||
|
book: book,
|
||||||
|
subtitle: state,
|
||||||
|
onTap: _openBook,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<Book> inWeekUpdated = [],
|
||||||
|
inWeekUnUpdated = [],
|
||||||
|
otherUpdated = [],
|
||||||
|
otherUnUpdated = [];
|
||||||
|
inWeek.forEach((book) {
|
||||||
|
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0)
|
||||||
|
inWeekUpdated.add(book);
|
||||||
|
else
|
||||||
|
inWeekUnUpdated.add(book);
|
||||||
|
});
|
||||||
|
other.forEach((book) {
|
||||||
|
if (hasNews.containsKey(book.aid) && hasNews[book.aid] > 0)
|
||||||
|
otherUpdated.add(book);
|
||||||
|
else
|
||||||
|
otherUnUpdated.add(book);
|
||||||
|
});
|
||||||
|
return Drawer(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
await checkNews();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: all.isEmpty
|
||||||
|
? Center(child: Text('没有收藏'))
|
||||||
|
: SafeArea(
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverExpandableGroup(
|
||||||
|
title: Text('7天内看过并且有更新的藏书(${inWeekUpdated.length})'),
|
||||||
|
expanded: true,
|
||||||
|
count: inWeekUpdated.length,
|
||||||
|
builder: (ctx, i) => bookBuilder(inWeekUpdated[i]),
|
||||||
|
),
|
||||||
|
SliverExpandableGroup(
|
||||||
|
title: Text('7天内看过的藏书(${inWeekUnUpdated.length})'),
|
||||||
|
count: inWeekUnUpdated.length,
|
||||||
|
builder: (ctx, i) => bookBuilder(inWeekUnUpdated[i]),
|
||||||
|
),
|
||||||
|
SliverExpandableGroup(
|
||||||
|
title: Text('有更新的藏书(${otherUpdated.length})'),
|
||||||
|
count: otherUpdated.length,
|
||||||
|
builder: (ctx, i) => bookBuilder(otherUpdated[i]),
|
||||||
|
),
|
||||||
|
SliverExpandableGroup(
|
||||||
|
title: Text('没有更新的藏书(${otherUnUpdated.length})'),
|
||||||
|
count: otherUnUpdated.length,
|
||||||
|
builder: (ctx, i) => bookBuilder(otherUnUpdated[i]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FBookItem extends StatelessWidget {
|
||||||
|
final Book book;
|
||||||
|
final TextSpan subtitle;
|
||||||
|
final void Function(Book book) onTap;
|
||||||
|
|
||||||
|
const FBookItem({
|
||||||
|
Key key,
|
||||||
|
@required this.book,
|
||||||
|
@required this.subtitle,
|
||||||
|
@required this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
onTap: () => onTap(book),
|
||||||
|
leading: Hero(tag: 'fb ${book.aid}', child: Image.network(book.avatar)),
|
||||||
|
title: Text(book.name, style: Theme.of(context).textTheme.body1),
|
||||||
|
subtitle: RichText(text: subtitle),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
97
lib/widgets/histories.dart
Normal file
97
lib/widgets/histories.dart
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class Histories extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_Histories createState() => _Histories();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Histories extends State<Histories> {
|
||||||
|
final List<Book> inWeek = [], other = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
loadBook();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadBook() {
|
||||||
|
inWeek.clear();
|
||||||
|
other.clear();
|
||||||
|
final list = Data.getHistories().values.toList();
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
list.sort((a, b) => b.history.time.compareTo(a.history.time));
|
||||||
|
list.forEach((book) {
|
||||||
|
if ((now - book.history.time) < weekTime) {
|
||||||
|
inWeek.add(book);
|
||||||
|
} else {
|
||||||
|
other.add(book);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear(bool inWeek) async {
|
||||||
|
final title = '确认清空 ' + (inWeek ? '7天内的' : '更早的') + '浏览记录 ?';
|
||||||
|
final res = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
textColor: Colors.grey,
|
||||||
|
child: Text('取消'),
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('确认'),
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
print('清理历史 $inWeek $res');
|
||||||
|
if (res == false) return;
|
||||||
|
List<Book> list = inWeek ? this.inWeek : this.other;
|
||||||
|
list.forEach((book) => Data.removeHistoryFromBook(book));
|
||||||
|
setState(() {
|
||||||
|
loadBook();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverExpandableGroup(
|
||||||
|
title: Text('7天内的浏览历史 (${inWeek.length})'),
|
||||||
|
expanded: true,
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('清空'),
|
||||||
|
onPressed: inWeek.length == 0 ? null : () => clear(true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
count: inWeek.length,
|
||||||
|
builder: (ctx, i) => WidgetBook(
|
||||||
|
inWeek[i],
|
||||||
|
subtitle: inWeek[i].history.cname,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverExpandableGroup(
|
||||||
|
title: Text('更早的浏览历史 (${other.length})'),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('清空'),
|
||||||
|
onPressed: other.length == 0 ? null : () => clear(false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
count: other.length,
|
||||||
|
builder: (ctx, i) => WidgetBook(
|
||||||
|
other[i],
|
||||||
|
subtitle: other[i].history.cname,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
72
lib/widgets/pullToRefreshHeader.dart
Normal file
72
lib/widgets/pullToRefreshHeader.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class SliverPullToRefreshHeader extends StatelessWidget {
|
||||||
|
final PullToRefreshScrollNotificationInfo info;
|
||||||
|
final void Function() onTap;
|
||||||
|
final double fontSize;
|
||||||
|
|
||||||
|
const SliverPullToRefreshHeader({
|
||||||
|
Key key,
|
||||||
|
@required this.info,
|
||||||
|
this.onTap,
|
||||||
|
this.fontSize = 16,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (info == null) return SliverToBoxAdapter(child: SizedBox());
|
||||||
|
double dragOffset = info?.dragOffset ?? 0.0;
|
||||||
|
TextSpan text = TextSpan(
|
||||||
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
fontSize: fontSize,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
baseline: TextBaseline.alphabetic,
|
||||||
|
child: Padding(
|
||||||
|
child: Image.asset("images/logo.png", height: 20),
|
||||||
|
padding: EdgeInsets.only(right: 5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
if (info.mode == RefreshIndicatorMode.error) {
|
||||||
|
text.children.addAll([
|
||||||
|
TextSpan(
|
||||||
|
text: '读取失败\n当失败次数太多请检查网络情况\n有些很旧的章节会看不到,请见谅\n',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
WidgetSpan(
|
||||||
|
child: RaisedButton.icon(
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
onPressed: onTap,
|
||||||
|
label: Text('再次尝试'))),
|
||||||
|
]);
|
||||||
|
} else if (info.mode == RefreshIndicatorMode.refresh ||
|
||||||
|
info.mode == RefreshIndicatorMode.snap) {
|
||||||
|
text.children.addAll([
|
||||||
|
TextSpan(text: '读取中,请稍候'),
|
||||||
|
]);
|
||||||
|
} else if ([
|
||||||
|
RefreshIndicatorMode.drag,
|
||||||
|
RefreshIndicatorMode.armed,
|
||||||
|
RefreshIndicatorMode.snap
|
||||||
|
].contains(info.mode)) {
|
||||||
|
text.children.add(TextSpan(text: '重新读取'));
|
||||||
|
} else {
|
||||||
|
text.children.add(TextSpan(text: 'Bye~'));
|
||||||
|
}
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
height: dragOffset,
|
||||||
|
child: Center(
|
||||||
|
child: Text.rich(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
215
lib/widgets/quick.dart
Normal file
215
lib/widgets/quick.dart
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class QuickBook extends DraggableItem {
|
||||||
|
static const heroTag = 'quickBookAvatar';
|
||||||
|
Widget child;
|
||||||
|
final BuildContext context;
|
||||||
|
final Book book;
|
||||||
|
|
||||||
|
QuickBook({@required this.book, @required this.context}) {
|
||||||
|
child = GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (ctx) => ActivityBook(
|
||||||
|
book: book,
|
||||||
|
heroTag: '$heroTag ${book.aid}',
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Hero(
|
||||||
|
tag: '$heroTag ${book.aid}',
|
||||||
|
child: Image.network(book.avatar),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUpdate() {
|
||||||
|
UserAgentClient.instance.getBook(aid: book.aid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
final List<String> id = [];
|
||||||
|
|
||||||
|
void exit() {
|
||||||
|
_key.currentState.draggableMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showSelectBookDialog() async {
|
||||||
|
final books = Data.getFavorites();
|
||||||
|
final list = books.values
|
||||||
|
.where((book) => !id.contains(book.aid))
|
||||||
|
.map((book) => ListTile(
|
||||||
|
title: Text(book.name),
|
||||||
|
leading: Image.network(book.avatar),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context, book);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
return showDialog<Book>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('将收藏的漫画添加到快速导航'),
|
||||||
|
content: Container(
|
||||||
|
width: double.maxFinite,
|
||||||
|
height: 300,
|
||||||
|
child: list.isNotEmpty
|
||||||
|
? ListView(
|
||||||
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: list,
|
||||||
|
).toList(),
|
||||||
|
)
|
||||||
|
: Center(child: Text('没有了')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QuickState() {
|
||||||
|
_addButton = DraggableItem(
|
||||||
|
deletable: false,
|
||||||
|
fixed: true,
|
||||||
|
child: FlatButton(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'添加',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
final items = _key.currentState.items;
|
||||||
|
final buttonIndex = items.indexOf(_addButton);
|
||||||
|
print('add $buttonIndex');
|
||||||
|
if (buttonIndex > -1) {
|
||||||
|
final book = await _showSelectBookDialog();
|
||||||
|
print('选择了 $book');
|
||||||
|
if (book == null) return;
|
||||||
|
_key.currentState.insteadOfIndex(
|
||||||
|
buttonIndex, QuickBook(book: book, context: context),
|
||||||
|
force: true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int length() {
|
||||||
|
// print(_key.currentState.items);
|
||||||
|
// return 0;
|
||||||
|
return _key.currentState.items.where((item) => item is QuickBook).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_draggableItems.addAll(Data.quickList().map((book) {
|
||||||
|
id.add(book.aid);
|
||||||
|
return QuickBook(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) {
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: widget.width,
|
||||||
|
child: DraggableContainer(
|
||||||
|
key: _key,
|
||||||
|
slotMargin: EdgeInsets.only(bottom: 8, left: 7, right: 7),
|
||||||
|
slotSize: Size(72, 100),
|
||||||
|
slotDecoration:
|
||||||
|
BoxDecoration(color: Colors.grey.withOpacity(0.3)),
|
||||||
|
dragDecoration: BoxDecoration(
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black, blurRadius: 10)]),
|
||||||
|
items: _draggableItems,
|
||||||
|
onDraggableModeChanged: widget.draggableModeChanged,
|
||||||
|
onChanged: (List<DraggableItem> items) {
|
||||||
|
id.clear();
|
||||||
|
items.forEach((item) {
|
||||||
|
if (item is QuickBook) id.add(item.book.aid);
|
||||||
|
});
|
||||||
|
Data.addQuickAll(id);
|
||||||
|
final nullIndex = items.indexOf(null);
|
||||||
|
final buttonIndex = items.indexOf(_addButton);
|
||||||
|
print('null $nullIndex, button $buttonIndex');
|
||||||
|
if (nullIndex > -1 && buttonIndex == -1) {
|
||||||
|
_key.currentState.insteadOfIndex(nullIndex, _addButton,
|
||||||
|
triggerEvent: false);
|
||||||
|
} else if (nullIndex > -1 &&
|
||||||
|
buttonIndex > -1 &&
|
||||||
|
nullIndex < buttonIndex) {
|
||||||
|
_key.currentState.removeItem(_addButton);
|
||||||
|
_key.currentState.insteadOfIndex(nullIndex, _addButton,
|
||||||
|
triggerEvent: false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
91
lib/widgets/sliverExpandableGroup.dart
Normal file
91
lib/widgets/sliverExpandableGroup.dart
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class SliverExpandableBuilder {
|
||||||
|
final int count;
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
|
const SliverExpandableBuilder(this.count, this.builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SliverExpandableGroup extends StatefulWidget {
|
||||||
|
final Widget title;
|
||||||
|
final bool expanded;
|
||||||
|
final List<Widget> actions;
|
||||||
|
final Color divideColor;
|
||||||
|
final double height;
|
||||||
|
final int count;
|
||||||
|
final IndexedWidgetBuilder builder;
|
||||||
|
|
||||||
|
const SliverExpandableGroup({
|
||||||
|
Key key,
|
||||||
|
@required this.title,
|
||||||
|
@required this.count,
|
||||||
|
@required this.builder,
|
||||||
|
this.expanded = false,
|
||||||
|
this.actions = const [],
|
||||||
|
this.divideColor = Colors.grey,
|
||||||
|
this.height = kToolbarHeight,
|
||||||
|
}) : assert(title != null),
|
||||||
|
assert(builder != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SliverExpandableGroup createState() => _SliverExpandableGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliverExpandableGroup extends State<SliverExpandableGroup> {
|
||||||
|
bool _expanded;
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_expanded = widget.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Decoration _decoration = BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: Divider.createBorderSide(context, color: widget.divideColor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return SliverStickyHeader(
|
||||||
|
header: InkWell(
|
||||||
|
child: Container(
|
||||||
|
height: widget.height,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).dialogBackgroundColor,
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Transform.rotate(
|
||||||
|
angle: _expanded ? 0 : math.pi,
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: widget.title),
|
||||||
|
...widget.actions,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_expanded = !_expanded;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
sliver: _expanded
|
||||||
|
? SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate((ctx, i) {
|
||||||
|
if (i < widget.count-1) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: _decoration,
|
||||||
|
child: widget.builder(context, i),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return widget.builder(context, i);
|
||||||
|
}, childCount: widget.count))
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
30
lib/widgets/utils.dart
Normal file
30
lib/widgets/utils.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
part of '../main.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,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
91
pubspec.yaml
Normal file
91
pubspec.yaml
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
name: weiman
|
||||||
|
description: 微漫
|
||||||
|
|
||||||
|
# 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.0.4
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.3.0 <3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
async: any
|
||||||
|
cupertino_icons: any
|
||||||
|
http: any
|
||||||
|
encrypt: any
|
||||||
|
html: any
|
||||||
|
shared_preferences: any
|
||||||
|
fluttertoast: any
|
||||||
|
draggable_container: any
|
||||||
|
flutter_sticky_header: any
|
||||||
|
extended_nested_scroll_view: any
|
||||||
|
dynamic_theme: any
|
||||||
|
package_info: any
|
||||||
|
url_launcher: any
|
||||||
|
font_awesome_flutter: any
|
||||||
|
loading_more_list: any
|
||||||
|
pull_to_refresh_notification: any
|
||||||
|
|
||||||
|
firebase_core: any
|
||||||
|
firebase_analytics: any
|
||||||
|
e2e: any
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
- images/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