完成添加收藏的功能

main
落雨楓 2 years ago
parent 4a0076d97d
commit 5ad50bbc58

@ -73,7 +73,7 @@ class MWResponse<T> {
class MWApi { class MWApi {
static Future<MWResponse<Map<String, dynamic>>> get(String action, static Future<MWResponse<Map<String, dynamic>>> get(String action,
{Map<String, dynamic>? params}) async { {Map<String, dynamic>? params, bool returnRoot = false}) async {
Map<String, String> paramsStr = Map<String, String> paramsStr =
params?.map((key, value) => MapEntry(key, value.toString())) ?? {}; params?.map((key, value) => MapEntry(key, value.toString())) ?? {};
paramsStr.addAll({ paramsStr.addAll({
@ -109,11 +109,11 @@ class MWApi {
} }
} }
return parseMWResponse(action, resText); return parseMWResponse(action, resText, returnRoot);
} }
static Future<MWResponse<Map<String, dynamic>>> post(String action, static Future<MWResponse<Map<String, dynamic>>> post<T>(String action,
{Map<String, dynamic>? params, String? withToken}) async { {Map<String, dynamic>? params, String? withToken, bool returnRoot = false}) async {
params ??= {}; params ??= {};
params.addAll({ params.addAll({
"action": action, "action": action,
@ -154,10 +154,11 @@ class MWApi {
} }
} }
return parseMWResponse(action, resText); return parseMWResponse(action, resText, returnRoot);
} }
static MWResponse<Map<String, dynamic>> parseMWResponse(String action, String resJson) { static MWResponse<Map<String, dynamic>> parseMWResponse(
String action, String resJson, bool returnRoot) {
var resData = jsonDecode(resJson); var resData = jsonDecode(resJson);
if (resData is! Map<String, dynamic>) { if (resData is! Map<String, dynamic>) {
throw MWApiEmptyBodyException(resData); throw MWApiEmptyBodyException(resData);
@ -169,12 +170,22 @@ class MWApi {
} }
// //
if (!resData.containsKey(action) || resData[action] is! Map<String, dynamic>) { MWResponse<Map<String, dynamic>> mwRes;
throw MWApiEmptyBodyException(resData);
if (returnRoot) {
var filteredResData = <String, dynamic>{}..addAll(resData);
filteredResData
..remove("error")
..remove("warnings")
..remove("batchcomplete");
mwRes = MWResponse(filteredResData);
} else {
if (!resData.containsKey(action) || resData[action] is! Map<String, dynamic>) {
throw MWApiEmptyBodyException(resData);
}
mwRes = MWResponse(resData[action] as Map<String, dynamic>);
} }
MWResponse<Map<String, dynamic>> mwRes = MWResponse(resData[action] as Map<String, dynamic>);
// warnings // warnings
if (resData.containsKey("warnings") && resData["warnings"] is Map<String, dynamic>) { if (resData.containsKey("warnings") && resData["warnings"] is Map<String, dynamic>) {
var warnings = resData["warnings"] as Map<String, dynamic>; var warnings = resData["warnings"] as Map<String, dynamic>;

@ -3,21 +3,28 @@ import 'package:isekai_wiki/api/response/watch.dart';
import 'mw_api.dart'; import 'mw_api.dart';
class MWApiWatch { class MWApiWatch {
static Future<MWResponse<List<WatchActionResponseList>>> watchPage(List<String> titles, static Future<MWResponse<List<WatchActionResponseList>>> watchPage(
{bool unwatch = false}) async { {List<String>? titles, List<int>? pageIds, bool unwatch = false}) async {
var query = {"titles": titles.join("|")}; Map<String, dynamic> query = {};
if (titles != null) {
query["titles"] = titles.join("|");
}
if (pageIds != null) {
query["pageids"] = pageIds.join("|");
}
if (unwatch) { if (unwatch) {
query["unwatch"] = "1"; query["unwatch"] = "1";
} }
var mwRes = await MWApi.post("watch", params: query, withToken: "watch"); var mwRes = await MWApi.post("watch", params: query, withToken: "watch", returnRoot: true);
var data = WatchActionResponse.fromJson(mwRes.data); var data = WatchActionResponse.fromJson(mwRes.data);
return mwRes.replaceData(data.watch); return mwRes.replaceData(data.watch);
} }
static Future<MWResponse<List<WatchActionResponseList>>> unwatchPage(List<String> titles) { static Future<MWResponse<List<WatchActionResponseList>>> unwatchPage(
return watchPage(titles, unwatch: true); {List<String>? titles, List<int>? pageIds, bool unwatch = false}) {
return watchPage(titles: titles, pageIds: pageIds, unwatch: true);
} }
} }

@ -12,11 +12,11 @@ class WatchActionResponseList with _$WatchActionResponseList {
factory WatchActionResponseList( factory WatchActionResponseList(
{required int ns, {required int ns,
required String title, required String title,
String? watch, bool? watched,
String? unwatch}) = _WatchActionResponseList; bool? unwatched}) = _WatchActionResponseList;
bool get isWatch { bool get isWatched {
return watch != null; return watched == true;
} }
factory WatchActionResponseList.fromJson(Map<String, dynamic> json) => factory WatchActionResponseList.fromJson(Map<String, dynamic> json) =>

@ -9,8 +9,7 @@ import 'package:get/get.dart';
const double _DEFAULT_SCALE_MIN_VALUE = 0.96; const double _DEFAULT_SCALE_MIN_VALUE = 0.96;
const double _DEFAULT_OPACITY_MIN_VALUE = 0.90; const double _DEFAULT_OPACITY_MIN_VALUE = 0.90;
final Curve _DEFAULT_SCALE_CURVE = final Curve _DEFAULT_SCALE_CURVE = CurveSpring(); // ignore: non_constant_identifier_names
CurveSpring(); // ignore: non_constant_identifier_names
const Curve _DEFAULT_OPACITY_CURVE = Curves.ease; const Curve _DEFAULT_OPACITY_CURVE = Curves.ease;
const Duration _DEFAULT_DURATION = Duration(milliseconds: 250); const Duration _DEFAULT_DURATION = Duration(milliseconds: 250);
@ -53,8 +52,7 @@ class ScaleTap extends StatefulWidget {
_ScaleTapState createState() => _ScaleTapState(); _ScaleTapState createState() => _ScaleTapState();
} }
class _ScaleTapState extends State<ScaleTap> class _ScaleTapState extends State<ScaleTap> with SingleTickerProviderStateMixin {
with SingleTickerProviderStateMixin {
late AnimationController _animationController; late AnimationController _animationController;
late Animation<double> _scale; late Animation<double> _scale;
late Animation<double> _opacity; late Animation<double> _opacity;
@ -73,8 +71,7 @@ class _ScaleTapState extends State<ScaleTap>
_animationController = AnimationController(vsync: this); _animationController = AnimationController(vsync: this);
_scale = Tween<double>(begin: 1.0, end: 1.0).animate(_animationController); _scale = Tween<double>(begin: 1.0, end: 1.0).animate(_animationController);
_opacity = _opacity = Tween<double>(begin: 1.0, end: 1.0).animate(_animationController);
Tween<double>(begin: 1.0, end: 1.0).animate(_animationController);
} }
@override @override
@ -91,18 +88,14 @@ class _ScaleTapState extends State<ScaleTap>
begin: _scale.value, begin: _scale.value,
end: scale, end: scale,
).animate(CurvedAnimation( ).animate(CurvedAnimation(
curve: widget.scaleCurve ?? curve: widget.scaleCurve ?? ScaleTapConfig.scaleCurve ?? _DEFAULT_SCALE_CURVE,
ScaleTapConfig.scaleCurve ??
_DEFAULT_SCALE_CURVE,
parent: _animationController, parent: _animationController,
)); ));
_opacity = Tween<double>( _opacity = Tween<double>(
begin: _opacity.value, begin: _opacity.value,
end: opacity, end: opacity,
).animate(CurvedAnimation( ).animate(CurvedAnimation(
curve: widget.opacityCurve ?? curve: widget.opacityCurve ?? ScaleTapConfig.opacityCurve ?? _DEFAULT_OPACITY_CURVE,
ScaleTapConfig.opacityCurve ??
_DEFAULT_OPACITY_CURVE,
parent: _animationController, parent: _animationController,
)); ));
_animationController.reset(); _animationController.reset();
@ -114,8 +107,7 @@ class _ScaleTapState extends State<ScaleTap>
return await anim( return await anim(
scale: 1.0, scale: 1.0,
opacity: 1.0, opacity: 1.0,
duration: duration: widget.duration ?? ScaleTapConfig.duration ?? _DEFAULT_DURATION,
widget.duration ?? ScaleTapConfig.duration ?? _DEFAULT_DURATION,
); );
} }
} }
@ -127,12 +119,9 @@ class _ScaleTapState extends State<ScaleTap>
_scaleAnimating = true; _scaleAnimating = true;
await anim( await anim(
scale: widget.scaleMinValue ?? scale: widget.scaleMinValue ?? ScaleTapConfig.scaleMinValue ?? _DEFAULT_SCALE_MIN_VALUE,
ScaleTapConfig.scaleMinValue ?? opacity:
_DEFAULT_SCALE_MIN_VALUE, widget.opacityMinValue ?? ScaleTapConfig.opacityMinValue ?? _DEFAULT_OPACITY_MIN_VALUE,
opacity: widget.opacityMinValue ??
ScaleTapConfig.opacityMinValue ??
_DEFAULT_OPACITY_MIN_VALUE,
duration: widget.duration ?? ScaleTapConfig.duration ?? _DEFAULT_DURATION, duration: widget.duration ?? ScaleTapConfig.duration ?? _DEFAULT_DURATION,
); );
@ -152,8 +141,7 @@ class _ScaleTapState extends State<ScaleTap>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isTapEnabled = final bool isTapEnabled = widget.onPressed != null || widget.onLongPress != null;
widget.onPressed != null || widget.onLongPress != null;
return AnimatedBuilder( return AnimatedBuilder(
animation: _animationController, animation: _animationController,
@ -219,9 +207,10 @@ class ScaleTapIgnore extends StatelessWidget {
c.ignoredAreaPressing = true; c.ignoredAreaPressing = true;
}, },
child: GestureDetector( child: GestureDetector(
child: child, behavior: HitTestBehavior.opaque,
onTap: () {}, onTap: () {},
onLongPress: () {}, onLongPress: () {},
child: child,
), ),
); );
} }

@ -11,8 +11,7 @@ import 'package:skeletons/skeletons.dart';
import '../styles.dart'; import '../styles.dart';
typedef AddFavoriteCallback = Future<bool> Function( typedef AddFavoriteCallback = Future<bool> Function(PageInfo pageInfo, bool localIsFavorite);
PageInfo pageInfo, bool localIsFavorite, bool showToast);
typedef PageInfoCallback = Future<void> Function(PageInfo pageInfo); typedef PageInfoCallback = Future<void> Function(PageInfo pageInfo);
@ -41,7 +40,7 @@ class PageCard extends StatelessWidget {
Future<bool> handleFavoriteClick(bool localIsFavorite) async { Future<bool> handleFavoriteClick(bool localIsFavorite) async {
if (pageInfo != null && onSetFavorite != null) { if (pageInfo != null && onSetFavorite != null) {
return await onSetFavorite!.call(pageInfo!, localIsFavorite, false); return await onSetFavorite!.call(pageInfo!, !localIsFavorite);
} else { } else {
return false; return false;
} }
@ -49,13 +48,13 @@ class PageCard extends StatelessWidget {
Future<void> handleAddFavoriteMenuItemClick() async { Future<void> handleAddFavoriteMenuItemClick() async {
if (pageInfo != null && onSetFavorite != null) { if (pageInfo != null && onSetFavorite != null) {
await onSetFavorite!.call(pageInfo!, true, true); await onSetFavorite!.call(pageInfo!, true);
} }
} }
Future<void> handleRemoveFavoriteMenuItemClick() async { Future<void> handleRemoveFavoriteMenuItemClick() async {
if (pageInfo != null && onSetFavorite != null) { if (pageInfo != null && onSetFavorite != null) {
await onSetFavorite!.call(pageInfo!, false, true); await onSetFavorite!.call(pageInfo!, false);
} }
} }
@ -249,7 +248,7 @@ class PageCard extends StatelessWidget {
: PullDownMenuItem( : PullDownMenuItem(
title: '收藏', title: '收藏',
icon: CupertinoIcons.heart, icon: CupertinoIcons.heart,
onTap: handleRemoveFavoriteMenuItemClick, onTap: handleAddFavoriteMenuItemClick,
), ),
const PullDownMenuDivider(), const PullDownMenuDivider(),
PullDownMenuItem( PullDownMenuItem(

@ -132,12 +132,11 @@ class RecentPageList extends StatelessWidget {
} }
Widget _buildPageCard( Widget _buildPageCard(
int index, PageInfo pageInfo, RecentPageListController c, FavoriteListController fc) { int index, PageInfo pageInfo, RecentPageListController c, FavoriteListController flc) {
return PageCard( return PageCard(
key: ValueKey("rpl-card-$index"),
pageInfo: c.pageList[index], pageInfo: c.pageList[index],
isFavorite: fc.isFavorite(c.pageList[index]), isFavorite: flc.isFavorite(c.pageList[index]),
onSetFavorite: fc.setFavorite, onSetFavorite: flc.setFavoriteImmediate,
); );
} }
@ -161,7 +160,7 @@ class RecentPageList extends StatelessWidget {
// //
if (index < c.pageList.length) { if (index < c.pageList.length) {
return _buildPageCard(index, c.pageList[index], c, flc); return Obx(() => _buildPageCard(index, c.pageList[index], c, flc));
} else if (index == c.pageList.length) { } else if (index == c.pageList.length) {
// //
return Padding( return Padding(

@ -64,8 +64,8 @@ Future<void> postInit() async {
Future<void> main() async { Future<void> main() async {
await init(); await init();
Get.put(LifeCycleController());
Get.put(SiteConfigController()); Get.put(SiteConfigController());
Get.put(LifeCycleController());
runApp(const IsekaiWikiApp()); runApp(const IsekaiWikiApp());

@ -1,10 +1,18 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:isekai_wiki/api/mw/watch.dart';
import 'package:isekai_wiki/api/response/page_info.dart'; import 'package:isekai_wiki/api/response/page_info.dart';
import 'package:isekai_wiki/api/response/watch.dart';
import 'package:isekai_wiki/models/user.dart'; import 'package:isekai_wiki/models/user.dart';
import 'package:isekai_wiki/utils/dialog.dart'; import 'package:isekai_wiki/utils/dialog.dart';
import 'package:isekai_wiki/utils/error.dart';
class FavoriteListController extends GetxController { class FavoriteListController extends GetxController {
var pageIds = RxList<int>(); bool syncRunning = false;
var favPageIds = RxList<int>();
void updateFromPageList(List<PageInfo> list) { void updateFromPageList(List<PageInfo> list) {
List<int> addList = []; List<int> addList = [];
@ -19,14 +27,14 @@ class FavoriteListController extends GetxController {
} }
} }
var newPageIds = pageIds.where((pageId) => !removeList.contains(pageId)).toList(); var newPageIds = favPageIds.where((pageId) => !removeList.contains(pageId)).toList();
for (var pageId in addList) { for (var pageId in addList) {
if (!newPageIds.contains(pageId)) { if (!newPageIds.contains(pageId)) {
newPageIds.add(pageId); newPageIds.add(pageId);
} }
} }
pageIds.value = newPageIds; favPageIds.value = newPageIds;
} }
void updateFromWatchList(List<PageInfo> list) { void updateFromWatchList(List<PageInfo> list) {
@ -36,29 +44,103 @@ class FavoriteListController extends GetxController {
} }
for (var pageId in addList) { for (var pageId in addList) {
if (!pageIds.contains(pageId)) { if (!favPageIds.contains(pageId)) {
pageIds.add(pageId); favPageIds.add(pageId);
} }
} }
} }
bool isFavorite(PageInfo pageInfo) { bool isFavorite(PageInfo pageInfo) {
return pageIds.contains(pageInfo.pageid); return favPageIds.contains(pageInfo.pageid);
} }
Future<bool> setFavorite(PageInfo pageInfo, bool isFavorite, bool showToast) async { Future<void> setFavorite(PageInfo pageInfo, bool isFavorite, bool showToast) async {
syncRunning = true;
List<WatchActionResponseList> watchRes = [];
if (isFavorite) {
//
try {
var res = await MWApiWatch.watchPage(pageIds: [pageInfo.pageid]);
watchRes = res.data;
//
if (!favPageIds.contains(pageInfo.pageid)) {
favPageIds.add(pageInfo.pageid);
}
} catch (err, stack) {
alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err), title: "错误");
if (kDebugMode) {
print("Exception in logout: $err");
stack.printError();
}
}
} else {
//
try {
var res = await MWApiWatch.unwatchPage(pageIds: [pageInfo.pageid]);
watchRes = res.data;
} catch (err, stack) {
alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err), title: "错误");
if (kDebugMode) {
print("Exception in logout: $err");
stack.printError();
}
}
}
if (watchRes.isNotEmpty) {
if (watchRes[0].isWatched) {
//
if (!favPageIds.contains(pageInfo.pageid)) {
favPageIds.add(pageInfo.pageid);
}
if (showToast) {
Fluttertoast.showToast(
msg: "已将《${pageInfo.displayTitle}》添加到收藏列表",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
fontSize: 16.0,
backgroundColor: CupertinoColors.black.withOpacity(0.8),
textColor: CupertinoColors.white,
);
}
} else {
//
if (favPageIds.contains(pageInfo.pageid)) {
favPageIds.remove(pageInfo.pageid);
}
if (showToast) {
Fluttertoast.showToast(
msg: "已将《${pageInfo.displayTitle}》从收藏列表中移除",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
fontSize: 16.0,
backgroundColor: CupertinoColors.black.withOpacity(0.8),
textColor: CupertinoColors.white,
);
}
}
}
syncRunning = false;
}
///
Future<bool> setFavoriteImmediate(PageInfo pageInfo, bool isFavorite) async {
// //
var uc = Get.find<UserController>(); var uc = Get.find<UserController>();
if (!uc.isLoggedIn) { if (!uc.isLoggedIn) {
alert(Get.overlayContext!, "使用收藏功能需要登录", title: "提示"); alert(Get.overlayContext!, "使用收藏功能需要登录", title: "提示");
return isFavorite; return !isFavorite;
} }
if (isFavorite) { if (syncRunning) {
// alert(Get.overlayContext!, "正在同步收藏列表,请稍后再试", title: "提示");
} else { return !isFavorite;
//
} }
return !isFavorite;
setFavorite(pageInfo, isFavorite, true);
// UI
return isFavorite;
} }
} }

@ -137,6 +137,7 @@ class SiteConfigController extends GetxController {
loadFromEntity(siteConfigData); loadFromEntity(siteConfigData);
isAppActive = true; isAppActive = true;
Global.isAppActive = true;
saveToStorage(); saveToStorage();
} }
} }

@ -15,6 +15,13 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.2.0" version: "5.2.0"
animated_snack_bar:
dependency: "direct main"
description:
name: animated_snack_bar
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.0"
animations: animations:
dependency: "direct main" dependency: "direct main"
description: description:
@ -385,6 +392,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
fluttertoast:
dependency: "direct main"
description:
name: fluttertoast
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.1.2"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:

@ -31,8 +31,8 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations: # Add this line flutter_localizations:
sdk: flutter # Add this line sdk: flutter
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
@ -48,6 +48,8 @@ dependencies:
like_button: ^2.0.4 like_button: ^2.0.4
skeletons: ^0.0.3 skeletons: ^0.0.3
modal_bottom_sheet: ^2.1.2 modal_bottom_sheet: ^2.1.2
fluttertoast: ^8.1.2
animated_snack_bar: ^0.3.0
responsive_builder: ^0.4.3 responsive_builder: ^0.4.3
url_launcher: ^6.1.7 url_launcher: ^6.1.7
flutter_web_browser: ^0.17.1 flutter_web_browser: ^0.17.1

Loading…
Cancel
Save