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.
356 lines
11 KiB
Dart
356 lines
11 KiB
Dart
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:isekai_wiki/reactive/reactive.dart';
|
|
import 'package:like_button/like_button.dart';
|
|
import 'package:pull_down_button/pull_down_button.dart';
|
|
import 'package:skeletons/skeletons.dart';
|
|
|
|
import '../styles.dart';
|
|
|
|
typedef AddFavoriteCallback = Future<bool> Function(
|
|
PageInfo pageInfo, bool localIsFavorite, bool showToast);
|
|
|
|
typedef PageInfoCallback = Future<void> Function(PageInfo pageInfo);
|
|
|
|
class PageCardStyles {
|
|
static const double cardInnerHeight = 150;
|
|
static const cardInnerPadding = EdgeInsets.only(top: 16, left: 20, right: 20, bottom: 12);
|
|
static const double footerButtonSize = 30;
|
|
static const double footerButtonInnerSize = 26;
|
|
}
|
|
|
|
class PageCardController extends GetxController {
|
|
var isLoading = false.obs;
|
|
|
|
var pageInfo = Rx<PageInfo?>(null);
|
|
|
|
var isFavorite = false.obs;
|
|
|
|
AddFavoriteCallback? onSetFavorite;
|
|
PageInfoCallback? onShare;
|
|
|
|
Future<bool> handleFavoriteClick(bool localIsFavorite) async {
|
|
if (pageInfo.value != null && onSetFavorite != null) {
|
|
return await onSetFavorite!.call(pageInfo.value!, localIsFavorite, false);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<void> handleAddFavoriteMenuItemClick() async {
|
|
if (pageInfo.value != null && onSetFavorite != null) {
|
|
await onSetFavorite!.call(pageInfo.value!, true, true);
|
|
}
|
|
}
|
|
|
|
Future<void> handleRemoveFavoriteMenuItemClick() async {
|
|
if (pageInfo.value != null && onSetFavorite != null) {
|
|
await onSetFavorite!.call(pageInfo.value!, false, true);
|
|
}
|
|
}
|
|
|
|
handleShareClick() async {
|
|
if (pageInfo.value != null && onShare != null) {
|
|
await onShare!.call(pageInfo.value!);
|
|
}
|
|
}
|
|
|
|
void handlePageInfoClick() {
|
|
if (pageInfo.value != null) {}
|
|
}
|
|
|
|
Future<void> handleCardClick() async {
|
|
if (isLoading.value) {
|
|
return;
|
|
}
|
|
|
|
if (pageInfo.value != null) {
|
|
var cPageInfo = pageInfo.value!;
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class PageCard extends StatefulWidget {
|
|
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,
|
|
});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _PageCardState();
|
|
}
|
|
|
|
class _PageCardState extends ReactiveState<PageCard> {
|
|
var c = PageCardController();
|
|
double textScale = 1;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void receiveProps() {
|
|
c.isLoading.value = widget.isLoading;
|
|
c.pageInfo.value = widget.pageInfo;
|
|
c.isFavorite.value = widget.isFavorite;
|
|
c.onSetFavorite = widget.onSetFavorite;
|
|
c.onShare = widget.onShare;
|
|
}
|
|
|
|
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) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: Skeleton(
|
|
isLoading: c.isLoading.value,
|
|
skeleton: SkeletonLine(
|
|
style: SkeletonLineStyle(
|
|
height: (Styles.pageCardTitle.fontSize! + 4) * textScale, randomLength: true),
|
|
),
|
|
child: Text(c.pageInfo.value?.mainTitle ?? "页面信息丢失", style: Styles.pageCardTitle),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCardBody(BuildContext context) {
|
|
return Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: IntrinsicHeight(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Expanded(
|
|
flex: 1,
|
|
child: c.isLoading.value
|
|
? ClipRect(
|
|
child: SkeletonParagraph(
|
|
style: SkeletonParagraphStyle(
|
|
lines: 3,
|
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
|
lineStyle: SkeletonLineStyle(
|
|
randomLength: true,
|
|
height: Styles.pageCardDescription.fontSize! * textScale),
|
|
),
|
|
),
|
|
)
|
|
: Text(c.pageInfo.value?.description ?? "没有简介",
|
|
overflow: TextOverflow.fade, style: Styles.pageCardDescription),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Skeleton(
|
|
isLoading: c.isLoading.value,
|
|
skeleton: const SkeletonAvatar(
|
|
style: SkeletonAvatarStyle(width: 114, height: 114),
|
|
),
|
|
child: Container(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCardFooter(BuildContext context) {
|
|
return ScaleTapIgnore(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
// 分类信息
|
|
c.pageInfo.value?.mainCategory != null
|
|
? Chip(
|
|
backgroundColor: const Color.fromARGB(1, 238, 238, 238),
|
|
label: Text(c.pageInfo.value!.mainCategory!,
|
|
style: const TextStyle(color: Colors.black54)),
|
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap)
|
|
: const SizedBox(),
|
|
c.pageInfo.value?.mainCategory != null ? const SizedBox(width: 10) : const SizedBox(),
|
|
// 发布日期
|
|
Skeleton(
|
|
isLoading: c.isLoading.value,
|
|
skeleton: const SkeletonLine(
|
|
style: SkeletonLineStyle(width: 100),
|
|
),
|
|
child: Text(
|
|
c.pageInfo.value?.updatedTime != null
|
|
? Utils.getFriendDate(c.pageInfo.value!.updatedTime!)
|
|
: "",
|
|
style: Styles.pageCardDescription),
|
|
),
|
|
const Spacer(),
|
|
// 收藏键
|
|
Obx(
|
|
() => _actionButtonSkeleton(
|
|
c.isLoading.value,
|
|
LikeButton(
|
|
size: PageCardStyles.footerButtonInnerSize,
|
|
isLiked: c.isFavorite.value,
|
|
onTap: c.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(
|
|
c.isLoading.value,
|
|
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: (context) => [
|
|
c.isFavorite.value
|
|
? PullDownMenuItem(
|
|
title: '取消收藏',
|
|
icon: CupertinoIcons.heart_fill,
|
|
onTap: c.handleRemoveFavoriteMenuItemClick,
|
|
)
|
|
: PullDownMenuItem(
|
|
title: '收藏',
|
|
icon: CupertinoIcons.heart,
|
|
onTap: c.handleRemoveFavoriteMenuItemClick,
|
|
),
|
|
const PullDownMenuDivider(),
|
|
PullDownMenuItem(
|
|
title: '分享',
|
|
icon: CupertinoIcons.share,
|
|
onTap: c.handleShareClick,
|
|
),
|
|
const PullDownMenuDivider.large(),
|
|
/*
|
|
PullDownMenuItem(
|
|
title: '相似推荐',
|
|
onTap: () {},
|
|
icon: CupertinoIcons.ellipsis_circle,
|
|
),
|
|
const PullDownMenuDivider.large(),
|
|
*/
|
|
PullDownMenuItem(
|
|
title: '页面详情',
|
|
onTap: c.handlePageInfoClick,
|
|
icon: CupertinoIcons.info_circle,
|
|
),
|
|
],
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCard(BuildContext context) {
|
|
return Card(
|
|
elevation: 4.0,
|
|
// 圆角
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(Styles.isXs ? 0 : 14.0)),
|
|
),
|
|
// 抗锯齿
|
|
clipBehavior: Clip.antiAlias,
|
|
semanticContainer: false,
|
|
child: Padding(
|
|
padding: PageCardStyles.cardInnerPadding,
|
|
child: SizedBox(
|
|
height: PageCardStyles.cardInnerHeight * textScale,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 标题
|
|
_buildCardHeader(context),
|
|
// 简介、图片
|
|
_buildCardBody(context),
|
|
// Footer
|
|
_buildCardFooter(context),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget render(BuildContext context) {
|
|
textScale = MediaQuery.of(context).textScaleFactor;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
child: Obx(
|
|
() => ScaleTap(
|
|
enableFeedback: c.isLoading.value,
|
|
onPressed: c.handleCardClick,
|
|
child: GetBuilder<PageCardController>(
|
|
init: c,
|
|
builder: (GetxController c) => _buildCard(context),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|