이클립스 흐름 — 배너 진입 · 이벤트 버튼 · 오버레이 · 방어 진행 표시 · 흔들기 감지 통일

This commit is contained in:
Sn-Kinos
2026-10-05 19:05:21 +09:00
parent be623d2e46
commit 576dcfbdfd
27 changed files with 1369 additions and 369 deletions
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import '../../../core/i18n/app_locale.dart';
import '../../../core/theme/tokens.dart';
import '../../../core/ui/live_stream.dart';
import '../eclipse_data.dart';
import '../eclipse_defend_screen.dart';
/// HOME 이클립스 이벤트 버튼 — 사본 「홈」(`373:443167`)의 `Group 563`.
///
/// 명세 `516:88125` *데이터: 이클립스 발생 잔여 시간 · 규칙: 성운(행성) 아래 중앙 정렬,
/// 어드민에서 설정된 이클립스 이벤트 발생 시간의 N분 전 노출 · 동작: 클릭 시 이클립스 상세로
/// 이동*. N분은 서버 회차의 `warnAt`(원장 C-22 발생 3분 전)이다 — 회차 문서가 그때부터
/// `scheduled` 로 있으므로 그 문서가 보이는 동안 띄운다. 발생 뒤 방어 창(60초) 동안은
/// 남은 초로 바뀌고, 결과가 나거나 창이 닫히면 접는다.
///
/// 모양: 위를 가리키는 꼭지 + 검정 80% 알약(229×46 · r 끝까지) · 42dot Medium 12 흰색 두 줄.
class EclipseEventButton extends StatelessWidget {
const EclipseEventButton({
super.key,
required this.repository,
required this.fandomId,
});
final EclipseRepository repository;
final String fandomId;
@override
Widget build(BuildContext context) => LiveStreamBuilder<EclipseState?>(
key: ValueKey('eclipse-button-$fandomId'),
create: () => repository.watch(fandomId: fandomId, includeScheduled: true),
builder: (context, state) {
if (state == null) return const SizedBox.shrink();
return EclipseTicker(
builder: (context, now) {
if (state.result != null || state.defended || state.isExpired(now)) {
return const SizedBox.shrink();
}
final text = state.hasStarted(now)
? context.l10n.eclipseButtonActive(
'${((state.remaining(now)?.inMilliseconds ?? 0) / 1000).ceil()}',
)
: context.l10n.eclipseButtonSoon(
eclipseLeftLabel(state.startAt!.difference(now)),
);
return Semantics(
button: true,
liveRegion: true,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
// 명세 `516:88164` 「닫기 — 하단 move out」 — 아래에서 올라오는 전체 화면.
fullscreenDialog: true,
builder: (_) => EclipseDefendScreen(
repository: repository,
fandomId: state.fandomId,
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CustomPaint(
size: const Size(16, 7),
painter: _Tip(),
),
Container(
constraints: const BoxConstraints(minWidth: 229, minHeight: 46),
padding: const EdgeInsets.symmetric(
horizontal: AppSpace.s4,
vertical: 6,
),
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xCC000000),
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Text(
text,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 17 / 12,
color: AppColors.onCosmic,
),
),
),
],
),
),
);
},
);
},
);
}
/// 말풍선 꼭지 — 행성을 가리킨다.
class _Tip extends CustomPainter {
@override
void paint(Canvas canvas, Size size) => canvas.drawPath(
Path()
..moveTo(size.width / 2, 0)
..lineTo(size.width, size.height)
..lineTo(0, size.height)
..close(),
Paint()..color = const Color(0xCC000000),
);
@override
bool shouldRepaint(_Tip old) => false;
}