생성 때 고른 행성 형태·색 표시 · HOME 주간 정보

This commit is contained in:
Sn-Kinos
2026-10-05 23:21:44 +09:00
parent 55424bd4a2
commit c8c1a1ae67
40 changed files with 688 additions and 40 deletions
@@ -11,6 +11,7 @@ class PlanetVisualState {
this.heroNicknames = const [],
this.heroClaimCount,
this.colorKey,
this.planetType,
this.evolutionImminent = false,
});
final int? stage;
@@ -36,6 +37,9 @@ class PlanetVisualState {
/// 생성 때 고른 행성 컬러 키(C-50 · `fn-planet-create`). 성운이면 null 이다.
final String? colorKey;
/// 생성 때 고른 행성 형태(C-50 · `fn-planet-create` 가 팬덤 문서에 싣는 `planetType`). 성운이면 null.
final String? planetType;
final int? level;
final bool isEclipsed;
final bool isHibernating;
@@ -54,6 +58,7 @@ class PlanetVisualState {
PlanetVisualState(
isHibernating: data['isHibernating'] == true,
colorKey: data['colorKey'] is String ? data['colorKey'] as String : null,
planetType: data['planetType'] is String ? data['planetType'] as String : null,
stage: switch (data['stage']) {
int n when n >= 0 && n <= 6 => n,
_ => null,
+1
View File
@@ -169,6 +169,7 @@ class Repositories {
PlanetVisualState(
stage: current.stage,
colorKey: current.colorKey,
planetType: current.planetType,
isHibernating: current.isHibernating,
level: current.level,
evolutionImminent: current.evolutionImminent,
+2 -1
View File
@@ -35,7 +35,8 @@ class LivePlanet25D extends StatelessWidget {
create: () => watch(fandomId),
builder: (context, state) => Planet25D(
key: ValueKey((fandomId, state != null)),
image: image,
// 생성 때 고른 형태가 있으면 그 그림, 없으면(성운 · 형태 없는 옛 행성) 화면이 준 그림.
image: planetShapeImage(state?.planetType) ?? image,
onOpenHistory: onOpenHistory,
size: size,
glow: glow,
+57
View File
@@ -59,6 +59,63 @@ Color? planetColorOf(String? key) =>
_planetColorMaster.where((c) => c.$1 == key).firstOrNull?.$2 ??
planetColors.where((c) => c.$1 == key).firstOrNull?.$2;
/// 생성 때 고른 형태(`planetType`, 서버 `PLANET_SHAPES`) → 형태 그림. 모르는 값 · 성운이면 null.
///
/// 그림은 Figma 「홈_성장 완료/행성 생성」(`344:214421`) 형태 칸이 쓰는 **행성 형태 시트 이미지
/// (1536×1024 · 투명 배경 · 2행 5열)** 에서 칸과 같은 형태를 잘라 낸 것이다(2026-10-06 재추출).
/// 칸 1~5 는 시트 1 · 2 · 3 · 4 · 10번째 형태다. 6번째 칸은 Figma 에서 「다음」 버튼에 가려
/// 어떤 형태인지 보이지 않아 자리표시(1번과 같은 그림)를 둔다 — 해석 확인 요청 Q-A14.
/// 시트 원본이 형태당 약 260px 라 큰 화면에서는 흐릴 수 있다(Admin 등록 이미지로 바뀌면 해소).
String? planetShapeImage(String? planetType) {
final n = int.tryParse(
RegExp(r'^shape([1-6])$').firstMatch(planetType ?? '')?.group(1) ?? '',
);
if (n == null) return null;
return n <= 5
? 'assets/figma/planet-shape-$n.png'
: 'assets/figma/planet-shape-placeholder.png';
}
/// 정지 그림 — 형태 · 색을 입힌 행성(목록 카드 · MY). 형태가 없으면 [fallback] 그림 그대로다.
/// 색은 [Planet25D] 와 같은 곱하기 합성이다.
class PlanetArt extends StatelessWidget {
const PlanetArt({
super.key,
required this.fallback,
this.planetType,
this.colorKey,
this.width,
this.height,
this.semanticLabel,
});
final String fallback;
final String? planetType;
final String? colorKey;
final double? width;
final double? height;
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final shape = planetShapeImage(planetType);
final image = Image.asset(
shape ?? fallback,
width: width,
height: height,
fit: BoxFit.contain,
semanticLabel: semanticLabel,
);
final tint = shape == null ? null : planetColorOf(colorKey);
return tint == null
? image
: ColorFiltered(
colorFilter: ColorFilter.mode(tint, BlendMode.modulate),
child: image,
);
}
}
/// Figma 본체에 UX/FX v1.5 §2의 Stage별 레이어를 조합한다.
/// Stage는 서버 확정값이며, null은 단계 데이터 미제공 상태다.
class Planet25D extends StatefulWidget {
+5
View File
@@ -19,6 +19,7 @@ class PlanetNode {
this.isMine = false,
this.stage,
this.colorKey,
this.planetType,
bool? isCenter,
bool? ranked,
}) : isCenter = isCenter ?? isMine,
@@ -34,6 +35,9 @@ class PlanetNode {
/// 팬덤 `colorKey`(행성 색 팔레트 `planetColors` 의 키). 미제공이면 null.
final String? colorKey;
/// 팬덤 `planetType`(생성 때 고른 형태). 미제공이면 null.
final String? planetType;
final String name;
/// 갤럭시 전체 순위. HUD 의 `Rank`.
@@ -140,6 +144,7 @@ class GalaxySnapshot {
isMine: p.isMine,
stage: p.stage,
colorKey: p.colorKey,
planetType: p.planetType,
ranked: p.ranked,
isCenter: identical(p, top),
),
@@ -4,6 +4,7 @@ import '../../core/data/planet_visual_state.dart';
import '../../core/theme/tokens.dart';
import '../../core/ui/app_dialog.dart';
import '../../core/ui/app_icons.dart';
import '../../core/ui/planet_25d.dart' show PlanetArt;
import '../notification/notification_screen.dart';
import '../eclipse/eclipse_data.dart';
import '../eclipse/widgets/eclipse_banner.dart';
@@ -29,8 +30,8 @@ class GalaxyHomeScreen extends StatefulWidget {
/// 카드 그림 — HOME · 맵과 같이 팬덤 `stage` 로 고른다. Stage0 성운만 성운 그림이고
/// 정식 행성은 시안 행성 그림(`image 8` · 파란 행성 벡터)을 칸마다 번갈아 쓴다.
/// 예전엔 칸 순서로 골라 둘째 칸이 행성이어도 성운 그림이 됐다.
// ponytail: 행성마다 고정된 두 그림 중 하나(id 기준) — 칸 순서가 바뀌어도 같은 그림.
// 탄생 때 고른 모양 · 색 에셋은 최신 Figma 행성 에셋 교체(10/8)에서 연결한다.
// ponytail: 형태가 없는 행성(옛 데이터)만 고정된 두 그림 중 하나(id 기준). 형태가 있으면
// 카드가 [PlanetArt] 로 생성 때 고른 형태 · 색을 그린다.
static String cardImageOf(PlanetNode p, [int _ = 0]) => p.stage == 0
? 'assets/figma/nebula-card.png'
: (p.id.codeUnits.fold<int>(0, (a, c) => a + c).isEven
@@ -349,9 +350,10 @@ class _PlanetCard extends StatelessWidget {
Positioned.fill(
child: Padding(
padding: const EdgeInsets.fromLTRB(11, 10, 10, 11),
child: Image.asset(
image,
fit: BoxFit.contain,
child: PlanetArt(
fallback: image,
planetType: planet.stage == 0 ? null : planet.planetType,
colorKey: planet.colorKey,
semanticLabel: planet.name,
),
),
@@ -152,6 +152,7 @@ class FirestoreGalaxyRepository implements GalaxyRepository {
_ => null,
},
colorKey: data['colorKey'] is String ? data['colorKey'] as String : null,
planetType: data['planetType'] is String ? data['planetType'] as String : null,
name: data.strOr('name', id),
rank: rank,
fpi: data.intOr('fpiScore'),
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import '../../../core/theme/tokens.dart';
import '../../../core/ui/live_planet.dart';
import '../../../core/ui/planet_25d.dart' show planetShapeImage;
import '../../../core/data/planet_visual_state.dart';
import '../galaxy_data.dart';
import '../galaxy_motion.dart';
@@ -133,12 +134,15 @@ class GalaxyField extends StatelessWidget {
return radiusOf(p) * depthScaleOf(at);
}
/// 행성 일러스트. 내 행성은 홈과 같은 그림, 나머지는 시안 행성 2종을 돌려 쓴다.
static String imageOf(PlanetNode p, int index) => p.isMine
? 'assets/figma/planet-home.png'
: (index.isEven
? 'assets/figma/planet-1.png'
: 'assets/figma/planet-2.png');
/// 행성 일러스트 — 생성 때 고른 형태([planetShapeImage]). 형태가 없는 행성(성운 · 옛 데이터)만
/// 내 행성은 홈과 같은 그림, 나머지는 시안 행성 2종을 돌려 쓴다. 색은 [LivePlanet25D] 가 입힌다.
static String imageOf(PlanetNode p, int index) =>
planetShapeImage(p.planetType) ??
(p.isMine
? 'assets/figma/planet-home.png'
: (index.isEven
? 'assets/figma/planet-1.png'
: 'assets/figma/planet-2.png'));
@override
Widget build(BuildContext context) {
+85
View File
@@ -333,8 +333,12 @@ class HomeSnapshot {
required this.insides,
this.items = const [],
this.nebulas = const [],
this.weekly,
});
/// HOME 주간 정보(A-37 · HOME 정보위계 v1.0 §2 · §3). 소속 행성이 없으면 null.
final HomeWeekly? weekly;
/// 활동 POP 잔액. 정책 설계서 §P-02 — 활동/지원 원장은 분리되어 있고
/// 홈 상단에는 활동 POP 을 띄운다.
final int popBalance;
@@ -364,6 +368,87 @@ class HomeSnapshot {
final List<ItemProduct> items;
}
/// HOME 주간 정보 — 발주사 9/9 검토회신 2항 「행성의 주간 순위 · 이번 주 성장 정도 · 이번 주
/// 기여량 및 기여 비율 · 보유 POP」(HOME 정보위계 v1.0 §2 의 2 · 3 · 6 · 7번). 값의 뜻은 §3(별첨 B):
///
/// - 순위는 **주간 공식 Snapshot**(`rankingSnapshots`, B-20)의 순위다 — 실시간 순위가 아니다.
/// 변동은 직전 Snapshot 대비(C-43 「순위 동향 · 변동값」). 양수 = 올라감.
/// - 이번 주 성장은 **두 축(Growth POP · sFPI)** 각각의 증분 — 단일 백분율로 합치지 않는다.
/// - 기여 비율은 **행성 주간 성장 총량 대비 본인 비중**.
/// - 보유 POP 은 **일반 POP / 기여 POP** 분리 표기.
///
/// 「이번 주」의 시작은 마지막 주간 Snapshot 시각(KST 월요일 00:10, Q-A11 임시값)이다. 그 시각의
/// 행성 누계가 기준선이라 Snapshot 이 없거나 기준선 필드가 없는 옛 Snapshot 이면 그 값은 null.
class HomeWeekly {
const HomeWeekly({
this.rank,
this.rankChange,
this.weekGrowthPop,
this.weekSfpi,
this.myWeekPop,
this.myShare,
required this.generalPop,
required this.supportPop,
});
final int? rank;
final int? rankChange;
final num? weekGrowthPop;
final num? weekSfpi;
/// 이번 주 내가 이 행성 성장에 보탠 Growth POP(`users/{uid}/growthEvents` 합). 못 읽으면 null.
final num? myWeekPop;
/// 0~1. 행성 주간 성장이 0 이거나 모르면 null.
final double? myShare;
final int generalPop;
final int supportPop;
/// Snapshot 두 개(최신 · 직전)의 `planets` 와 현재 행성 누계 · 내 이번 주 성장 기여로 만든다.
factory HomeWeekly.from({
required String fandomId,
required List<Map<String, dynamic>> snapshots,
required Map<String, dynamic> fandom,
required num? myWeekPop,
required Map<String, dynamic> wallet,
}) {
Map<String, dynamic>? entryOf(Map<String, dynamic>? snap) {
final planets = snap?['planets'];
if (planets is! List) return null;
for (final p in planets) {
if (p is Map && p['fandomId'] == fandomId) return p.cast<String, dynamic>();
}
return null;
}
final latest = entryOf(snapshots.firstOrNull);
final previous = entryOf(snapshots.length > 1 ? snapshots[1] : null);
int? rankOf(Map<String, dynamic>? e) =>
e?['rank'] is int ? e!['rank'] as int : null;
num? delta(String key) {
final now = fandom[key];
final base = latest?[key];
return now is num && base is num ? now - base : null;
}
final rank = rankOf(latest);
final prev = rankOf(previous);
final weekGrowth = delta('growthPop');
int intOf(dynamic v) => v is num ? v.toInt() : 0;
return HomeWeekly(
rank: rank,
rankChange: rank != null && prev != null ? prev - rank : null,
weekGrowthPop: weekGrowth,
weekSfpi: delta('sfpi'),
myWeekPop: myWeekPop,
myShare: weekGrowth != null && weekGrowth > 0 && myWeekPop != null
? (myWeekPop / weekGrowth).clamp(0.0, 1.0).toDouble()
: null,
generalPop: intOf(wallet['activityBalance']),
supportPop: intOf(wallet['supportBalance']),
);
}
}
/// 아이템(Hero Gift) 상품 한 줄 — 사본 `아이템 구입`(`252:160233`) 카드.
///
/// 구매하면 구매자에게 기여 POP, 팬덤 Drop Pool 에 같은 수량의 일반 POP 이 생긴다
@@ -184,6 +184,17 @@ class FirestoreHomeRepository implements HomeRepository {
.limit(20)
.get();
// HOME 주간 정보(A-37) — 주간 공식 Snapshot 최신 · 직전 두 개(순위 · 변동 · 성장 기준선).
final snapshotsF = fandomId.isEmpty
? null
: db
.collection('rankingSnapshots')
.orderBy('createdAt', descending: true)
.limit(2)
.get()
.then((q) => [for (final d in q.docs) d.data()])
.catchError((Object _) => <Map<String, dynamic>>[]);
final wallet = (await walletF).data() ?? const <String, dynamic>{};
final fandom = fandomF == null
? const <String, dynamic>{}
@@ -226,7 +237,23 @@ class FirestoreHomeRepository implements HomeRepository {
final progressById = {for (final doc in progress) doc.id: doc.data()};
HomeWeekly? weekly;
if (snapshotsF != null) {
final snapshots = await snapshotsF;
final since = snapshots.firstOrNull?['createdAt'] is Timestamp
? (snapshots.first['createdAt'] as Timestamp).toDate()
: kstWeekStart(DateTime.now());
weekly = HomeWeekly.from(
fandomId: fandomId,
snapshots: snapshots,
fandom: fandom,
myWeekPop: await _myWeekGrowth(uid, fandomId, since),
wallet: wallet,
);
}
return HomeSnapshot(
weekly: weekly,
popBalance: wallet.intOr('activityBalance'),
fandomId: fandomId,
liveTicker: fandom.strOr('liveTicker').isEmpty
@@ -300,6 +327,27 @@ class FirestoreHomeRepository implements HomeRepository {
);
}
/// 이번 주 내가 [fandomId] 행성 성장에 보탠 Growth POP — 원장과 같은 트랜잭션에서 쓰는
/// `users/{uid}/growthEvents`(역분개는 음수 행)의 [since] 이후 합. 읽지 못하면 null.
// ponytail: 한 주치 성장 행을 전부 읽어 합한다. 한 사용자가 주에 수천 건을 넘기면 서버 집계 필드로 옮긴다.
Future<num?> _myWeekGrowth(String uid, String fandomId, DateTime since) async {
try {
final q = await _backend.db
.collection('users/$uid/growthEvents')
.where('createdAt', isGreaterThanOrEqualTo: Timestamp.fromDate(since))
.get();
num sum = 0;
for (final d in q.docs) {
final data = d.data();
final amount = data['amount'];
if (data['fandomIdAtPayout'] == fandomId && amount is num) sum += amount;
}
return sum;
} catch (_) {
return null;
}
}
Future<PlanetGrowth> _growth(
Map<String, dynamic> fandom,
String fandomId,
@@ -529,6 +577,13 @@ String kstDayKey([DateTime? at]) => (at ?? DateTime.now().toUtc())
.toIso8601String()
.substring(0, 10);
/// 이번 주 시작 — KST 월요일 00:00(서버 `weekKeyOf` 와 같은 주 경계). 주간 Snapshot 이 아직 없을 때만 쓴다.
DateTime kstWeekStart(DateTime at) {
final kst = at.toUtc().add(const Duration(hours: 9));
final monday = DateTime.utc(kst.year, kst.month, kst.day - (kst.weekday - 1));
return monday.subtract(const Duration(hours: 9));
}
/// HOME TODAY'S QUEST 의 관리자 퀘스트 — `authorUid` 가 없는 것만, `createdAt` 최신순
/// (설명 프레임 `2097:16397` 「관리자 퀘스트 - 최신순으로 최대 3개 노출」).
List<T> adminQuestsLatest<T>(
+14
View File
@@ -25,6 +25,7 @@ import '../eclipse/widgets/live_proof.dart';
import 'home_data.dart';
import 'nebula_create_screen.dart';
import 'planet_create_screen.dart';
import 'widgets/home_weekly.dart';
import 'widgets/briefing_list.dart';
import 'widgets/home_hero.dart';
import 'widgets/item_sheet.dart';
@@ -619,6 +620,19 @@ class _HomeBody extends StatelessWidget {
// 이하 구획은 Figma 「홈」 `2097:16428` `Frame 4241`(패딩 24/16 · 구획 사이 36 · 머리와
// 본문 16) 순서 — TODAY'S QUEST · 팬덤행성 인큐베이터 · 아티스트 소식 · HOT ARCHIVE ·
// 인기 K-INSIDE.
// HOME 주간 정보(A-37 · 정보위계 v1.0 §2 의 2 · 3 · 6 · 7번) — 🟡 자리 임시(Q-A16).
if (data.weekly case final weekly?) ...[
Padding(
padding: _h,
child: HomeSectionHeader(title: context.l10n.homeWeeklyTitle),
),
const SizedBox(height: _headerGap),
Padding(
padding: _h,
child: HomeWeeklyCard(weekly: weekly),
),
const SizedBox(height: _sectionGap),
],
Padding(
padding: _h,
child: HomeSectionHeader(
+12 -16
View File
@@ -4,7 +4,7 @@ import '../../core/data/backend.dart' show BackendException;
import '../../core/theme/tokens.dart';
import '../../core/ui/app_dialog.dart';
import '../../core/ui/app_icons.dart';
import '../../core/ui/planet_25d.dart' show planetColors;
import '../../core/ui/planet_25d.dart' show planetColors, planetShapeImage;
import 'home_data.dart';
import '../../core/i18n/app_locale.dart';
@@ -15,7 +15,7 @@ import '../../core/i18n/app_locale.dart';
/// 생성 뒤 사용자가 외형을 바꾸는 입구는 없다. 그래서 사본의 「오브젝트 색 선택」 줄과
/// 직접 색 고르기(스포이트) 칩은 두지 않는다.
///
/// 형태 칸은 사본에서도 모두 같은 자리표시 이미지다(개별 형태 에셋은 S08 확인 대상).
/// 형태 칸 · 미리보기는 행성 형태 시트 그림이다([planetShapeImage] — 6번 칸만 자리표시, Q-A14).
class PlanetCreateScreen extends StatefulWidget {
const PlanetCreateScreen({
super.key,
@@ -68,10 +68,6 @@ class _PlanetCreateScreenState extends State<PlanetCreateScreen> {
});
}
static const _placeholder = AssetImage(
'assets/figma/planet-shape-placeholder.png',
);
static const _heading = TextStyle(
fontFamily: AppFonts.body,
fontSize: 18,
@@ -80,6 +76,10 @@ class _PlanetCreateScreenState extends State<PlanetCreateScreen> {
color: AppColors.onCosmic,
);
/// 고른 형태 그림 — HOME · GALAXY · 행성 상세 · MY 가 그리는 그림과 같은 표([planetShapeImage]).
Image get _shapeImage =>
Image.asset(planetShapeImage(PlanetCreateScreen.shapes[_shape])!);
Widget _preview() => Center(
child: SizedBox.square(
dimension: 212,
@@ -89,9 +89,9 @@ class _PlanetCreateScreenState extends State<PlanetCreateScreen> {
_colors[_color].$2,
BlendMode.modulate,
),
child: const Image(image: _placeholder),
child: _shapeImage,
)
: const Image(image: _placeholder),
: _shapeImage,
),
);
@@ -112,14 +112,10 @@ class _PlanetCreateScreenState extends State<PlanetCreateScreen> {
? Border.all(color: AppColors.cosmicAccent, width: 2)
: null,
),
// 사본 칸 그림(렌더에서 4 안쪽을 자른 것). 6번 칸은 사본에서 버튼에 가려
// 모양이 보이지 않아 자리표시를 둔다.
padding: i < 5 ? const EdgeInsets.all(4) : null,
child: Image.asset(
i < 5
? 'assets/figma/planet-shape-${i + 1}.png'
: 'assets/figma/planet-shape-placeholder.png',
),
// 사본 칸 그림 — 행성 형태 시트에서 잘라 낸 투명 그림([planetShapeImage]). 6번 칸은
// 사본에서 버튼에 가려 모양이 보이지 않아 자리표시를 둔다(Q-A14).
padding: const EdgeInsets.all(4),
child: Image.asset(planetShapeImage(PlanetCreateScreen.shapes[i])!),
),
),
);
@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import '../../../core/i18n/app_locale.dart';
import '../../../core/theme/tokens.dart';
import '../home_data.dart';
import '../home_screen.dart' show formatPop;
/// HOME 주간 정보 — 발주사 9/9 검토회신 2항 · HOME 정보위계 v1.0 §2 의 2 · 3 · 6 · 7번(A-37).
///
/// 🟡 **배치는 임시다.** 기준 화면 「REFINE v5A · HOME · Default」 를 아직 실측하지 못해(당사 사본에
/// 없고 원본 파일은 로그인 필요) 정보 위계 v1.0 §7 「프레임 실측과 무관하게 반드시 포함」에 따라
/// 값만 먼저 싣는다. 자리 · 모양은 해석 확인 요청 영역A Q-A16 회신 또는 실측 후 맞춘다.
/// 값의 뜻은 [HomeWeekly] — 모르는 값은 0 으로 채우지 않고 「—」.
class HomeWeeklyCard extends StatelessWidget {
const HomeWeeklyCard({super.key, required this.weekly});
final HomeWeekly weekly;
static const _label = TextStyle(
fontFamily: AppFonts.body,
fontSize: 13,
height: 18 / 13,
color: AppColors.onCosmicVariant,
);
static const _value = TextStyle(
fontFamily: AppFonts.body,
fontSize: 14,
fontWeight: FontWeight.w600,
height: 18 / 14,
color: AppColors.onCosmic,
);
static String _signed(num v) => '${v >= 0 ? '+' : '−'}${formatPop(v.abs().round())}';
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final w = weekly;
final change = w.rankChange;
Widget row(String label, Widget value) => Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpace.s2),
child: Row(
children: [
Expanded(child: Text(label, style: _label)),
value,
],
),
);
Text text(String s) => Text(s, style: _value);
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpace.s4,
vertical: AppSpace.s2,
),
decoration: BoxDecoration(
color: AppColors.cosmicSurfaceContainer,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: AppColors.cosmicOutline),
),
child: Column(
children: [
row(
l10n.homeWeeklyRank,
Row(
mainAxisSize: MainAxisSize.min,
children: [
text(w.rank == null ? '—' : l10n.homeWeeklyRankValue('${w.rank}')),
if (change != null && change != 0) ...[
const SizedBox(width: AppSpace.s1),
Text(
'${change > 0 ? '▲' : '▼'}${change.abs()}',
style: _value.copyWith(
fontSize: 12,
color: change > 0
? AppColors.cosmicAccent
: AppColors.cosmicBlue,
),
),
],
],
),
),
// 두 축 각각 — 단일 백분율로 합치지 않는다(§3 4번).
row(
l10n.homeWeeklyGrowth,
text(
'${w.weekGrowthPop == null ? '—' : _signed(w.weekGrowthPop!)} POP · '
'${w.weekSfpi == null ? '—' : _signed(w.weekSfpi!)} sFPI',
),
),
row(
l10n.homeWeeklyMine,
text(
'${w.myWeekPop == null ? '—' : formatPop(w.myWeekPop!.round())} POP'
'${w.myShare == null ? '' : ' · ${(w.myShare! * 100).toStringAsFixed(1)}%'}',
),
),
row(
l10n.homeWeeklyBalance,
text(
'${l10n.popKindGeneral} ${formatPop(w.generalPop)} · '
'${l10n.popKindContribution} ${formatPop(w.supportPop)}',
),
),
],
),
);
}
}
+6
View File
@@ -264,8 +264,14 @@ class PlanetTier {
this.fandomId = '',
this.passportTier = PassportTier.silver,
this.joined = true,
this.planetType,
this.colorKey,
});
/// 팬덤 문서의 생성 때 고른 형태 · 색(`fn-planet-create`). 성운이면 null.
final String? planetType;
final String? colorKey;
final String fandomName;
final int contributedPop;
@@ -93,6 +93,8 @@ class FirestoreMyRepository extends SampleMyRepository {
isPrimary: id == primaryId,
passportTier: PassportTier.parse(passport.strOr('tier')),
joined: joined,
planetType: fandom['planetType'] is String ? fandom['planetType'] as String : null,
colorKey: fandom['colorKey'] is String ? fandom['colorKey'] as String : null,
);
}));
final primaryTier = tiers.where((t) => t.isPrimary).firstOrNull?.passportTier ?? PassportTier.silver;
+7 -3
View File
@@ -6,6 +6,7 @@ import '../../core/i18n/app_locale.dart';
import '../../core/push/push_messaging.dart';
import '../../core/theme/tokens.dart';
import '../../core/ui/app_icons.dart';
import '../../core/ui/planet_25d.dart' show PlanetArt;
import '../auth/auth.dart';
import '../auth/auth_repository.dart';
import '../campaign/campaign_data.dart' show CampaignRepository;
@@ -260,12 +261,15 @@ class _MyScreenState extends State<MyScreen> {
),
),
child: i < _planets.length
? Image.asset(
GalaxyHomeScreen.cardImageOf(
? PlanetArt(
fallback: GalaxyHomeScreen.cardImageOf(
_planets[i],
i,
),
fit: BoxFit.contain,
planetType: _planets[i].stage == 0
? null
: _planets[i].planetType,
colorKey: _planets[i].colorKey,
semanticLabel: _planets[i].name,
)
: null,
+5 -2
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../../core/theme/tokens.dart';
import '../../core/ui/planet_25d.dart' show PlanetArt;
import '../home/home_screen.dart' show formatPop;
import '../planet/planet_data.dart' show PlanetRepositoryFactory;
import '../planet/planet_history_screen.dart';
@@ -211,8 +212,10 @@ class _PlanetCard extends StatelessWidget {
width: 60,
child: Column(
children: [
Image.asset(
'assets/figma/my-planet-1.png',
PlanetArt(
fallback: 'assets/figma/my-planet-1.png',
planetType: planet.planetType,
colorKey: planet.colorKey,
width: 60,
height: 60,
),
+7 -1
View File
@@ -867,5 +867,11 @@
"onbPermContinue": "Continue",
"coachZap": "Tap to send POP to our planet",
"coachMy": "Your records are here",
"settingsWithdrawLinked": "Please unlink your connected accounts before deleting your account."
"settingsWithdrawLinked": "Please unlink your connected accounts before deleting your account.",
"homeWeeklyTitle": "Our planet this week",
"homeWeeklyRank": "Weekly rank",
"homeWeeklyRankValue": "#{rank}",
"homeWeeklyGrowth": "Growth this week",
"homeWeeklyMine": "My contribution this week",
"homeWeeklyBalance": "My POP"
}
+7 -1
View File
@@ -867,5 +867,11 @@
"onbPermContinue": "Continuar",
"coachZap": "Toca para enviar POP a nuestro planeta",
"coachMy": "Aquí puedes ver tus registros",
"settingsWithdrawLinked": "Desvincula tus cuentas conectadas antes de eliminar tu cuenta."
"settingsWithdrawLinked": "Desvincula tus cuentas conectadas antes de eliminar tu cuenta.",
"homeWeeklyTitle": "Nuestro planeta esta semana",
"homeWeeklyRank": "Ranking semanal",
"homeWeeklyRankValue": "#{rank}",
"homeWeeklyGrowth": "Crecimiento semanal",
"homeWeeklyMine": "Mi aporte semanal",
"homeWeeklyBalance": "Mis POP"
}
+7 -1
View File
@@ -867,5 +867,11 @@
"onbPermContinue": "続ける",
"coachZap": "タップしてPOPを私たちの惑星に送ろう",
"coachMy": "自分の記録はここで見られます",
"settingsWithdrawLinked": "連携中のアカウントを解除してから退会してください。"
"settingsWithdrawLinked": "連携中のアカウントを解除してから退会してください。",
"homeWeeklyTitle": "今週の私たちの惑星",
"homeWeeklyRank": "週間順位",
"homeWeeklyRankValue": "{rank}位",
"homeWeeklyGrowth": "今週の成長",
"homeWeeklyMine": "今週の私の貢献",
"homeWeeklyBalance": "保有POP"
}
+14 -1
View File
@@ -1814,5 +1814,18 @@
"onbPermContinue": "계속",
"coachZap": "눌러서 POP 을 우리 행성에 보내요",
"coachMy": "내 기록은 여기서 볼 수 있어요",
"settingsWithdrawLinked": "연동된 계정 해제 후 계정 탈퇴를 진행해주세요."
"settingsWithdrawLinked": "연동된 계정 해제 후 계정 탈퇴를 진행해주세요.",
"homeWeeklyTitle": "이번 주 우리 행성",
"homeWeeklyRank": "주간 순위",
"homeWeeklyRankValue": "{rank}위",
"@homeWeeklyRankValue": {
"placeholders": {
"rank": {
"type": "String"
}
}
},
"homeWeeklyGrowth": "이번 주 성장",
"homeWeeklyMine": "이번 주 내 기여",
"homeWeeklyBalance": "보유 POP"
}
+36
View File
@@ -5323,6 +5323,42 @@ abstract class AppLocalizations {
/// In ko, this message translates to:
/// **'연동된 계정 해제 후 계정 탈퇴를 진행해주세요.'**
String get settingsWithdrawLinked;
/// No description provided for @homeWeeklyTitle.
///
/// In ko, this message translates to:
/// **'이번 주 우리 행성'**
String get homeWeeklyTitle;
/// No description provided for @homeWeeklyRank.
///
/// In ko, this message translates to:
/// **'주간 순위'**
String get homeWeeklyRank;
/// No description provided for @homeWeeklyRankValue.
///
/// In ko, this message translates to:
/// **'{rank}위'**
String homeWeeklyRankValue(String rank);
/// No description provided for @homeWeeklyGrowth.
///
/// In ko, this message translates to:
/// **'이번 주 성장'**
String get homeWeeklyGrowth;
/// No description provided for @homeWeeklyMine.
///
/// In ko, this message translates to:
/// **'이번 주 내 기여'**
String get homeWeeklyMine;
/// No description provided for @homeWeeklyBalance.
///
/// In ko, this message translates to:
/// **'보유 POP'**
String get homeWeeklyBalance;
}
class _AppLocalizationsDelegate
+20
View File
@@ -3029,4 +3029,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsWithdrawLinked =>
'Please unlink your connected accounts before deleting your account.';
@override
String get homeWeeklyTitle => 'Our planet this week';
@override
String get homeWeeklyRank => 'Weekly rank';
@override
String homeWeeklyRankValue(String rank) {
return '#$rank';
}
@override
String get homeWeeklyGrowth => 'Growth this week';
@override
String get homeWeeklyMine => 'My contribution this week';
@override
String get homeWeeklyBalance => 'My POP';
}
+20
View File
@@ -3046,4 +3046,24 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settingsWithdrawLinked =>
'Desvincula tus cuentas conectadas antes de eliminar tu cuenta.';
@override
String get homeWeeklyTitle => 'Nuestro planeta esta semana';
@override
String get homeWeeklyRank => 'Ranking semanal';
@override
String homeWeeklyRankValue(String rank) {
return '#$rank';
}
@override
String get homeWeeklyGrowth => 'Crecimiento semanal';
@override
String get homeWeeklyMine => 'Mi aporte semanal';
@override
String get homeWeeklyBalance => 'Mis POP';
}
+20
View File
@@ -2924,4 +2924,24 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get settingsWithdrawLinked => '連携中のアカウントを解除してから退会してください。';
@override
String get homeWeeklyTitle => '今週の私たちの惑星';
@override
String get homeWeeklyRank => '週間順位';
@override
String homeWeeklyRankValue(String rank) {
return '$rank位';
}
@override
String get homeWeeklyGrowth => '今週の成長';
@override
String get homeWeeklyMine => '今週の私の貢献';
@override
String get homeWeeklyBalance => '保有POP';
}
+20
View File
@@ -2928,4 +2928,24 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get settingsWithdrawLinked => '연동된 계정 해제 후 계정 탈퇴를 진행해주세요.';
@override
String get homeWeeklyTitle => '이번 주 우리 행성';
@override
String get homeWeeklyRank => '주간 순위';
@override
String homeWeeklyRankValue(String rank) {
return '$rank위';
}
@override
String get homeWeeklyGrowth => '이번 주 성장';
@override
String get homeWeeklyMine => '이번 주 내 기여';
@override
String get homeWeeklyBalance => '보유 POP';
}
+20
View File
@@ -2920,4 +2920,24 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settingsWithdrawLinked => '请先解除关联账号后再注销账号。';
@override
String get homeWeeklyTitle => '本周我们的星球';
@override
String get homeWeeklyRank => '周排名';
@override
String homeWeeklyRankValue(String rank) {
return '第$rank名';
}
@override
String get homeWeeklyGrowth => '本周成长';
@override
String get homeWeeklyMine => '本周我的贡献';
@override
String get homeWeeklyBalance => '持有POP';
}
+7 -1
View File
@@ -867,5 +867,11 @@
"onbPermContinue": "继续",
"coachZap": "点击将POP发送到我们的星球",
"coachMy": "我的记录可以在这里查看",
"settingsWithdrawLinked": "请先解除关联账号后再注销账号。"
"settingsWithdrawLinked": "请先解除关联账号后再注销账号。",
"homeWeeklyTitle": "本周我们的星球",
"homeWeeklyRank": "周排名",
"homeWeeklyRankValue": "第{rank}名",
"homeWeeklyGrowth": "本周成长",
"homeWeeklyMine": "本周我的贡献",
"homeWeeklyBalance": "持有POP"
}
@@ -970,6 +970,24 @@ class RemoteAppLocalizations extends AppLocalizations {
@override
String get homeRetryLater => _remote['homeRetryLater'] ?? _base.homeRetryLater;
@override
String get homeWeeklyBalance => _remote['homeWeeklyBalance'] ?? _base.homeWeeklyBalance;
@override
String get homeWeeklyGrowth => _remote['homeWeeklyGrowth'] ?? _base.homeWeeklyGrowth;
@override
String get homeWeeklyMine => _remote['homeWeeklyMine'] ?? _base.homeWeeklyMine;
@override
String get homeWeeklyRank => _remote['homeWeeklyRank'] ?? _base.homeWeeklyRank;
@override
String homeWeeklyRankValue(String rank) => _fill('homeWeeklyRankValue', {'rank': rank}) ?? _base.homeWeeklyRankValue(rank);
@override
String get homeWeeklyTitle => _remote['homeWeeklyTitle'] ?? _base.homeWeeklyTitle;
@override
String get homeWhereBriefing => _remote['homeWhereBriefing'] ?? _base.homeWhereBriefing;