diff --git a/lib/common/widgets/stat/stat.dart b/lib/common/widgets/stat/stat.dart index 24d6fa2b..b3a0263c 100644 --- a/lib/common/widgets/stat/stat.dart +++ b/lib/common/widgets/stat/stat.dart @@ -64,9 +64,11 @@ class StatView extends _StatItemBase { }) : super(iconSize: 13); @override - IconData get iconData => goto == 'picture' - ? Icons.remove_red_eye_outlined - : Icons.play_circle_outlined; + IconData get iconData => switch (goto) { + 'picture' => Icons.remove_red_eye_outlined, + 'like' => Icons.thumb_up_outlined, + _ => Icons.play_circle_outlined, + }; @override String get semanticsLabel => diff --git a/lib/http/api.dart b/lib/http/api.dart index 20b3684c..ad2a0b8a 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -735,4 +735,13 @@ class Api { static const String delPublishNote = '/x/note/publish/del'; static const String archiveNote = '/x/note/list/archive'; + + static const String favArticle = '/x/polymer/web-dynamic/v1/opus/feed/fav'; + + static const String communityAction = + '/x/community/cosmo/interface/simple_action'; + + static const String delFavArticle = '/x/article/favorites/del'; + + static const String addFavArticle = '/x/article/favorites/add'; } diff --git a/lib/http/user.dart b/lib/http/user.dart index 1b11702f..c592c1b5 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -468,6 +468,84 @@ class UserHttp { } } + static Future favArticle({ + required int page, + }) async { + var res = await Request().get(Api.favArticle, queryParameters: { + 'page_size': 20, + 'page': page, + }); + if (res.data['code'] == 0) { + return LoadingState.success(res.data['data']?['items']); + } else { + return LoadingState.error(res.data['message']); + } + } + + static Future addFavArticle({ + required int id, + }) async { + var res = await Request().post( + Api.addFavArticle, + data: { + 'id': id, + 'csrf': await Request.getCsrf(), + }, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future delFavArticle({ + required int id, + }) async { + var res = await Request().post( + Api.delFavArticle, + data: { + 'id': id, + 'csrf': await Request.getCsrf(), + }, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future communityAction({ + required dynamic opusId, + required dynamic action, + }) async { + var res = await Request().post( + Api.communityAction, + queryParameters: { + 'csrf': await Request.getCsrf(), + }, + data: { + "entity": { + "object_id_str": opusId, + "type": {"biz": 2} + }, + "action": action, // 3 fav, 4 unfav + }, + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + static Future favResourceList({ required int id, required int pn, diff --git a/lib/pages/fav/article/controller.dart b/lib/pages/fav/article/controller.dart new file mode 100644 index 00000000..806e08c0 --- /dev/null +++ b/lib/pages/fav/article/controller.dart @@ -0,0 +1,28 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/user.dart'; +import 'package:PiliPlus/pages/common/common_controller.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; + +class FavArticleController extends CommonController { + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + Future customGetData() => + UserHttp.favArticle(page: currentPage); + + void onRemove(index, id) async { + final res = await UserHttp.communityAction(opusId: id, action: 4); + if (res['status']) { + List list = (loadingState.value as Success).response; + list.removeAt(index); + loadingState.value = LoadingState.success(list); + SmartDialog.showToast('已取消收藏'); + } else { + SmartDialog.showToast(res['msg']); + } + } +} diff --git a/lib/pages/fav/article/view.dart b/lib/pages/fav/article/view.dart new file mode 100644 index 00000000..c37e8ed5 --- /dev/null +++ b/lib/pages/fav/article/view.dart @@ -0,0 +1,102 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/skeleton/video_card_h.dart'; +import 'package:PiliPlus/common/widgets/dialog.dart'; +import 'package:PiliPlus/common/widgets/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/fav/article/controller.dart'; +import 'package:PiliPlus/pages/fav/article/widget/item.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class FavArticlePage extends StatefulWidget { + const FavArticlePage({super.key}); + + @override + State createState() => _FavArticlePageState(); +} + +class _FavArticlePageState extends State + with AutomaticKeepAliveClientMixin { + final FavArticleController _favArticleController = + Get.put(FavArticleController()); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return refreshIndicator( + onRefresh: () async { + await _favArticleController.onRefresh(); + }, + child: CustomScrollView( + slivers: [ + Obx(() => _buildBody(_favArticleController.loadingState.value)), + ], + ), + ); + } + + Widget _buildBody(LoadingState loadingState) { + return switch (loadingState) { + Loading() => SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + mainAxisSpacing: 2, + maxCrossAxisExtent: Grid.mediumCardWidth * 2, + childAspectRatio: StyleString.aspectRatio * 2.2, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoCardHSkeleton(); + }, + childCount: 10, + ), + ), + Success() => (loadingState.response as List?)?.isNotEmpty == true + ? SliverPadding( + padding: EdgeInsets.only( + top: StyleString.safeSpace - 5, + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + mainAxisSpacing: 2, + maxCrossAxisExtent: Grid.mediumCardWidth * 2, + childAspectRatio: StyleString.aspectRatio * 2.6, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == loadingState.response.length - 1) { + _favArticleController.onLoadMore(); + } + return FavArticleItem( + item: loadingState.response[index], + onDelete: () { + showConfirmDialog( + context: context, + title: '确定取消收藏?', + onConfirm: () { + _favArticleController.onRemove( + index, + loadingState.response[index]['opus_id'], + ); + }); + }, + ); + }, + childCount: loadingState.response.length, + ), + ), + ) + : HttpError(callback: _favArticleController.onReload), + Error() => HttpError( + errMsg: loadingState.errMsg, + callback: _favArticleController.onReload, + ), + LoadingState() => throw UnimplementedError(), + }; + } +} diff --git a/lib/pages/fav/article/widget/item.dart b/lib/pages/fav/article/widget/item.dart new file mode 100644 index 00000000..a79dd921 --- /dev/null +++ b/lib/pages/fav/article/widget/item.dart @@ -0,0 +1,117 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/icon_button.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/stat/stat.dart'; +import 'package:PiliPlus/utils/app_scheme.dart'; +import 'package:flutter/material.dart'; + +class FavArticleItem extends StatelessWidget { + const FavArticleItem({ + super.key, + required this.item, + required this.onDelete, + }); + + final dynamic item; + final VoidCallback onDelete; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Stack( + clipBehavior: Clip.none, + children: [ + InkWell( + onTap: () { + PiliScheme.routePushFromUrl(item['jump_url']); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, + vertical: 5, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (item['cover'] != null) ...[ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (BuildContext context, + BoxConstraints boxConstraints) { + return NetworkImgLayer( + src: item['cover']['url'], + width: boxConstraints.maxWidth, + height: boxConstraints.maxHeight, + ); + }, + ), + ), + const SizedBox(width: 10), + ], + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item['content'], + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + Row( + children: [ + StatView( + context: context, + value: item['stat']['view'] == '' + ? 0 + : item['stat']['view'], + goto: 'picture', + textColor: Theme.of(context).colorScheme.outline, + ), + const SizedBox(width: 16), + StatView( + context: context, + goto: 'like', + value: item['stat']['like'] == '' + ? 0 + : item['stat']['like'], + textColor: Theme.of(context).colorScheme.outline, + ), + ], + ), + const SizedBox(height: 4), + Text( + '${item['author']['name']} · ${item['pub_time']}', + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + ), + ), + ], + ), + ), + ), + Positioned( + right: 12, + bottom: 0, + child: iconButton( + iconSize: 22, + context: context, + onPressed: onDelete, + icon: Icons.clear, + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, + bgColor: Colors.transparent, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/fav/pgc/widget/item.dart b/lib/pages/fav/pgc/widget/item.dart index 935156d9..318112f7 100644 --- a/lib/pages/fav/pgc/widget/item.dart +++ b/lib/pages/fav/pgc/widget/item.dart @@ -171,7 +171,7 @@ class FavPgcItem extends StatelessWidget { right: 12, bottom: 0, child: iconButton( - iconSize: 22, + iconSize: 20, context: context, onPressed: onUpdateStatus, icon: Icons.more_vert, diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart index 5c32cb01..5c851c93 100644 --- a/lib/pages/fav/view.dart +++ b/lib/pages/fav/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/fav/article/view.dart'; import 'package:PiliPlus/pages/fav/note/view.dart'; import 'package:PiliPlus/pages/fav/pgc/view.dart'; import 'package:PiliPlus/pages/fav/video/index.dart'; @@ -96,7 +97,7 @@ class _FavPageState extends State with SingleTickerProviderStateMixin { _FavType.video => const FavVideoPage(), _FavType.bangumi => const FavPgcPage(type: 1), _FavType.cinema => const FavPgcPage(type: 2), - _FavType.article => Center(child: Text(item.title)), + _FavType.article => const FavArticlePage(), _FavType.note => const FavNotePage(), }, ) diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 0a7fedc7..a7f712cc 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -182,9 +182,19 @@ class PiliScheme { return false; case 'opus': // bilibili://opus/detail/12345678?h5awaken=random - if (path.startsWith('/detail')) { - bool hasMatch = await _onPushDynDetail(path, off); - return hasMatch; + String? id = uriDigitRegExp.firstMatch(path)?.group(1); + if (id != null) { + Utils.toDupNamed( + '/htmlRender', + parameters: { + 'url': 'https://www.bilibili.com/opus/$id', + 'title': '', + 'id': id, + 'dynamicType': 'opus' + }, + off: off, + ); + return true; } return false; case 'search': @@ -459,7 +469,24 @@ class PiliScheme { } launchURL(); return false; - case 'opus' || 'dynamic': + case 'opus': + String? id = uriDigitRegExp.firstMatch(path)?.group(1); + if (id != null) { + Utils.toDupNamed( + '/htmlRender', + parameters: { + 'url': 'https://www.bilibili.com/opus/$id', + 'title': '', + 'id': id, + 'dynamicType': 'opus' + }, + off: off, + ); + return true; + } + launchURL(); + return false; + case 'dynamic': bool hasMatch = await _onPushDynDetail(path, off); if (hasMatch.not) { launchURL();