Files
fanuzu/app/lib/features/planet/widgets/planet_detail_hero.dart
T

266 lines
9.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import '../../../core/ui/hero_time_status.dart';
import 'package:flutter/material.dart';
import '../../../core/theme/tokens.dart';
import '../../../core/ui/live_planet.dart';
import '../../../core/ui/planet_25d.dart' show planetFallbackImage;
import '../../../core/data/planet_visual_state.dart';
import '../../home/home_screen.dart' show formatPop;
import '../../../core/i18n/app_locale.dart';
import 'fandom_size_badge.dart';
/// 행성 상세 히어로 — 시안 `373:441117` 의 `Frame 1984081117`(375×543).
///
/// 우주 배경 사진(`373:441126` IMAGE 채움) 위, 상단 104(상태 표시줄 48 + 내비 56)
/// 아래 영역에 개설자(아바타 26 · ABeeZee 16), 행성 일러스트 200, 규모 칩과 멤버 수,
/// 행성 이름 ABeeZee 46, 「FPI : n | 1인당 화력 : n」, 「가입중」 알약을 25 · 12 간격으로
/// 쌓는다. 뒤로가기 · 더보기는 화면의 앱 바가 그린다(스크롤하면 앱 바만 남는다).
class PlanetDetailHero extends StatelessWidget {
const PlanetDetailHero({
super.key,
required this.name,
required this.members,
required this.totalFpi,
required this.joined,
this.nebula = false,
this.onHeroParticipate,
this.onJoin,
this.isOwner = false,
this.onOpenHistory,
this.fandomId = '',
this.watchPlanet = noPlanetWatch,
this.stage,
this.ownerName,
this.ownerPhoto,
this.onOpenOwner,
});
/// 개설자 줄을 눌렀을 때 — Figma `544:19765` 「프로필 및 닉네임 클릭 시 해당 사용자의 MY로 이동」.
/// 타인 프로필 화면(`549:21969`)이 생기면 연결한다(3차 MY). null 이면 누르지 않는다.
final VoidCallback? onOpenOwner;
final VoidCallback? onOpenHistory;
final String fandomId;
final PlanetWatch watchPlanet;
final int? stage;
final String name;
final int members;
final int totalFpi;
final bool joined;
final VoidCallback? onHeroParticipate;
/// 알약을 눌렀을 때 — 「가입하기」면 가입, 「가입중」이면 탈퇴 시트. null 이면 상태만 보인다.
final VoidCallback? onJoin;
/// 내가 만든 행성이면 가입 알약을 숨긴다(Figma `544:19765` 「가입 버튼 - 내가 생성했을 경우 hide」).
final bool isOwner;
/// 성운 상세(`373:442708`)는 규모 칩과 FPI 줄이 없다.
final bool nebula;
/// 개설자 표시명. 서버에 읽을 자리가 없으면 null 이고 줄을 그리지 않는다 —
/// 예전에는 시안의 `BTS_Maker` 가 모든 행성에 박혀 있었다(인계 결함).
final String? ownerName;
/// 개설자 사진(URL 또는 에셋). 없으면 회색 원.
final String? ownerPhoto;
static const double designHeight = 543;
/// 시안의 상태 표시줄 + 내비 높이. 본문은 그 아래 439 안에 가운데 놓인다.
static const double topBar = 104;
static TextStyle _body(double size, Color color) => TextStyle(
fontFamily: AppFonts.body,
fontSize: size,
fontWeight: FontWeight.w500,
height: 1,
color: color,
);
@override
Widget build(BuildContext context) {
// 1인당 화력 — 시안 「1.44」. Figma `544:19765` 정의(「모든 포인트 획득, 소비 활동 및 1인당 활동 값을
// 집계한 값」)와 FPI 원천(C-34 활동 기준)이 갈려 해석 확인 중(전수대조 영역A Q-A23). 회신 전 임시로 총 FPI ÷ 멤버 수.
final perHead = members == 0 ? 0.0 : totalFpi / members;
const info = Color(0xFFDEDEDE);
return Stack(
fit: StackFit.expand,
children: [
LivePlanetLighting(
fandomId: fandomId,
watch: watchPlanet,
child: Image.asset(
'assets/figma/bg-space-detail.png',
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [0, 0.7, 1],
colors: [
Color(0x00141414),
Color(0x22141414),
AppColors.cosmicVoid,
],
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, topBar, 16, 0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (ownerName case final owner?) ...[
GestureDetector(
onTap: onOpenOwner,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 13,
backgroundColor: const Color(0xFFD9D9D9),
backgroundImage: switch (ownerPhoto) {
final p? when p.startsWith('http') => NetworkImage(
p,
),
final p? => AssetImage(p),
null => null,
},
),
const SizedBox(width: 9),
Text(
owner,
style: const TextStyle(
fontFamily: AppFonts.display,
fontWeight: FontWeight.w400,
fontSize: 16,
height: 1.3,
letterSpacing: -0.4,
color: AppColors.onCosmic,
),
),
],
),
),
const SizedBox(height: 25),
],
LivePlanet25D(
onOpenHistory: onOpenHistory,
fandomId: fandomId,
watch: watchPlanet,
image: nebula
? 'assets/figma/planet-detail-hero.png'
: planetFallbackImage(fandomId),
size: 200,
glow: AppColors.neonViolet,
stage: stage,
),
HeroTimeStatus(
fandomId: fandomId,
watch: watchPlanet,
onParticipate: onHeroParticipate,
),
const SizedBox(height: 25),
// 규모 칩 + 멤버 수
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!nebula) ...[
// 소형 · 중형 · 대형 — 가입자 수 기준(Figma `544:19765`).
FandomSizeBadge(members: members, large: true),
const SizedBox(width: 4),
],
Image.asset(
'assets/figma/icon-person.png',
width: 20,
height: 20,
),
const SizedBox(width: 3),
Text(
formatPop(members),
style: _body(14, AppColors.onCosmic),
),
],
),
const SizedBox(height: 12),
Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: AppFonts.display,
fontWeight: FontWeight.w400,
fontSize: 46,
height: 1,
color: AppColors.onCosmic,
),
),
const SizedBox(height: 12),
if (!nebula) ...[
// 값이 커지면(백만 단위 FPI) 한 줄을 줄여 맞춘다.
FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'FPI : ${formatPop(totalFpi)}',
style: _body(14, info),
),
const SizedBox(width: 4),
Text('|', style: _body(14, info)),
const SizedBox(width: 4),
Text(
context.l10n.planetPerHead(
perHead.toStringAsFixed(2),
),
style: _body(14, info),
),
],
),
),
const SizedBox(height: 12),
],
if (!isOwner)
Semantics(
button: onJoin != null,
child: GestureDetector(
onTap: onJoin,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Text(
joined
? context.l10n.planetJoined
: context.l10n.planetJoin,
style: _body(
14,
const Color(0xFFD5D7D9),
).copyWith(letterSpacing: -0.35),
),
),
),
),
],
),
),
),
],
);
}
}