You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

386 lines
12 KiB
Dart

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/utils.dart';
import 'package:isekai_wiki/pages/article.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<bool> Function(PageInfo pageInfo, bool localIsFavorite);
typedef PageInfoCallback = Future<void> 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(
color: CupertinoDynamicColor.withBrightness(color: Colors.black, darkColor: Colors.white),
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<bool> handleFavoriteClick(bool localIsFavorite) async {
if (pageInfo != null && onSetFavorite != null) {
return await onSetFavorite!.call(pageInfo!, !localIsFavorite);
} else {
return false;
}
}
Future<void> handleAddFavoriteMenuItemClick() async {
if (pageInfo != null && onSetFavorite != null) {
await onSetFavorite!.call(pageInfo!, true);
}
}
Future<void> 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() {
if (pageInfo != null) {}
}
Future<void> handleCardClick() async {
if (isLoading) {
return;
}
if (pageInfo != null) {
var cPageInfo = pageInfo!;
await Navigator.of(Get.context!).push(
CupertinoPageRoute(
builder: (_) => ArticlePage(
targetPage: cPageInfo.title,
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: Text(
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: PullDownMenuRouteTheme(
endShadow: BoxShadow(
color: Colors.grey.withOpacity(0.6),
spreadRadius: 1,
blurRadius: 20,
offset: const 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<PullDownMenuEntry> _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) {
var textScale = MediaQuery.of(context).textScaleFactor;
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(8),
boxShadow: [
BoxShadow(
color: Theme.of(context).cardTheme.shadowColor ?? Colors.transparent,
blurRadius: 16,
offset: const Offset(0, 2),
blurStyle: BlurStyle.outer),
],
),
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),
),
);
}
}