Figma 설명 프레임 반영 — 로그인·온보딩·스플래시 · HOME · GALAXY · 퀘스트·이클립스·알림

This commit is contained in:
Sn-Kinos
2026-10-06 03:03:20 +09:00
parent 64825e72b0
commit ecf5998725
77 changed files with 2703 additions and 640 deletions
+300 -103
View File
@@ -70,6 +70,9 @@ class GalaxyHomeScreen extends StatefulWidget {
class _GalaxyHomeScreenState extends State<GalaxyHomeScreen> {
late Future<GalaxySnapshot> _snapshot = widget.repository.load();
/// Figma `540:19580` 그리드 변경 영역 — 디폴트 격자, 누르면 격자 ↔ 리스트(`354:221533`).
bool _list = false;
/// 명세 §2.58-3 — *행성/성운 클릭 시 기본 행성 변경 안내 모달 노출*. 이미 대표면 궤도 맵으로.
Future<void> _choose(PlanetNode p) async {
if (p.isMine) return _openMap();
@@ -104,7 +107,14 @@ class _GalaxyHomeScreenState extends State<GalaxyHomeScreen> {
/// 만든 성운으로 바로 들어간다. 돌아오면 「내 행성」 그리드에 새 성운이 선다.
Future<void> _createNebula() async {
final navigator = Navigator.of(context, rootNavigator: true);
final myPop = (await _snapshot).myPop;
final snapshot = await _snapshot;
// Figma `540:19592` · `543:19609` — 일 1회. 오늘 이미 만들었으면 화면을 열지 않고 초과 안내 모달.
if (snapshot.nebulaCreatedToday) {
if (mounted)
await showAlertDialog(context, context.l10n.nebulaAlreadyToday);
return;
}
final myPop = snapshot.myPop;
final fandomId = await navigator.push(
MaterialPageRoute<String>(
builder: (_) =>
@@ -155,16 +165,14 @@ class _GalaxyHomeScreenState extends State<GalaxyHomeScreen> {
),
);
}
// 시안은 내 행성 셋을 보여 준다 — 가입하거나 만든 행성/성운, 최신 가입순(§2.58).
// 「선택」 알약은 대표 팬덤(C-15)이다.
final mine = data.memberships.take(3).toList();
// 가입하거나 만든 행성/성운 전부, 최신 가입순(Figma `540:19586`). 「선택」 알약은 대표 팬덤(C-15).
final mine = data.memberships;
return Stack(
children: [
ListView(
padding: const EdgeInsets.only(bottom: AppSpace.s9),
Column(
children: [
// 상단 — 시안 `Top Navigation`(54 · 좌우 16) 「GALAXY」 ABeeZee 20 · 검색 · 알림 42
// 상단 — 상단 고정 노출(Figma `540:19574`). 시안 `Top Navigation`(54 · 좌우 16) 「GALAXY」 ABeeZee 20 · 검색 · 알림 42
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
@@ -191,6 +199,8 @@ class _GalaxyHomeScreenState extends State<GalaxyHomeScreen> {
MaterialPageRoute<void>(
builder: (_) => GalaxySearchScreen(
repository: widget.repository,
onJoin: (p) =>
widget.planetRepository(p.id).join(),
onOpen: (p) =>
Navigator.of(
context,
@@ -226,73 +236,77 @@ class _GalaxyHomeScreenState extends State<GalaxyHomeScreen> {
],
),
),
// 이클립스 인앱 경보 — 별첨 1 ① 「푸시 미수신 단말 — 인앱
// 워닝 배너 병행」. 대상은 내 행성이다.
if (mine.where((p) => p.isMine).firstOrNull case final my?)
EclipseBanner(
repository: widget.eclipse,
fandomId: my.id,
margin: const EdgeInsets.fromLTRB(
16,
AppSpace.s3,
16,
0,
),
onTap: _openMap,
),
// 「내 행성」 ABeeZee 26 · 우측 목록 아이콘 24 → 궤도 맵 (줄 44 · 위아래 4)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Row(
Expanded(
child: ListView(
padding: const EdgeInsets.only(bottom: AppSpace.s9),
children: [
Text(
context.l10n.galaxyMyPlanets,
style: const TextStyle(
fontFamily: AppFonts.display,
fontWeight: FontWeight.w400,
fontSize: 26,
height: 1.4,
color: AppColors.onCosmic,
),
),
const Spacer(),
InkResponse(
onTap: _openMap,
radius: 20,
child: Image.asset(
'assets/figma/icon-list.png',
width: 24,
height: 24,
color: const Color(0xFF777777),
semanticLabel: context.l10n.galaxyMap,
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
for (final (i, p) in mine.indexed) ...[
if (i > 0) const SizedBox(width: AppSpace.s2),
Expanded(
child: _PlanetCard(
planet: p,
image: GalaxyHomeScreen.cardImageOf(p, i),
selected: p.isMine,
onTap: () => _choose(p),
// 이클립스 인앱 경보 — 별첨 1 ① 「푸시 미수신 단말 — 인앱
// 워닝 배너 병행」. 대상은 내 행성이다.
if (mine.where((p) => p.isMine).firstOrNull
case final my?)
EclipseBanner(
repository: widget.eclipse,
fandomId: my.id,
margin: const EdgeInsets.fromLTRB(
16,
AppSpace.s3,
16,
0,
),
onTap: _openMap,
),
],
// 시안은 항상 세 칸이다. 행성이 모자라면 빈 칸을 둔다.
for (var i = mine.length; i < 3; i++) ...[
if (i > 0) const SizedBox(width: AppSpace.s2),
const Expanded(child: _EmptySlot()),
],
// 「내 행성」 ABeeZee 26 · 우측 목록 아이콘 24 → 궤도 맵 (줄 44 · 위아래 4)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Row(
children: [
Text(
context.l10n.galaxyMyPlanets,
style: const TextStyle(
fontFamily: AppFonts.display,
fontWeight: FontWeight.w400,
fontSize: 26,
height: 1.4,
color: AppColors.onCosmic,
),
),
const Spacer(),
// 격자 ↔ 리스트(Figma `540:19580`). 궤도 맵은 선택된 카드를 눌러 연다.
InkResponse(
onTap: () => setState(() => _list = !_list),
radius: 20,
child: Semantics(
container: true,
button: true,
label: _list
? context.l10n.galaxyGridView
: context.l10n.galaxyListView,
child: _list
? const _GridIcon()
: Image.asset(
'assets/figma/icon-list.png',
width: 24,
height: 24,
color: const Color(0xFF777777),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: _list
? _PlanetList(
planets: mine,
imageOf: GalaxyHomeScreen.cardImageOf,
onTap: _choose,
)
: _PlanetGrid(planets: mine, onTap: _choose),
),
],
),
),
@@ -303,7 +317,13 @@ class _GalaxyHomeScreenState extends State<GalaxyHomeScreen> {
left: 0,
right: 0,
bottom: 16,
child: Center(child: _NebulaPill(onTap: _createNebula)),
child: Center(
child: _NebulaPill(
onTap: _createNebula,
// 일 1회 생성 뒤 비활성화(Figma `540:19592`). 눌러도 초과 안내 모달만 뜬다.
disabled: data.nebulaCreatedToday,
),
),
),
],
);
@@ -426,44 +446,221 @@ class _EmptySlot extends StatelessWidget {
}
class _NebulaPill extends StatelessWidget {
const _NebulaPill({required this.onTap});
const _NebulaPill({required this.onTap, this.disabled = false});
final VoidCallback onTap;
final bool disabled;
@override
Widget build(BuildContext context) {
return Material(
color: const Color(0xFF19191C),
borderRadius: BorderRadius.circular(AppRadius.full),
child: InkWell(
onTap: onTap,
return Opacity(
opacity: disabled ? 0.4 : 1,
child: Material(
color: const Color(0xFF19191C),
borderRadius: BorderRadius.circular(AppRadius.full),
child: Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.full),
border: Border.all(color: AppColors.cosmicSurfaceRaised),
),
// 시안 — Iconset/badge 16 · 간격 6 · 「성운 생성」 14/500 #a3a2a0
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const ImageIcon(
AppIcons.badge,
size: 16,
color: AppColors.cosmicAccent,
),
const SizedBox(width: 6),
Text(
context.l10n.nebulaCreate,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14,
color: AppColors.onCosmicVariant,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.full),
child: Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.full),
border: Border.all(color: AppColors.cosmicSurfaceRaised),
),
// 시안 — Iconset/badge 16 · 간격 6 · 「성운 생성」 14/500 #a3a2a0
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const ImageIcon(
AppIcons.badge,
size: 16,
color: AppColors.cosmicAccent,
),
const SizedBox(width: 6),
Text(
context.l10n.nebulaCreate,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14,
color: AppColors.onCosmicVariant,
),
),
],
),
),
),
),
);
}
}
/// 격자 — 세 칸씩 줄을 채운다. 행성이 셋보다 적으면 빈 칸으로 한 줄을 맞춘다(시안 세 칸).
class _PlanetGrid extends StatelessWidget {
const _PlanetGrid({required this.planets, required this.onTap});
final List<PlanetNode> planets;
final ValueChanged<PlanetNode> onTap;
@override
Widget build(BuildContext context) {
final cells = planets.length < 3 ? 3 : (planets.length + 2) ~/ 3 * 3;
return Column(
children: [
for (var row = 0; row < cells; row += 3) ...[
if (row > 0) const SizedBox(height: AppSpace.s2),
Row(
children: [
for (var i = row; i < row + 3; i++) ...[
if (i > row) const SizedBox(width: AppSpace.s2),
Expanded(
child: i < planets.length
? _PlanetCard(
planet: planets[i],
image: GalaxyHomeScreen.cardImageOf(planets[i], i),
selected: planets[i].isMine,
onTap: () => onTap(planets[i]),
)
: const _EmptySlot(),
),
],
],
),
],
],
);
}
}
/// 리스트 그리드 — 시안 `354:221533`: 80×79 #2c2c2c r8 칸(그림 64) · 간격 15 · 이름 16/600,
/// 줄 사이 1px #2c2c2c. 선택된 행성은 칸 위에 「선택」 알약.
class _PlanetList extends StatelessWidget {
const _PlanetList({
required this.planets,
required this.imageOf,
required this.onTap,
});
final List<PlanetNode> planets;
final String Function(PlanetNode, [int]) imageOf;
final ValueChanged<PlanetNode> onTap;
@override
Widget build(BuildContext context) => Column(
children: [
for (final (i, p) in planets.indexed) ...[
if (i > 0)
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Divider(height: 1, thickness: 1, color: Color(0xFF2C2C2C)),
),
InkWell(
onTap: () => onTap(p),
child: Row(
children: [
SizedBox(
width: 80,
height: 79,
child: Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xFF2C2C2C),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(8),
child: PlanetArt(
fallback: imageOf(p, i),
planetType: p.stage == 0 ? null : p.planetType,
colorKey: p.colorKey,
semanticLabel: p.name,
),
),
),
),
if (p.isMine)
Positioned(
top: 0,
right: 0,
child: Container(
width: 32,
height: 24,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.cosmicAccent,
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Text(
context.l10n.commonSelect,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 12,
fontWeight: FontWeight.w600,
height: 1,
color: AppColors.onCosmic,
),
),
),
),
],
),
),
const SizedBox(width: 15),
Expanded(
child: Text(
p.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 22 / 16,
color: AppColors.onCosmic,
),
),
),
],
),
),
],
],
);
}
/// 리스트 상태의 「격자로」 아이콘 — 시안 9×9 r1 #777777 네 칸(20×20).
class _GridIcon extends StatelessWidget {
const _GridIcon();
@override
Widget build(BuildContext context) {
Widget sq() => Container(
width: 9,
height: 9,
decoration: BoxDecoration(
color: const Color(0xFF777777),
borderRadius: BorderRadius.circular(1),
),
);
return SizedBox.square(
dimension: 24,
child: Center(
child: SizedBox.square(
dimension: 20,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [sq(), sq()],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [sq(), sq()],
),
],
),