import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:isekai_wiki/api/response/page_info.dart'; import 'package:isekai_wiki/components/flutter_scale_tap/flutter_scale_tap.dart'; import 'package:isekai_wiki/components/isekai_text.dart'; import 'package:isekai_wiki/components/utils.dart'; import 'package:isekai_wiki/pages/wiki/info.dart'; import 'package:isekai_wiki/pages/wiki/view.dart'; import 'package:like_button/like_button.dart'; import 'package:pull_down_button/pull_down_button.dart'; import 'package:skeletons/skeletons.dart'; typedef AddFavoriteCallback = Future Function( PageInfo pageInfo, bool localIsFavorite); typedef PageInfoCallback = Future Function(PageInfo pageInfo); class PageCardStyles { static const cardContainerHeight = 100.0; static const cardContentHeight = 100.0; static const cardHeaderPadding = EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 16); static const cardHeaderCompactPadding = EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 10); static const cardContentPadding = EdgeInsets.only(top: 0, left: 20, right: 20, bottom: 14); static const cardFooterPadding = EdgeInsets.only(top: 0, left: 20, right: 20, bottom: 14); static const titleFontSize = 24.0; static const subTitleFontSize = 12.0; static const contentFontSize = 14.0; static const TextStyle titleTextStyle = TextStyle( fontSize: titleFontSize, fontStyle: FontStyle.normal, fontWeight: FontWeight.w700, height: 1.2, ); static const TextStyle subTitleTextStyle = TextStyle( color: Color.fromRGBO(102, 102, 102, 1), fontSize: subTitleFontSize, fontStyle: FontStyle.normal, fontWeight: FontWeight.w700, height: 1.2, ); static const TextStyle contentTextStyle = TextStyle( color: Color.fromRGBO(102, 102, 102, 1), fontSize: contentFontSize, fontStyle: FontStyle.normal, fontWeight: FontWeight.normal, height: 1.4); static const double footerButtonSize = 30; static const double footerButtonInnerSize = 26; } class PageCard extends StatelessWidget { final bool isLoading; final PageInfo? pageInfo; final bool isFavorite; final AddFavoriteCallback? onSetFavorite; final PageInfoCallback? onShare; const PageCard({ super.key, this.isLoading = false, this.pageInfo, this.isFavorite = false, this.onSetFavorite, this.onShare, }); Future handleFavoriteClick(bool localIsFavorite) async { if (pageInfo != null && onSetFavorite != null) { return await onSetFavorite!.call(pageInfo!, !localIsFavorite); } else { return false; } } Future handleAddFavoriteMenuItemClick() async { if (pageInfo != null && onSetFavorite != null) { await onSetFavorite!.call(pageInfo!, true); } } Future handleRemoveFavoriteMenuItemClick() async { if (pageInfo != null && onSetFavorite != null) { await onSetFavorite!.call(pageInfo!, false); } } handleShareClick() async { if (pageInfo != null && onShare != null) { await onShare!.call(pageInfo!); } } void handlePageInfoClick() async { if (pageInfo != null) { var cPageInfo = pageInfo!; await Navigator.of(Get.context!).push( CupertinoPageRoute( builder: (_) => WikiInfoPage( targetPage: cPageInfo.title, targetPageId: cPageInfo.pageid, ), ), ); } } Future handleCardClick() async { if (isLoading) { return; } if (pageInfo != null) { var cPageInfo = pageInfo!; await Navigator.of(Get.context!).push( CupertinoPageRoute( builder: (_) => WikiViewPage( targetPage: cPageInfo.title, targetPageId: cPageInfo.pageid, initialArticleData: MinimumArticleData( title: cPageInfo.mainTitle, description: cPageInfo.description, mainCategory: cPageInfo.mainCategory, updateTime: cPageInfo.updatedTime, ), ), ), ); } } Widget _actionButtonSkeleton(bool isLoading, Widget child) { return Skeleton( isLoading: isLoading, skeleton: const SkeletonLine( style: SkeletonLineStyle( width: PageCardStyles.footerButtonSize, height: PageCardStyles.footerButtonSize), ), child: child, ); } Widget _buildCardHeader(BuildContext context) { var textScale = MediaQuery.of(context).textScaleFactor; return Padding( padding: pageInfo?.subtitle == null ? PageCardStyles.cardHeaderPadding : PageCardStyles.cardHeaderCompactPadding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (pageInfo?.subtitle != null) Text( pageInfo!.subtitle!, style: PageCardStyles.subTitleTextStyle, textScaleFactor: max(1, MediaQuery.of(context).textScaleFactor), ), if (pageInfo?.subtitle != null) const SizedBox(height: 6), Skeleton( isLoading: isLoading, skeleton: SkeletonLine( style: SkeletonLineStyle( height: (PageCardStyles.titleFontSize * 1.1) * textScale, randomLength: true), ), child: IsekaiText( pageInfo?.mainTitle ?? "页面信息丢失", style: PageCardStyles.titleTextStyle, textScaleFactor: 1, ), ), ], ), ); } Widget _buildCardBody(BuildContext context) { double textScale = max(1, MediaQuery.of(context).textScaleFactor); return Expanded( child: Padding( padding: PageCardStyles.cardContentPadding, child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: 1, child: isLoading ? ClipRect( clipBehavior: Clip.antiAlias, child: SkeletonParagraph( style: SkeletonParagraphStyle( lines: 3, padding: EdgeInsets.symmetric( vertical: PageCardStyles.contentFontSize * textScale * 0.2, horizontal: 0, ), lineStyle: SkeletonLineStyle( randomLength: true, height: PageCardStyles.contentFontSize * textScale), ), ), ) : Text( pageInfo?.description ?? "没有简介", overflow: TextOverflow.fade, style: PageCardStyles.contentTextStyle, textScaleFactor: textScale, ), ), const SizedBox(width: 10), Skeleton( isLoading: isLoading, skeleton: const SkeletonAvatar( style: SkeletonAvatarStyle(width: 114, height: 114), ), child: Container(), ), ], ), ), ), ); } Widget _buildCardFooter(BuildContext context) { return ScaleTapIgnore( child: Padding( padding: PageCardStyles.cardFooterPadding, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // 分类信息 pageInfo?.mainCategory != null ? Chip( backgroundColor: const Color.fromARGB(1, 238, 238, 238), label: Text(pageInfo!.mainCategory!, style: const TextStyle(color: Colors.black54)), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap) : const SizedBox(), pageInfo?.mainCategory != null ? const SizedBox(width: 10) : const SizedBox(), // 发布日期 Skeleton( isLoading: isLoading, skeleton: const SkeletonLine( style: SkeletonLineStyle(width: 100), ), child: Text( pageInfo?.updatedTime != null ? Utils.getFriendDate(pageInfo!.updatedTime!) : "", overflow: TextOverflow.fade, style: PageCardStyles.contentTextStyle, textScaleFactor: max(1, MediaQuery.of(context).textScaleFactor), ), ), const Spacer(), // 收藏键 _actionButtonSkeleton( isLoading, LikeButton( size: PageCardStyles.footerButtonInnerSize, isLiked: isFavorite, onTap: handleFavoriteClick, likeBuilder: (bool isLiked) { return Icon( isLiked ? Icons.favorite : Icons.favorite_border, color: isLiked ? Colors.red : Colors.grey, size: PageCardStyles.footerButtonInnerSize, ); }, ), ), const SizedBox(width: 18), // 菜单键 _actionButtonSkeleton( isLoading, SizedBox( height: PageCardStyles.footerButtonSize, width: PageCardStyles.footerButtonSize, child: PullDownButton( routeTheme: const PullDownMenuRouteTheme( endShadow: BoxShadow( color: Colors.black26, spreadRadius: 1, blurRadius: 20, offset: Offset(0, 2), ), ), itemBuilder: _buildMenuItem, position: PullDownMenuPosition.under, buttonBuilder: (context, showMenu) => IconButton( onPressed: showMenu, padding: const EdgeInsets.all(0.0), splashRadius: PageCardStyles.footerButtonInnerSize - 4, iconSize: PageCardStyles.footerButtonInnerSize, icon: const Icon( Icons.more_horiz, color: Colors.grey, ), ), ), ), ), ], ), ), ); } List _buildMenuItem(BuildContext context) { return [ isFavorite ? PullDownMenuItem( title: '取消收藏', icon: CupertinoIcons.heart_fill, onTap: handleRemoveFavoriteMenuItemClick, ) : PullDownMenuItem( title: '收藏', icon: CupertinoIcons.heart, onTap: handleAddFavoriteMenuItemClick, ), const PullDownMenuDivider(), PullDownMenuItem( title: '分享', icon: CupertinoIcons.share, onTap: handleShareClick, ), const PullDownMenuDivider.large(), /* PullDownMenuItem( title: '相似推荐', onTap: () {}, icon: CupertinoIcons.ellipsis_circle, ), const PullDownMenuDivider.large(), */ PullDownMenuItem( title: '页面详情', onTap: handlePageInfoClick, icon: CupertinoIcons.info_circle, ), ]; } Widget _buildCard(BuildContext context) { return Container( margin: Theme.of(context).cardTheme.margin, clipBehavior: Theme.of(context).cardTheme.clipBehavior ?? Clip.antiAlias, decoration: BoxDecoration( color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Theme.of(context).cardTheme.shadowColor ?? Colors.transparent, blurRadius: 12, offset: const Offset(0, 2), ), ], ), child: SizedBox( height: PageCardStyles.cardContainerHeight + (PageCardStyles.cardContentHeight * max(1, MediaQuery.of(context).textScaleFactor)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题 _buildCardHeader(context), // 简介、图片 _buildCardBody(context), // Footer _buildCardFooter(context), ], ), ), ); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ScaleTap( enableFeedback: isLoading, onPressed: handleCardClick, child: _buildCard(context), ), ); } }