:sparkless: Add comics to download list

This commit is contained in:
niuhuan 2023-04-10 23:05:53 +08:00
parent ccae23ed75
commit 2c30885bc3
10 changed files with 488 additions and 29 deletions

View File

@ -1014,13 +1014,86 @@ class PkzComicViewLog {
} }
} }
class IsPro { class ProInfoAll {
late bool isPro; ProInfoAll({
late int expire; required this.proInfoAf,
required this.proInfoPat,
});
late final ProInfoAf proInfoAf;
late final ProInfoPat proInfoPat;
IsPro.fromJson(Map<String, dynamic> json) { ProInfoAll.fromJson(Map<String, dynamic> json){
this.isPro = json["isPro"]; proInfoAf = ProInfoAf.fromJson(json['pro_info_af']);
this.expire = json["expire"]; proInfoPat = ProInfoPat.fromJson(json['pro_info_pat']);
}
Map<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_data['pro_info_normal'] = proInfoAf.toJson();
_data['pro_info_pat'] = proInfoPat.toJson();
return _data;
}
}
class ProInfoAf {
ProInfoAf({
required this.isPro,
required this.expire,
});
late final bool isPro;
late final int expire;
ProInfoAf.fromJson(Map<String, dynamic> json){
isPro = json['is_pro'];
expire = json['expire'];
}
Map<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_data['is_pro'] = isPro;
_data['expire'] = expire;
return _data;
}
}
class ProInfoPat {
ProInfoPat({
required this.isPro,
required this.patId,
required this.bindUid,
required this.requestDelete,
required this.reBind,
required this.errorType,
required this.errorMsg,
});
late final bool isPro;
late final String patId;
late final String bindUid;
late final int requestDelete;
late final int reBind;
late final int errorType;
late final String errorMsg;
ProInfoPat.fromJson(Map<String, dynamic> json){
isPro = json['is_pro'];
patId = json['pat_id'];
bindUid = json['bind_uid'];
requestDelete = json['request_delete'];
reBind = json['re_bind'];
errorType = json['error_type'];
errorMsg = json['error_msg'];
}
Map<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_data['is_pro'] = isPro;
_data['pat_id'] = patId;
_data['bind_uid'] = bindUid;
_data['request_delete'] = requestDelete;
_data['re_bind'] = reBind;
_data['error_type'] = errorType;
_data['error_msg'] = errorMsg;
return _data;
} }
} }

View File

