이클립스 사전 경고 연출 · 브리핑 출처·게시시각 · PASSPORT 역사적 사건 Stamp
This commit is contained in:
@@ -69,6 +69,26 @@ fvm flutter build apk --release # build/app/outputs/flutter-apk/app-relea
|
||||
fvm flutter build appbundle --release # build/app/outputs/bundle/release/app-release.aab
|
||||
```
|
||||
|
||||
### Android Apple 로그인 — 빌드 옵션 두 개를 꼭 넣는다
|
||||
|
||||
Android 의 Apple 로그인은 iOS 와 달리 Apple 개발자 계정에 등록한 **서비스 ID** 와 **반환 주소**를
|
||||
빌드 때 받아야 동작한다(`lib/features/auth/social_providers.dart` `appleWebAuthenticationOptionsFromEnvironment`).
|
||||
둘 중 하나라도 비었거나 반환 주소가 `https://호스트` 꼴이 아니면 Apple 버튼이 비활성(「준비 중」)으로 남는다.
|
||||
비밀값이 아니지만 환경별 값이라 저장소에 상수로 두지 않는다.
|
||||
|
||||
```bash
|
||||
fvm flutter build apk --release \
|
||||
--dart-define-from-file=config/kakao.env \
|
||||
--dart-define=FANUZU_APPLE_SERVICE_ID=<서비스 ID> \
|
||||
--dart-define=FANUZU_APPLE_REDIRECT_URI=<서비스 ID 에 등록한 Return URL>
|
||||
```
|
||||
|
||||
- 값은 Apple 개발자 계정 「Identifiers → Services IDs」의 식별자와 그 「Sign in with Apple → Return URLs」 항목이다.
|
||||
- 반환 주소가 Apple 응답을 앱으로 돌려보내야 한다 — `sign_in_with_apple` 패키지 규약대로
|
||||
`intent://callback?<응답>#Intent;package=kr.co.fanuzu.app;scheme=signinwithapple;end` 로 넘기면
|
||||
`AndroidManifest.xml` 의 `SignInWithAppleCallback`(scheme `signinwithapple`)이 받는다.
|
||||
- 확인: 빌드한 APK 의 첫 화면에서 Apple 버튼이 눌리는지(비활성이면 옵션 누락).
|
||||
|
||||
## 서명
|
||||
|
||||
`android/key.properties` 가 없으면 **릴리스 빌드가 의도적으로 실패**한다
|
||||
|
||||
@@ -6,6 +6,8 @@ class PlanetVisualState {
|
||||
this.isEclipsed = false,
|
||||
this.eclipseFailed = false,
|
||||
this.eclipseSuccessId,
|
||||
this.eclipseWarnAt,
|
||||
this.eclipseStartAt,
|
||||
this.isHibernating = false,
|
||||
this.isHeroTime = false,
|
||||
this.heroEventIds,
|
||||
@@ -51,6 +53,10 @@ class PlanetVisualState {
|
||||
|
||||
/// 서버 판정 성공 회차 ID — 바뀌는 순간 Orbit Burst 한 번(v1.5 §5 성공).
|
||||
final String? eclipseSuccessId;
|
||||
|
||||
/// 경보 단계(`scheduled`) 회차의 사전 경고 창 — v1.5 §5 「그림자 접근」. 그 밖에는 null.
|
||||
final DateTime? eclipseWarnAt;
|
||||
final DateTime? eclipseStartAt;
|
||||
final bool isHibernating;
|
||||
final bool isHeroTime;
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ class Repositories {
|
||||
var isEclipsed = false;
|
||||
var eclipseFailed = false;
|
||||
String? eclipseSuccessId;
|
||||
(DateTime, DateTime)? eclipseWarning;
|
||||
var isHeroTime = false;
|
||||
Set<String>? heroEventIds;
|
||||
Timer? heroExpiry;
|
||||
@@ -178,6 +179,8 @@ class Repositories {
|
||||
isEclipsed: isEclipsed,
|
||||
eclipseFailed: eclipseFailed,
|
||||
eclipseSuccessId: eclipseSuccessId,
|
||||
eclipseWarnAt: eclipseWarning?.$1,
|
||||
eclipseStartAt: eclipseWarning?.$2,
|
||||
isHeroTime: isHeroTime,
|
||||
heroEventIds: heroEventIds,
|
||||
heroEndsAt: heroEndsAt,
|
||||
@@ -196,8 +199,12 @@ class Repositories {
|
||||
emit();
|
||||
}, onError: out.addError);
|
||||
// 판정 결과까지 받는다 — 활성 회차만 보면 마감 순간 null 이 되어 성공 · 실패를 가를 수 없다.
|
||||
final danger = eclipse.watch(fandomId: fandomId, includeResults: true).listen((state) {
|
||||
// 경보 단계 회차도 받는다 — 사전 경고 「그림자 접근」(v1.5 §5).
|
||||
final danger = eclipse
|
||||
.watch(fandomId: fandomId, includeResults: true, includeScheduled: true)
|
||||
.listen((state) {
|
||||
(isEclipsed, eclipseFailed, eclipseSuccessId) = eclipseLook(state, DateTime.now());
|
||||
eclipseWarning = eclipseWarnWindow(state);
|
||||
emit();
|
||||
}, onError: out.addError);
|
||||
// 기존 선물 이벤트만 읽는다. 구매 시도/로컬 캐시는 축제 확정이 아니다.
|
||||
@@ -272,7 +279,9 @@ class Repositories {
|
||||
/// 최근 이클립스 회차 → 행성 표현 (진행 중 감소 · 실패 감소 · 성공 회차 ID).
|
||||
/// 판정 대기(마감 후 결과 전)는 셋 다 아니다 — 오류를 실패처럼 보이지 않는다(v1.5 §5 집계 오류).
|
||||
(bool, bool, String?) eclipseLook(EclipseState? state, DateTime now) {
|
||||
if (state == null) return (false, false, null);
|
||||
// 경보 단계는 아직 이클립스가 아니다(그림자 접근은 [eclipseWarnWindow]). 다음 회차가 열렸으니
|
||||
// 이전 회차의 실패 감소도 여기서 풀린다.
|
||||
if (state == null || state.scheduled) return (false, false, null);
|
||||
final won = state.defended || state.result == EclipseResult.success;
|
||||
return (
|
||||
state.result == null && !won && !state.isExpired(now),
|
||||
@@ -280,3 +289,9 @@ class Repositories {
|
||||
won ? state.eclipseId : null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 경보 단계 회차의 (warnAt, startAt). 경보가 아니거나 시각이 없으면 null.
|
||||
(DateTime, DateTime)? eclipseWarnWindow(EclipseState? state) => switch (state) {
|
||||
EclipseState(scheduled: true, warnAt: final warn?, startAt: final start?) => (warn, start),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -47,6 +47,8 @@ class LivePlanet25D extends StatelessWidget {
|
||||
isEclipsed: state?.isEclipsed ?? false,
|
||||
eclipseFailed: state?.eclipseFailed ?? false,
|
||||
eclipseSuccessId: state?.eclipseSuccessId,
|
||||
eclipseWarnAt: state?.eclipseWarnAt,
|
||||
eclipseStartAt: state?.eclipseStartAt,
|
||||
isHibernating: state?.isHibernating ?? false,
|
||||
isHeroTime: state?.isHeroTime ?? false,
|
||||
heroEventIds: state?.heroEventIds,
|
||||
|
||||
@@ -137,6 +137,8 @@ class Planet25D extends StatefulWidget {
|
||||
this.isEclipsed = false,
|
||||
this.eclipseFailed = false,
|
||||
this.eclipseSuccessId,
|
||||
this.eclipseWarnAt,
|
||||
this.eclipseStartAt,
|
||||
this.isHibernating = false,
|
||||
this.isHeroTime = false,
|
||||
this.heroEventIds,
|
||||
@@ -170,6 +172,11 @@ class Planet25D extends StatefulWidget {
|
||||
|
||||
/// 이클립스 성공 회차. 바뀌면 광량 복원 뒤 짧은 Orbit Burst(v1.5 §5 「그림자 이탈 → 복원 → Orbit Burst」).
|
||||
final String? eclipseSuccessId;
|
||||
|
||||
/// 사전 경고 창(서버 회차 `warnAt` → `startAt`, 원장 C-22 발생 3분 전). 이 사이에만
|
||||
/// v1.5 §5 「행성 뒤 어두운 그림자 접근, 별빛·Glow 점진 감소」를 그린다([eclipseApproach]).
|
||||
final DateTime? eclipseWarnAt;
|
||||
final DateTime? eclipseStartAt;
|
||||
final bool isHibernating;
|
||||
final bool isHeroTime;
|
||||
final Set<String>? heroEventIds;
|
||||
@@ -204,12 +211,12 @@ class _Planet25DState extends State<Planet25D>
|
||||
double get _light => widget.isHibernating
|
||||
? 0.35
|
||||
: widget.isEclipsed
|
||||
? 0.5
|
||||
? eclipseLight
|
||||
: _celebrating
|
||||
? 1.3
|
||||
: widget.eclipseFailed
|
||||
// 실패의 감소 폭은 원문에 수치가 없어 이클립스 진행 중과 같은 값을 쓴다(해석 확인 항목).
|
||||
? 0.5
|
||||
? eclipseLight
|
||||
: 1;
|
||||
late final AnimationController _c = AnimationController(
|
||||
vsync: this,
|
||||
@@ -376,6 +383,13 @@ class _Planet25DState extends State<Planet25D>
|
||||
builder: (context, _) {
|
||||
final t = _motionEnabled ? _c.value : 0.0; // 0~1
|
||||
final wave = math.sin(t * 2 * math.pi); // -1~1
|
||||
// 사전 경고 — 매 프레임 서버 시각 기준으로 다시 잰다. 동작 줄이기면 프레임이 돌지 않아
|
||||
// 그린 시점의 위치에 멈춰 있다(움직임 없이 상태만 전달).
|
||||
final warn = widget.isEclipsed
|
||||
? 0.0
|
||||
: eclipseApproach(widget.eclipseWarnAt, widget.eclipseStartAt, DateTime.now()) ?? 0.0;
|
||||
// 별빛(입자) · Glow 를 이클립스 진행 중 광량(0.5)까지 점진적으로 낮춘다.
|
||||
final dim = 1 - (1 - eclipseLight) * warn;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
@@ -400,9 +414,10 @@ class _Planet25DState extends State<Planet25D>
|
||||
? 0.55
|
||||
: 0.42) +
|
||||
wave * (stage == 6 ? 0.02 : 0.05)) *
|
||||
glowStrength,
|
||||
glowStrength *
|
||||
dim,
|
||||
),
|
||||
glow.withValues(alpha: 0.10 * glowStrength),
|
||||
glow.withValues(alpha: 0.10 * glowStrength * dim),
|
||||
glow.withValues(alpha: 0),
|
||||
],
|
||||
stops: const [0.45, 0.72, 1],
|
||||
@@ -423,6 +438,31 @@ class _Planet25DState extends State<Planet25D>
|
||||
),
|
||||
),
|
||||
),
|
||||
// 이클립스 사전 경고 — 행성 뒤로 어두운 그림자가 우상단에서 다가와 Glow 를 가린다.
|
||||
if (warn > 0)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Transform.translate(
|
||||
key: const ValueKey('eclipse-shadow'),
|
||||
offset: Offset(s, -s * 0.5) * eclipseShadowTravel * (1 - warn),
|
||||
child: Transform.scale(
|
||||
scale: 1.15,
|
||||
child: Opacity(
|
||||
opacity: 0.85 * warn,
|
||||
child: const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [Color(0xFF05040D), Color(0xCC05040D), Color(0x0005040D)],
|
||||
stops: [0, 0.6, 1],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 6. 링 — 뒤쪽 절반. 본체 뒤에 깔린다.
|
||||
if (ring)
|
||||
Positioned.fill(
|
||||
@@ -522,6 +562,7 @@ class _Planet25DState extends State<Planet25D>
|
||||
count:
|
||||
(particleCount *
|
||||
light *
|
||||
dim *
|
||||
(_quality == 2
|
||||
? 0.3
|
||||
: _quality == 1
|
||||
@@ -883,6 +924,21 @@ class _SustainedAura extends CustomPainter {
|
||||
old.color != color || old.lowEffects != lowEffects;
|
||||
}
|
||||
|
||||
/// 이클립스 진행 중 광량. 사전 경고의 Glow 감소 · 실패 감소가 같은 값을 쓴다(해석 확인 Q-A28).
|
||||
const eclipseLight = 0.5;
|
||||
|
||||
/// 사전 경고 그림자의 출발 거리(행성 한 변 대비). 원문에 수치가 없는 임시값(해석 확인 항목).
|
||||
const eclipseShadowTravel = 0.9;
|
||||
|
||||
/// 사전 경고 진행 0~1 — [warnAt] 에 0, 발생 시각 [startAt] 에 1. 창이 없거나 경고 전이면 null.
|
||||
/// 발생 시각이 지나도 서버가 회차를 열기 전(스케줄러 지연 수 초)에는 1 로 둔다 — 그림자가 빠졌다 다시 어두워지지 않게.
|
||||
double? eclipseApproach(DateTime? warnAt, DateTime? startAt, DateTime now) {
|
||||
if (warnAt == null || startAt == null || !startAt.isAfter(warnAt)) return null;
|
||||
if (now.isBefore(warnAt)) return null;
|
||||
return (now.difference(warnAt).inMilliseconds / startAt.difference(warnAt).inMilliseconds)
|
||||
.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
/// 이클립스 성공 Orbit Burst 길이. 원문은 「짧은」만 적어 Level Up 경량 연출(850ms)과 같게 둔다(해석 확인 항목).
|
||||
const orbitBurstDuration = Duration(milliseconds: 850);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class EclipseState {
|
||||
required this.defended,
|
||||
this.qualifyShakes = 60,
|
||||
this.startAt,
|
||||
this.warnAt,
|
||||
this.scheduled = false,
|
||||
this.endsAt,
|
||||
this.result,
|
||||
@@ -73,6 +74,9 @@ class EclipseState {
|
||||
/// 없으면 이미 시작한 회차로 본다.
|
||||
final DateTime? startAt;
|
||||
|
||||
/// 사전 경고 시각(`warnAt` = 발생 − 사전 알림, 원장 C-22 3분). 행성 그림자 접근의 출발점.
|
||||
final DateTime? warnAt;
|
||||
|
||||
/// 서버 회차가 아직 `scheduled`(경보 단계)인가. 방어 호출은 `active` 에서만 받는다.
|
||||
final bool scheduled;
|
||||
|
||||
|
||||
@@ -452,6 +452,7 @@ class _EclipseDefendScreenState extends State<EclipseDefendScreen>
|
||||
remaining: remaining,
|
||||
pending: _pending,
|
||||
pulse: _pulse,
|
||||
shield: state.progress,
|
||||
image: _imageOf(widget.nebula),
|
||||
),
|
||||
),
|
||||
@@ -713,8 +714,12 @@ class _DefendPlanet extends StatelessWidget {
|
||||
required this.pending,
|
||||
required this.image,
|
||||
this.pulse = 0,
|
||||
this.shield = 0,
|
||||
});
|
||||
|
||||
/// 고유 유효 참여자 / 목표(서버 값 [EclipseState.progress]) — Shield 조각이 이어지는 정도.
|
||||
final double shield;
|
||||
|
||||
/// 흔들기 일련번호. 0 이면 아직 파동이 없다.
|
||||
final int pulse;
|
||||
|
||||
@@ -767,6 +772,23 @@ class _DefendPlanet extends StatelessWidget {
|
||||
CustomPaint(painter: _RemainingRing(remaining)),
|
||||
),
|
||||
_at(67.5, 300, 240, 240, Image(image: image)),
|
||||
// Shield — 끊어진 Ring 이 참여자 비율만큼 이어지고 목표 달성 시 완전한 원(v1.5 §5.1).
|
||||
// 값이 바뀌면 Shield Pulse 와 같은 길이로 이어 붙인다. 동작 줄이기면 바로 바뀐다.
|
||||
_at(
|
||||
48,
|
||||
280,
|
||||
280,
|
||||
280,
|
||||
TweenAnimationBuilder<double>(
|
||||
key: const ValueKey('shield-ring'),
|
||||
tween: Tween(end: shield.clamp(0.0, 1.0)),
|
||||
duration: MediaQuery.disableAnimationsOf(context)
|
||||
? Duration.zero
|
||||
: shieldPulseDuration,
|
||||
builder: (context, p, _) =>
|
||||
CustomPaint(painter: ShieldRingPainter(p)),
|
||||
),
|
||||
),
|
||||
// Shield Pulse — 흔들기마다 행성 가장자리에서 원형 파동이 한 번 퍼진다. 동작 줄이기면 그리지 않는다.
|
||||
if (pulse > 0 && !MediaQuery.disableAnimationsOf(context))
|
||||
_at(
|
||||
@@ -978,3 +1000,53 @@ class ShieldPulsePainter extends CustomPainter {
|
||||
@override
|
||||
bool shouldRepaint(ShieldPulsePainter old) => old.t != t;
|
||||
}
|
||||
|
||||
/// Shield Ring 조각 수. 원문(v1.5 §5.1)에 수치가 없는 임시값(해석 확인 항목).
|
||||
const shieldSegments = 12;
|
||||
|
||||
/// 진행 [p] 0~1 → Ring 조각들의 (시작각, 길이). 처음엔 조각 사이 틈이 절반이고, 참여자가 늘수록
|
||||
/// 틈이 좁아져 이어지며, 목표 달성(1)이면 틈 없는 원 하나다.
|
||||
List<(double, double)> shieldArcs(double p) {
|
||||
if (p >= 1) return const [(-math.pi / 2, 2 * math.pi)];
|
||||
const slot = 2 * math.pi / shieldSegments;
|
||||
final sweep = slot * (0.5 + 0.5 * p.clamp(0.0, 1.0));
|
||||
return [
|
||||
for (var i = 0; i < shieldSegments; i++)
|
||||
(-math.pi / 2 + i * slot + (slot - sweep) / 2, sweep),
|
||||
];
|
||||
}
|
||||
|
||||
/// 행성(240)과 잔여 시간 원호(280) 사이의 Shield Ring. 완성되면 바깥 번짐을 한 겹 더한다.
|
||||
class ShieldRingPainter extends CustomPainter {
|
||||
const ShieldRingPainter(this.progress);
|
||||
|
||||
final double progress;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final r = size.shortestSide / 2 * (260 / 280);
|
||||
final rect = Rect.fromCircle(center: size.center(Offset.zero), radius: r);
|
||||
final full = progress >= 1;
|
||||
final paint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = full ? 5 : 3
|
||||
..strokeCap = StrokeCap.round
|
||||
..color = AppColors.neonEmerald.withValues(alpha: 0.35 + 0.55 * progress.clamp(0.0, 1.0));
|
||||
if (full) {
|
||||
canvas.drawCircle(
|
||||
rect.center,
|
||||
r,
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 12
|
||||
..color = AppColors.neonEmerald.withValues(alpha: 0.25),
|
||||
);
|
||||
}
|
||||
for (final (start, sweep) in shieldArcs(progress)) {
|
||||
canvas.drawArc(rect, start, sweep, false, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(ShieldRingPainter old) => old.progress != progress;
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ class FirestoreEclipseRepository implements EclipseRepository {
|
||||
_ => null,
|
||||
},
|
||||
startAt: startAt is Timestamp ? startAt.toDate() : null,
|
||||
warnAt: data['warnAt'] is Timestamp ? (data['warnAt'] as Timestamp).toDate() : null,
|
||||
scheduled: data['status'] == 'scheduled',
|
||||
endsAt: endsAt is Timestamp ? endsAt.toDate() : null,
|
||||
);
|
||||
|
||||
@@ -12,9 +12,10 @@ import '../../../core/i18n/app_locale.dart';
|
||||
/// *섹션 숨김, 모두 0 건이면 전체 Briefing 블록을 숨김*. 구획이 없으면 「섹션만
|
||||
/// 숨김」이라는 판정 자체가 성립하지 않는다. 블록 전체 숨김은 홈 화면이 맡는다.
|
||||
///
|
||||
/// **한 줄은 Figma 대로 번호 + 제목만 띄운다.** 정책 원장 B-75(확정) 「각 항목에 출처명 ·
|
||||
/// 게시/발행시각 · 원본 URL · 원문보기 · AI 요약 표시」는 Figma 에 배치가 없어, 줄을 누르면
|
||||
/// 여는 상세 시트([showBriefingDetail])에 모두 싣는다. 시트의 「원문 보기」가 원문으로 나간다.
|
||||
/// **한 줄은 Figma 의 번호 + 제목 아래에 출처명 · 게시시각을 12/500 으로 덧붙인다** — UX·FX v1.5
|
||||
/// 「AI Briefing 출처 표시」(B-3 8-4). 정책 원장 B-75(확정)의 나머지(공식/팬덤 구분 · 원본 URL ·
|
||||
/// 원문보기 · AI 요약 표시)는 Figma 에 배치가 없어, 줄을 누르면 여는 상세 시트([showBriefingDetail])에
|
||||
/// 싣는다. 시트의 「원문 보기」가 원문으로 나간다.
|
||||
class BriefingList extends StatelessWidget {
|
||||
const BriefingList({super.key, required this.items, this.onOpen});
|
||||
|
||||
@@ -132,17 +133,35 @@ class _Row extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 28),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.headline,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.body,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
color: AppColors.onCosmicVariant,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.headline,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.body,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
color: AppColors.onCosmicVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpace.s1),
|
||||
Text(
|
||||
'${item.sourceName} · ${briefingStamp(item.sourceAt)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.body,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.4,
|
||||
color: AppColors.onCosmicMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -151,6 +170,13 @@ class _Row extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 게시시각 표기 `yyyy.MM.dd HH:mm`(기기 시간대) — 줄과 상세 시트가 같이 쓴다.
|
||||
String briefingStamp(DateTime sourceAt) {
|
||||
String two(int v) => v.toString().padLeft(2, '0');
|
||||
final at = sourceAt.toLocal();
|
||||
return '${at.year}.${two(at.month)}.${two(at.day)} ${two(at.hour)}:${two(at.minute)}';
|
||||
}
|
||||
|
||||
/// 브리핑 항목 상세 — B-75 5 요소(출처명 · 게시시각 · 원본 URL · 원문보기 · AI 요약 표시)와
|
||||
/// 공식/팬덤 출처 구분을 한 시트에 싣는다.
|
||||
Future<void> showBriefingDetail(
|
||||
@@ -171,10 +197,7 @@ Future<void> showBriefingDetail(
|
||||
'fandom' => l10n.briefingSourceFandom,
|
||||
_ => null,
|
||||
};
|
||||
String two(int v) => v.toString().padLeft(2, '0');
|
||||
final at = item.sourceAt.toLocal();
|
||||
final stamp =
|
||||
'${at.year}.${two(at.month)}.${two(at.day)} ${two(at.hour)}:${two(at.minute)}';
|
||||
final stamp = briefingStamp(item.sourceAt);
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
|
||||
@@ -377,7 +377,14 @@ enum PlanetHistoryKind {
|
||||
/// PASSPORT Stamp — Eclipse 성공 참여(FX §5.2) · 진화 기념 Emblem(상세정의 22장) · 최초 100명 Badge(별첨 B).
|
||||
eclipseSuccess,
|
||||
emblemEvolution,
|
||||
founderBadge,
|
||||
founderBadge;
|
||||
|
||||
/// UX·FX v1.5 §7.2 「역사적 사건」 — Planet Evolution · Eclipse 성공 참여 · 주요 Badge 획득은
|
||||
/// 일반 활동 로그와 구분되는 Stamp 로 보인다. Campaign 성공은 역사 응답에 종류가 없어 아직 빠진다.
|
||||
bool get isHistoric => switch (this) {
|
||||
evolution || eclipseSuccess || emblemEvolution || founderBadge => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
class PlanetHistoryEntry {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'planet_data.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import '../../core/ui/bottom_inset.dart';
|
||||
@@ -86,47 +87,52 @@ class _PlanetHistoryScreenState extends State<PlanetHistoryScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
for (final item in _items)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(switch (item.kind) {
|
||||
PlanetHistoryKind.evolution => Icons.auto_awesome,
|
||||
PlanetHistoryKind.hibernation => Icons.bedtime_outlined,
|
||||
PlanetHistoryKind.wakeUp => Icons.wb_sunny_outlined,
|
||||
PlanetHistoryKind.zap => Icons.bolt,
|
||||
PlanetHistoryKind.questReward => Icons.task_alt,
|
||||
PlanetHistoryKind.companyAdBonus => Icons.play_circle_outline,
|
||||
PlanetHistoryKind.funding => Icons.favorite_border,
|
||||
PlanetHistoryKind.eclipseSuccess => Icons.shield_outlined,
|
||||
PlanetHistoryKind.emblemEvolution => Icons.military_tech_outlined,
|
||||
PlanetHistoryKind.founderBadge => Icons.workspace_premium_outlined,
|
||||
}),
|
||||
title: Text(switch (item.kind) {
|
||||
PlanetHistoryKind.evolution => context.l10n.historyEvolution(
|
||||
'${item.fromStage}',
|
||||
'${item.toStage}',
|
||||
_Stamp(
|
||||
historic: item.kind.isHistoric,
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(switch (item.kind) {
|
||||
PlanetHistoryKind.evolution => Icons.auto_awesome,
|
||||
PlanetHistoryKind.hibernation => Icons.bedtime_outlined,
|
||||
PlanetHistoryKind.wakeUp => Icons.wb_sunny_outlined,
|
||||
PlanetHistoryKind.zap => Icons.bolt,
|
||||
PlanetHistoryKind.questReward => Icons.task_alt,
|
||||
PlanetHistoryKind.companyAdBonus => Icons.play_circle_outline,
|
||||
PlanetHistoryKind.funding => Icons.favorite_border,
|
||||
PlanetHistoryKind.eclipseSuccess => Icons.shield_outlined,
|
||||
PlanetHistoryKind.emblemEvolution =>
|
||||
Icons.military_tech_outlined,
|
||||
PlanetHistoryKind.founderBadge =>
|
||||
Icons.workspace_premium_outlined,
|
||||
}),
|
||||
title: Text(switch (item.kind) {
|
||||
PlanetHistoryKind.evolution => context.l10n.historyEvolution(
|
||||
'${item.fromStage}',
|
||||
'${item.toStage}',
|
||||
),
|
||||
PlanetHistoryKind.hibernation =>
|
||||
context.l10n.historyHibernation,
|
||||
PlanetHistoryKind.wakeUp => context.l10n.historyWakeUp,
|
||||
PlanetHistoryKind.zap => context.l10n.historyZap,
|
||||
PlanetHistoryKind.questReward => context.l10n.historyQuest,
|
||||
PlanetHistoryKind.companyAdBonus => context.l10n.historyAdBonus,
|
||||
PlanetHistoryKind.funding => context.l10n.historyFunding,
|
||||
PlanetHistoryKind.eclipseSuccess =>
|
||||
context.l10n.historyEclipseSuccess,
|
||||
PlanetHistoryKind.emblemEvolution =>
|
||||
'${context.l10n.historyEmblemEvolution} · STAGE ${item.toStage ?? ''}',
|
||||
PlanetHistoryKind.founderBadge =>
|
||||
'${context.l10n.historyFounderBadge} · #${(item.memberNo ?? 0).toString().padLeft(3, '0')}',
|
||||
}),
|
||||
subtitle: Text(
|
||||
[
|
||||
MaterialLocalizations.of(context)
|
||||
.formatMediumDate(item.occurredAt.toLocal()),
|
||||
if (item.amount != null)
|
||||
context.l10n.historyGrowthPop('${item.amount}'),
|
||||
if (item.participationConfirmed) context.l10n.historyJoined,
|
||||
].join('\n'),
|
||||
),
|
||||
PlanetHistoryKind.hibernation => context.l10n.historyHibernation,
|
||||
PlanetHistoryKind.wakeUp => context.l10n.historyWakeUp,
|
||||
PlanetHistoryKind.zap => context.l10n.historyZap,
|
||||
PlanetHistoryKind.questReward => context.l10n.historyQuest,
|
||||
PlanetHistoryKind.companyAdBonus => context.l10n.historyAdBonus,
|
||||
PlanetHistoryKind.funding => context.l10n.historyFunding,
|
||||
PlanetHistoryKind.eclipseSuccess =>
|
||||
context.l10n.historyEclipseSuccess,
|
||||
PlanetHistoryKind.emblemEvolution =>
|
||||
'${context.l10n.historyEmblemEvolution} · STAGE ${item.toStage ?? ''}',
|
||||
PlanetHistoryKind.founderBadge =>
|
||||
'${context.l10n.historyFounderBadge} · #${(item.memberNo ?? 0).toString().padLeft(3, '0')}',
|
||||
}),
|
||||
subtitle: Text(
|
||||
[
|
||||
MaterialLocalizations.of(
|
||||
context,
|
||||
).formatMediumDate(item.occurredAt.toLocal()),
|
||||
if (item.amount != null)
|
||||
context.l10n.historyGrowthPop('${item.amount}'),
|
||||
if (item.participationConfirmed) context.l10n.historyJoined,
|
||||
].join('\n'),
|
||||
),
|
||||
),
|
||||
if (_loading) const Center(child: CircularProgressIndicator()),
|
||||
@@ -145,3 +151,36 @@ class _PlanetHistoryScreenState extends State<PlanetHistoryScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 「역사적 사건」(UX·FX v1.5 §7.2)은 테두리 Stamp 와 표지로 일반 활동 줄과 가른다.
|
||||
/// 앱 Figma 에 PASSPORT 기록 화면이 없어 화면 테마 색만 쓴다.
|
||||
class _Stamp extends StatelessWidget {
|
||||
const _Stamp({required this.historic, required this.child});
|
||||
final bool historic;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!historic) return child;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: scheme.primary),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.historyHistoric,
|
||||
style: Theme.of(context).textTheme.labelSmall
|
||||
?.copyWith(color: scheme.primary),
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,6 +544,7 @@
|
||||
"historyEclipseSuccess": "Joined a successful eclipse defense",
|
||||
"historyEmblemEvolution": "Evolution emblem",
|
||||
"historyFounderBadge": "First 100 commemorative badge",
|
||||
"historyHistoric": "Historic moment",
|
||||
"historyGrowthPop": "{amount} POP added to growth",
|
||||
"historyJoined": "You were part of this evolution",
|
||||
"historyLoadFailed": "Couldn't load history.",
|
||||
|
||||
@@ -544,6 +544,7 @@
|
||||
"historyEclipseSuccess": "Participaste en una defensa exitosa del eclipse",
|
||||
"historyEmblemEvolution": "Emblema de evolución",
|
||||
"historyFounderBadge": "Insignia conmemorativa de los primeros 100",
|
||||
"historyHistoric": "Momento histórico",
|
||||
"historyGrowthPop": "{amount} POP sumados al crecimiento",
|
||||
"historyJoined": "Participaste en esta evolución",
|
||||
"historyLoadFailed": "No se pudo cargar el historial.",
|
||||
|
||||
@@ -544,6 +544,7 @@
|
||||
"historyEclipseSuccess": "イクリプス防御成功に参加",
|
||||
"historyEmblemEvolution": "進化記念エンブレム",
|
||||
"historyFounderBadge": "最初の100人記念バッジ",
|
||||
"historyHistoric": "歴史的な出来事",
|
||||
"historyGrowthPop": "成長に{amount} POP反映",
|
||||
"historyJoined": "この進化に参加しました",
|
||||
"historyLoadFailed": "記録を読み込めませんでした。",
|
||||
|
||||
@@ -1065,6 +1065,7 @@
|
||||
"historyEclipseSuccess": "이클립스 방어 성공 참여",
|
||||
"historyEmblemEvolution": "진화 기념 Emblem",
|
||||
"historyFounderBadge": "최초 100명 기념 Badge",
|
||||
"historyHistoric": "역사적 사건",
|
||||
"historyGrowthPop": "성장에 {amount} POP 반영",
|
||||
"@historyGrowthPop": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -65,8 +65,7 @@ import 'app_localizations_zh.dart';
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
@@ -74,8 +73,7 @@ abstract class AppLocalizations {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations);
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
@@ -87,13 +85,12 @@ abstract class AppLocalizations {
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
@@ -101,7 +98,7 @@ abstract class AppLocalizations {
|
||||
Locale('es'),
|
||||
Locale('ja'),
|
||||
Locale('ko'),
|
||||
Locale('zh'),
|
||||
Locale('zh')
|
||||
];
|
||||
|
||||
/// No description provided for @commonConfirm.
|
||||
@@ -3368,6 +3365,12 @@ abstract class AppLocalizations {
|
||||
/// **'최초 100명 기념 Badge'**
|
||||
String get historyFounderBadge;
|
||||
|
||||
/// No description provided for @historyHistoric.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'역사적 사건'**
|
||||
String get historyHistoric;
|
||||
|
||||
/// No description provided for @historyGrowthPop.
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
@@ -4350,12 +4353,7 @@ abstract class AppLocalizations {
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'{startMonth}월 {startDay}일 ~ {endMonth}월 {endDay}일'**
|
||||
String campaignPeriodRange(
|
||||
String startMonth,
|
||||
String startDay,
|
||||
String endMonth,
|
||||
String endDay,
|
||||
);
|
||||
String campaignPeriodRange(String startMonth, String startDay, String endMonth, String endDay);
|
||||
|
||||
/// No description provided for @campaignPeriodSelect.
|
||||
///
|
||||
@@ -4817,14 +4815,7 @@ abstract class AppLocalizations {
|
||||
///
|
||||
/// In ko, this message translates to:
|
||||
/// **'{title}\n보상 {reward} POP × {count}명 = {pool} POP\n수수료 {fee} POP (공개 시 소멸)\n총 {total} POP 이 차감됩니다.'**
|
||||
String questCreateCost(
|
||||
String title,
|
||||
String reward,
|
||||
String count,
|
||||
String pool,
|
||||
String fee,
|
||||
String total,
|
||||
);
|
||||
String questCreateCost(String title, String reward, String count, String pool, String fee, String total);
|
||||
|
||||
/// No description provided for @questNoReward.
|
||||
///
|
||||
@@ -5541,8 +5532,7 @@ abstract class AppLocalizations {
|
||||
String liveHeroTimeBy(String fandom, String name);
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
extends LocalizationsDelegate<AppLocalizations> {
|
||||
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
@@ -5551,32 +5541,28 @@ class _AppLocalizationsDelegate
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['en', 'es', 'ja', 'ko', 'zh'].contains(locale.languageCode);
|
||||
bool isSupported(Locale locale) => <String>['en', 'es', 'ja', 'ko', 'zh'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
|
||||
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
case 'es':
|
||||
return AppLocalizationsEs();
|
||||
case 'ja':
|
||||
return AppLocalizationsJa();
|
||||
case 'ko':
|
||||
return AppLocalizationsKo();
|
||||
case 'zh':
|
||||
return AppLocalizationsZh();
|
||||
case 'en': return AppLocalizationsEn();
|
||||
case 'es': return AppLocalizationsEs();
|
||||
case 'ja': return AppLocalizationsJa();
|
||||
case 'ko': return AppLocalizationsKo();
|
||||
case 'zh': return AppLocalizationsZh();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
'that was used.'
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -63,25 +63,31 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String authProviderLogin(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'apple': 'Appleでログイン',
|
||||
'google': 'Googleでログイン',
|
||||
'kakao': 'Kakaoでログイン',
|
||||
'line': 'LINEでログイン',
|
||||
'other': 'メールでログイン',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'apple': 'Appleでログイン',
|
||||
'google': 'Googleでログイン',
|
||||
'kakao': 'Kakaoでログイン',
|
||||
'line': 'LINEでログイン',
|
||||
'other': 'メールでログイン',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String authProviderSignup(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'apple': 'Appleで登録',
|
||||
'google': 'Googleで登録',
|
||||
'kakao': 'Kakaoで登録',
|
||||
'line': 'LINEで登録',
|
||||
'other': 'メールで登録',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'apple': 'Appleで登録',
|
||||
'google': 'Googleで登録',
|
||||
'kakao': 'Kakaoで登録',
|
||||
'line': 'LINEで登録',
|
||||
'other': 'メールで登録',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@@ -519,25 +525,31 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String authProviderStart(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'email': 'メールではじめる',
|
||||
'kakao': 'Kakaoではじめる',
|
||||
'line': 'LINEではじめる',
|
||||
'apple': 'Appleではじめる',
|
||||
'other': 'Googleではじめる',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'email': 'メールではじめる',
|
||||
'kakao': 'Kakaoではじめる',
|
||||
'line': 'LINEではじめる',
|
||||
'apple': 'Appleではじめる',
|
||||
'other': 'Googleではじめる',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String authProviderName(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'email': 'メール',
|
||||
'kakao': 'Kakao',
|
||||
'line': 'LINE',
|
||||
'apple': 'Apple',
|
||||
'other': 'Google',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'email': 'メール',
|
||||
'kakao': 'Kakao',
|
||||
'line': 'LINE',
|
||||
'apple': 'Apple',
|
||||
'other': 'Google',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@@ -869,8 +881,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get nebulaNameTaken => 'すでに使用されている名前です。';
|
||||
|
||||
@override
|
||||
String get nebulaNameInvalid =>
|
||||
'2〜20文字のハングル・英字・数字で入力してください(記号・絵文字・連続/前後の空白は不可)。';
|
||||
String get nebulaNameInvalid => '2〜20文字のハングル・英字・数字で入力してください(記号・絵文字・連続/前後の空白は不可)。';
|
||||
|
||||
@override
|
||||
String get nebulaNameHint => 'ファンダム名';
|
||||
@@ -1187,8 +1198,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get pushTopicEssentialDesc => 'アカウント・決済・メンテナンスのお知らせです。オフにできません。';
|
||||
|
||||
@override
|
||||
String get pushTopicFandomEventDesc =>
|
||||
'イクリプス・キャンペーン・クエスト・アーティストホットタイムのお知らせです。';
|
||||
String get pushTopicFandomEventDesc => 'イクリプス・キャンペーン・クエスト・アーティストホットタイムのお知らせです。';
|
||||
|
||||
@override
|
||||
String get pushTopicBriefingDesc => '惑星の一日をまとめたブリーフィングカードをお届けします。';
|
||||
@@ -1294,8 +1304,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get payEmpty => '決済履歴はありません。';
|
||||
|
||||
@override
|
||||
String get payRefundGuide =>
|
||||
'消費型商品:購入後7日以内かつ未使用の商品は返金できます。一部でも使用した商品は返金対象外です。\nサブスク商品:決済後24時間以内に特典(追加報酬など)を未使用の場合、返金できます。\n返金承認後7〜14日以内に返金が完了します。';
|
||||
String get payRefundGuide => '消費型商品:購入後7日以内かつ未使用の商品は返金できます。一部でも使用した商品は返金対象外です。\nサブスク商品:決済後24時間以内に特典(追加報酬など)を未使用の場合、返金できます。\n返金承認後7〜14日以内に返金が完了します。';
|
||||
|
||||
@override
|
||||
String get payDetailTitle => '決済履歴の詳細';
|
||||
@@ -1809,6 +1818,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get historyFounderBadge => '最初の100人記念バッジ';
|
||||
|
||||
@override
|
||||
String get historyHistoric => '歴史的な出来事';
|
||||
|
||||
@override
|
||||
String historyGrowthPop(String amount) {
|
||||
return '成長に$amount POP反映';
|
||||
@@ -1831,31 +1843,34 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String countryName(String code) {
|
||||
String _temp0 = intl.Intl.selectLogic(code, {
|
||||
'KR': '韓国',
|
||||
'JP': '日本',
|
||||
'US': 'アメリカ',
|
||||
'CN': '中国',
|
||||
'TW': '台湾',
|
||||
'HK': '香港',
|
||||
'TH': 'タイ',
|
||||
'VN': 'ベトナム',
|
||||
'ID': 'インドネシア',
|
||||
'PH': 'フィリピン',
|
||||
'MY': 'マレーシア',
|
||||
'SG': 'シンガポール',
|
||||
'IN': 'インド',
|
||||
'GB': 'イギリス',
|
||||
'FR': 'フランス',
|
||||
'DE': 'ドイツ',
|
||||
'ES': 'スペイン',
|
||||
'BR': 'ブラジル',
|
||||
'MX': 'メキシコ',
|
||||
'CA': 'カナダ',
|
||||
'AU': 'オーストラリア',
|
||||
'ZZ': '未指定',
|
||||
'other': '-',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
code,
|
||||
{
|
||||
'KR': '韓国',
|
||||
'JP': '日本',
|
||||
'US': 'アメリカ',
|
||||
'CN': '中国',
|
||||
'TW': '台湾',
|
||||
'HK': '香港',
|
||||
'TH': 'タイ',
|
||||
'VN': 'ベトナム',
|
||||
'ID': 'インドネシア',
|
||||
'PH': 'フィリピン',
|
||||
'MY': 'マレーシア',
|
||||
'SG': 'シンガポール',
|
||||
'IN': 'インド',
|
||||
'GB': 'イギリス',
|
||||
'FR': 'フランス',
|
||||
'DE': 'ドイツ',
|
||||
'ES': 'スペイン',
|
||||
'BR': 'ブラジル',
|
||||
'MX': 'メキシコ',
|
||||
'CA': 'カナダ',
|
||||
'AU': 'オーストラリア',
|
||||
'ZZ': '未指定',
|
||||
'other': '-',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@@ -2051,8 +2066,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get adWarpBefore =>
|
||||
'Warping...ファンダムとアーティストにもっと貢献できるよう、ぴったりの広告を探しています。';
|
||||
String get adWarpBefore => 'Warping...ファンダムとアーティストにもっと貢献できるよう、ぴったりの広告を探しています。';
|
||||
|
||||
@override
|
||||
String get adWarpAfter => 'Warping...あなたの貢献への報酬を準備しています。';
|
||||
@@ -2205,8 +2219,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get nebulaAlreadyToday => '今日はすでに星雲を作成しました。';
|
||||
|
||||
@override
|
||||
String get nebulaCreatedKeepPrimary =>
|
||||
'星雲を作成しました。メイン惑星は前回の変更から30日経過後に変更できるため、そのままです。';
|
||||
String get nebulaCreatedKeepPrimary => '星雲を作成しました。メイン惑星は前回の変更から30日経過後に変更できるため、そのままです。';
|
||||
|
||||
@override
|
||||
String get planetLeave => '脱退する';
|
||||
@@ -2372,12 +2385,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get campaignWeekdays => '日,月,火,水,木,金,土';
|
||||
|
||||
@override
|
||||
String campaignPeriodRange(
|
||||
String startMonth,
|
||||
String startDay,
|
||||
String endMonth,
|
||||
String endDay,
|
||||
) {
|
||||
String campaignPeriodRange(String startMonth, String startDay, String endMonth, String endDay) {
|
||||
return '$startMonth月$startDay日 ~ $endMonth月$endDay日';
|
||||
}
|
||||
|
||||
@@ -2421,12 +2429,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get campaignSubmitTitle => 'この内容で投稿しますか?';
|
||||
|
||||
@override
|
||||
String campaignSubmitBody(
|
||||
String title,
|
||||
String goal,
|
||||
String days,
|
||||
String fee,
|
||||
) {
|
||||
String campaignSubmitBody(String title, String goal, String days, String fee) {
|
||||
return '$title · 目標 $goal POP · $days日\n登録料 $fee POP が差し引かれ、運営の審査後に支援が始まります。';
|
||||
}
|
||||
|
||||
@@ -2633,14 +2636,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String questCreateCost(
|
||||
String title,
|
||||
String reward,
|
||||
String count,
|
||||
String pool,
|
||||
String fee,
|
||||
String total,
|
||||
) {
|
||||
String questCreateCost(String title, String reward, String count, String pool, String fee, String total) {
|
||||
return '$title\n報酬 $reward POP × $count人 = $pool POP\n手数料 $fee POP(公開時に消滅)\n合計 $total POP が差し引かれます。';
|
||||
}
|
||||
|
||||
@@ -2849,8 +2845,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get questCancelInReview => '審査中に取り消すと、預けた報酬POPと手数料はすべて返還されます。';
|
||||
|
||||
@override
|
||||
String get questCancelPublished =>
|
||||
'公開中のクエストを取り消すと参加はすぐ締め切られ、残りの報酬POPは返還されます。手数料は返還されません。';
|
||||
String get questCancelPublished => '公開中のクエストを取り消すと参加はすぐ締め切られ、残りの報酬POPは返還されます。手数料は返還されません。';
|
||||
|
||||
@override
|
||||
String get questUnderReview => '運営の審査中です。承認されると公開されます。';
|
||||
|
||||
@@ -63,25 +63,31 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String authProviderLogin(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'apple': 'Apple로 로그인',
|
||||
'google': 'Google로 로그인',
|
||||
'kakao': 'Kakao로 로그인',
|
||||
'line': 'LINE으로 로그인',
|
||||
'other': '이메일로 로그인',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'apple': 'Apple로 로그인',
|
||||
'google': 'Google로 로그인',
|
||||
'kakao': 'Kakao로 로그인',
|
||||
'line': 'LINE으로 로그인',
|
||||
'other': '이메일로 로그인',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String authProviderSignup(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'apple': 'Apple로 가입하기',
|
||||
'google': 'Google로 가입하기',
|
||||
'kakao': 'Kakao로 가입하기',
|
||||
'line': 'LINE으로 가입하기',
|
||||
'other': '이메일로 가입하기',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'apple': 'Apple로 가입하기',
|
||||
'google': 'Google로 가입하기',
|
||||
'kakao': 'Kakao로 가입하기',
|
||||
'line': 'LINE으로 가입하기',
|
||||
'other': '이메일로 가입하기',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@@ -519,25 +525,31 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String authProviderStart(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'email': '이메일로 시작하기',
|
||||
'kakao': '카카오로 시작하기',
|
||||
'line': 'LINE으로 시작하기',
|
||||
'apple': 'Apple로 시작하기',
|
||||
'other': 'Google로 시작하기',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'email': '이메일로 시작하기',
|
||||
'kakao': '카카오로 시작하기',
|
||||
'line': 'LINE으로 시작하기',
|
||||
'apple': 'Apple로 시작하기',
|
||||
'other': 'Google로 시작하기',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String authProviderName(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'email': '이메일',
|
||||
'kakao': '카카오',
|
||||
'line': 'LINE',
|
||||
'apple': 'Apple',
|
||||
'other': 'Google',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'email': '이메일',
|
||||
'kakao': '카카오',
|
||||
'line': 'LINE',
|
||||
'apple': 'Apple',
|
||||
'other': 'Google',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@@ -869,8 +881,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get nebulaNameTaken => '이미 사용중인 이름입니다.';
|
||||
|
||||
@override
|
||||
String get nebulaNameInvalid =>
|
||||
'2~20자 한글·영문·숫자로 입력해 주세요(특수문자·이모지·연속/앞뒤 공백 불가).';
|
||||
String get nebulaNameInvalid => '2~20자 한글·영문·숫자로 입력해 주세요(특수문자·이모지·연속/앞뒤 공백 불가).';
|
||||
|
||||
@override
|
||||
String get nebulaNameHint => '팬덤 이름';
|
||||
@@ -1293,8 +1304,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get payEmpty => '결제 내역이 없습니다.';
|
||||
|
||||
@override
|
||||
String get payRefundGuide =>
|
||||
'소비형 상품 : 구매 후 7일 이내이며 사용하지 않은 상품은 환불 가능합니다. 일부라도 사용된 상품은 환불 대상에서 제외됩니다.\n구독형 상품 : 결제 후 24시간 이내 혜택 미사용(추가 보상 등) 시 환불 가능합니다.\n환불 승인 후 7-14 이내 환불 완료 처리가 됩니다.';
|
||||
String get payRefundGuide => '소비형 상품 : 구매 후 7일 이내이며 사용하지 않은 상품은 환불 가능합니다. 일부라도 사용된 상품은 환불 대상에서 제외됩니다.\n구독형 상품 : 결제 후 24시간 이내 혜택 미사용(추가 보상 등) 시 환불 가능합니다.\n환불 승인 후 7-14 이내 환불 완료 처리가 됩니다.';
|
||||
|
||||
@override
|
||||
String get payDetailTitle => '결제 내역 상세';
|
||||
@@ -1480,8 +1490,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get tierPromptSuffix => '를 올려보세요';
|
||||
|
||||
@override
|
||||
String get tierPassportNote =>
|
||||
'Passport Tier 는 레벨과 별개인 카드 외형 등급이며 내려가지 않습니다.';
|
||||
String get tierPassportNote => 'Passport Tier 는 레벨과 별개인 카드 외형 등급이며 내려가지 않습니다.';
|
||||
|
||||
@override
|
||||
String get tierTotalContribution => '누적 기여';
|
||||
@@ -1585,8 +1594,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get stationReason => '추천 이유 (선택)';
|
||||
|
||||
@override
|
||||
String get stationSuggestNote =>
|
||||
'제안하신 링크는 운영 검토를 거쳐 등록됩니다. 검토 결과는 알림으로 알려 드립니다.';
|
||||
String get stationSuggestNote => '제안하신 링크는 운영 검토를 거쳐 등록됩니다. 검토 결과는 알림으로 알려 드립니다.';
|
||||
|
||||
@override
|
||||
String get heroGiftUnavailable => '이 선물은 현재 수령할 수 없습니다.';
|
||||
@@ -1810,6 +1818,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get historyFounderBadge => '최초 100명 기념 Badge';
|
||||
|
||||
@override
|
||||
String get historyHistoric => '역사적 사건';
|
||||
|
||||
@override
|
||||
String historyGrowthPop(String amount) {
|
||||
return '성장에 $amount POP 반영';
|
||||
@@ -1832,31 +1843,34 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String countryName(String code) {
|
||||
String _temp0 = intl.Intl.selectLogic(code, {
|
||||
'KR': '대한민국',
|
||||
'JP': '일본',
|
||||
'US': '미국',
|
||||
'CN': '중국',
|
||||
'TW': '대만',
|
||||
'HK': '홍콩',
|
||||
'TH': '태국',
|
||||
'VN': '베트남',
|
||||
'ID': '인도네시아',
|
||||
'PH': '필리핀',
|
||||
'MY': '말레이시아',
|
||||
'SG': '싱가포르',
|
||||
'IN': '인도',
|
||||
'GB': '영국',
|
||||
'FR': '프랑스',
|
||||
'DE': '독일',
|
||||
'ES': '스페인',
|
||||
'BR': '브라질',
|
||||
'MX': '멕시코',
|
||||
'CA': '캐나다',
|
||||
'AU': '호주',
|
||||
'ZZ': '미지정',
|
||||
'other': '-',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
code,
|
||||
{
|
||||
'KR': '대한민국',
|
||||
'JP': '일본',
|
||||
'US': '미국',
|
||||
'CN': '중국',
|
||||
'TW': '대만',
|
||||
'HK': '홍콩',
|
||||
'TH': '태국',
|
||||
'VN': '베트남',
|
||||
'ID': '인도네시아',
|
||||
'PH': '필리핀',
|
||||
'MY': '말레이시아',
|
||||
'SG': '싱가포르',
|
||||
'IN': '인도',
|
||||
'GB': '영국',
|
||||
'FR': '프랑스',
|
||||
'DE': '독일',
|
||||
'ES': '스페인',
|
||||
'BR': '브라질',
|
||||
'MX': '멕시코',
|
||||
'CA': '캐나다',
|
||||
'AU': '호주',
|
||||
'ZZ': '미지정',
|
||||
'other': '-',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@@ -2052,8 +2066,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get adWarpBefore =>
|
||||
'Warping...우리 팬덤과 아티스트에 더 많은 기여를 위해 적합한 광고를 열심히 찾고 있어요.';
|
||||
String get adWarpBefore => 'Warping...우리 팬덤과 아티스트에 더 많은 기여를 위해 적합한 광고를 열심히 찾고 있어요.';
|
||||
|
||||
@override
|
||||
String get adWarpAfter => 'Warping...당신의 기여에 대한 보상을 준비하고 있어요.';
|
||||
@@ -2206,8 +2219,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get nebulaAlreadyToday => '이미 오늘 성운을 생성하셨습니다.';
|
||||
|
||||
@override
|
||||
String get nebulaCreatedKeepPrimary =>
|
||||
'성운을 만들었습니다. 기본 행성은 최근 변경 후 30일이 지나야 바꿀 수 있어 그대로 유지됩니다.';
|
||||
String get nebulaCreatedKeepPrimary => '성운을 만들었습니다. 기본 행성은 최근 변경 후 30일이 지나야 바꿀 수 있어 그대로 유지됩니다.';
|
||||
|
||||
@override
|
||||
String get planetLeave => '탈퇴하기';
|
||||
@@ -2234,8 +2246,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get campaignStatusWithdrawn => '개설자가 철회한 캠페인입니다.';
|
||||
|
||||
@override
|
||||
String get campaignStatusCancelled =>
|
||||
'운영 사유로 취소되었습니다. 등록비와 후원 POP 을 돌려드렸습니다.';
|
||||
String get campaignStatusCancelled => '운영 사유로 취소되었습니다. 등록비와 후원 POP 을 돌려드렸습니다.';
|
||||
|
||||
@override
|
||||
String get rankWindowDaily => '일간';
|
||||
@@ -2374,12 +2385,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get campaignWeekdays => '일,월,화,수,목,금,토';
|
||||
|
||||
@override
|
||||
String campaignPeriodRange(
|
||||
String startMonth,
|
||||
String startDay,
|
||||
String endMonth,
|
||||
String endDay,
|
||||
) {
|
||||
String campaignPeriodRange(String startMonth, String startDay, String endMonth, String endDay) {
|
||||
return '$startMonth월 $startDay일 ~ $endMonth월 $endDay일';
|
||||
}
|
||||
|
||||
@@ -2423,12 +2429,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get campaignSubmitTitle => '이 내용으로 올릴까요?';
|
||||
|
||||
@override
|
||||
String campaignSubmitBody(
|
||||
String title,
|
||||
String goal,
|
||||
String days,
|
||||
String fee,
|
||||
) {
|
||||
String campaignSubmitBody(String title, String goal, String days, String fee) {
|
||||
return '$title · 목표 $goal POP · $days일\n등록비 $fee POP 이 빠지고 운영 검수 뒤 모금이 열립니다.';
|
||||
}
|
||||
|
||||
@@ -2635,14 +2636,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String questCreateCost(
|
||||
String title,
|
||||
String reward,
|
||||
String count,
|
||||
String pool,
|
||||
String fee,
|
||||
String total,
|
||||
) {
|
||||
String questCreateCost(String title, String reward, String count, String pool, String fee, String total) {
|
||||
return '$title\n보상 $reward POP × $count명 = $pool POP\n수수료 $fee POP (공개 시 소멸)\n총 $total POP 이 차감됩니다.';
|
||||
}
|
||||
|
||||
@@ -2848,12 +2842,10 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get questCancel => '퀘스트 취소';
|
||||
|
||||
@override
|
||||
String get questCancelInReview =>
|
||||
'검수 중인 퀘스트를 취소하면 예치한 보상 POP 과 수수료를 모두 돌려드려요.';
|
||||
String get questCancelInReview => '검수 중인 퀘스트를 취소하면 예치한 보상 POP 과 수수료를 모두 돌려드려요.';
|
||||
|
||||
@override
|
||||
String get questCancelPublished =>
|
||||
'공개된 퀘스트를 취소하면 참여가 바로 마감되고 남은 보상 POP 은 돌려드려요. 수수료는 돌려드리지 않아요.';
|
||||
String get questCancelPublished => '공개된 퀘스트를 취소하면 참여가 바로 마감되고 남은 보상 POP 은 돌려드려요. 수수료는 돌려드리지 않아요.';
|
||||
|
||||
@override
|
||||
String get questUnderReview => '운영 검수 중이에요. 승인되면 공개됩니다.';
|
||||
@@ -2974,8 +2966,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get onbPermLocation => '위치';
|
||||
|
||||
@override
|
||||
String get onbPermLocationDesc =>
|
||||
'위치 퀘스트의 장소 도착을 확인합니다. 위치 퀘스트를 처음 시작할 때 요청해요.';
|
||||
String get onbPermLocationDesc => '위치 퀘스트의 장소 도착을 확인합니다. 위치 퀘스트를 처음 시작할 때 요청해요.';
|
||||
|
||||
@override
|
||||
String get onbPermContinue => '계속';
|
||||
|
||||
@@ -63,25 +63,31 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String authProviderLogin(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'apple': '通过Apple登录',
|
||||
'google': '通过Google登录',
|
||||
'kakao': '通过Kakao登录',
|
||||
'line': '通过LINE登录',
|
||||
'other': '通过邮箱登录',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'apple': '通过Apple登录',
|
||||
'google': '通过Google登录',
|
||||
'kakao': '通过Kakao登录',
|
||||
'line': '通过LINE登录',
|
||||
'other': '通过邮箱登录',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String authProviderSignup(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'apple': '通过Apple注册',
|
||||
'google': '通过Google注册',
|
||||
'kakao': '通过Kakao注册',
|
||||
'line': '通过LINE注册',
|
||||
'other': '通过邮箱注册',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'apple': '通过Apple注册',
|
||||
'google': '通过Google注册',
|
||||
'kakao': '通过Kakao注册',
|
||||
'line': '通过LINE注册',
|
||||
'other': '通过邮箱注册',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@@ -519,25 +525,31 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String authProviderStart(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'email': '使用邮箱开始',
|
||||
'kakao': '使用 Kakao 开始',
|
||||
'line': '使用 LINE 开始',
|
||||
'apple': '使用 Apple 开始',
|
||||
'other': '使用 Google 开始',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'email': '使用邮箱开始',
|
||||
'kakao': '使用 Kakao 开始',
|
||||
'line': '使用 LINE 开始',
|
||||
'apple': '使用 Apple 开始',
|
||||
'other': '使用 Google 开始',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String authProviderName(String provider) {
|
||||
String _temp0 = intl.Intl.selectLogic(provider, {
|
||||
'email': '邮箱',
|
||||
'kakao': 'Kakao',
|
||||
'line': 'LINE',
|
||||
'apple': 'Apple',
|
||||
'other': 'Google',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
provider,
|
||||
{
|
||||
'email': '邮箱',
|
||||
'kakao': 'Kakao',
|
||||
'line': 'LINE',
|
||||
'apple': 'Apple',
|
||||
'other': 'Google',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@@ -1292,8 +1304,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get payEmpty => '暂无支付记录。';
|
||||
|
||||
@override
|
||||
String get payRefundGuide =>
|
||||
'消耗型商品:购买后 7 天内且未使用可退款,已部分使用的商品不予退款。\n订阅型商品:支付后 24 小时内未使用权益(如额外奖励)可退款。\n退款批准后 7–14 天内完成退款。';
|
||||
String get payRefundGuide => '消耗型商品:购买后 7 天内且未使用可退款,已部分使用的商品不予退款。\n订阅型商品:支付后 24 小时内未使用权益(如额外奖励)可退款。\n退款批准后 7–14 天内完成退款。';
|
||||
|
||||
@override
|
||||
String get payDetailTitle => '支付详情';
|
||||
@@ -1807,6 +1818,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get historyFounderBadge => '前100名纪念徽章';
|
||||
|
||||
@override
|
||||
String get historyHistoric => '历史性事件';
|
||||
|
||||
@override
|
||||
String historyGrowthPop(String amount) {
|
||||
return '$amount POP 计入成长';
|
||||
@@ -1829,31 +1843,34 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String countryName(String code) {
|
||||
String _temp0 = intl.Intl.selectLogic(code, {
|
||||
'KR': '韩国',
|
||||
'JP': '日本',
|
||||
'US': '美国',
|
||||
'CN': '中国',
|
||||
'TW': '台湾',
|
||||
'HK': '香港',
|
||||
'TH': '泰国',
|
||||
'VN': '越南',
|
||||
'ID': '印度尼西亚',
|
||||
'PH': '菲律宾',
|
||||
'MY': '马来西亚',
|
||||
'SG': '新加坡',
|
||||
'IN': '印度',
|
||||
'GB': '英国',
|
||||
'FR': '法国',
|
||||
'DE': '德国',
|
||||
'ES': '西班牙',
|
||||
'BR': '巴西',
|
||||
'MX': '墨西哥',
|
||||
'CA': '加拿大',
|
||||
'AU': '澳大利亚',
|
||||
'ZZ': '未指定',
|
||||
'other': '-',
|
||||
});
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
code,
|
||||
{
|
||||
'KR': '韩国',
|
||||
'JP': '日本',
|
||||
'US': '美国',
|
||||
'CN': '中国',
|
||||
'TW': '台湾',
|
||||
'HK': '香港',
|
||||
'TH': '泰国',
|
||||
'VN': '越南',
|
||||
'ID': '印度尼西亚',
|
||||
'PH': '菲律宾',
|
||||
'MY': '马来西亚',
|
||||
'SG': '新加坡',
|
||||
'IN': '印度',
|
||||
'GB': '英国',
|
||||
'FR': '法国',
|
||||
'DE': '德国',
|
||||
'ES': '西班牙',
|
||||
'BR': '巴西',
|
||||
'MX': '墨西哥',
|
||||
'CA': '加拿大',
|
||||
'AU': '澳大利亚',
|
||||
'ZZ': '未指定',
|
||||
'other': '-',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@@ -2368,12 +2385,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get campaignWeekdays => '日,一,二,三,四,五,六';
|
||||
|
||||
@override
|
||||
String campaignPeriodRange(
|
||||
String startMonth,
|
||||
String startDay,
|
||||
String endMonth,
|
||||
String endDay,
|
||||
) {
|
||||
String campaignPeriodRange(String startMonth, String startDay, String endMonth, String endDay) {
|
||||
return '$startMonth月$startDay日 ~ $endMonth月$endDay日';
|
||||
}
|
||||
|
||||
@@ -2417,12 +2429,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get campaignSubmitTitle => '按此内容提交吗?';
|
||||
|
||||
@override
|
||||
String campaignSubmitBody(
|
||||
String title,
|
||||
String goal,
|
||||
String days,
|
||||
String fee,
|
||||
) {
|
||||
String campaignSubmitBody(String title, String goal, String days, String fee) {
|
||||
return '$title · 目标 $goal POP · $days天\n将扣除 $fee POP 登记费,运营审核后开放筹集。';
|
||||
}
|
||||
|
||||
@@ -2629,14 +2636,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String questCreateCost(
|
||||
String title,
|
||||
String reward,
|
||||
String count,
|
||||
String pool,
|
||||
String fee,
|
||||
String total,
|
||||
) {
|
||||
String questCreateCost(String title, String reward, String count, String pool, String fee, String total) {
|
||||
return '$title\n奖励 $reward POP × $count人 = $pool POP\n手续费 $fee POP(发布时销毁)\n共扣除 $total POP。';
|
||||
}
|
||||
|
||||
|
||||
@@ -544,6 +544,7 @@
|
||||
"historyEclipseSuccess": "参与日食防御成功",
|
||||
"historyEmblemEvolution": "进化纪念徽章",
|
||||
"historyFounderBadge": "前100名纪念徽章",
|
||||
"historyHistoric": "历史性事件",
|
||||
"historyGrowthPop": "{amount} POP 计入成长",
|
||||
"historyJoined": "你参与了这次进化",
|
||||
"historyLoadFailed": "无法加载记录。",
|
||||
|
||||
@@ -934,6 +934,9 @@ class RemoteAppLocalizations extends AppLocalizations {
|
||||
@override
|
||||
String get historyHibernation => _remote['historyHibernation'] ?? _base.historyHibernation;
|
||||
|
||||
@override
|
||||
String get historyHistoric => _remote['historyHistoric'] ?? _base.historyHistoric;
|
||||
|
||||
@override
|
||||
String get historyJoined => _remote['historyJoined'] ?? _base.historyJoined;
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fancake/core/data/repositories.dart'
|
||||
show eclipseLook, eclipseWarnWindow;
|
||||
import 'package:fancake/core/theme/app_theme.dart';
|
||||
import 'package:fancake/core/ui/planet_25d.dart';
|
||||
import 'package:fancake/features/eclipse/eclipse_data.dart';
|
||||
import 'package:fancake/features/eclipse/eclipse_defend_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'eclipse_stubs.dart';
|
||||
|
||||
/// UX·FX v1.5 §5 사전 경고 「그림자 접근」 · §5.1 「끊어진 Ring → 완전한 Shield」
|
||||
/// (진척현황 부록 B-3 5-1 · 5-4).
|
||||
void main() {
|
||||
final warn = DateTime(2026, 10, 8, 7, 47);
|
||||
final start = DateTime(2026, 10, 8, 7, 50);
|
||||
|
||||
test('그림자 접근 진행 — 경고 시각 0 → 발생 시각 1, 경고 전 · 창 없음은 null', () {
|
||||
expect(eclipseApproach(null, start, warn), isNull);
|
||||
expect(eclipseApproach(warn, start, warn.subtract(const Duration(seconds: 1))), isNull);
|
||||
expect(eclipseApproach(warn, start, warn), 0);
|
||||
expect(eclipseApproach(warn, start, warn.add(const Duration(seconds: 90))), 0.5);
|
||||
expect(
|
||||
eclipseApproach(warn, start, start.add(const Duration(seconds: 3))),
|
||||
1,
|
||||
reason: '서버가 회차를 열기 전 지연 동안 그림자가 빠지지 않는다',
|
||||
);
|
||||
});
|
||||
|
||||
test('경보 단계 회차 — 이클립스 감소가 아니라 경고 창만', () {
|
||||
final scheduled = EclipseState(
|
||||
fandomId: 'bts',
|
||||
eclipseId: 'ecl-2',
|
||||
fandomName: 'BTS',
|
||||
erosionRate: 0,
|
||||
defense: 0,
|
||||
targetDefense: 10,
|
||||
defenseCount: 0,
|
||||
defended: false,
|
||||
scheduled: true,
|
||||
warnAt: warn,
|
||||
startAt: start,
|
||||
endsAt: start.add(const Duration(seconds: 60)),
|
||||
);
|
||||
expect(eclipseLook(scheduled, warn), (false, false, null));
|
||||
expect(eclipseWarnWindow(scheduled), (warn, start));
|
||||
expect(eclipseWarnWindow(eclipseFixture()), isNull, reason: '진행 중 회차');
|
||||
expect(eclipseWarnWindow(null), isNull);
|
||||
});
|
||||
|
||||
group('행성 그림자', () {
|
||||
Widget planet({DateTime? warnAt, DateTime? startAt, bool eclipsed = false}) =>
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: Planet25D(
|
||||
image: 'assets/figma/planet-home.png',
|
||||
size: 200,
|
||||
stage: 3,
|
||||
isEclipsed: eclipsed,
|
||||
eclipseWarnAt: warnAt,
|
||||
eclipseStartAt: startAt,
|
||||
),
|
||||
),
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final w = now.subtract(const Duration(seconds: 90));
|
||||
final s = now.add(const Duration(seconds: 90));
|
||||
|
||||
testWidgets('경고 창 안이면 그림자를 그린다', (tester) async {
|
||||
await tester.pumpWidget(planet(warnAt: w, startAt: s));
|
||||
expect(find.byKey(const ValueKey('eclipse-shadow')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('경고가 없거나 이미 이클립스 진행 중이면 그리지 않는다', (tester) async {
|
||||
await tester.pumpWidget(planet());
|
||||
expect(find.byKey(const ValueKey('eclipse-shadow')), findsNothing);
|
||||
await tester.pumpWidget(planet(warnAt: w, startAt: s, eclipsed: true));
|
||||
expect(find.byKey(const ValueKey('eclipse-shadow')), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
test('Shield Ring — 처음엔 끊어진 조각, 참여자만큼 이어지고 목표 달성이면 원 하나', () {
|
||||
const slot = 2 * math.pi / shieldSegments;
|
||||
final broken = shieldArcs(0);
|
||||
expect(broken, hasLength(shieldSegments));
|
||||
expect(broken.first.$2, closeTo(slot * 0.5, 1e-9));
|
||||
expect(shieldArcs(0.5).first.$2, closeTo(slot * 0.75, 1e-9));
|
||||
expect(shieldArcs(0.99).first.$2, lessThan(slot));
|
||||
expect(shieldArcs(1), [(-math.pi / 2, 2 * math.pi)]);
|
||||
});
|
||||
|
||||
testWidgets('방어 화면 Shield 는 서버 고유 참여자 / 목표 비율을 따른다', (tester) async {
|
||||
tester.view.physicalSize = const Size(1080, 2400);
|
||||
tester.view.devicePixelRatio = 2;
|
||||
addTearDown(tester.view.reset);
|
||||
final shakes = StreamController<DateTime>.broadcast();
|
||||
addTearDown(shakes.close);
|
||||
final repo = FakeEclipseRepository(
|
||||
state: eclipseFixture(
|
||||
defense: 3,
|
||||
targetDefense: 4,
|
||||
startAt: DateTime.now().subtract(const Duration(seconds: 1)),
|
||||
endsAt: DateTime.now().add(const Duration(seconds: 59)),
|
||||
),
|
||||
);
|
||||
addTearDown(repo.dispose);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: buildAppTheme(),
|
||||
home: EclipseDefendScreen(
|
||||
repository: repo,
|
||||
fandomId: 'bts',
|
||||
shakes: () => shakes.stream,
|
||||
),
|
||||
),
|
||||
);
|
||||
for (var i = 0; i < 6; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 120));
|
||||
}
|
||||
final ring = tester
|
||||
.widgetList<CustomPaint>(find.descendant(
|
||||
of: find.byKey(const ValueKey('shield-ring')),
|
||||
matching: find.byType(CustomPaint),
|
||||
))
|
||||
.map((p) => p.painter)
|
||||
.whereType<ShieldRingPainter>()
|
||||
.single;
|
||||
expect(ring.progress, 0.75);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
});
|
||||
}
|
||||
@@ -359,6 +359,16 @@ void main() {
|
||||
expect(find.text('NEWS'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('B-3 8-4 — 카드 줄에 출처명 · 게시시각이 보인다(상세 시트를 열지 않아도)', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: buildAppTheme(),
|
||||
home: Scaffold(body: BriefingList(items: [_brief('출처가 보이는 줄')])),
|
||||
));
|
||||
expect(find.text('연합뉴스 · ${briefingStamp(DateTime(2026, 9, 14, 8))}'), findsOneWidget);
|
||||
expect(briefingStamp(DateTime(2026, 9, 4, 7, 5)), '2026.09.04 07:05');
|
||||
expect(find.text('AI 요약'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('줄을 누르면 B-75 5 요소 상세가 뜨고, 원문 보기로 나간다', (tester) async {
|
||||
final opened = <String>[];
|
||||
tester.view.physicalSize = const Size(1080, 2400);
|
||||
|
||||
@@ -48,20 +48,34 @@ class _Backend extends Backend {
|
||||
class _PersonalHistory extends SamplePlanetRepository {
|
||||
final scopes = <bool>[];
|
||||
@override
|
||||
Future<PlanetHistoryPage> history({String? cursor, bool personal = false}) async {
|
||||
Future<PlanetHistoryPage> history({
|
||||
String? cursor,
|
||||
bool personal = false,
|
||||
}) async {
|
||||
scopes.add(personal);
|
||||
return PlanetHistoryPage(items: [PlanetHistoryEntry(
|
||||
id: 'one', kind: personal ? PlanetHistoryKind.zap : PlanetHistoryKind.evolution,
|
||||
occurredAt: DateTime.utc(2026, 9, 22), fromStage: personal ? null : 1,
|
||||
toStage: personal ? null : 2, amount: personal ? 37 : null,
|
||||
)], nextCursor: personal ? null : 'older');
|
||||
return PlanetHistoryPage(
|
||||
items: [
|
||||
PlanetHistoryEntry(
|
||||
id: 'one',
|
||||
kind: personal ? PlanetHistoryKind.zap : PlanetHistoryKind.evolution,
|
||||
occurredAt: DateTime.utc(2026, 9, 22),
|
||||
fromStage: personal ? null : 1,
|
||||
toStage: personal ? null : 2,
|
||||
amount: personal ? 37 : null,
|
||||
),
|
||||
],
|
||||
nextCursor: personal ? null : 'older',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _History extends SamplePlanetRepository {
|
||||
final cursors = <String?>[];
|
||||
@override
|
||||
Future<PlanetHistoryPage> history({String? cursor, bool personal = false}) async {
|
||||
Future<PlanetHistoryPage> history({
|
||||
String? cursor,
|
||||
bool personal = false,
|
||||
}) async {
|
||||
cursors.add(cursor);
|
||||
if (cursors.length == 2) throw StateError('private details');
|
||||
return PlanetHistoryPage(
|
||||
@@ -83,9 +97,11 @@ class _History extends SamplePlanetRepository {
|
||||
void main() {
|
||||
testWidgets('내 활동 전환은 이전 목록과 커서를 지우고 실제 성장량을 표시한다', (tester) async {
|
||||
final repository = _PersonalHistory();
|
||||
await tester.pumpWidget(MaterialApp(home: PlanetHistoryScreen(
|
||||
repository: repository, fandomName: '팬덤',
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: PlanetHistoryScreen(repository: repository, fandomName: '팬덤'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('내 활동'));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -101,10 +117,15 @@ void main() {
|
||||
const repository = SamplePlanetRepository();
|
||||
final states = StreamController<PlanetVisualState>.broadcast();
|
||||
addTearDown(states.close);
|
||||
await tester.pumpWidget(MaterialApp(home: PlanetDetailScreen(
|
||||
repository: repository, eclipse: const NoEclipseRepository(),
|
||||
watchPlanet: (_) => states.stream,
|
||||
)));
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: PlanetDetailScreen(
|
||||
repository: repository,
|
||||
eclipse: const NoEclipseRepository(),
|
||||
watchPlanet: (_) => states.stream,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
states.add(const PlanetVisualState(stage: 1));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -114,7 +135,12 @@ void main() {
|
||||
await tester.pump(const Duration(seconds: 4));
|
||||
await tester.tap(find.text('PASSPORT 기록 보기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<PlanetHistoryScreen>(find.byType(PlanetHistoryScreen)).repository, same(repository));
|
||||
expect(
|
||||
tester
|
||||
.widget<PlanetHistoryScreen>(find.byType(PlanetHistoryScreen))
|
||||
.repository,
|
||||
same(repository),
|
||||
);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
});
|
||||
|
||||
@@ -131,26 +157,54 @@ void main() {
|
||||
'nextCursor': null,
|
||||
};
|
||||
await expectLater(repository.history(), throwsStateError);
|
||||
backend.response = {'items': [{'id': 'zap', 'kind': 'zap',
|
||||
'occurredAt': '2026-09-22T00:00:00Z', 'amount': 37, 'participationConfirmed': true}], 'nextCursor': null};
|
||||
backend.response = {
|
||||
'items': [
|
||||
{
|
||||
'id': 'zap',
|
||||
'kind': 'zap',
|
||||
'occurredAt': '2026-09-22T00:00:00Z',
|
||||
'amount': 37,
|
||||
'participationConfirmed': true,
|
||||
},
|
||||
],
|
||||
'nextCursor': null,
|
||||
};
|
||||
final personal = await repository.history(personal: true);
|
||||
expect(backend.payload, {'fandomId': 'target', 'scope': 'personal'});
|
||||
expect(personal.items.single.amount, 37);
|
||||
expect(personal.items.single.participationConfirmed, isFalse);
|
||||
// PASSPORT Stamp 는 내 기록에서만 받는다(FX §5.2 · §7.2).
|
||||
backend.response = {'items': [
|
||||
{'id': 'e', 'kind': 'eclipse_success', 'occurredAt': '2026-09-23T00:00:00Z'},
|
||||
{'id': 'v', 'kind': 'emblem_evolution', 'occurredAt': '2026-09-22T00:00:00Z', 'toStage': 3},
|
||||
{'id': 'f', 'kind': 'badge_founder100', 'occurredAt': '2026-09-21T00:00:00Z', 'memberNo': 7},
|
||||
], 'nextCursor': null};
|
||||
backend.response = {
|
||||
'items': [
|
||||
{
|
||||
'id': 'e',
|
||||
'kind': 'eclipse_success',
|
||||
'occurredAt': '2026-09-23T00:00:00Z',
|
||||
},
|
||||
{
|
||||
'id': 'v',
|
||||
'kind': 'emblem_evolution',
|
||||
'occurredAt': '2026-09-22T00:00:00Z',
|
||||
'toStage': 3,
|
||||
},
|
||||
{
|
||||
'id': 'f',
|
||||
'kind': 'badge_founder100',
|
||||
'occurredAt': '2026-09-21T00:00:00Z',
|
||||
'memberNo': 7,
|
||||
},
|
||||
],
|
||||
'nextCursor': null,
|
||||
};
|
||||
final stamps = await repository.history(personal: true);
|
||||
expect(stamps.items.map((i) => i.kind), [
|
||||
PlanetHistoryKind.eclipseSuccess, PlanetHistoryKind.emblemEvolution, PlanetHistoryKind.founderBadge,
|
||||
PlanetHistoryKind.eclipseSuccess,
|
||||
PlanetHistoryKind.emblemEvolution,
|
||||
PlanetHistoryKind.founderBadge,
|
||||
]);
|
||||
expect(stamps.items[1].toStage, 3);
|
||||
expect(stamps.items[2].memberNo, 7);
|
||||
await expectLater(repository.history(), throwsStateError);
|
||||
|
||||
});
|
||||
|
||||
testWidgets('행성 상세에서 같은 저장소의 팬덤 역사를 연다', (tester) async {
|
||||
@@ -211,4 +265,31 @@ void main() {
|
||||
expect(find.text('아직 기록된 팬덤 역사가 없습니다.'), findsOneWidget);
|
||||
expect(find.text('다시 시도'), findsNothing);
|
||||
});
|
||||
|
||||
test('UX·FX §7.2 — 진화 · Eclipse 성공 · 기념 Badge 만 역사적 사건', () {
|
||||
expect(PlanetHistoryKind.values.where((k) => k.isHistoric).toSet(), {
|
||||
PlanetHistoryKind.evolution,
|
||||
PlanetHistoryKind.eclipseSuccess,
|
||||
PlanetHistoryKind.emblemEvolution,
|
||||
PlanetHistoryKind.founderBadge,
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('역사적 사건 줄에만 「역사적 사건」 표지가 붙는다', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: PlanetHistoryScreen(
|
||||
repository: _PersonalHistory(),
|
||||
fandomName: '팬덤',
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Stage 1 → 2 진화'), findsOneWidget);
|
||||
expect(find.text('역사적 사건'), findsOneWidget);
|
||||
await tester.tap(find.text('내 활동'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('ZAP 기여'), findsOneWidget);
|
||||
expect(find.text('역사적 사건'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user