HOME · GALAXY · 행성·성운 상세 · 팬덤 라운지 · 이클립스 화면

This commit is contained in:
Sn-Kinos
2026-09-29 17:06:48 +09:00
parent c4e9a9430e
commit 5de04ac281
44 changed files with 14898 additions and 16 deletions
@@ -0,0 +1,813 @@
import '../../../core/ui/planet_fx_scope.dart';
import '../../galaxy/galaxy_data.dart';
import '../../galaxy/zap_controller.dart';
import '../../galaxy/widgets/zap_button.dart';
import '../../galaxy/widgets/zap_feedback.dart';
import '../../../core/ui/live_stream.dart';
import '../../../core/ui/hero_time_status.dart';
import 'growth_conditions.dart';
import 'dart:math' as math;
import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import '../../../core/theme/tokens.dart';
import '../../../core/ui/app_icons.dart';
import '../../../core/ui/live_planet.dart';
import '../../../core/data/planet_visual_state.dart';
import '../home_data.dart';
import '../home_screen.dart' show formatPop;
import '../../../core/i18n/app_locale.dart';
/// 홈 상단 히어로 — Figma 사본 `252:159354` 홈의 `Frame 1984081117`.
///
/// 배치는 사본의 기본 상태 변형(`343:205364` 등 ZAP 이 닫힌 홈)을 따른다. 배경은 상태
/// 표시줄 포함 680 높이, 그 아래(124~680)에 「아바타·문구 → 48 → 행성 212 → 48 → 성장
/// 카드」를 세로 가운데로 쌓는다(`Frame 1984081116` · gap 48 · CENTER). 원형 ZAP 은 카드
/// 아래 변에 중심을 걸치므로 히어로 상자를 배경보다 24 길게 잡아 누를 자리를 넣는다.
///
/// 문구 규칙(Figma 명세) — *「{닉네임} 님의」 작게 · 「{POP} POP 기여로 / 꾸준하게
/// 성장하고 있어요.」 크게 두 줄*. 텍스트는 왼쪽 정렬, 행성은 가운데 정렬이다.
class HomeHero extends StatefulWidget {
const HomeHero({
super.key,
required this.growth,
this.fandomId = '',
this.watchPlanet = noPlanetWatch,
this.onHeroParticipate,
this.onOpenHistory,
this.onOpenPlanet,
this.zap,
this.zapData,
this.ticker,
this.onBirth,
this.onCreateNebula,
this.onJoin,
this.unjoined = false,
this.onItem,
this.miniMap,
});
final VoidCallback? onOpenHistory;
/// 성장 카드의 「BTS ›」 — 사본 흐름대로 행성 상세(F3)로 간다.
final VoidCallback? onOpenPlanet;
final String fandomId;
final PlanetWatch watchPlanet;
final VoidCallback? onHeroParticipate;
final PlanetGrowth growth;
final ZapController? zap;
final GalaxySnapshot? zapData;
/// 헤더 바로 아래(y102 · 높이 22) 라이브 띠. 없으면 자리도 비운다.
final Widget? ticker;
/// 성운 상태에서 성운을 누를 때 — 행성 생성(F1-04)으로 간다. 전환 조건(B-10)을 둘 다
/// 채웠을 때만 불린다.
final VoidCallback? onBirth;
/// 소속이 없는 첫 사용자 홈(F1-05)의 「+」 시트 — 행성 생성(성운) · 행성 가입.
final VoidCallback? onCreateNebula;
final VoidCallback? onJoin;
/// 소속 팬덤이 없는 첫 사용자 상태(`343:212939`) — [HomeSnapshot.fandomId] 가 빈 경우.
final bool unjoined;
/// ZAP 옵션 「아이템」 — 사본 「아이템 선택」 시트(`343:204868`).
final VoidCallback? onItem;
/// GALAXY 미니맵 — 행성 영역 우측 상단(해석 확인 요청서 Q-DS-06 권고안 A · 회신 전 임시).
/// 사본 홈에는 없는 요소라 행성 칸 모서리에만 얹고 문구·카드와 겹치지 않게 한다.
final Widget? miniMap;
/// 사본 기준 배경 높이(상태 표시줄 48 포함).
static const double designHeight = 680;
/// 배경 아래로 ZAP 원이 걸쳐 내려오는 몫. 다음 구획의 위 여백(24)과 같다.
static const double zapOverhang = 24;
@override
State<HomeHero> createState() => _HomeHeroState();
}
class _HomeHeroState extends State<HomeHero> {
final _fxKey = GlobalKey();
final _planetKey = GlobalKey();
final _buttonKey = GlobalKey();
PlanetGrowth get growth => widget.growth;
String get fandomId => widget.fandomId;
PlanetWatch get watchPlanet => widget.watchPlanet;
VoidCallback? get onHeroParticipate => widget.onHeroParticipate;
Offset _center(GlobalKey key) {
final box = key.currentContext?.findRenderObject();
final root = _fxKey.currentContext?.findRenderObject();
return box is RenderBox && box.hasSize && root is RenderBox
? box.localToGlobal(box.size.center(Offset.zero), ancestor: root)
: Offset.zero;
}
@override
Widget build(BuildContext context) {
// 사본 좌표는 상태 표시줄 48 을 포함한다. 기기 상단 여백으로 바꿔 쓴다.
final top = MediaQuery.paddingOf(context).top - 48;
final background = top + HomeHero.designHeight;
const planet = 212.0;
final hasZap =
widget.zap != null &&
widget.zapData?.joined == true &&
widget.zapData?.mine?.id == fandomId;
return PlanetFxScope(
child: SizedBox(
height: background + HomeHero.zapOverhang,
child: Stack(
key: _fxKey,
children: [
// 1. 우주 배경 — 시안 `252:159356`. 아래로 갈수록 화면 배경색으로 잠긴다.
Positioned(
top: 0,
left: 0,
right: 0,
height: background,
child: LivePlanetLighting(
fandomId: fandomId,
watch: watchPlanet,
child: Image.asset(
'assets/figma/bg-space-home.png',
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
),
// 2. 문구 · 행성 · 성장 카드 — `Frame 1984081116`(y124~680, gap 48).
// 사본은 가운데 정렬이지만 카드가 사본(151)보다 한 줄 길어 가운데로 두면
// 문구가 위로 밀린다. 사본 좌표(문구 y130 · 행성 y263)에 고정해 위에서 쌓는다.
Positioned(
top: top + 124,
left: AppSpace.s4,
right: AppSpace.s4,
height: HomeHero.designHeight - 124,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 첫 사용자 홈(`343:212939`)은 아바타가 y124, 나머지 상태는 y130.
if (!widget.unjoined) const SizedBox(height: 6),
_Contribution(growth: growth, joined: !widget.unjoined),
const SizedBox(height: AppSpace.s7),
// HERO TIME 안내·큰 글자처럼 아래가 길어지면 행성이 먼저 줄어든다.
Flexible(
child: LayoutBuilder(
builder: (context, box) => SizedBox(
width: double.infinity,
height: math.min(planet, box.maxHeight),
child: widget.unjoined
? _EmptyPlanet(
onCreateNebula: widget.onCreateNebula,
onJoin: widget.onJoin,
)
: growth.isNebula
? _Nebula(
key: _planetKey,
// 탄생은 창립자만(Q-PN-01 A안) — 다른 멤버에게는 안내 · 탭이 없다.
ready: growth.nebulaReady && growth.isFounder,
onBirth: widget.onBirth,
)
: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: LivePlanet25D(
onOpenHistory: widget.onOpenHistory,
key: _planetKey,
fandomId: fandomId,
watch: watchPlanet,
stage: growth.stage,
image: 'assets/figma/planet-home.png',
size: math.min(planet, box.maxHeight),
glow: AppColors.neonViolet,
),
),
if (widget.miniMap case final mini?)
Positioned(top: 0, right: 0, child: mini),
],
),
),
),
),
if (!widget.unjoined) ...[
const SizedBox(height: AppSpace.s7),
HeroTimeStatus(
fandomId: fandomId,
watch: watchPlanet,
onParticipate: hasZap ? null : onHeroParticipate,
),
_GrowthOverlayCard(
growth: growth,
onOpen: widget.onOpenPlanet,
zapClearance: hasZap,
),
],
],
),
),
if (widget.ticker case final ticker?)
Positioned(top: top + 102, left: 0, right: 0, child: ticker),
// 3. 원형 ZAP — 사본 `Frame 1984081111`. 중심이 카드 아래 변에 걸친다.
if (widget.zapData case final data?)
if (widget.zap case final zap?)
if (data.joined && data.mine?.id == fandomId)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: LiveStreamBuilder<PlanetVisualState>(
key: ValueKey(('home-zap', fandomId)),
create: () => watchPlanet(fandomId),
builder: (context, state) {
final hero =
state?.isHeroTime == true &&
state?.heroEndsAt?.isAfter(DateTime.now()) ==
true &&
onHeroParticipate != null &&
zap.pending == 0 &&
!zap.sending &&
!zap.needsRetry;
return Column(
key: _buttonKey,
mainAxisSize: MainAxisSize.min,
children: [
if (zap.needsRetry)
TextButton(
onPressed: zap.retry,
child: Text(context.l10n.zapRetrySame),
),
ZapButton(
round: true,
showLabel: !growth.isNebula,
onItem: widget.onItem,
zapPop: data.zapPop,
myPop:
(zap.balance ?? data.myPop) -
zap.pending * data.zapPop,
isHeroTime: hero,
blockedReason: zap.sending
? context.l10n.zapApplying
: zap.needsRetry
? context.l10n.zapNeedsCheck
: null,
onZap: () => hero
? onHeroParticipate!()
: zap.tap(fandomId),
),
],
);
},
),
),
if (widget.zap case final zap?)
Positioned.fill(
child: IgnorePointer(
child: ZapFeedback(
touchSerial: zap.touchSerial,
pending: zap.pending > 0 || zap.sending,
result: zap.result,
source: _center(_buttonKey),
target: _center(_planetKey),
radius:
(_planetKey.currentContext?.findRenderObject()
is RenderBox
? (_planetKey.currentContext!.findRenderObject()
as RenderBox)
.size
.shortestSide
: planet) *
0.4,
),
),
),
],
),
),
);
}
}
/// 「BTS_Hong 님의 / 1,230 POP 기여로 꾸준하게 성장하고 있어요.」 — `Frame 1984081113`.
///
/// 아바타 26 원 + 9 + 닉네임(ABeeZee 12 흰색) 2 「님의」(42dot 12 #A3A2A0), 3 아래 문구
/// (42dot Medium 20 · 행간 140%). 아바타 줄에는 검정 70% 4 그림자.
class _Contribution extends StatelessWidget {
const _Contribution({required this.growth, required this.joined});
final PlanetGrowth growth;
/// 소속 팬덤이 있는가. 없으면 첫 사용자 문구(`343:212939`)를 쓴다.
final bool joined;
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 성운·첫 사용자 상태의 「{닉네임} 님의」는 기여 여부와 무관한 인사 줄이다.
if (growth.hasRecentContribution ||
((!joined || growth.isNebula) &&
growth.contributorName.trim().isNotEmpty)) ...[
Row(
children: [
// 프로필 사진 자산이 데이터에 없어 이니셜로 그린다.
Container(
width: 26,
height: 26,
alignment: Alignment.center,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.cosmicSurfaceRaised,
boxShadow: [BoxShadow(color: Color(0xB3000000), blurRadius: 4)],
),
child: Text(
growth.contributorName.characters.firstOrNull ?? '—',
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.onCosmic,
),
),
),
const SizedBox(width: 9),
Text(
growth.contributorName,
style: const TextStyle(
fontFamily: AppFonts.display,
fontSize: 12,
height: 1.3,
letterSpacing: -0.3,
color: AppColors.onCosmic,
),
),
const SizedBox(width: 2),
Text(
context.l10n.heroOwnerSuffix,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.3,
letterSpacing: -0.3,
color: AppColors.onCosmicVariant,
),
),
],
),
const SizedBox(height: 3),
],
Text(
// 사본 문구의 「팬케이크」는 사용자 노출 브랜드라 「팬우주」로 바꾼다(공통 기준 4항).
!joined
? context.l10n.heroStartPrompt
: growth.isNebula
? context.l10n.heroNebulaPrompt
: growth.hasRecentContribution
? context.l10n.heroRecentContribution(formatPop(growth.contributedPop))
: context.l10n.heroGrowingTogether,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 20,
fontWeight: FontWeight.w500,
height: 1.4,
letterSpacing: -0.5,
color: AppColors.onCosmic,
),
),
],
);
}
/// 성장 카드 — 사본 기본 상태의 「BTS › / 행성 성장률 67% / 진행 바 / 6,270 / 10,000」
/// (`Frame 1984081073` 343 폭 · `#040304` 40% · r12 · 패딩 12/16 · 간격 12 · 유리 흐림 4).
///
/// 둘째 줄 「POP 진행률」과 바 아래 두 조건 줄은 정책값이다 — growthPercent 는 POP 진행률
/// 이고(전체 충족률 아님) 승급은 POP·sFPI 동시 충족이다(정책 설계서 §P-08 · UX·FX v1.5
/// §2.1). 말풍선 꼬리는 바 채움 끝을 가리킨다(사본 `Group 563` 가운데 = 채움 끝 x238).
class _GrowthOverlayCard extends StatelessWidget {
const _GrowthOverlayCard({
required this.growth,
this.onOpen,
this.zapClearance = false,
});
final PlanetGrowth growth;
final VoidCallback? onOpen;
/// 원형 ZAP(52)이 카드 아래 변에 걸친다. 둘째 조건 줄이 가려지지 않게 아래 여백을
/// ZAP 윗변(아래 변 − 28) 위로 올린다. ZAP 좌표는 사본 그대로 둔다.
final bool zapClearance;
static const _white14 = TextStyle(
fontFamily: AppFonts.body,
fontSize: 14,
fontWeight: FontWeight.w500,
height: 24 / 14,
color: AppColors.onCosmic,
);
@override
Widget build(BuildContext context) {
final ratio = growth.popRatio;
return ClipRRect(
borderRadius: BorderRadius.circular(AppRadius.md),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 2, sigmaY: 2),
child: Container(
width: double.infinity,
padding: EdgeInsets.fromLTRB(
AppSpace.s4,
AppSpace.s3,
AppSpace.s4,
zapClearance ? 32 : AppSpace.s3,
),
decoration: BoxDecoration(
color: const Color(0x66040304),
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: AppFx.glassStroke),
),
child: LayoutBuilder(
builder: (context, box) => Stack(
clipBehavior: Clip.none,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onOpen,
child: Row(
children: [
Expanded(
child: Text(
growth.fandomName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: AppFonts.display,
fontSize: 32,
height: 1.4,
letterSpacing: -0.8,
color: AppColors.onCosmic,
),
),
),
if (onOpen != null)
const ImageIcon(
AppIcons.chevronRight,
size: 24,
color: AppColors.onCosmicVariant,
),
],
),
),
Row(
children: [
Text(context.l10n.heroPopProgress, style: _white14),
const SizedBox(width: 4),
Text(
growth.growthPercent == null
? '—'
: '${growth.growthPercent}%',
style: _white14,
),
],
),
const SizedBox(height: AppSpace.s3),
_Bar(ratio: ratio ?? 0),
const SizedBox(height: AppSpace.s3),
GrowthConditions(
growth: growth,
style: _white14.copyWith(height: 16 / 14),
),
],
),
// 말풍선 — 꼬리 끝이 바 위 9.5 에 닿는다(사본 581.5+40 → 바 631).
// 성운 카드(`344:213809`)에는 말풍선이 없다.
if (!growth.isNebula)
Positioned(
top: 45 + 24 + AppSpace.s3 - 49.5,
left: (box.maxWidth * (ratio ?? 0.5)).clamp(
84.0,
box.maxWidth - 84,
),
child: FractionalTranslation(
translation: const Offset(-0.5, 0),
child: _SpeechBubble(text: growth.growthStatus),
),
),
],
),
),
),
),
);
}
}
/// 진행 바 — 10 높이 · 바탕 검정 30% · 채움 `#D496C4 → #FF7DDD`.
class _Bar extends StatelessWidget {
const _Bar({required this.ratio});
final double ratio;
@override
Widget build(BuildContext context) => Container(
height: 10,
decoration: BoxDecoration(
color: const Color(0x4D000000),
borderRadius: BorderRadius.circular(AppRadius.full),
),
alignment: Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: ratio,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.full),
gradient: const LinearGradient(
colors: [Color(0xFFD496C4), Color(0xFFFF7DDD)],
),
),
),
),
);
}
/// 말풍선 — 사본 `Union`(검정 80% · 본체 30 + 아래 꼬리 10) · 42dot Medium 12 흰색.
class _SpeechBubble extends StatelessWidget {
const _SpeechBubble({required this.text});
final String text;
static const _fill = Color(0xCC000000);
@override
Widget build(BuildContext context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.center,
decoration: BoxDecoration(
color: _fill,
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Text(
text,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1,
color: AppColors.onCosmic,
),
),
),
CustomPaint(
size: const Size(10, 10),
painter: _TailPainter(color: _fill),
),
],
);
}
class _TailPainter extends CustomPainter {
const _TailPainter({required this.color, this.up = false});
final Color color;
/// 위를 가리키는 꼬리(말풍선이 대상 아래에 있을 때).
final bool up;
@override
void paint(Canvas canvas, Size size) {
final path = up
? (Path()
..moveTo(0, size.height)
..lineTo(size.width, size.height)
..lineTo(size.width / 2, 0)
..close())
: (Path()
..moveTo(0, 0)
..lineTo(size.width, 0)
..lineTo(size.width / 2, size.height)
..close());
canvas.drawPath(path, Paint()..color = color);
}
@override
bool shouldRepaint(_TailPainter old) => old.color != color || old.up != up;
}
/// 성운 — 사본 `홈_성장 완료`(`344:213808` 212). 두 전환 조건을 채우면 아래 말풍선
/// 「성운을 클릭해서 / 행성으로 탄생 시켜보세요!」(`Group 563` 181×53 · 꼬리 위)가 뜨고
/// 누르면 행성 생성으로 간다. 채우기 전에는 눌러도 아무 일이 없다(B-10 동시 충족).
class _Nebula extends StatelessWidget {
const _Nebula({super.key, required this.ready, this.onBirth});
final bool ready;
final VoidCallback? onBirth;
@override
Widget build(BuildContext context) => Center(
child: SizedBox(
width: 212,
height: 212,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.topCenter,
children: [
// 사본 `Ellipse 40~45`(74×71 #D9D9D9) — 성운 뒤 왼쪽에 걸친 흰 원반.
Positioned(
left: -1,
top: 69.5,
child: Container(
width: 74,
height: 71,
decoration: const ShapeDecoration(
shape: OvalBorder(),
color: Color(0xFFD9D9D9),
),
),
),
Semantics(
button: ready && onBirth != null,
label: context.l10n.heroNebula,
child: GestureDetector(
onTap: ready ? onBirth : null,
child: Image.asset(
'assets/figma/nebula-home.png',
width: 212,
height: 212,
),
),
),
if (ready)
Positioned(
top: 177,
child: IgnorePointer(
child: Column(
children: [
CustomPaint(
size: const Size(12, 9),
painter: _TailPainter(
color: const Color(0xCC000000),
up: true,
),
),
Container(
width: 181,
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xCC000000),
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Text(
context.l10n.heroNebulaTap,
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: AppFonts.body,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.4,
color: AppColors.onCosmic,
),
),
),
],
),
),
),
],
),
),
);
}
/// 소속이 없는 첫 사용자 — 사본 `홈_초기사용자`(`Frame 1984081310`): 검정 원 190 안에
/// 검정 40 상자(r8 · 분홍 빛 4) + 「+」. 누르면 「행성 생성 (성운) / 행성 가입 / 취소」 시트.
class _EmptyPlanet extends StatelessWidget {
const _EmptyPlanet({this.onCreateNebula, this.onJoin});
final VoidCallback? onCreateNebula;
final VoidCallback? onJoin;
Future<void> _open(BuildContext context) async {
final pick = await showModalBottomSheet<VoidCallback>(
context: context,
useRootNavigator: true,
backgroundColor: Colors.transparent,
barrierColor: const Color(0xB3000000),
builder: (context) => _StartSheet(
options: [
(context.l10n.heroCreateNebula, onCreateNebula),
(context.l10n.heroJoinPlanet, onJoin),
],
),
);
pick?.call();
}
@override
Widget build(BuildContext context) => Center(
child: Container(
width: 190,
height: 190,
alignment: Alignment.center,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xB3000000), // `Ellipse 20` 검정 · 투명도 0.7
),
child: Semantics(
button: true,
label: context.l10n.heroCreateOrJoin,
child: GestureDetector(
onTap: () => _open(context),
// `Frame 1984081024` 에셋(40 상자 + 분홍 빛 4 여백 + 「+」).
child: Image.asset(
'assets/figma/home-plus-box.png',
width: 48,
height: 48,
),
),
),
),
);
}
/// 사본 `Frame 1984081055` — 좌우·아래 10 띄운 #2C2C2E r16 시트, 손잡이 36×4, 줄 높이 47.
class _StartSheet extends StatelessWidget {
const _StartSheet({required this.options});
final List<(String, VoidCallback?)> options;
static const _item = TextStyle(
fontFamily: AppFonts.body,
fontSize: 15,
fontWeight: FontWeight.w500,
height: 1.3,
color: AppColors.onCosmic,
);
@override
// 사본은 홈 인디케이터 위가 아니라 화면 아래에서 10 띄운다.
Widget build(BuildContext context) => SafeArea(
bottom: false,
child: Container(
margin: const EdgeInsets.fromLTRB(10, 0, 10, 10),
padding: const EdgeInsets.fromLTRB(AppSpace.s4, 12, AppSpace.s4, 12),
decoration: BoxDecoration(
color: AppColors.cosmicSheet,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: const Color(0xFF4F4F4F),
borderRadius: BorderRadius.circular(AppRadius.full),
),
),
),
const SizedBox(height: 8),
for (final (label, action) in options)
InkWell(
onTap: () => Navigator.of(context).pop(action),
child: SizedBox(
height: 47,
child: Align(
alignment: Alignment.centerLeft,
child: Text(label, style: _item),
),
),
),
InkWell(
onTap: () => Navigator.of(context).pop(),
child: SizedBox(
height: 47,
child: Center(child: Text(context.l10n.commonCancel, style: _item)),
),
),
],
),
),
);
}