新增:首页 Feed 接入真实数据——四态/尾部三态/聚合曝光埋点,社区 demo 消亡第一页(T3-14)
CI / flutter-gates (push) Successful in 2m41s
CI / flutter-gates (push) Successful in 2m41s
- home_page Feed segment 改接 CommunityController:骨架屏首载、空态 CTA、 错误横幅重试、下拉刷新(失败保留旧列表 + SnackBar)、触底游标翻页 (失败态只走显式重试,不随滚动风暴自动重打) - PostCard 三形态(单图通栏 4:3 / 多图折叠封面 +N 角标 / 纯文字 6 行)+ PostMediaGrid(列数规则 + ink 80% scrim 精算)+ LikeButton(error 族修订 Colors.red,T3-14 纯展示禁用态,ToggleSync 接线留 T3-15/16)+ FeedSkeleton (呼吸动效尊重减弱动态设置);降级作者统一「宠友」占位 - SignedNetworkImage:预签名 URL 缓存 key 剥离 X-Amz-* 签名参数, RemoteImage 全仓换用(同图不同签名命中同一缓存条目) - feed 域埋点:feed_viewed 聚合曝光(浏览段开/结算于切 Tab、切分段、 退后台、销毁;≥50% 可见 ≥500ms 段内按帖去重,postId 不上报)+ feed_load_failed(refresh/load_more 双路,会话失效不报) - integration_test/feed_live_test.dart:compose 六容器 Linux 桌面真链路 实测入口(PATBOND_FEED_LIVE=1 门控,默认跳过) - 测试 379 → 421(+42):四态/尾部三态/翻页不丢不重/降级作者/曝光结算 时机/缓存 key/事件属性全覆盖;analyze 0,format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||||
|
||||
/// Feed 骨架屏单元(05 号规范 §3.7):模拟单图卡——头部行(32 圆 +
|
||||
/// 两条横条)→ 4:3 通栏块 → 两条正文横条。块色 `surfaceTint`
|
||||
/// (1.18:1,装饰性占位不受对比度约束)。
|
||||
///
|
||||
/// 动效:整体不透明度 0.6 ↔ 1.0 呼吸循环 1200ms;系统「减弱动态效果」
|
||||
/// 开启时静止在 1.0。首载用法为连排 3 张(P1/P4)。
|
||||
class FeedSkeleton extends StatefulWidget {
|
||||
const FeedSkeleton({super.key});
|
||||
|
||||
@override
|
||||
State<FeedSkeleton> createState() => _FeedSkeletonState();
|
||||
}
|
||||
|
||||
class _FeedSkeletonState extends State<FeedSkeleton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
lowerBound: 0.6,
|
||||
value: 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reduceMotion = MediaQuery.of(context).disableAnimations;
|
||||
if (reduceMotion) {
|
||||
_controller.stop();
|
||||
_controller.value = 1;
|
||||
} else if (!_controller.isAnimating) {
|
||||
_controller.repeat(reverse: true);
|
||||
}
|
||||
return FadeTransition(
|
||||
opacity: _controller,
|
||||
child: Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
const _SkeletonBlock(width: 32, height: 32, circle: true),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_SkeletonBlock(
|
||||
width: _relative(context, 0.40),
|
||||
height: 12,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_SkeletonBlock(
|
||||
width: _relative(context, 0.24),
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const AspectRatio(
|
||||
aspectRatio: 4 / 3,
|
||||
child: ColoredBox(color: AppColors.surfaceTint),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_SkeletonBlock(width: _relative(context, 0.90), height: 12),
|
||||
const SizedBox(height: 8),
|
||||
_SkeletonBlock(width: _relative(context, 0.60), height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 横条宽度按屏宽比例取(规范 40%/24%/90%/60%),免依赖父约束测量。
|
||||
double _relative(BuildContext context, double fraction) =>
|
||||
MediaQuery.of(context).size.width * fraction;
|
||||
}
|
||||
|
||||
class _SkeletonBlock extends StatelessWidget {
|
||||
const _SkeletonBlock({
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.circle = false,
|
||||
});
|
||||
|
||||
final double width;
|
||||
final double height;
|
||||
final bool circle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceTint,
|
||||
borderRadius: BorderRadius.circular(circle ? width / 2 : 6),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||||
|
||||
/// 点赞 / 收藏按钮语义变体(05 号规范 §3.5:同一组件,图标与语义色
|
||||
/// 参数化)。点赞激活 = `error` 图标 + `errorDark` 计数(demo 的
|
||||
/// `Colors.red` 3.13:1 修订弃用,D6);收藏激活 = `accentDark` 同色。
|
||||
enum LikeButtonVariant {
|
||||
like(
|
||||
inactiveIcon: Icons.favorite_border,
|
||||
activeIcon: Icons.favorite,
|
||||
activeIconColor: AppColors.error,
|
||||
activeCountColor: AppColors.errorDark,
|
||||
),
|
||||
bookmark(
|
||||
inactiveIcon: Icons.bookmark_border,
|
||||
activeIcon: Icons.bookmark,
|
||||
activeIconColor: AppColors.accentDark,
|
||||
activeCountColor: AppColors.accentDark,
|
||||
);
|
||||
|
||||
const LikeButtonVariant({
|
||||
required this.inactiveIcon,
|
||||
required this.activeIcon,
|
||||
required this.activeIconColor,
|
||||
required this.activeCountColor,
|
||||
});
|
||||
|
||||
final IconData inactiveIcon;
|
||||
final IconData activeIcon;
|
||||
final Color activeIconColor;
|
||||
final Color activeCountColor;
|
||||
}
|
||||
|
||||
/// 点赞/收藏交互钮(05 号规范 §3.5 静态规格):图标 20 + 计数 13/w600,
|
||||
/// 未激活一律 `inkSoft`(6.59:1)。触控 44×44 由 padding 撑足。
|
||||
///
|
||||
/// T3-14 只做展示([onPressed] 传 null 即禁用态,仍按正常色渲染计数与
|
||||
/// 状态);乐观更新动画与 ToggleSync 接线属 T3-15/16。
|
||||
class LikeButton extends StatelessWidget {
|
||||
const LikeButton({
|
||||
required this.variant,
|
||||
required this.active,
|
||||
required this.count,
|
||||
super.key,
|
||||
this.onPressed,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final LikeButtonVariant variant;
|
||||
final bool active;
|
||||
final int count;
|
||||
final VoidCallback? onPressed;
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconColor = active ? variant.activeIconColor : AppColors.inkSoft;
|
||||
final countColor = active ? variant.activeCountColor : AppColors.inkSoft;
|
||||
return Semantics(
|
||||
label: semanticLabel,
|
||||
button: onPressed != null,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(AppRadius.pill),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
active ? variant.activeIcon : variant.inactiveIcon,
|
||||
size: 20,
|
||||
color: iconColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$count',
|
||||
style: TextStyle(
|
||||
color: countColor,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||||
import 'package:patbond_flutter/core/widgets/like_button.dart';
|
||||
import 'package:patbond_flutter/core/widgets/pet_avatar.dart';
|
||||
import 'package:patbond_flutter/core/widgets/post_media_grid.dart';
|
||||
import 'package:patbond_flutter/features/community/community_display.dart';
|
||||
import 'package:patbond_flutter/features/community/community_models.dart';
|
||||
import 'package:patbond_flutter/widgets/common.dart';
|
||||
|
||||
/// 帖子卡三形态(05 号规范 §2.1/§3.1):home 私有 `_PostCard` 的升级
|
||||
/// 迁移,按媒体形态分支——
|
||||
///
|
||||
/// | 形态 | 媒体区 |
|
||||
/// | --- | --- |
|
||||
/// | 单图(mediaCount ≤ 1 且有封面) | 通栏出血 4:3 |
|
||||
/// | 多图(mediaCount > 1) | [PostMediaGrid] 折叠封面(FeedCard 契约只带封面 + 计数),水平 padding 14 |
|
||||
/// | 纯文字(无封面) | 无媒体区,正文放宽 ≤6 行、15/1.6 |
|
||||
///
|
||||
/// 头部行:作者头像 32 + 名字 14/w700 + 时间 12 `inkSoft`;求助帖
|
||||
/// 元信息尾追加 `TagPill(accent)`。降级作者([AuthorSummary.isDegraded])
|
||||
/// 渲染占位头像 + 「宠友」。操作行:[LikeButton](点赞 / 收藏)+ 评论
|
||||
/// 计数 + 分享,各钮触控 44;互动回调传 null 即纯展示(T3-14 形态,
|
||||
/// ToggleSync 接线属 T3-15/16)。
|
||||
class PostCard extends StatelessWidget {
|
||||
const PostCard({
|
||||
required this.card,
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.onLikeTap,
|
||||
this.onCommentTap,
|
||||
this.onBookmarkTap,
|
||||
this.onShareTap,
|
||||
this.now,
|
||||
});
|
||||
|
||||
final FeedCard card;
|
||||
|
||||
/// 整卡点按(帖子详情入口)。
|
||||
final VoidCallback? onTap;
|
||||
|
||||
final VoidCallback? onLikeTap;
|
||||
final VoidCallback? onCommentTap;
|
||||
final VoidCallback? onBookmarkTap;
|
||||
final VoidCallback? onShareTap;
|
||||
|
||||
/// 相对时间的参考时钟(测试注入;缺省取当前时间)。
|
||||
final DateTime? now;
|
||||
|
||||
bool get _isTextOnly => card.coverImage == null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_header(context),
|
||||
if (card.coverImage != null)
|
||||
card.mediaCount > 1
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: PostMediaGrid(
|
||||
urls: [card.coverImage!.url],
|
||||
totalCount: card.mediaCount,
|
||||
),
|
||||
)
|
||||
: AspectRatio(
|
||||
aspectRatio: 4 / 3,
|
||||
child: RemoteImage(url: card.coverImage!.url),
|
||||
),
|
||||
_body(context),
|
||||
_actionRow(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _header(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
PetAvatar(size: PetAvatarSize.sm, url: card.author.avatarUrl),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
authorDisplayName(card.author),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
feedRelativeTime(card.publishedAt, now: now),
|
||||
style: const TextStyle(
|
||||
color: AppColors.inkSoft,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (card.category == PostCategory.help) ...[
|
||||
const SizedBox(width: 6),
|
||||
const TagPill('求助', color: AppColors.accent),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _body(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (card.title != null) ...[
|
||||
Text(
|
||||
card.title!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
Text(
|
||||
card.contentPreview,
|
||||
// 纯文字帖放宽至 6 行并升字号补偿视觉重量(§2.1)。
|
||||
maxLines: _isTextOnly ? 6 : 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _isTextOnly
|
||||
? const TextStyle(
|
||||
color: AppColors.ink,
|
||||
fontSize: 15,
|
||||
height: 1.6,
|
||||
)
|
||||
: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionRow() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
LikeButton(
|
||||
variant: LikeButtonVariant.like,
|
||||
active: card.likedByMe,
|
||||
count: card.likeCount,
|
||||
onPressed: onLikeTap,
|
||||
semanticLabel: '点赞',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_CommentCount(count: card.commentCount, onPressed: onCommentTap),
|
||||
const SizedBox(width: 4),
|
||||
LikeButton(
|
||||
variant: LikeButtonVariant.bookmark,
|
||||
active: card.bookmarkedByMe,
|
||||
count: card.bookmarkCount,
|
||||
onPressed: onBookmarkTap,
|
||||
semanticLabel: '收藏',
|
||||
),
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
onTap: onShareTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.pill),
|
||||
child: const SizedBox(
|
||||
width: 44,
|
||||
height: 44,
|
||||
child: Icon(
|
||||
Icons.ios_share_outlined,
|
||||
size: 20,
|
||||
color: AppColors.inkSoft,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 评论计数钮(与 LikeButton 同视觉规格,无激活态)。
|
||||
class _CommentCount extends StatelessWidget {
|
||||
const _CommentCount({required this.count, this.onPressed});
|
||||
|
||||
final int count;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(AppRadius.pill),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.chat_bubble_outline,
|
||||
size: 20,
|
||||
color: AppColors.inkSoft,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$count',
|
||||
style: const TextStyle(
|
||||
color: AppColors.inkSoft,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||||
import 'package:patbond_flutter/widgets/common.dart';
|
||||
|
||||
/// 图片九宫格展示态(05 号规范 §3.2)。
|
||||
///
|
||||
/// - 列数规则:2、4 图 → 2 列;3、5–9 图 → 3 列;全部 1:1 cover、
|
||||
/// 格间距 4、单格圆角 `sm`(12),`RemoteImage` 复用(loading tint 块 /
|
||||
/// 失败图标兜底,缓存 key 已剥签名)。
|
||||
/// - 超出折叠:最多显 9 格,[totalCount] 超过显示数时末格叠
|
||||
/// `ink` 80% scrim + 白字「+N」20/w800(合成最亮白图 7.10:1;60% 档
|
||||
/// 3.88:1 不达标弃用,§5.1 精算)。
|
||||
/// - 单图折叠形态:Feed 卡片契约只带封面 + mediaCount(FeedCard 裁剪),
|
||||
/// [urls] 单元素而 [totalCount] > 1 时渲染 4:3 单格 + 右下「+N」角标
|
||||
/// 胶囊(同 80% scrim 精算)。
|
||||
///
|
||||
/// 编辑态(「+」格 / 删除角标)随发布页工单(T3-17)扩展。
|
||||
class PostMediaGrid extends StatelessWidget {
|
||||
const PostMediaGrid({
|
||||
required this.urls,
|
||||
super.key,
|
||||
this.totalCount,
|
||||
this.onCellTap,
|
||||
}) : assert(urls.length > 0, 'PostMediaGrid 至少一张图');
|
||||
|
||||
/// 可展示的图片 URL(Feed 卡片仅封面一张;详情页全量)。
|
||||
final List<String> urls;
|
||||
|
||||
/// 实际总张数(缺省 = urls.length);大于可展示数时渲染「+N」。
|
||||
final int? totalCount;
|
||||
|
||||
/// 格子点按(全屏浏览入口,T3-15 详情页接线)。
|
||||
final ValueChanged<int>? onCellTap;
|
||||
|
||||
static const _maxCells = 9;
|
||||
static const _spacing = 4.0;
|
||||
|
||||
int get _effectiveTotal => totalCount ?? urls.length;
|
||||
|
||||
/// 列数规则(§3.2):2、4 → 2 列;其余 → 3 列(1 图不走网格)。
|
||||
static int columnsFor(int cellCount) =>
|
||||
(cellCount == 2 || cellCount == 4) ? 2 : 3;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (urls.length == 1) {
|
||||
return _CollapsedCover(
|
||||
url: urls.first,
|
||||
hiddenCount: _effectiveTotal - 1,
|
||||
onTap: onCellTap == null ? null : () => onCellTap!(0),
|
||||
);
|
||||
}
|
||||
|
||||
final cellCount = urls.length > _maxCells ? _maxCells : urls.length;
|
||||
final overflow = _effectiveTotal - cellCount;
|
||||
final columns = columnsFor(cellCount);
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: _spacing,
|
||||
crossAxisSpacing: _spacing,
|
||||
),
|
||||
itemCount: cellCount,
|
||||
itemBuilder: (context, index) {
|
||||
final isOverflowCell = overflow > 0 && index == cellCount - 1;
|
||||
Widget cell = RemoteImage(
|
||||
url: urls[index],
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
);
|
||||
if (isOverflowCell) {
|
||||
cell = Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
cell,
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.ink.withAlpha(204),
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'+$overflow',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (onCellTap != null) {
|
||||
cell = InkWell(
|
||||
onTap: () => onCellTap!(index),
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
child: cell,
|
||||
);
|
||||
}
|
||||
return cell;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 单图折叠形态:4:3 圆角封面 + 右下「+N」胶囊角标。
|
||||
class _CollapsedCover extends StatelessWidget {
|
||||
const _CollapsedCover({
|
||||
required this.url,
|
||||
required this.hiddenCount,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final String url;
|
||||
final int hiddenCount;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget cover = AspectRatio(
|
||||
aspectRatio: 4 / 3,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
RemoteImage(
|
||||
url: url,
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
if (hiddenCount > 0)
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.ink.withAlpha(204),
|
||||
borderRadius: BorderRadius.circular(AppRadius.pill),
|
||||
),
|
||||
child: Text(
|
||||
'+$hiddenCount',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (onTap != null) {
|
||||
cover = InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
child: cover,
|
||||
);
|
||||
}
|
||||
return cover;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user