라이브 티커 · 갤럭시 검색 색인 · 주간 랭킹 스냅샷 · 연속 ZAP 상한 · 가입 행성 Zap · 성운 목표 · 랭킹 갱신 안내 · 행성 터치 반응
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../core/theme/tokens.dart';
|
||||
import '../../core/ui/play_banner.dart';
|
||||
@@ -32,20 +35,62 @@ class _GalaxySearchScreenState extends State<GalaxySearchScreen> {
|
||||
late Future<GalaxySnapshot> _snapshot;
|
||||
final _controller = TextEditingController();
|
||||
|
||||
/// 최근 검색어. 기기 안에만 두는 값이라 서버가 붙어도 그대로다.
|
||||
/// 최근 검색어 — 기기에 남긴다(재진입해도 유지). 명세 `523:18429` 「최신순 · 중복 제거」.
|
||||
final List<String> _recent = [];
|
||||
static const _recentKey = 'galaxyRecentSearches';
|
||||
|
||||
String _query = '';
|
||||
|
||||
/// 전체 행성 검색 결과 — 입력이 멈춘 뒤 서버에 묻는다.
|
||||
Future<List<PlanetNode>>? _hits;
|
||||
Timer? _debounce;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_snapshot = widget.repository.load();
|
||||
_controller.addListener(() => setState(() => _query = _controller.text));
|
||||
_controller.addListener(_onChanged);
|
||||
_loadRecent();
|
||||
}
|
||||
|
||||
Future<void> _loadRecent() async {
|
||||
try {
|
||||
final saved = (await SharedPreferences.getInstance()).getStringList(_recentKey);
|
||||
if (mounted && saved != null) setState(() => _recent..clear()..addAll(saved));
|
||||
} catch (_) {
|
||||
// 기기 저장소를 못 쓰면 이번 화면 안에서만 기억한다.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveRecent() async {
|
||||
try {
|
||||
await (await SharedPreferences.getInstance()).setStringList(_recentKey, _recent);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _onChanged() {
|
||||
if (_controller.text == _query) return;
|
||||
setState(() => _query = _controller.text);
|
||||
_debounce?.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 300), () {
|
||||
if (!mounted) return;
|
||||
final q = _query.trim();
|
||||
setState(() {
|
||||
_hits = q.isEmpty ? null : _search(q);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 서버 결과에 맵 스냅샷의 순위를 입힌다 — 맵 상위에 있는 행성은 같은 순위로 보인다.
|
||||
Future<List<PlanetNode>> _search(String q) async {
|
||||
final hits = await widget.repository.search(q);
|
||||
final known = {for (final p in (await _snapshot).planets) p.id: p};
|
||||
return [for (final h in hits) known[h.id] ?? h];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounce?.cancel();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -59,6 +104,7 @@ class _GalaxySearchScreenState extends State<GalaxySearchScreen> {
|
||||
..insert(0, term);
|
||||
if (_recent.length > 10) _recent.removeLast();
|
||||
});
|
||||
_saveRecent();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -135,34 +181,41 @@ class _GalaxySearchScreenState extends State<GalaxySearchScreen> {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final planets = snap.data?.planets ?? const <PlanetNode>[];
|
||||
if (_query.trim().isEmpty) {
|
||||
return _Recent(
|
||||
terms: _recent,
|
||||
onClear: () => setState(_recent.clear),
|
||||
onClear: () {
|
||||
setState(_recent.clear);
|
||||
_saveRecent();
|
||||
},
|
||||
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]),
|
||||
),
|
||||
return FutureBuilder<List<PlanetNode>>(
|
||||
future: _hits,
|
||||
builder: (context, result) {
|
||||
if (_hits == null ||
|
||||
result.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final hits = result.data ?? const <PlanetNode>[];
|
||||
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]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user