: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 {
late bool isPro;
late int expire;
class ProInfoAll {
ProInfoAll({
required this.proInfoAf,
required this.proInfoPat,
});
late final ProInfoAf proInfoAf;
late final ProInfoPat proInfoPat;
IsPro.fromJson(Map<String, dynamic> json) {
this.isPro = json["isPro"];
this.expire = json["expire"];
ProInfoAll.fromJson(Map<String, dynamic> json){
proInfoAf = ProInfoAf.fromJson(json['pro_info_af']);
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();
}
Future<IsPro> isPro() async {
return IsPro.fromJson(jsonDecode(await _flatInvoke("isPro", "")));
Future<ProInfoAll> proInfoAll() async {
return ProInfoAll.fromJson(jsonDecode(await _flatInvoke("proInfoAll", "")));
}
Future reloadPro() {
@ -1009,4 +1009,8 @@ class Method {
Future androidMkdirs(String path) async {
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:pikapika/basic/Method.dart';
var isPro = false;
var isProEx = 0;
import '../Entities.dart';
bool get isPro {
return _proInfoAll.proInfoAf.isPro || _proInfoAll.proInfoPat.isPro;
}
ProInfoAf get proInfoAf => _proInfoAll.proInfoAf;
ProInfoPat get proInfoPat => _proInfoAll.proInfoPat;
final proEvent = Event();
late ProInfoAll _proInfoAll;
Future reloadIsPro() async {
final p = await method.isPro();
isPro = p.isPro;
isProEx = p.expire;
_proInfoAll = await method.proInfoAll();
proEvent.broadcast();
}

View File

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

View File

@ -68,13 +68,26 @@ class _ProScreenState extends State<ProScreen> {
),
const Divider(),
ListTile(
title: const Text("发电详情"),
title: const Text("签到或礼物卡"),
subtitle: Text(
isPro
? "发电中 (${DateTime.fromMillisecondsSinceEpoch(1000 * isProEx).toString()})"
proInfoAf.isPro
? "发电中 (${DateTime.fromMillisecondsSinceEpoch(1000 * proInfoAf.expire).toString()})"
: "未发电",
),
),
...(proInfoPat.patId.isNotEmpty ? [
ListTile(
onTap: () {
managementPat();
},
title: const Text("P站支持"),
subtitle: Text((
proInfoPat.isPro
? "发电中"
: "未发电") + ("(点击管理)"),
),
),
] : []),
const Divider(),
ListTile(
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 '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 {
final Widget? appendWidget;
final List<ComicSimple> comicList;
final ScrollController? controller;
final ScrollController? scrollController;
final ComicListController? listController;
const ComicList(
this.comicList, {
this.appendWidget,
this.controller,
this.scrollController,
Key? key,
// required
this.listController,
}) : super(key: key);
@override
@ -32,6 +49,25 @@ class ComicList extends StatefulWidget {
class _ComicListState extends State<ComicList> {
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 {
if (widget.comicList.isNotEmpty) {
@ -43,6 +79,7 @@ class _ComicListState extends State<ComicList> {
@override
void initState() {
widget.listController?._state = this;
_loadViewed();
listLayoutEvent.subscribe(_onLayoutChange);
super.initState();
@ -50,6 +87,9 @@ class _ComicListState extends State<ComicList> {
@override
void dispose() {
if (widget.listController?._state == this) {
widget.listController?._state = null;
}
listLayoutEvent.unsubscribe(_onLayoutChange);
super.dispose();
}
@ -74,7 +114,7 @@ class _ComicListState extends State<ComicList> {
Widget _buildInfoCardList() {
return ListView(
controller: widget.controller,
controller: widget.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
children: [
...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(
comicId: e.id,
child: ComicInfoCard(
@ -242,7 +318,7 @@ class _ComicListState extends State<ComicList> {
}
//
return ListView(
controller: widget.controller,
controller: widget.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(top: gap, bottom: gap),
children: wraps,
@ -380,7 +456,7 @@ class _ComicListState extends State<ComicList> {
}
//
return ListView(
controller: widget.controller,
controller: widget.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(top: gap, bottom: gap),
children: wraps,

View File

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

View File

@ -1,10 +1,16 @@
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/ShadowCategories.dart';
import '../../basic/config/ShadowCategoriesMode.dart';
Widget commonPopMenu(BuildContext context) {
Widget commonPopMenu(
BuildContext context, {
ComicListController? comicListController,
void Function(VoidCallback fn)? setState,
}) {
return PopupMenuButton<int>(
itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[
const PopupMenuItem<int>(
@ -28,6 +34,22 @@ Widget commonPopMenu(BuildContext context) {
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) {
switch (value) {
@ -40,6 +62,15 @@ Widget commonPopMenu(BuildContext context) {
case 2:
chooseShadowCategories(context);
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()),
],
),
),
],
);
}