import 'package:flutter/material.dart'; import '../../core/theme/tokens.dart'; import '../../core/ui/play_banner.dart'; import 'galaxy_data.dart'; import '../../core/format.dart'; import '../../core/i18n/app_locale.dart'; /// 갤럭시 검색 — Figma `03_Wireframe` 「Galaxy_검색」(`353:217832`). /// /// 화면 대조 원장 F2 의 11번. 명세 *돋보기 — 검색페이지 이동*. /// /// Figma 검색 화면이 공통으로 갖는 것을 그대로 따랐다 — /// 「최근 검색어 / 전체 삭제 / 최근 검색어가 없습니다.」 class GalaxySearchScreen extends StatefulWidget { const GalaxySearchScreen({ super.key, this.repository = const SampleGalaxyRepository(), this.onOpen, }); final GalaxyRepository repository; /// 결과 줄을 누르면 그 행성 상세로 — 상세 화면 저장소는 부르는 쪽이 안다. final void Function(PlanetNode planet)? onOpen; @override State createState() => _GalaxySearchScreenState(); } class _GalaxySearchScreenState extends State { late Future _snapshot; final _controller = TextEditingController(); /// 최근 검색어. 기기 안에만 두는 값이라 서버가 붙어도 그대로다. final List _recent = []; String _query = ''; @override void initState() { super.initState(); _snapshot = widget.repository.load(); _controller.addListener(() => setState(() => _query = _controller.text)); } @override void dispose() { _controller.dispose(); super.dispose(); } void _submit(String q) { final term = q.trim(); if (term.isEmpty) return; setState(() { _recent ..remove(term) ..insert(0, term); if (_recent.length > 10) _recent.removeLast(); }); } @override Widget build(BuildContext context) => Scaffold( // 시안 `Top Navigation`(54 · 좌우 16) — 뒤로 42 + 검색 칸 #282828 r10 높이 42. appBar: AppBar( toolbarHeight: 54, automaticallyImplyLeading: false, titleSpacing: 16, centerTitle: false, title: Row( children: [ const SizedBox.square(dimension: 42, child: BackButton()), Expanded( child: Container( height: 42, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: const Color(0xFF282828), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Image.asset( 'assets/figma/icon-search-20.png', width: 20, height: 20, ), const SizedBox(width: 6), Expanded( child: TextField( controller: _controller, autofocus: true, textInputAction: TextInputAction.search, onSubmitted: _submit, style: _t(16, AppColors.onCosmic, FontWeight.w500, 1.4), decoration: InputDecoration( hintText: context.l10n.galaxySearchHint, hintStyle: _t( 16, const Color(0xFF6E6E6E), FontWeight.w500, 1.4, ), border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, filled: false, isDense: true, contentPadding: EdgeInsets.zero, ), ), ), if (_query.isNotEmpty) GestureDetector( onTap: _controller.clear, child: const Icon( Icons.close, size: 18, color: Color(0xFF6E6E6E), ), ), ], ), ), ), ], ), ), body: SafeArea( child: FutureBuilder( future: _snapshot, builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } final planets = snap.data?.planets ?? const []; if (_query.trim().isEmpty) { return _Recent( terms: _recent, onClear: () => setState(_recent.clear), onPick: (t) { _controller.text = t; _submit(t); }, ); } final hits = planets .where( (p) => p.name.toLowerCase().contains(_query.trim().toLowerCase()), ) .toList(); if (hits.isEmpty) return const _NoHit(); return ListView.separated( padding: const EdgeInsets.all(AppSpace.s4), itemCount: hits.length, separatorBuilder: (_, _) => const SizedBox(height: AppSpace.s2), itemBuilder: (context, i) => _Hit( planet: hits[i], onTap: widget.onOpen == null ? null : () => widget.onOpen!(hits[i]), ), ); }, ), ), ); } TextStyle _t(double size, Color color, FontWeight weight, double height) => TextStyle( fontFamily: AppFonts.body, fontSize: size, fontWeight: weight, height: height, color: color, ); /// 최근 검색어 — 시안 `Frame 4241`(안쪽 16 · 간격 24): 배너, 「최근 검색어 16/600 · /// 전체 삭제 14/500 #c2c2c2」, 비었으면 140 칸 가운데 「최근 검색어가 없습니다.」 14/600 #6e6e6e. class _Recent extends StatelessWidget { const _Recent({ required this.terms, required this.onClear, required this.onPick, }); final List terms; final VoidCallback onClear; final ValueChanged onPick; @override Widget build(BuildContext context) => ListView( // 시안 배너만 좌우 9(357 폭), 나머지는 16. padding: const EdgeInsets.fromLTRB(9, 16, 9, 16), children: [ const PlayBanner(), const SizedBox(height: 24), Padding( padding: const EdgeInsets.symmetric(horizontal: 7), child: _body(context), ), ], ); Widget _body(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( context.l10n.commonRecentSearches, style: _t(16, AppColors.onCosmic, FontWeight.w600, 20 / 16), ), GestureDetector( onTap: onClear, child: Text( context.l10n.commonClearAll, style: _t(14, const Color(0xFFC2C2C2), FontWeight.w500, 20 / 14), ), ), ], ), const SizedBox(height: AppSpace.s4), if (terms.isEmpty) SizedBox( height: 140, child: Center( child: Text( context.l10n.commonNoRecentSearches, style: _t(14, const Color(0xFF6E6E6E), FontWeight.w600, 20 / 14), ), ), ) else Wrap( spacing: AppSpace.s2, runSpacing: AppSpace.s2, children: [ for (final term in terms) ActionChip(label: Text(term), onPressed: () => onPick(term)), ], ), ], ); } class _NoHit extends StatelessWidget { const _NoHit(); @override Widget build(BuildContext context) => Center( child: Padding( padding: const EdgeInsets.all(AppSpace.s5), child: Text( context.l10n.galaxyNoHit, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center, ), ), ); } class _Hit extends StatelessWidget { const _Hit({required this.planet, this.onTap}); final PlanetNode planet; final VoidCallback? onTap; @override Widget build(BuildContext context) { final t = Theme.of(context).textTheme; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Container( padding: const EdgeInsets.all(AppSpace.s4), decoration: BoxDecoration( color: AppColors.cosmicSurfaceContainer, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Row( children: [ Text( planet.rankLabel, style: t.titleMedium?.copyWith(color: AppColors.cosmicAccent), ), const SizedBox(width: AppSpace.s4), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(planet.name, style: t.bodyMedium), Text( context.l10n.galaxyMembers(formatPop(planet.members)), style: t.labelSmall, ), ], ), ), Text('FPI ${formatPop(planet.fpi)}', style: t.bodySmall), ], ), ), ); } }