@ -910,8 +910,8 @@ class Method {
.toList(); .toList();
} }
Future<IsPro> isPro() async { Future<ProInfoAll> proInfoAll() async {
return IsPro.fromJson(jsonDecode(await _flatInvoke("isPro", ""))); return ProInfoAll.fromJson(jsonDecode(await _flatInvoke("proInfoAll", "")));
} }
Future reloadPro() { Future reloadPro() {
@ -1009,4 +1009,8 @@ class Method {
Future androidMkdirs(String path) async { Future androidMkdirs(String path) async {
return await _channel.invokeMethod("androidMkdirs", path); return await _channel.invokeMethod("androidMkdirs", path);
} }
Future downloadAll(List<String> comicIds) {
return _flatInvoke("downloadAll", comicIds);
}
} }

View File

@ -1,14 +1,19 @@
import 'package:event/event.dart'; import 'package:event/event.dart';
import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/Method.dart';
var isPro = false; import '../Entities.dart';
var isProEx = 0;
bool get isPro {
return _proInfoAll.proInfoAf.isPro || _proInfoAll.proInfoPat.isPro;
}
ProInfoAf get proInfoAf => _proInfoAll.proInfoAf;
ProInfoPat get proInfoPat => _proInfoAll.proInfoPat;
final proEvent = Event(); final proEvent = Event();
late ProInfoAll _proInfoAll;
Future reloadIsPro() async { Future reloadIsPro() async {
final p = await method.isPro(); _proInfoAll = await method.proInfoAll();
isPro = p.isPro;
isProEx = p.expire;
proEvent.broadcast(); proEvent.broadcast();
} }

View File

@ -1,17 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_search_bar/flutter_search_bar.dart'; import 'package:flutter_search_bar/flutter_search_bar.dart';
import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Common.dart';
import 'package:pikapika/basic/config/PagerAction.dart';
import 'package:pikapika/basic/config/ShadowCategories.dart'; import 'package:pikapika/basic/config/ShadowCategories.dart';
import 'package:pikapika/basic/config/ShadowCategoriesMode.dart'; import 'package:pikapika/basic/config/ShadowCategoriesMode.dart';
import 'package:pikapika/basic/store/Categories.dart'; import 'package:pikapika/basic/store/Categories.dart';
import 'package:pikapika/basic/config/ListLayout.dart';
import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/Method.dart';
import 'package:pikapika/screens/components/ComicList.dart';
import '../basic/Entities.dart'; import '../basic/Entities.dart';
import '../basic/config/Address.dart'; import '../basic/config/Address.dart';
import '../basic/config/IconLoading.dart'; import '../basic/config/IconLoading.dart';
import 'SearchScreen.dart'; import 'SearchScreen.dart';
import 'components/ComicPager.dart'; import 'components/ComicPager.dart';
import 'components/Common.dart'; import 'components/Common.dart';
import 'components/GoDownloadSelect.dart';
import 'components/RightClickPop.dart'; import 'components/RightClickPop.dart';
// //
@ -36,6 +38,7 @@ class ComicsScreen extends StatefulWidget {
} }
class _ComicsScreenState extends State<ComicsScreen> { class _ComicsScreenState extends State<ComicsScreen> {
late final _comicListController = ComicListController();
late final SearchBar _categorySearchBar = SearchBar( late final SearchBar _categorySearchBar = SearchBar(
hintText: '搜索分类 - ${categoryTitle(widget.category)}', hintText: '搜索分类 - ${categoryTitle(widget.category)}',
inBar: false, inBar: false,
@ -55,7 +58,11 @@ class _ComicsScreenState extends State<ComicsScreen> {
return AppBar( return AppBar(
title: Text(categoryTitle(widget.category)), title: Text(categoryTitle(widget.category)),
actions: [ actions: [
commonPopMenu(context), commonPopMenu(
context,
setState: setState,
comicListController: _comicListController,
),
addressPopMenu(context), addressPopMenu(context),
_chooseCategoryAction(), _chooseCategoryAction(),
_categorySearchBar.getSearchAction(context), _categorySearchBar.getSearchAction(context),
@ -104,6 +111,12 @@ class _ComicsScreenState extends State<ComicsScreen> {
); );
Future<ComicsPage> _load(String _currentSort, int _currentPage) { Future<ComicsPage> _load(String _currentSort, int _currentPage) {
if (currentPagerAction() == PagerAction.CONTROLLER &&
_comicListController.selecting) {
setState(() {
_comicListController.selecting = false;
});
}
return method.comics( return method.comics(
_currentSort, _currentSort,
_currentPage, _currentPage,
@ -115,7 +128,7 @@ class _ComicsScreenState extends State<ComicsScreen> {
} }
@override @override
Widget build(BuildContext context){ Widget build(BuildContext context) {
return rightClickPop( return rightClickPop(
child: buildScreen(context), child: buildScreen(context),
context: context, context: context,
@ -154,10 +167,15 @@ class _ComicsScreenState extends State<ComicsScreen> {
); );
} }
if (_comicListController.selecting) {
appBar = downAppBar(context, _comicListController, setState);
}
return Scaffold( return Scaffold(
appBar: appBar, appBar: appBar,
body: ComicPager( body: ComicPager(
fetchPage: _load, fetchPage: _load,
comicListController: _comicListController,
), ),
); );
} }

View File

@ -68,13 +68,26 @@ class _ProScreenState extends State<ProScreen> {
), ),
const Divider(), const Divider(),
ListTile( ListTile(
title: const Text("发电详情"), title: const Text("签到或礼物卡"),
subtitle: Text( subtitle: Text(
isPro proInfoAf.isPro
? "发电中 (${DateTime.fromMillisecondsSinceEpoch(1000 * isProEx).toString()})" ? "发电中 (${DateTime.fromMillisecondsSinceEpoch(1000 * proInfoAf.expire).toString()})"
: "未发电", : "未发电",
), ),
), ),
...(proInfoPat.patId.isNotEmpty ? [
ListTile(
onTap: () {
managementPat();
},
title: const Text("P站支持"),
subtitle: Text((
proInfoPat.isPro
? "发电中"
: "未发电") + ("(点击管理)"),
),
),
] : []),
const Divider(), const Divider(),
ListTile( ListTile(
title: const Text("我曾经发过电"), title: const Text("我曾经发过电"),
@ -111,4 +124,8 @@ class _ProScreenState extends State<ProScreen> {
), ),
); );
} }
void managementPat() {
// todo
}
} }

View File

@ -13,17 +13,34 @@ import 'ComicInfoCard.dart';
import 'Images.dart'; import 'Images.dart';
import 'LinkToComicInfo.dart'; import 'LinkToComicInfo.dart';
class ComicListController {
_ComicListState? _state;
bool get selecting => _state?._selecting ?? false;
set selecting(bool value) => _state?._setSelect(value);
List<String> get selected => _state?._selected ?? [];
selectAll() {
_state?._selectAll();
}
}
// //
class ComicList extends StatefulWidget { class ComicList extends StatefulWidget {
final Widget? appendWidget; final Widget? appendWidget;
final List<ComicSimple> comicList; final List<ComicSimple> comicList;
final ScrollController? controller; final ScrollController? scrollController;
final ComicListController? listController;
const ComicList( const ComicList(
this.comicList, { this.comicList, {
this.appendWidget, this.appendWidget,
this.controller, this.scrollController,
Key? key, Key? key,
// required
this.listController,
}) : super(key: key); }) : super(key: key);
@override @override
@ -32,6 +49,25 @@ class ComicList extends StatefulWidget {
class _ComicListState extends State<ComicList> { class _ComicListState extends State<ComicList> {
final List<String> viewedList = []; final List<String> viewedList = [];
bool _selecting = false;
List<String> _selected = [];
_selectAll() {
setState(() {
if (_selected.length == widget.comicList.length) {
_selected.clear();
} else {
_selected.addAll(widget.comicList.map((e) => e.id));
}
});
}
_setSelect(bool value) {
setState(() {
_selected.clear();
_selecting = value;
});
}
Future _loadViewed() async { Future _loadViewed() async {
if (widget.comicList.isNotEmpty) { if (widget.comicList.isNotEmpty) {
@ -43,6 +79,7 @@ class _ComicListState extends State<ComicList> {
@override @override
void initState() { void initState() {
widget.listController?._state = this;
_loadViewed(); _loadViewed();
listLayoutEvent.subscribe(_onLayoutChange); listLayoutEvent.subscribe(_onLayoutChange);
super.initState(); super.initState();
@ -50,6 +87,9 @@ class _ComicListState extends State<ComicList> {
@override @override
void dispose() { void dispose() {
if (widget.listController?._state == this) {
widget.listController?._state = null;
}
listLayoutEvent.unsubscribe(_onLayoutChange); listLayoutEvent.unsubscribe(_onLayoutChange);
super.dispose(); super.dispose();
} }
@ -74,7 +114,7 @@ class _ComicListState extends State<ComicList> {
Widget _buildInfoCardList() { Widget _buildInfoCardList() {
return ListView( return ListView(
controller: widget.controller, controller: widget.scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
children: [ children: [
...widget.comicList.map((e) { ...widget.comicList.map((e) {
@ -122,6 +162,42 @@ class _ComicListState extends State<ComicList> {
), ),
); );
} }
if (_selecting) {
return GestureDetector(
onTap: () {
setState(() {
if (_selected.contains(e.id)) {
_selected.remove(e.id);
} else {
_selected.add(e.id);
}
});
},
child: Stack(children: [
AbsorbPointer(
child: LinkToComicInfo(
comicId: e.id,
child: ComicInfoCard(
e,
viewed: viewedList.contains(e.id),
),
),
),
Row(children: [
Expanded(child: Container()),
Padding(
padding: const EdgeInsets.all(5),
child: Icon(
_selected.contains(e.id)
? Icons.check_circle_sharp
: Icons.circle_outlined,
color: Theme.of(context).colorScheme.secondary,
),
),
]),
]),
);
}
return LinkToComicInfo( return LinkToComicInfo(
comicId: e.id, comicId: e.id,
child: ComicInfoCard( child: ComicInfoCard(
@ -242,7 +318,7 @@ class _ComicListState extends State<ComicList> {
} }
// //
return ListView( return ListView(
controller: widget.controller, controller: widget.scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(top: gap, bottom: gap), padding: EdgeInsets.only(top: gap, bottom: gap),
children: wraps, children: wraps,
@ -380,7 +456,7 @@ class _ComicListState extends State<ComicList> {
} }
// //
return ListView( return ListView(
controller: widget.controller, controller: widget.scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(top: gap, bottom: gap), padding: EdgeInsets.only(top: gap, bottom: gap),
children: wraps, children: wraps,

View File

@ -14,9 +14,15 @@ import 'ContentLoading.dart';
// //
class ComicPager extends StatefulWidget { class ComicPager extends StatefulWidget {
final ComicListController? comicListController;
final Future<ComicsPage> Function(String sort, int page) fetchPage; final Future<ComicsPage> Function(String sort, int page) fetchPage;
const ComicPager({required this.fetchPage, Key? key}) : super(key: key); const ComicPager({
required this.fetchPage,
Key? key,
// required
this.comicListController,
}) : super(key: key);
@override @override
State<StatefulWidget> createState() => _ComicPagerState(); State<StatefulWidget> createState() => _ComicPagerState();
@ -43,9 +49,15 @@ class _ComicPagerState extends State<ComicPager> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
switch (currentPagerAction()) { switch (currentPagerAction()) {
case PagerAction.CONTROLLER: case PagerAction.CONTROLLER:
return ControllerComicPager(fetchPage: widget.fetchPage); return ControllerComicPager(
fetchPage: widget.fetchPage,
comicListController: widget.comicListController,
);
case PagerAction.STREAM: case PagerAction.STREAM:
return StreamComicPager(fetchPage: widget.fetchPage); return StreamComicPager(
fetchPage: widget.fetchPage,
comicListController: widget.comicListController,
);
default: default:
return Container(); return Container();
} }
@ -53,11 +65,13 @@ class _ComicPagerState extends State<ComicPager> {
} }
class ControllerComicPager extends StatefulWidget { class ControllerComicPager extends StatefulWidget {
final ComicListController? comicListController;
final Future<ComicsPage> Function(String sort, int page) fetchPage; final Future<ComicsPage> Function(String sort, int page) fetchPage;
const ControllerComicPager({ const ControllerComicPager({
Key? key, Key? key,
required this.fetchPage, required this.fetchPage,
required this.comicListController,
}) : super(key: key); }) : super(key: key);
@override @override
@ -107,6 +121,7 @@ class _ControllerComicPagerState extends State<ControllerComicPager> {
body: ComicList( body: ComicList(
comicsPage.docs, comicsPage.docs,
appendWidget: _buildNextButton(comicsPage), appendWidget: _buildNextButton(comicsPage),
listController: widget.comicListController,
), ),
); );
}, },
@ -255,11 +270,13 @@ class _ControllerComicPagerState extends State<ControllerComicPager> {
} }
class StreamComicPager extends StatefulWidget { class StreamComicPager extends StatefulWidget {
final ComicListController? comicListController;
final Future<ComicsPage> Function(String sort, int page) fetchPage; final Future<ComicsPage> Function(String sort, int page) fetchPage;
const StreamComicPager({ const StreamComicPager({
Key? key, Key? key,
required this.fetchPage, required this.fetchPage,
required this.comicListController,
}) : super(key: key); }) : super(key: key);
@override @override
@ -351,8 +368,9 @@ class _StreamComicPagerState extends State<StreamComicPager> {
appBar: _buildAppBar(context), appBar: _buildAppBar(context),
body: ComicList( body: ComicList(
_list, _list,
controller: _scrollController, scrollController: _scrollController,
appendWidget: _buildLoadingCell(), appendWidget: _buildLoadingCell(),
listController: widget.comicListController,
), ),
); );
} }

View File

@ -1,10 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pikapika/screens/components/ComicList.dart';
import '../../basic/config/IsPro.dart';
import '../../basic/config/ListLayout.dart'; import '../../basic/config/ListLayout.dart';
import '../../basic/config/ShadowCategories.dart'; import '../../basic/config/ShadowCategories.dart';
import '../../basic/config/ShadowCategoriesMode.dart'; import '../../basic/config/ShadowCategoriesMode.dart';
Widget commonPopMenu(BuildContext context) { Widget commonPopMenu(
BuildContext context, {
ComicListController? comicListController,
void Function(VoidCallback fn)? setState,
}) {
return PopupMenuButton<int>( return PopupMenuButton<int>(
itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[ itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[
const PopupMenuItem<int>( const PopupMenuItem<int>(
@ -28,6 +34,22 @@ Widget commonPopMenu(BuildContext context) {
title: Text("封印列表"), title: Text("封印列表"),
), ),
), ),
...comicListController != null && setState != null
? [
PopupMenuItem<int>(
value: 3,
child: ListTile(
leading: const Icon(Icons.download),
title: Text(
"下载" + (isPro ? "" : "Pro"),
style: TextStyle(
color: isPro ? null : Colors.grey,
),
),
),
)
]
: [],
], ],
onSelected: (int value) { onSelected: (int value) {
switch (value) { switch (value) {
@ -40,6 +62,15 @@ Widget commonPopMenu(BuildContext context) {
case 2: case 2:
chooseShadowCategories(context); chooseShadowCategories(context);
break; break;
case 3:
if (setState != null) {
if (comicListController != null) {
setState(() {
comicListController.selecting = !comicListController.selecting;
});
}
}
break;
} }
}, },
); );

View File

@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import '../../basic/Channels.dart';
import '../../basic/Common.dart';
import '../../basic/Method.dart';
import 'ContentLoading.dart';
class DownloadComicsScreen extends StatefulWidget {
final List<String> comicIds;
const DownloadComicsScreen(this.comicIds, {Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _DownloadComicsScreenState();
}
class _DownloadComicsScreenState extends State<DownloadComicsScreen> {
bool exporting = false;
bool exported = false;
bool exportFail = false;
dynamic e;
String exportMessage = "正在创建下载任务";
@override
void initState() {
registerEvent(_onMessageChange, "EXPORT");
super.initState();
}
@override
void dispose() {
unregisterEvent(_onMessageChange);
super.dispose();
}
void _onMessageChange(event) {
setState(() {
exportMessage = event;
});
}
@override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
appBar: AppBar(
title: const Text("批量下载"),
),
body: _body(),
),
onWillPop: () async {
if (exporting) {
defaultToast(context, "创建下载任务中, 请稍后");
return false;
}
return true;
},
);
}
Widget _body() {
if (exporting) {
return ContentLoading(label: exportMessage);
}
if (exportFail) {
return Center(child: Text("失败\n$e"));
}
if (exported) {
return const Center(child: Text("成功"));
}
return ListView(
children: [
Container(height: 20),
Container(height: 20),
_buildButtonInner("您即将下载${widget.comicIds.length}部漫画, 如果漫画已经存在, 则补充新增加的章节"),
Container(height: 20),
Container(height: 20),
MaterialButton(
onPressed: _create,
child: _buildButtonInner("确认"),
),
Container(height: 20),
Container(height: 20),
Container(height: 20),
],
);
}
_create() async {
var name = "";
try {
setState(() {
exporting = true;
});
await method.downloadAll(
widget.comicIds,
);
exported = true;
} catch (err) {
e = err;
exportFail = true;
} finally {
setState(() {
exporting = false;
});
}
}
Widget _buildButtonInner(String text) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Container(
width: constraints.maxWidth,
padding: const EdgeInsets.all(15),
color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)
.withOpacity(.05),
child: Text(
text,
textAlign: TextAlign.center,
),
);
},
);
}
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:pikapika/basic/Common.dart';
import 'package:pikapika/screens/components/ComicList.dart';
import 'DownloadComicsScreen.dart';
AppBar downAppBar(
BuildContext context,
ComicListController _comicListController,
void Function(VoidCallback fn) setState,
) {
return AppBar(
actions: [
MaterialButton(
minWidth: 0,
onPressed: () async {
setState(() {
_comicListController.selecting = false;
});
},
child: Column(
children: [
Expanded(child: Container()),
const Icon(
Icons.cancel_outlined,
size: 18,
color: Colors.white,
),
const Text(
'取消',
style: TextStyle(fontSize: 14, color: Colors.white),
),
Expanded(child: Container()),
],
),
),
MaterialButton(
minWidth: 0,
onPressed: () async {
_comicListController.selectAll();
},
child: Column(
children: [
Expanded(child: Container()),
const Icon(
Icons.select_all,
size: 18,
color: Colors.white,
),
const Text(
'全选',
style: TextStyle(fontSize: 14, color: Colors.white),
),
Expanded(child: Container()),
],
),
),
MaterialButton(
minWidth: 0,
onPressed: () async {
// todo
final list = _comicListController.selected;
if (list.isEmpty) {
defaultToast(context, "请选择漫画");
return;
}
_comicListController.selecting = false;
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) {
return DownloadComicsScreen(list);
},
));
},
child: Column(
children: [
Expanded(child: Container()),
const Icon(
Icons.check,
size: 18,
color: Colors.white,
),
const Text(
'确认',
style: TextStyle(fontSize: 14, color: Colors.white),
),
Expanded(child: Container()),
],
),
),
],
);
}