갤럭시 검색 결과에서 행성 상세로 이동

This commit is contained in:
Sn-Kinos
2026-10-05 17:39:23 +09:00
parent 54d0b23260
commit 8a1b824307
11 changed files with 394 additions and 171 deletions
+150 -103
View File
@@ -125,7 +125,8 @@ class _GalaxyScreenState extends State<GalaxyScreen>
void initState() {
super.initState();
_snapshot = widget.repository.load();
_zap = ZapController(widget.repository, onFailure: _notify)..addListener(_onZapChanged);
_zap = ZapController(widget.repository, onFailure: _notify)
..addListener(_onZapChanged);
_fly = AnimationController(vsync: this, duration: GalaxyMotion.recenter)
..addListener(_onFly);
_ticker = createTicker(_tick);
@@ -182,12 +183,18 @@ class _GalaxyScreenState extends State<GalaxyScreen>
void _follow() {
final p = _selected;
if (p == null || _viewport.isEmpty || _fly.isAnimating) return;
final at = GalaxyField.positionOf(p, _selectedIndex, _clock.spin, _outerRank);
final at = GalaxyField.positionOf(
p,
_selectedIndex,
_clock.spin,
_outerRank,
);
final s = _view.value.getMaxScaleOnAxis();
final tx = _viewport.width / 2 - at.dx * s;
final ty = _viewport.height / 2 - at.dy * s;
final m = _view.value;
if ((m.storage[12] - tx).abs() < 0.01 && (m.storage[13] - ty).abs() < 0.01) {
if ((m.storage[12] - tx).abs() < 0.01 &&
(m.storage[13] - ty).abs() < 0.01) {
return;
}
_view.value = m.clone()
@@ -258,50 +265,57 @@ class _GalaxyScreenState extends State<GalaxyScreen>
return LiveStreamBuilder<EclipseState?>(
key: ValueKey('eclipse-${myFandomId ?? 'mine'}'),
create: () => widget.eclipse.watch(fandomId: myFandomId),
builder: (context, eclipse) => LiveStreamBuilder<PlanetVisualState>(
key: ValueKey(('galaxy-orbit', (_selected ?? data.mine)?.id)),
create: () => widget.watchPlanet((_selected ?? data.mine)?.id ?? ''),
builder: (context, state) {
_clock.idleSpeedScale = state?.isHibernating == true
? 0.25
: state?.isEclipsed == true
? 0.5
: state?.isHeroTime == true
? 1.5
: 1;
return _Map(
data: data,
watchPlanet: widget.watchPlanet,
eclipse: eclipse,
eclipseRepository: widget.eclipse,
repository: widget.repository,
planetRepository: widget.planetRepository,
clock: _clock,
view: _view,
selected: _selected ?? data.mine,
selectedIndex: _selected == null
? data.planets.indexWhere((p) => p.isMine).clamp(0, 1 << 30)
: _selectedIndex,
balance: _zap.balance,
pending: _zap.pending,
sending: _zap.sending,
touchSerial: _zap.touchSerial,
zapResult: _zap.result,
retryZap: _zap.needsRetry ? _zap.retry : null,
outerRank: _outerRank,
ranking: _ranking,
onToggleRanking: () => _toggleRanking(snap.data!),
fly: _fly,
onSelect: _center,
onDrag: _clock.drag,
onGrab: () {
// 날아가는 도중에 손을 대면 사용자가 이긴다.
if (_fly.isAnimating) _fly.stop();
builder: (context, eclipse) =>
LiveStreamBuilder<PlanetVisualState>(
key: ValueKey((
'galaxy-orbit',
(_selected ?? data.mine)?.id,
)),
create: () =>
widget.watchPlanet((_selected ?? data.mine)?.id ?? ''),
builder: (context, state) {
_clock.idleSpeedScale = state?.isHibernating == true
? 0.25
: state?.isEclipsed == true
? 0.5
: state?.isHeroTime == true
? 1.5
: 1;
return _Map(
data: data,
watchPlanet: widget.watchPlanet,
eclipse: eclipse,
eclipseRepository: widget.eclipse,
repository: widget.repository,
planetRepository: widget.planetRepository,
clock: _clock,
view: _view,
selected: _selected ?? data.mine,
selectedIndex: _selected == null
? data.planets
.indexWhere((p) => p.isMine)
.clamp(0, 1 << 30)
: _selectedIndex,
balance: _zap.balance,
pending: _zap.pending,
sending: _zap.sending,
touchSerial: _zap.touchSerial,
zapResult: _zap.result,
retryZap: _zap.needsRetry ? _zap.retry : null,
outerRank: _outerRank,
ranking: _ranking,
onToggleRanking: () => _toggleRanking(snap.data!),
fly: _fly,
onSelect: _center,
onDrag: _clock.drag,
onGrab: () {
// 날아가는 도중에 손을 대면 사용자가 이긴다.
if (_fly.isAnimating) _fly.stop();
},
onZap: _zap.tap,
);
},
onZap: _zap.tap,
);
},
),
),
);
},
),
@@ -433,9 +447,14 @@ class _Map extends StatelessWidget {
child: AnimatedBuilder(
animation: clock,
builder: (context, _) => GalaxyField(
onOpenHistory: (planet) => Navigator.of(context).push(MaterialPageRoute<void>(
builder: (_) => PlanetHistoryScreen(repository: planetRepository(planet.id), fandomName: planet.name),
)),
onOpenHistory: (planet) => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => PlanetHistoryScreen(
repository: planetRepository(planet.id),
fandomName: planet.name,
),
),
),
planets: data.planets,
watchPlanet: watchPlanet,
selected: selected,
@@ -522,7 +541,19 @@ class _Map extends StatelessWidget {
tooltip: context.l10n.commonSearch,
onPressed: () => Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
builder: (_) => GalaxySearchScreen(repository: repository),
builder: (_) => GalaxySearchScreen(
repository: repository,
onOpen: (p) =>
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
builder: (_) => PlanetDetailScreen(
repository: planetRepository(p.id),
watchPlanet: watchPlanet,
eclipse: eclipseRepository,
),
),
),
),
),
),
icon: const Icon(Icons.search),
@@ -540,24 +571,31 @@ class _Map extends StatelessWidget {
if (eclipse case final e?)
EclipseBannerCard(
state: e,
onTap: () => Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(builder: (_) => EclipseDefendScreen(
repository: eclipseRepository, fandomId: e.fandomId,
)),
),
onTap: () =>
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
builder: (_) => EclipseDefendScreen(
repository: eclipseRepository,
fandomId: e.fandomId,
),
),
),
),
if (selected case final target?)
HeroTimeBillboard(
fandomId: target.id,
fandomName: target.name,
watch: watchPlanet,
onOpen: () => Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(builder: (_) => PlanetDetailScreen(
repository: planetRepository(target.id),
watchPlanet: watchPlanet,
eclipse: eclipseRepository,
)),
),
onOpen: () =>
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
builder: (_) => PlanetDetailScreen(
repository: planetRepository(target.id),
watchPlanet: watchPlanet,
eclipse: eclipseRepository,
),
),
),
),
],
),
@@ -618,48 +656,54 @@ class _Map extends StatelessWidget {
key: ValueKey(('zap-mode', data.mine?.id)),
create: () => watchPlanet(data.mine?.id ?? ''),
builder: (context, state) {
final hero = state?.isHeroTime == true &&
final hero =
state?.isHeroTime == true &&
state?.heroEndsAt?.isAfter(DateTime.now()) == true &&
pending == 0 && !sending && retryZap == null;
pending == 0 &&
!sending &&
retryZap == null;
return ZapButton(
isHeroTime: hero,
// 단가는 `config/policy.zapPop` 이다 — Figma 「Zap 버튼」이
// 어드민 설정값이라 못 박았다.
zapPop: data.zapPop,
// 표시 잔액 = 서버 확정값(없으면 스냅샷) − 아직 안 보낸 연타분.
myPop: (balance ?? data.myPop) - pending * data.zapPop,
// **이클립스와 무관하다.** ZAP 은 행성 성장 기여이며(B-23), 방어는
// 「흔들어 방어」 화면에서 POP 없이 참여한다.
blockedReason: sending
? context.l10n.zapApplying
: retryZap != null
? context.l10n.zapNeedsCheck
: data.mine == null
? context.l10n.zapNoPlanet
: null,
onZap: () {
if (data.mine case final mine?) {
if (hero) {
showModalBottomSheet<void>(
context: context, isScrollControlled: true,
builder: (_) => HeroParticipationSheet(
fandomId: mine.id, watch: watchPlanet,
repository: planetRepository(mine.id),
),
);
return;
}
onSelect(
mine,
data.planets.indexOf(mine),
viewport,
animate: !MediaQuery.disableAnimationsOf(context),
);
onZap(mine.id);
}
},
onContribute: () => _todo(context, context.l10n.commonContribute),
onItem: () => _todo(context, context.l10n.commonItem),
isHeroTime: hero,
// 단가는 `config/policy.zapPop` 이다 — Figma 「Zap 버튼」이
// 어드민 설정값이라 못 박았다.
zapPop: data.zapPop,
// 표시 잔액 = 서버 확정값(없으면 스냅샷) − 아직 안 보낸 연타분.
myPop: (balance ?? data.myPop) - pending * data.zapPop,
// **이클립스와 무관하다.** ZAP 은 행성 성장 기여이며(B-23), 방어는
// 「흔들어 방어」 화면에서 POP 없이 참여한다.
blockedReason: sending
? context.l10n.zapApplying
: retryZap != null
? context.l10n.zapNeedsCheck
: data.mine == null
? context.l10n.zapNoPlanet
: null,
onZap: () {
if (data.mine case final mine?) {
if (hero) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (_) => HeroParticipationSheet(
fandomId: mine.id,
watch: watchPlanet,
repository: planetRepository(mine.id),
),
);
return;
}
onSelect(
mine,
data.planets.indexOf(mine),
viewport,
animate: !MediaQuery.disableAnimationsOf(context),
);
onZap(mine.id);
}
},
onContribute: () =>
_todo(context, context.l10n.commonContribute),
onItem: () => _todo(context, context.l10n.commonItem),
);
},
),
@@ -866,7 +910,10 @@ class _HudCard extends StatelessWidget {
),
),
// Stage0 성운은 랭킹 모집단이 아니다(B-17).
_Metric(label: 'RANK', value: planet.stage == 0 ? '-' : '${planet.rank}'),
_Metric(
label: 'RANK',
value: planet.stage == 0 ? '-' : '${planet.rank}',
),
],
),
const SizedBox(height: AppSpace.s1),