Zap·후원 FPI 귀속 · 이클립스 방어 Shake 전용 · 가입 팬덤만 방어

This commit is contained in:
Sn-Kinos
2026-10-05 21:53:20 +09:00
parent 17c658b272
commit 71ec17dc3c
21 changed files with 196 additions and 134 deletions
@@ -11,7 +11,7 @@ import '../../core/theme/tokens.dart';
import '../../core/ui/app_icons.dart';
import '../../core/ui/live_stream.dart';
import '../home/home_screen.dart' show formatPop;
import '../quest/quest_evidence.dart' show deviceShakes;
import '../quest/quest_evidence.dart' show ShakeProbe, deviceShakes;
import 'eclipse_data.dart';
import '../../core/i18n/app_locale.dart';
@@ -34,10 +34,14 @@ class EclipseDefendScreen extends StatefulWidget {
super.key,
required this.repository,
required this.fandomId,
this.shakes = deviceShakes,
});
final EclipseRepository repository;
/// 흔들기 입력. 테스트는 가짜 스트림을 넣는다 — 방어 수단은 **Shake 하나**다(FX §5).
final ShakeProbe shakes;
/// 어느 팬덤의 이클립스인가. 화면은 구독으로 현재 상태를 따라간다.
final String fandomId;
@@ -67,6 +71,9 @@ class _EclipseDefendScreenState extends State<EclipseDefendScreen>
Timer? _burst;
bool _sending = false;
/// 가속도 센서가 없는 단말. 탭 대체 참여는 두지 않는다(FX §5 「수동 방어 Shake only」).
bool _noSensor = false;
({String fandomId, String eclipseId, String seq, int shakes})? _batch;
/// 묶어 보내는 시간창. 흔들 때마다 부르면 호출이 초당 여러 번 나간다.
@@ -77,11 +84,12 @@ class _EclipseDefendScreenState extends State<EclipseDefendScreen>
super.initState();
WidgetsBinding.instance.addObserver(this);
// 흔들기 판정 규칙(문턱 · 표본 주기)은 Music Shake 와 같다 — [ShakeDetector].
_accel = deviceShakes().listen(
_accel = widget.shakes().listen(
(_) => _shake(),
onError: (_) {
// 센서가 없는 단말(에뮬레이터·일부 태블릿)이다. 화면은 그대로 두고
// 아래의 「직접 누르기」로 참여할 수 있게 한다.
// 센서가 없는 단말(에뮬레이터·일부 태블릿)이다. 방어는 Shake only(FX §5)라
// 대체 입력을 두지 않고 지원하지 않는다고만 알린다.
if (mounted) setState(() => _noSensor = true);
},
);
}
@@ -396,7 +404,6 @@ class _EclipseDefendScreenState extends State<EclipseDefendScreen>
final remaining = window.inMilliseconds <= 0
? 0.0
: (left.inMilliseconds / window.inMilliseconds).clamp(0.0, 1.0);
final canShake = state.canShake(now);
return Column(
children: [
const _CloseBar(),
@@ -467,26 +474,8 @@ class _EclipseDefendScreenState extends State<EclipseDefendScreen>
// 소모가 없다는 것을 화면이 직접 말한다 — 「POP 이 빠지나?」를 묻게 두지 않는다(설계서 v1.1 §9-#5).
_Note(context.l10n.eclipseNoPopNote('${state.qualifyShakes}')),
if (_batch != null) _retryButton(),
// 센서가 없는 단말에서도 참여할 수 있게 둔다. 판정은 서버가 하므로 이 버튼이
// 흔들기보다 유리하지 않다(상한·속도 제한이 같다). 사본에 없는 보조 입력이라
// 채움 없는 낮은 강조로 둔다.
Padding(
padding: const EdgeInsets.fromLTRB(
AppSpace.s4,
AppSpace.s2,
AppSpace.s4,
AppSpace.s4,
),
child: FilledButton(
onPressed: canShake && (_batch == null || _sending) ? _shake : null,
style: FilledButton.styleFrom(
backgroundColor: AppColors.cosmicSurfaceContainer,
foregroundColor: AppColors.onCosmicVariant,
minimumSize: const Size.fromHeight(AppSizing.touchMinApp),
),
child: Text(context.l10n.eclipseTapToJoin),
),
),
if (_noSensor) _Note(context.l10n.eclipseNoSensor),
const SizedBox(height: AppSpace.s4),
],
);
}