From 2c30885bc35633c54c7e68562fbc7ac9254bfa5e Mon Sep 17 00:00:00 2001 From: niuhuan Date: Mon, 10 Apr 2023 23:05:53 +0800 Subject: [PATCH] :sparkless: Add comics to download list --- lib/basic/Entities.dart | 85 +++++++++++- lib/basic/Method.dart | 8 +- lib/basic/config/IsPro.dart | 15 ++- lib/screens/ComicsScreen.dart | 24 +++- lib/screens/ProScreen.dart | 23 +++- lib/screens/components/ComicList.dart | 86 +++++++++++- lib/screens/components/ComicPager.dart | 26 +++- lib/screens/components/Common.dart | 33 ++++- .../components/DownloadComicsScreen.dart | 125 ++++++++++++++++++ lib/screens/components/GoDownloadSelect.dart | 92 +++++++++++++ 10 files changed, 488 insertions(+), 29 deletions(-) create mode 100644 lib/screens/components/DownloadComicsScreen.dart create mode 100644 lib/screens/components/GoDownloadSelect.dart diff --git a/lib/basic/Entities.dart b/lib/basic/Entities.dart index 690d8d6..a176358 100644 --- a/lib/basic/Entities.dart +++ b/lib/basic/Entities.dart @@ -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 json) { - this.isPro = json["isPro"]; - this.expire = json["expire"]; + ProInfoAll.fromJson(Map json){ + proInfoAf = ProInfoAf.fromJson(json['pro_info_af']); + proInfoPat = ProInfoPat.fromJson(json['pro_info_pat']); + } + + Map toJson() { + final _data = {}; + _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 json){ + isPro = json['is_pro']; + expire = json['expire']; + } + + Map toJson() { + final _data = {}; + _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 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 toJson() { + final _data = {}; + _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; } } diff --git a/lib/basic/Method.dart b/lib/basic/Method.dart index d2f5d8c..0b0d0c1 100644 --- a/lib/basic/Method.dart +++ b/lib/basic/Method.dart @@ -910,8 +910,8 @@ class Method { .toList(); } - Future isPro() async { - return IsPro.fromJson(jsonDecode(await _flatInvoke("isPro", ""))); + Future 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 comicIds) { + return _flatInvoke("downloadAll", comicIds); + } } diff --git a/lib/basic/config/IsPro.dart b/lib/basic/config/IsPro.dart index 3ff794f..0c06bf8 100644 --- a/lib/basic/config/IsPro.dart +++ b/lib/basic/config/IsPro.dart @@ -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(); } diff --git a/lib/screens/ComicsScreen.dart b/lib/screens/ComicsScreen.dart index c452616..a40a372 100644 --- a/lib/screens/ComicsScreen.dart +++ b/lib/screens/ComicsScreen.dart @@ -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 { + late final _comicListController = ComicListController(); late final SearchBar _categorySearchBar = SearchBar( hintText: '搜索分类 - ${categoryTitle(widget.category)}', inBar: false, @@ -55,7 +58,11 @@ class _ComicsScreenState extends State { 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 { ); Future _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 { } @override - Widget build(BuildContext context){ + Widget build(BuildContext context) { return rightClickPop( child: buildScreen(context), context: context, @@ -154,10 +167,15 @@ class _ComicsScreenState extends State { ); } + if (_comicListController.selecting) { + appBar = downAppBar(context, _comicListController, setState); + } + return Scaffold( appBar: appBar, body: ComicPager( fetchPage: _load, + comicListController: _comicListController, ), ); } diff --git a/lib/screens/ProScreen.dart b/lib/screens/ProScreen.dart index 25c6d64..71413b2 100644 --- a/lib/screens/ProScreen.dart +++ b/lib/screens/ProScreen.dart @@ -68,13 +68,26 @@ class _ProScreenState extends State { ), 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 { ), ); } + + void managementPat() { + // todo + } } diff --git a/lib/screens/components/ComicList.dart b/lib/screens/components/ComicList.dart index 1bbf9a7..b8ee3e4 100644 --- a/lib/screens/components/ComicList.dart +++ b/lib/screens/components/ComicList.dart @@ -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 get selected => _state?._selected ?? []; + + selectAll() { + _state?._selectAll(); + } +} + // 漫画列表页 class ComicList extends StatefulWidget { final Widget? appendWidget; final List 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 { final List viewedList = []; + bool _selecting = false; + List _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 { @override void initState() { + widget.listController?._state = this; _loadViewed(); listLayoutEvent.subscribe(_onLayoutChange); super.initState(); @@ -50,6 +87,9 @@ class _ComicListState extends State { @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 { 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 { ), ); } + 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 { } // 返回 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 { } // 返回 return ListView( - controller: widget.controller, + controller: widget.scrollController, physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.only(top: gap, bottom: gap), children: wraps, diff --git a/lib/screens/components/ComicPager.dart b/lib/screens/components/ComicPager.dart index 40a03f2..e1fbbac 100644 --- a/lib/screens/components/ComicPager.dart +++ b/lib/screens/components/ComicPager.dart @@ -14,9 +14,15 @@ import 'ContentLoading.dart'; // 漫画列页 class ComicPager extends StatefulWidget { + final ComicListController? comicListController; final Future 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 createState() => _ComicPagerState(); @@ -43,9 +49,15 @@ class _ComicPagerState extends State { 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 { } class ControllerComicPager extends StatefulWidget { + final ComicListController? comicListController; final Future 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 { body: ComicList( comicsPage.docs, appendWidget: _buildNextButton(comicsPage), + listController: widget.comicListController, ), ); }, @@ -255,11 +270,13 @@ class _ControllerComicPagerState extends State { } class StreamComicPager extends StatefulWidget { + final ComicListController? comicListController; final Future 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 { appBar: _buildAppBar(context), body: ComicList( _list, - controller: _scrollController, + scrollController: _scrollController, appendWidget: _buildLoadingCell(), + listController: widget.comicListController, ), ); } diff --git a/lib/screens/components/Common.dart b/lib/screens/components/Common.dart index dd8b7ae..b4b926f 100644 --- a/lib/screens/components/Common.dart +++ b/lib/screens/components/Common.dart @@ -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( itemBuilder: (BuildContext context) => >[ const PopupMenuItem( @@ -28,6 +34,22 @@ Widget commonPopMenu(BuildContext context) { title: Text("封印列表"), ), ), + ...comicListController != null && setState != null + ? [ + PopupMenuItem( + 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; } }, ); diff --git a/lib/screens/components/DownloadComicsScreen.dart b/lib/screens/components/DownloadComicsScreen.dart new file mode 100644 index 0000000..483dc9a --- /dev/null +++ b/lib/screens/components/DownloadComicsScreen.dart @@ -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 comicIds; + + const DownloadComicsScreen(this.comicIds, {Key? key}) : super(key: key); + + @override + State createState() => _DownloadComicsScreenState(); +} + +class _DownloadComicsScreenState extends State { + 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, + ), + ); + }, + ); + } +} diff --git a/lib/screens/components/GoDownloadSelect.dart b/lib/screens/components/GoDownloadSelect.dart new file mode 100644 index 0000000..dd3cbbd --- /dev/null +++ b/lib/screens/components/GoDownloadSelect.dart @@ -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()), + ], + ), + ), + ], + ); +}