Figma 설명 프레임 반영 — 로그인·온보딩·스플래시 · HOME · GALAXY · 퀘스트·이클립스·알림
This commit is contained in:
@@ -24,6 +24,7 @@ class PlanetGrowth {
|
||||
this.requiredMembers,
|
||||
this.nebulaGoalPassed = false,
|
||||
this.isFounder = false,
|
||||
this.photoUrl,
|
||||
required this.fandomName,
|
||||
required this.contributorName,
|
||||
required this.contributedPop,
|
||||
@@ -36,8 +37,15 @@ class PlanetGrowth {
|
||||
final int? stage;
|
||||
final String fandomName;
|
||||
|
||||
/// 직전 기여자. 「BTS_Hong 님의 1,230 POP 기여로 …」 문구에 쓴다.
|
||||
/// 히어로 인사 줄의 이름 — 설명 프레임 `2097:16391` 「프로필 · 닉네임 · 타이틀 - **로그인
|
||||
/// 사용자 기준** 노출」이라 로그인한 사용자다(팬덤의 직전 기여자가 아니다).
|
||||
final String contributorName;
|
||||
|
||||
/// 인사 줄 아바타 — 로그인 사용자의 프로필 사진(`users/{uid}.photoUrl`). 없으면 이니셜.
|
||||
final String? photoUrl;
|
||||
|
||||
/// 「{N} POP 기여로 …」의 N. 로그인 사용자의 어느 기여량인지 자료에 정의가 없어(해석 확인
|
||||
/// Q-A17) 실 서버는 0 으로 두고 숫자 없는 문구를 쓴다.
|
||||
final int contributedPop;
|
||||
|
||||
bool get hasRecentContribution =>
|
||||
|
||||
@@ -26,6 +26,10 @@ class FirestoreHomeRepository implements HomeRepository {
|
||||
|
||||
final Backend _backend;
|
||||
|
||||
/// 히어로 인사 줄의 로그인 사용자(설명 프레임 `2097:16391`). [load] 가 채우고 [watchGrowth] 도 쓴다.
|
||||
String _myName = '';
|
||||
String? _myPhoto;
|
||||
|
||||
@override
|
||||
Stream<PlanetGrowth> watchGrowth(String fandomId) {
|
||||
if (fandomId.isEmpty) return const Stream.empty();
|
||||
@@ -107,6 +111,8 @@ class FirestoreHomeRepository implements HomeRepository {
|
||||
final userSnap = await db.doc('users/$uid').get();
|
||||
final user = userSnap.data() ?? const <String, dynamic>{};
|
||||
final fandomId = user.strOr('fandomId');
|
||||
_myName = publicName(user['nickname']);
|
||||
_myPhoto = user.strOr('photoUrl').isEmpty ? null : user.strOr('photoUrl');
|
||||
|
||||
// 홈 한 화면에 필요한 것을 병렬로 띄운다. 순차로 기다리면 왕복이 그대로 쌓인다.
|
||||
final walletF = db.doc('users/$uid/wallet/main').get();
|
||||
@@ -366,11 +372,12 @@ class FirestoreHomeRepository implements HomeRepository {
|
||||
return PlanetGrowth(
|
||||
stage: stage,
|
||||
fandomName: fandom.strOr('name', fandomId),
|
||||
// 기존 데이터에 이메일 형태가 남아 있어도 가린다.
|
||||
contributorName: fandom.containsKey('lastContributorName')
|
||||
? publicName(fandom['lastContributorName'])
|
||||
: '',
|
||||
contributedPop: fandom.intOr('lastContributedPop'),
|
||||
// 설명 프레임 `2097:16391` — 프로필 · 닉네임은 로그인 사용자 기준. 팬덤 문서의 직전
|
||||
// 기여자(`lastContributorName`)를 쓰면 남의 이름이 「님의 … 기여로」에 올라 내 것처럼 읽힌다.
|
||||
contributorName: _myName,
|
||||
photoUrl: _myPhoto,
|
||||
// 「N POP 기여로」의 N 이 어느 기여량인지 정의가 없다(Q-A17) — 회신 전에는 숫자 없는 문구.
|
||||
contributedPop: 0,
|
||||
growthPercent: current != null && goal != null && goal > 0
|
||||
? ((current / goal) * 100).clamp(0, 100).round()
|
||||
: null,
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../core/format.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import '../../core/theme/tokens.dart';
|
||||
import '../../core/ui/app_dialog.dart' show slideUpRoute;
|
||||
import '../../core/ui/app_icons.dart';
|
||||
import '../../core/data/planet_visual_state.dart';
|
||||
import '../../core/ui/live_stream.dart';
|
||||
@@ -167,7 +168,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||
final created = await Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).push(MaterialPageRoute<String>(builder: (_) => screen));
|
||||
).push(slideUpRoute<String>((_) => screen));
|
||||
if (created == null || !mounted) return;
|
||||
if (widget.galaxyRepository case final repository?) {
|
||||
setState(() {
|
||||
@@ -287,7 +288,7 @@ class _LiveTickerSlot extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// 홈 헤더 — 성장 완료 · 첫 사용자 상태 화면도 같은 헤더를 쓴다.
|
||||
class HomeHeader extends StatelessWidget {
|
||||
class HomeHeader extends StatefulWidget {
|
||||
const HomeHeader({
|
||||
super.key,
|
||||
required this.pop,
|
||||
@@ -306,71 +307,116 @@ class HomeHeader extends StatelessWidget {
|
||||
final List<ItemProduct> items;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
// 사본 `Top Navigation` — 높이 54 · 패딩 6/16 · 오른쪽 묶음은 알약 + 42 아이콘 두 개.
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4, vertical: 6),
|
||||
child: SizedBox(
|
||||
height: 42,
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
'FANUZU',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1.4,
|
||||
letterSpacing: -0.5,
|
||||
color: AppColors.cosmicAccent,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_PopBalance(value: pop),
|
||||
IconButton(
|
||||
constraints: const BoxConstraints.tightFor(width: 42, height: 42),
|
||||
// 사본 `icon` 42 칸 — 테마 최소 크기(48)로 늘어나 POP 알약이 왼쪽으로 밀리지 않게.
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: const Size.square(42),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Image(
|
||||
image: AssetImage('assets/figma/icon-wallet.png'),
|
||||
width: 42,
|
||||
height: 42,
|
||||
),
|
||||
// 사본 `252:159367` — 홈 주 프레임에 열린 「아이템 구입」 시트.
|
||||
onPressed: () => showItemSheet(
|
||||
context,
|
||||
items: items,
|
||||
purchase: true,
|
||||
fandomId: fandomId,
|
||||
),
|
||||
tooltip: context.l10n.homeItemsTooltip,
|
||||
),
|
||||
IconButton(
|
||||
constraints: const BoxConstraints.tightFor(width: 42, height: 42),
|
||||
// 사본 `icon` 42 칸 — 테마 최소 크기(48)로 늘어나 POP 알약이 왼쪽으로 밀리지 않게.
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: const Size.square(42),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
// 사본 `icon` 42 칸 안 lucide/bell 20.
|
||||
iconSize: 20,
|
||||
icon: const ImageIcon(AppIcons.bell, color: AppColors.onCosmic),
|
||||
// 알림 — 화면 대조 원장 F0 · `229:11265`.
|
||||
onPressed: () => Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => NotificationScreen(repository: notifications),
|
||||
State<HomeHeader> createState() => _HomeHeaderState();
|
||||
}
|
||||
|
||||
class _HomeHeaderState extends State<HomeHeader> {
|
||||
/// 안 읽은 알림이 있는가 — 설명 프레임 `2097:16385` 「알림 - 알림 아이콘 / 알림 상태값 ·
|
||||
/// 알림 존재 시 배찌 알림으로 변경」. 알림함은 닫힐 때 읽음 상태를 돌려주므로(`518:88347`)
|
||||
/// 돌아오면 다시 읽는다.
|
||||
bool _unread = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkUnread();
|
||||
}
|
||||
|
||||
Future<void> _checkUnread() async {
|
||||
try {
|
||||
final items = await widget.notifications.load();
|
||||
if (mounted) setState(() => _unread = items.any((i) => !i.read));
|
||||
} catch (_) {
|
||||
// 읽지 못하면 배지를 띄우지 않는다 — 알림함 자체는 열린다.
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pop = widget.pop;
|
||||
final items = widget.items;
|
||||
final fandomId = widget.fandomId;
|
||||
final notifications = widget.notifications;
|
||||
return Padding(
|
||||
// 사본 `Top Navigation` — 높이 54 · 패딩 6/16 · 오른쪽 묶음은 알약 + 42 아이콘 두 개.
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4, vertical: 6),
|
||||
child: SizedBox(
|
||||
height: 42,
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
'FANUZU',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1.4,
|
||||
letterSpacing: -0.5,
|
||||
color: AppColors.cosmicAccent,
|
||||
),
|
||||
),
|
||||
tooltip: context.l10n.homeNotificationsTooltip,
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
_PopBalance(value: pop),
|
||||
IconButton(
|
||||
constraints: const BoxConstraints.tightFor(width: 42, height: 42),
|
||||
// 사본 `icon` 42 칸 — 테마 최소 크기(48)로 늘어나 POP 알약이 왼쪽으로 밀리지 않게.
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: const Size.square(42),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Image(
|
||||
image: AssetImage('assets/figma/icon-wallet.png'),
|
||||
width: 42,
|
||||
height: 42,
|
||||
),
|
||||
// 사본 `252:159367` — 홈 주 프레임에 열린 「아이템 구입」 시트.
|
||||
onPressed: () => showItemSheet(
|
||||
context,
|
||||
items: items,
|
||||
purchase: true,
|
||||
fandomId: fandomId,
|
||||
),
|
||||
tooltip: context.l10n.homeItemsTooltip,
|
||||
),
|
||||
IconButton(
|
||||
constraints: const BoxConstraints.tightFor(width: 42, height: 42),
|
||||
// 사본 `icon` 42 칸 — 테마 최소 크기(48)로 늘어나 POP 알약이 왼쪽으로 밀리지 않게.
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: const Size.square(42),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
// 사본 `icon` 42 칸 안 lucide/bell 20.
|
||||
iconSize: 20,
|
||||
// 배지 모양은 Figma 에 그림이 없어 테마 기본 점 배지(강조색)를 쓴다.
|
||||
icon: Badge(
|
||||
key: const ValueKey('home-bell-badge'),
|
||||
isLabelVisible: _unread,
|
||||
smallSize: 6,
|
||||
backgroundColor: AppColors.cosmicAccent,
|
||||
child: const ImageIcon(
|
||||
AppIcons.bell,
|
||||
color: AppColors.onCosmic,
|
||||
),
|
||||
),
|
||||
// 알림 — 화면 대조 원장 F0 · `229:11265`.
|
||||
onPressed: () async {
|
||||
await Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute<Object?>(
|
||||
builder: (_) =>
|
||||
NotificationScreen(repository: notifications),
|
||||
),
|
||||
);
|
||||
await _checkUnread();
|
||||
},
|
||||
tooltip: context.l10n.homeNotificationsTooltip,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeBody extends StatelessWidget {
|
||||
@@ -488,35 +534,7 @@ class _HomeBody extends StatelessWidget {
|
||||
fallback: data.liveTicker,
|
||||
),
|
||||
onClose: () => onMap(false),
|
||||
onSearch: () =>
|
||||
Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => GalaxySearchScreen(
|
||||
repository: zap!.repository,
|
||||
onOpen: (p) =>
|
||||
Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) =>
|
||||
PlanetDetailScreen(
|
||||
repository:
|
||||
planetRepository!(
|
||||
p.id,
|
||||
),
|
||||
watchPlanet:
|
||||
watchPlanet,
|
||||
eclipse: eclipse,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onSearch: () => _openSearch(context),
|
||||
)
|
||||
: HomeHero(
|
||||
key: const ValueKey('home-hero'),
|
||||
@@ -604,9 +622,8 @@ class _HomeBody extends StatelessWidget {
|
||||
myPop: data.popBalance,
|
||||
),
|
||||
),
|
||||
// 행성 가입은 GALAXY 에서 한다(사본 시트 「행성 가입」).
|
||||
onJoin: () =>
|
||||
GoRouter.maybeOf(context)?.go('/galaxy'),
|
||||
// 설명 프레임 `516:87335` 「행성 가입 버튼 - 클릭 시 Galaxy 검색 화면 이동」.
|
||||
onJoin: () => _openSearch(context),
|
||||
zapButtonKey: zapKey,
|
||||
onHeroParticipate: _heroParticipate(context),
|
||||
eclipseButton: data.fandomId.isEmpty
|
||||
@@ -614,6 +631,7 @@ class _HomeBody extends StatelessWidget {
|
||||
: EclipseEventButton(
|
||||
repository: eclipse,
|
||||
fandomId: data.fandomId,
|
||||
nebula: (growth ?? data.growth).isNebula,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -842,6 +860,33 @@ class _HomeBody extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 갤럭시 검색 — 확장 맵 돋보기(`2008:14560`)와 첫 사용자 시트 「행성 가입」(`516:87335`).
|
||||
/// 지도 저장소가 없으면(표본 모드) GALAXY 탭으로 물러난다.
|
||||
void _openSearch(BuildContext context) {
|
||||
final galaxyRepository = zap?.repository;
|
||||
if (galaxyRepository == null || planetRepository == null) {
|
||||
GoRouter.maybeOf(context)?.go('/galaxy');
|
||||
return;
|
||||
}
|
||||
final navigator = Navigator.of(context, rootNavigator: true);
|
||||
navigator.push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => GalaxySearchScreen(
|
||||
repository: galaxyRepository,
|
||||
onOpen: (p) => navigator.push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => PlanetDetailScreen(
|
||||
repository: planetRepository!(p.id),
|
||||
watchPlanet: watchPlanet,
|
||||
eclipse: eclipse,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
VoidCallback? _heroParticipate(BuildContext context) =>
|
||||
planetRepository == null || data.fandomId.isEmpty
|
||||
? null
|
||||
|
||||
@@ -13,7 +13,9 @@ import '../../core/i18n/app_locale.dart';
|
||||
/// 틀은 사본(이름 입력 → 성운 → 목표 영역 → 「성운 생성」)이고 숫자는 정책 원장이다.
|
||||
/// B-10 — 성운은 **10명 이상 + 1,000 POP 이상**을 기본 목표 **7일** 안에 채우면 정식
|
||||
/// 행성이 된다. 목표가 고정값이라 사본의 목표 슬라이더 자리에는 조작할 수 없는 고정
|
||||
/// 표시를 둔다(사본 슬라이더 ↔ 원장 고정값, 해석 확인 문항). B-19 — 진화 참여자에게는
|
||||
/// 표시를 둔다. 근거는 발주사 10/3 통합회신(원장 C-112) 「성운 생성은 10 명 + 1,000 POP +
|
||||
/// 기본 목표 7 일, 사용자 목표 POP 조작 없음」 — Figma `516:87407` 목표 POP · 창립멤버 핸들러를
|
||||
/// 이 회신이 대체한다. B-19 — 진화 참여자에게는
|
||||
/// 기념 배지만 지급하므로 사본의 「1T 등급 · 거버넌스」 문장은 그 내용으로 바꾼다.
|
||||
///
|
||||
/// 사본이 함께 그린 다이얼로그 두 개를 그대로 쓴다.
|
||||
@@ -52,6 +54,10 @@ class _NebulaCreateScreenState extends State<NebulaCreateScreen> {
|
||||
String? _takenName;
|
||||
bool get _taken => _takenName != null && _takenName == _value;
|
||||
|
||||
/// 설명 프레임 `516:87397` 「성운명 입력 필드 - 유효성 검증 오류 메시지」 — 서버
|
||||
/// `isValidNebulaName` 과 같은 규칙을 입력 칸에서 먼저 보인다(중복 · 금칙어는 서버 판정).
|
||||
bool get _invalid => _name.text.isNotEmpty && !isValidNebulaName(_name.text);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -129,13 +135,15 @@ class _NebulaCreateScreenState extends State<NebulaCreateScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpace.s5),
|
||||
_NameInput(controller: _name, error: _taken),
|
||||
_NameInput(controller: _name, error: _taken || _invalid),
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
height: 17,
|
||||
child: _taken
|
||||
child: _taken || _invalid
|
||||
? Text(
|
||||
context.l10n.nebulaNameTaken,
|
||||
_taken
|
||||
? context.l10n.nebulaNameTaken
|
||||
: context.l10n.nebulaNameInvalid,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: AppFonts.display,
|
||||
@@ -171,7 +179,7 @@ class _NebulaCreateScreenState extends State<NebulaCreateScreen> {
|
||||
0,
|
||||
),
|
||||
child: FilledButton(
|
||||
onPressed: _taken ? null : _confirm,
|
||||
onPressed: _taken || _invalid ? null : _confirm,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(56),
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -407,3 +415,10 @@ class _Target extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 성운명 규칙 — 서버 `isValidNebulaName`(`account.ts`)과 같다: 2~20자 · 한글/영문/숫자/공백 ·
|
||||
/// 앞뒤 공백 · 연속 공백 불가.
|
||||
bool isValidNebulaName(String name) =>
|
||||
RegExp(r'^[가-힣A-Za-z0-9 ]{2,20}$').hasMatch(name) &&
|
||||
name == name.trim() &&
|
||||
!name.contains(' ');
|
||||
|
||||
@@ -13,7 +13,9 @@ import '../../core/i18n/app_locale.dart';
|
||||
///
|
||||
/// 정책 원장 C-50 — 생성 때 고르는 것은 **최초 기본 모델의 형태와 컬러 두 가지**뿐이고,
|
||||
/// 생성 뒤 사용자가 외형을 바꾸는 입구는 없다. 그래서 사본의 「오브젝트 색 선택」 줄과
|
||||
/// 직접 색 고르기(스포이트) 칩은 두지 않는다.
|
||||
/// 직접 색 고르기(스포이트) 칩은 두지 않는다. 사용자화(컬러 팔레트)를 빼는 근거는 발주사
|
||||
/// 10/3 통합회신 #6(원장 C-115) 「사용자 자유 HEX/RGB 컬러피커는 적용하지 않음」 — Figma
|
||||
/// `2066:17910` 의 「사용자화」를 이 회신이 대체한다. 「오브젝트 색」 줄은 Q-A14 회신 대기.
|
||||
///
|
||||
/// 형태 칸 · 미리보기는 행성 형태 시트 그림이다([planetShapeImage] — 6번 칸만 자리표시, Q-A14).
|
||||
class PlanetCreateScreen extends StatefulWidget {
|
||||
|
||||
@@ -314,32 +314,42 @@ class _Contribution extends StatelessWidget {
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 성운·첫 사용자 상태의 「{닉네임} 님의」는 기여 여부와 무관한 인사 줄이다.
|
||||
if (growth.hasRecentContribution ||
|
||||
((!joined || growth.isNebula) &&
|
||||
growth.contributorName.trim().isNotEmpty)) ...[
|
||||
// 「{닉네임} 님의」는 로그인 사용자의 인사 줄이다(설명 프레임 `2097:16391`).
|
||||
if (growth.contributorName.trim().isNotEmpty) ...[
|
||||
Row(
|
||||
children: [
|
||||
// 프로필 사진 자산이 데이터에 없어 이니셜로 그린다.
|
||||
// 로그인 사용자의 프로필 사진(`2097:16391` 「프로필 - 이미지」). 없으면 이니셜.
|
||||
Container(
|
||||
width: 26,
|
||||
height: 26,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
// 사본 `Ellipse 3` #9BF8A8(프로필 사진이 없을 때의 바탕).
|
||||
color: Color(0xFF9BF8A8),
|
||||
boxShadow: [BoxShadow(color: Color(0xB3000000), blurRadius: 4)],
|
||||
),
|
||||
child: Text(
|
||||
growth.contributorName.characters.firstOrNull ?? '—',
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.body,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
color: const Color(0xFF9BF8A8),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Color(0xB3000000), blurRadius: 4),
|
||||
],
|
||||
image: growth.photoUrl == null
|
||||
? null
|
||||
: DecorationImage(
|
||||
image: NetworkImage(growth.photoUrl!),
|
||||
fit: BoxFit.cover,
|
||||
onError: (_, _) {},
|
||||
),
|
||||
),
|
||||
child: growth.photoUrl != null
|
||||
? null
|
||||
: Text(
|
||||
growth.contributorName.characters.firstOrNull ?? '—',
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.body,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 9),
|
||||
Text(
|
||||
@@ -686,8 +696,10 @@ class _Nebula extends StatelessWidget {
|
||||
Semantics(
|
||||
button: ready && onBirth != null,
|
||||
label: context.l10n.heroNebula,
|
||||
child: GestureDetector(
|
||||
onTap: ready ? onBirth : null,
|
||||
// Figma `2013:15377` 「성운 터치시 흔들림」 — 조건 전에도 흔들림 · 가벼운 햅틱으로 반응하고,
|
||||
// 탄생 조건을 채웠으면 흔들린 뒤 행성 생성으로 간다.
|
||||
child: _TapShake(
|
||||
onTap: ready && onBirth != null ? onBirth : () {},
|
||||
child: Image.asset(
|
||||
'assets/figma/nebula-home.png',
|
||||
width: 212,
|
||||
@@ -1018,6 +1030,9 @@ class HomeZap extends StatelessWidget {
|
||||
onItem: onItem,
|
||||
zapPop: data.zapPop,
|
||||
myPop: (zap.balance ?? data.myPop) - zap.pending * data.zapPop,
|
||||
// Figma `516:88102` Zap_버프(아이템 배수 · 지속시간)는 별첨 B B-22 「배수 기능 제거」 ·
|
||||
// 10/3 통합회신(C-124) 「사용 시 Drop Pool 발생 후 HERO TIME 발동」이 대체한다 —
|
||||
// 같은 팬덤의 아이템 사용으로 HERO TIME 이 열리면 ZAP 이 Drop POP 참여로 바뀐다.
|
||||
isHeroTime: hero,
|
||||
blockedReason: zap.sending
|
||||
? context.l10n.zapApplying
|
||||
|
||||
@@ -2,12 +2,10 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/i18n/app_locale.dart';
|
||||
import '../../../core/theme/tokens.dart';
|
||||
import '../../../core/ui/app_icons.dart';
|
||||
import '../../quest/widgets/quest_card_tile.dart'
|
||||
show QuestIconBox, questImageFor;
|
||||
import '../../quest/widgets/time_attack_countdown.dart';
|
||||
import '../home_data.dart';
|
||||
|
||||
/// 홈 「TODAY'S QUEST」 묶음 — Figma 「홈」 `2097:16428`: 343×270 · r12 안에 343×90 줄
|
||||
@@ -45,8 +43,9 @@ class QuestRows extends StatelessWidget {
|
||||
/// 오른쪽 chevron 24 #4F4F4F.
|
||||
///
|
||||
/// 보조 줄은 카테고리(사본 「오늘의 출석체크」 · 「커뮤니티」)다. 「홈」 프레임에는 보상 숫자 ·
|
||||
/// 진행 상태가 없어 그리지 않는다. 예외는 긴급 타임어택의 남은 시간 하나 — FR-QST-06
|
||||
/// 「홈 카운트다운」이 요구한다.
|
||||
/// 진행 상태가 없어 그리지 않는다. 타임어택 남은 시간도 그리지 않는다 — 설명 프레임
|
||||
/// `2097:16397` 데이터는 「어드민에서 등록된 퀘스트」뿐이고, FR-QST-06 · 원장 C-94 의 카운트다운
|
||||
/// 표시 위치는 퀘스트 목록 · 상세다.
|
||||
class QuestRow extends StatelessWidget {
|
||||
const QuestRow({
|
||||
super.key,
|
||||
@@ -65,7 +64,6 @@ class QuestRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final done = quest.state == QuestState.done;
|
||||
return Material(
|
||||
color: AppColors.cosmicSurfaceContainer,
|
||||
shape: divider
|
||||
@@ -100,20 +98,7 @@ class QuestRow extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 타임어택은 받기 전까지 남은 시간을 센다(FR-QST-06).
|
||||
if (quest.endsAt case final endsAt? when !done)
|
||||
TimeAttackCountdown(
|
||||
endsAt: endsAt,
|
||||
builder: (context, left) => _sub(
|
||||
left == null
|
||||
? context.l10n.questTimeAttackClosed
|
||||
: context.l10n.questTimeAttackLeft(
|
||||
formatLeft(left),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
_sub(quest.category ?? ''),
|
||||
_sub(quest.category ?? ''),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user