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
+141 -96
View File
@@ -16,6 +16,7 @@ import 'package:go_router/go_router.dart';
import '../../core/format.dart';
import '../../core/i18n/app_locale.dart';
import '../../core/theme/tokens.dart';
import '../../core/ui/app_dialog.dart' show slideUpRoute;
import '../../core/ui/app_icons.dart';
import '../../core/data/planet_visual_state.dart';
import '../../core/ui/live_stream.dart';
@@ -167,7 +168,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
final created = await Navigator.of(
context,
rootNavigator: true,
).push(MaterialPageRoute<String>(builder: (_) => screen));
).push(slideUpRoute<String>((_) => screen));
if (created == null || !mounted) return;
if (widget.galaxyRepository case final repository?) {
setState(() {
@@ -287,7 +288,7 @@ class _LiveTickerSlot extends StatelessWidget {
}
/// 홈 헤더 — 성장 완료 · 첫 사용자 상태 화면도 같은 헤더를 쓴다.
class HomeHeader extends StatelessWidget {
class HomeHeader extends StatefulWidget {
const HomeHeader({
super.key,
required this.pop,
@@ -306,71 +307,116 @@ class HomeHeader extends StatelessWidget {
final List<ItemProduct> items;
@override
Widget build(BuildContext context) => Padding(
// 사본 `Top Navigation` — 높이 54 · 패딩 6/16 · 오른쪽 묶음은 알약 + 42 아이콘 두 개.
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4, vertical: 6),
child: SizedBox(
height: 42,
child: Row(
children: [
const Text(
'FANUZU',
style: TextStyle(
fontFamily: 'Pretendard',
fontSize: 20,
fontWeight: FontWeight.w900,
height: 1.4,
letterSpacing: -0.5,
color: AppColors.cosmicAccent,
),
),
const Spacer(),
_PopBalance(value: pop),
IconButton(
constraints: const BoxConstraints.tightFor(width: 42, height: 42),
// 사본 `icon` 42 칸 — 테마 최소 크기(48)로 늘어나 POP 알약이 왼쪽으로 밀리지 않게.
style: IconButton.styleFrom(
minimumSize: const Size.square(42),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
padding: EdgeInsets.zero,
icon: const Image(
image: AssetImage('assets/figma/icon-wallet.png'),
width: 42,
height: 42,
),
// 사본 `252:159367` — 홈 주 프레임에 열린 「아이템 구입」 시트.
onPressed: () => showItemSheet(
context,
items: items,
purchase: true,
fandomId: fandomId,
),
tooltip: context.l10n.homeItemsTooltip,
),
IconButton(
constraints: const BoxConstraints.tightFor(width: 42, height: 42),
// 사본 `icon` 42 칸 — 테마 최소 크기(48)로 늘어나 POP 알약이 왼쪽으로 밀리지 않게.
style: IconButton.styleFrom(
minimumSize: const Size.square(42),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
padding: EdgeInsets.zero,
// 사본 `icon` 42 칸 안 lucide/bell 20.
iconSize: 20,
icon: const ImageIcon(AppIcons.bell, color: AppColors.onCosmic),
// 알림 — 화면 대조 원장 F0 · `229:11265`.
onPressed: () => Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
builder: (_) => NotificationScreen(repository: notifications),
State<HomeHeader> createState() => _HomeHeaderState();
}
class _HomeHeaderState extends State<HomeHeader> {
/// 안 읽은 알림이 있는가 — 설명 프레임 `2097:16385` 「알림 - 알림 아이콘 / 알림 상태값 ·
/// 알림 존재 시 배찌 알림으로 변경」. 알림함은 닫힐 때 읽음 상태를 돌려주므로(`518:88347`)
/// 돌아오면 다시 읽는다.
bool _unread = false;
@override
void initState() {
super.initState();
_checkUnread();
}
Future<void> _checkUnread() async {
try {
final items = await widget.notifications.load();
if (mounted) setState(() => _unread = items.any((i) => !i.read));
} catch (_) {
// 읽지 못하면 배지를 띄우지 않는다 — 알림함 자체는 열린다.
}
}
@override
Widget build(BuildContext context) {
final pop = widget.pop;
final items = widget.items;
final fandomId = widget.fandomId;
final notifications = widget.notifications;
return Padding(
// 사본 `Top Navigation` — 높이 54 · 패딩 6/16 · 오른쪽 묶음은 알약 + 42 아이콘 두 개.
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4, vertical: 6),
child: SizedBox(
height: 42,
child: Row(
children: [
const Text(
'FANUZU',
style: TextStyle(
fontFamily: 'Pretendard',
fontSize: 20,
fontWeight: FontWeight.w900,
height: 1.4,
letterSpacing: -0.5,
color: AppColors.cosmicAccent,
),
),
tooltip: context.l10n.homeNotificationsTooltip,
),
],
const Spacer(),
_PopBalance(value: pop),
IconButton(
constraints: const BoxConstraints.tightFor(width: 42, height: 42),
// 사본 `icon` 42 칸 — 테마 최소 크기(48)로 늘어나 POP 알약이 왼쪽으로 밀리지 않게.
style: IconButton.styleFrom(
minimumSize: const Size.square(42),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
padding: EdgeInsets.zero,
icon: const Image(
image: AssetImage('assets/figma/icon-wallet.png'),
width: 42,
height: 42,
),
// 사본 `252:159367` — 홈 주 프레임에 열린 「아이템 구입」 시트.
onPressed: () => showItemSheet(
context,
items: items,
purchase: true,
fandomId: fandomId,
),
tooltip: context.l10n.homeItemsTooltip,
),
IconButton(
constraints: const BoxConstraints.tightFor(width: 42, height: 42),
// 사본 `icon` 42 칸 — 테마 최소 크기(48)로 늘어나 POP 알약이 왼쪽으로 밀리지 않게.
style: IconButton.styleFrom(
minimumSize: const Size.square(42),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
padding: EdgeInsets.zero,
// 사본 `icon` 42 칸 안 lucide/bell 20.
iconSize: 20,
// 배지 모양은 Figma 에 그림이 없어 테마 기본 점 배지(강조색)를 쓴다.
icon: Badge(
key: const ValueKey('home-bell-badge'),
isLabelVisible: _unread,
smallSize: 6,
backgroundColor: AppColors.cosmicAccent,
child: const ImageIcon(
AppIcons.bell,
color: AppColors.onCosmic,
),
),
// 알림 — 화면 대조 원장 F0 · `229:11265`.
onPressed: () async {
await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<Object?>(
builder: (_) =>
NotificationScreen(repository: notifications),
),
);
await _checkUnread();
},
tooltip: context.l10n.homeNotificationsTooltip,
),
],
),
),
),
);
);
}
}
class _HomeBody extends StatelessWidget {
@@ -488,35 +534,7 @@ class _HomeBody extends StatelessWidget {
fallback: data.liveTicker,
),
onClose: () => onMap(false),
onSearch: () =>
Navigator.of(
context,
rootNavigator: true,
).push(
MaterialPageRoute<void>(
builder: (_) => GalaxySearchScreen(
repository: zap!.repository,
onOpen: (p) =>
Navigator.of(
context,
rootNavigator: true,
).push(
MaterialPageRoute<void>(
builder: (_) =>
PlanetDetailScreen(
repository:
planetRepository!(
p.id,
),
watchPlanet:
watchPlanet,
eclipse: eclipse,
),
),
),
),
),
),
onSearch: () => _openSearch(context),
)
: HomeHero(
key: const ValueKey('home-hero'),
@@ -604,9 +622,8 @@ class _HomeBody extends StatelessWidget {
myPop: data.popBalance,
),
),
// 행성 가입은 GALAXY 에서 한다(사본 시트 「행성 가입」).
onJoin: () =>
GoRouter.maybeOf(context)?.go('/galaxy'),
// 설명 프레임 `516:87335` 「행성 가입 버튼 - 클릭 시 Galaxy 검색 화면 이동」.
onJoin: () => _openSearch(context),
zapButtonKey: zapKey,
onHeroParticipate: _heroParticipate(context),
eclipseButton: data.fandomId.isEmpty
@@ -614,6 +631,7 @@ class _HomeBody extends StatelessWidget {
: EclipseEventButton(
repository: eclipse,
fandomId: data.fandomId,
nebula: (growth ?? data.growth).isNebula,
),
),
),
@@ -842,6 +860,33 @@ class _HomeBody extends StatelessWidget {
);
}
/// 갤럭시 검색 — 확장 맵 돋보기(`2008:14560`)와 첫 사용자 시트 「행성 가입」(`516:87335`).
/// 지도 저장소가 없으면(표본 모드) GALAXY 탭으로 물러난다.
void _openSearch(BuildContext context) {
final galaxyRepository = zap?.repository;
if (galaxyRepository == null || planetRepository == null) {
GoRouter.maybeOf(context)?.go('/galaxy');
return;
}
final navigator = Navigator.of(context, rootNavigator: true);
navigator.push(
MaterialPageRoute<void>(
builder: (_) => GalaxySearchScreen(
repository: galaxyRepository,
onOpen: (p) => navigator.push(
MaterialPageRoute<void>(
builder: (_) => PlanetDetailScreen(
repository: planetRepository!(p.id),
watchPlanet: watchPlanet,
eclipse: eclipse,
),
),
),
),
),
);
}
VoidCallback? _heroParticipate(BuildContext context) =>
planetRepository == null || data.fandomId.isEmpty
? null