diff --git a/lib/api/mw/mw_api.dart b/lib/api/mw/mw_api.dart index 07829a1..c9df8fc 100755 --- a/lib/api/mw/mw_api.dart +++ b/lib/api/mw/mw_api.dart @@ -73,7 +73,7 @@ class MWResponse { class MWApi { static Future>> get(String action, - {Map? params}) async { + {Map? params, bool returnRoot = false}) async { Map paramsStr = params?.map((key, value) => MapEntry(key, value.toString())) ?? {}; paramsStr.addAll({ @@ -109,11 +109,11 @@ class MWApi { } } - return parseMWResponse(action, resText); + return parseMWResponse(action, resText, returnRoot); } - static Future>> post(String action, - {Map? params, String? withToken}) async { + static Future>> post(String action, + {Map? params, String? withToken, bool returnRoot = false}) async { params ??= {}; params.addAll({ "action": action, @@ -154,10 +154,11 @@ class MWApi { } } - return parseMWResponse(action, resText); + return parseMWResponse(action, resText, returnRoot); } - static MWResponse> parseMWResponse(String action, String resJson) { + static MWResponse> parseMWResponse( + String action, String resJson, bool returnRoot) { var resData = jsonDecode(resJson); if (resData is! Map) { throw MWApiEmptyBodyException(resData); @@ -169,12 +170,22 @@ class MWApi { } // 请求结果 - if (!resData.containsKey(action) || resData[action] is! Map) { - throw MWApiEmptyBodyException(resData); + MWResponse> mwRes; + + if (returnRoot) { + var filteredResData = {}..addAll(resData); + filteredResData + ..remove("error") + ..remove("warnings") + ..remove("batchcomplete"); + mwRes = MWResponse(filteredResData); + } else { + if (!resData.containsKey(action) || resData[action] is! Map) { + throw MWApiEmptyBodyException(resData); + } + mwRes = MWResponse(resData[action] as Map); } - MWResponse> mwRes = MWResponse(resData[action] as Map); - // warnings if (resData.containsKey("warnings") && resData["warnings"] is Map) { var warnings = resData["warnings"] as Map; diff --git a/lib/api/mw/watch.dart b/lib/api/mw/watch.dart index 717b4d9..0228cb0 100644 --- a/lib/api/mw/watch.dart +++ b/lib/api/mw/watch.dart @@ -3,21 +3,28 @@ import 'package:isekai_wiki/api/response/watch.dart'; import 'mw_api.dart'; class MWApiWatch { - static Future>> watchPage(List titles, - {bool unwatch = false}) async { - var query = {"titles": titles.join("|")}; + static Future>> watchPage( + {List? titles, List? pageIds, bool unwatch = false}) async { + Map query = {}; + if (titles != null) { + query["titles"] = titles.join("|"); + } + if (pageIds != null) { + query["pageids"] = pageIds.join("|"); + } if (unwatch) { 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); return mwRes.replaceData(data.watch); } - static Future>> unwatchPage(List titles) { - return watchPage(titles, unwatch: true); + static Future>> unwatchPage( + {List? titles, List? pageIds, bool unwatch = false}) { + return watchPage(titles: titles, pageIds: pageIds, unwatch: true); } } diff --git a/lib/api/response/watch.dart b/lib/api/response/watch.dart index 78a5800..f5bd877 100644 --- a/lib/api/response/watch.dart +++ b/lib/api/response/watch.dart @@ -12,11 +12,11 @@ class WatchActionResponseList with _$WatchActionResponseList { factory WatchActionResponseList( {required int ns, required String title, - String? watch, - String? unwatch}) = _WatchActionResponseList; + bool? watched, + bool? unwatched}) = _WatchActionResponseList; - bool get isWatch { - return watch != null; + bool get isWatched { + return watched == true; } factory WatchActionResponseList.fromJson(Map json) => diff --git a/lib/components/flutter_scale_tap/flutter_scale_tap.dart b/lib/components/flutter_scale_tap/flutter_scale_tap.dart index af38114..87fe2b9 100644 --- a/lib/components/flutter_scale_tap/flutter_scale_tap.dart +++ b/lib/components/flutter_scale_tap/flutter_scale_tap.dart @@ -9,8 +9,7 @@ import 'package:get/get.dart'; const double _DEFAULT_SCALE_MIN_VALUE = 0.96; const double _DEFAULT_OPACITY_MIN_VALUE = 0.90; -final Curve _DEFAULT_SCALE_CURVE = - CurveSpring(); // ignore: non_constant_identifier_names +final Curve _DEFAULT_SCALE_CURVE = CurveSpring(); // ignore: non_constant_identifier_names const Curve _DEFAULT_OPACITY_CURVE = Curves.ease; const Duration _DEFAULT_DURATION = Duration(milliseconds: 250); @@ -53,8 +52,7 @@ class ScaleTap extends StatefulWidget { _ScaleTapState createState() => _ScaleTapState(); } -class _ScaleTapState extends State - with SingleTickerProviderStateMixin { +class _ScaleTapState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _scale; late Animation _opacity; @@ -73,8 +71,7 @@ class _ScaleTapState extends State _animationController = AnimationController(vsync: this); _scale = Tween(begin: 1.0, end: 1.0).animate(_animationController); - _opacity = - Tween(begin: 1.0, end: 1.0).animate(_animationController); + _opacity = Tween(begin: 1.0, end: 1.0).animate(_animationController); } @override @@ -91,18 +88,14 @@ class _ScaleTapState extends State begin: _scale.value, end: scale, ).animate(CurvedAnimation( - curve: widget.scaleCurve ?? - ScaleTapConfig.scaleCurve ?? - _DEFAULT_SCALE_CURVE, + curve: widget.scaleCurve ?? ScaleTapConfig.scaleCurve ?? _DEFAULT_SCALE_CURVE, parent: _animationController, )); _opacity = Tween( begin: _opacity.value, end: opacity, ).animate(CurvedAnimation( - curve: widget.opacityCurve ?? - ScaleTapConfig.opacityCurve ?? - _DEFAULT_OPACITY_CURVE, + curve: widget.opacityCurve ?? ScaleTapConfig.opacityCurve ?? _DEFAULT_OPACITY_CURVE, parent: _animationController, )); _animationController.reset(); @@ -114,8 +107,7 @@ class _ScaleTapState extends State return await anim( scale: 1.0, opacity: 1.0, - duration: - widget.duration ?? ScaleTapConfig.duration ?? _DEFAULT_DURATION, + duration: widget.duration ?? ScaleTapConfig.duration ?? _DEFAULT_DURATION, ); } } @@ -127,12 +119,9 @@ class _ScaleTapState extends State _scaleAnimating = true; await anim( - scale: widget.scaleMinValue ?? - ScaleTapConfig.scaleMinValue ?? - _DEFAULT_SCALE_MIN_VALUE, - opacity: widget.opacityMinValue ?? - ScaleTapConfig.opacityMinValue ?? - _DEFAULT_OPACITY_MIN_VALUE, + scale: widget.scaleMinValue ?? ScaleTapConfig.scaleMinValue ?? _DEFAULT_SCALE_MIN_VALUE, + opacity: + widget.opacityMinValue ?? ScaleTapConfig.opacityMinValue ?? _DEFAULT_OPACITY_MIN_VALUE, duration: widget.duration ?? ScaleTapConfig.duration ?? _DEFAULT_DURATION, ); @@ -152,8 +141,7 @@ class _ScaleTapState extends State @override Widget build(BuildContext context) { - final bool isTapEnabled = - widget.onPressed != null || widget.onLongPress != null; + final bool isTapEnabled = widget.onPressed != null || widget.onLongPress != null; return AnimatedBuilder( animation: _animationController, @@ -219,9 +207,10 @@ class ScaleTapIgnore extends StatelessWidget { c.ignoredAreaPressing = true; }, child: GestureDetector( - child: child, + behavior: HitTestBehavior.opaque, onTap: () {}, onLongPress: () {}, + child: child, ), ); } diff --git a/lib/components/page_card.dart b/lib/components/page_card.dart index 84b3986..04e2d7e 100755 --- a/lib/components/page_card.dart +++ b/lib/components/page_card.dart @@ -11,8 +11,7 @@ import 'package:skeletons/skeletons.dart'; import '../styles.dart'; -typedef AddFavoriteCallback = Future Function( - PageInfo pageInfo, bool localIsFavorite, bool showToast); +typedef AddFavoriteCallback = Future Function(PageInfo pageInfo, bool localIsFavorite); typedef PageInfoCallback = Future Function(PageInfo pageInfo); @@ -41,7 +40,7 @@ class PageCard extends StatelessWidget { Future handleFavoriteClick(bool localIsFavorite) async { if (pageInfo != null && onSetFavorite != null) { - return await onSetFavorite!.call(pageInfo!, localIsFavorite, false); + return await onSetFavorite!.call(pageInfo!, !localIsFavorite); } else { return false; } @@ -49,13 +48,13 @@ class PageCard extends StatelessWidget { Future handleAddFavoriteMenuItemClick() async { if (pageInfo != null && onSetFavorite != null) { - await onSetFavorite!.call(pageInfo!, true, true); + await onSetFavorite!.call(pageInfo!, true); } } Future handleRemoveFavoriteMenuItemClick() async { 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( title: '收藏', icon: CupertinoIcons.heart, - onTap: handleRemoveFavoriteMenuItemClick, + onTap: handleAddFavoriteMenuItemClick, ), const PullDownMenuDivider(), PullDownMenuItem( diff --git a/lib/components/recent_page_list.dart b/lib/components/recent_page_list.dart index 104bf93..13ba8e7 100755 --- a/lib/components/recent_page_list.dart +++ b/lib/components/recent_page_list.dart @@ -132,12 +132,11 @@ class RecentPageList extends StatelessWidget { } Widget _buildPageCard( - int index, PageInfo pageInfo, RecentPageListController c, FavoriteListController fc) { + int index, PageInfo pageInfo, RecentPageListController c, FavoriteListController flc) { return PageCard( - key: ValueKey("rpl-card-$index"), pageInfo: c.pageList[index], - isFavorite: fc.isFavorite(c.pageList[index]), - onSetFavorite: fc.setFavorite, + isFavorite: flc.isFavorite(c.pageList[index]), + onSetFavorite: flc.setFavoriteImmediate, ); } @@ -161,7 +160,7 @@ class RecentPageList extends StatelessWidget { // 后续页数据 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) { // 显示加载动画 return Padding( diff --git a/lib/main.dart b/lib/main.dart index 93979f3..2379a20 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -64,8 +64,8 @@ Future postInit() async { Future main() async { await init(); - Get.put(LifeCycleController()); Get.put(SiteConfigController()); + Get.put(LifeCycleController()); runApp(const IsekaiWikiApp()); diff --git a/lib/models/favorite_list.dart b/lib/models/favorite_list.dart index 02c6ca0..92175a6 100644 --- a/lib/models/favorite_list.dart +++ b/lib/models/favorite_list.dart @@ -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:isekai_wiki/api/mw/watch.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/utils/dialog.dart'; +import 'package:isekai_wiki/utils/error.dart'; class FavoriteListController extends GetxController { - var pageIds = RxList(); + bool syncRunning = false; + + var favPageIds = RxList(); void updateFromPageList(List list) { List 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) { if (!newPageIds.contains(pageId)) { newPageIds.add(pageId); } } - pageIds.value = newPageIds; + favPageIds.value = newPageIds; } void updateFromWatchList(List list) { @@ -36,29 +44,103 @@ class FavoriteListController extends GetxController { } for (var pageId in addList) { - if (!pageIds.contains(pageId)) { - pageIds.add(pageId); + if (!favPageIds.contains(pageId)) { + favPageIds.add(pageId); } } } bool isFavorite(PageInfo pageInfo) { - return pageIds.contains(pageInfo.pageid); + return favPageIds.contains(pageInfo.pageid); } - Future setFavorite(PageInfo pageInfo, bool isFavorite, bool showToast) async { + Future setFavorite(PageInfo pageInfo, bool isFavorite, bool showToast) async { + syncRunning = true; + List 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 setFavoriteImmediate(PageInfo pageInfo, bool isFavorite) async { // 如果未登录,则提示需要登录 var uc = Get.find(); if (!uc.isLoggedIn) { alert(Get.overlayContext!, "使用收藏功能需要登录", title: "提示"); - return isFavorite; + return !isFavorite; } - if (isFavorite) { - // 添加收藏 - } else { - // 删除收藏 + if (syncRunning) { + alert(Get.overlayContext!, "正在同步收藏列表,请稍后再试", title: "提示"); + return !isFavorite; } - return !isFavorite; + + setFavorite(pageInfo, isFavorite, true); + + // 立即在UI上显示收藏动画 + return isFavorite; } } diff --git a/lib/models/site_config.dart b/lib/models/site_config.dart index 47736a2..9b52a32 100644 --- a/lib/models/site_config.dart +++ b/lib/models/site_config.dart @@ -137,6 +137,7 @@ class SiteConfigController extends GetxController { loadFromEntity(siteConfigData); isAppActive = true; + Global.isAppActive = true; saveToStorage(); } } diff --git a/pubspec.lock b/pubspec.lock index 7a4bd1d..2313a3c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dependency: "direct main" description: @@ -385,6 +392,13 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.1.2" freezed: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1a45b4a..5d5cd36 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,8 +31,8 @@ environment: dependencies: flutter: sdk: flutter - flutter_localizations: # Add this line - sdk: flutter # Add this line + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -48,6 +48,8 @@ dependencies: like_button: ^2.0.4 skeletons: ^0.0.3 modal_bottom_sheet: ^2.1.2 + fluttertoast: ^8.1.2 + animated_snack_bar: ^0.3.0 responsive_builder: ^0.4.3 url_launcher: ^6.1.7 flutter_web_browser: ^0.17.1