하단 내비게이션 바 여백 · 인증 칩 · 위치 선택 지도 · 퀘스트 상세 값 정렬
This commit is contained in:
+14
-2
@@ -12,6 +12,7 @@ import 'core/push/push_messaging.dart';
|
|||||||
import 'core/router/app_router.dart';
|
import 'core/router/app_router.dart';
|
||||||
import 'features/home/home_screen.dart' show homeRefreshSignal;
|
import 'features/home/home_screen.dart' show homeRefreshSignal;
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
|
import 'core/theme/tokens.dart';
|
||||||
import 'features/auth/auth_repository.dart';
|
import 'features/auth/auth_repository.dart';
|
||||||
import 'features/campaign/campaign_detail_screen.dart';
|
import 'features/campaign/campaign_detail_screen.dart';
|
||||||
import 'features/notification/notification_screen.dart';
|
import 'features/notification/notification_screen.dart';
|
||||||
@@ -241,8 +242,19 @@ class _FancakeAppState extends State<FancakeApp> with WidgetsBindingObserver {
|
|||||||
screenFor: _notificationScreen,
|
screenFor: _notificationScreen,
|
||||||
child: MaterialApp.router(
|
child: MaterialApp.router(
|
||||||
title: 'FANUZU',
|
title: 'FANUZU',
|
||||||
builder: (context, child) =>
|
// 하단 시스템 내비게이션 바(Android 15 edge-to-edge · iOS 홈 표시줄) 위로 모든 화면을 올린다.
|
||||||
AdaptivePlanetEffects(child: child ?? const SizedBox()),
|
// 본문이 `ListView(padding: …)` 처럼 하단 여백을 직접 정하면 안전 영역이 빠져 마지막 칸 ·
|
||||||
|
// 하단 버튼이 바에 가렸다(10/6 실기기 — 약관 「이전 · 완료」 · 퀘스트 생성 「총 예치 · 수수료」 ·
|
||||||
|
// 설정 「서비스 탈퇴」). 화면마다 고치지 않고 여기서 한 번에. 띠는 앱 바탕색이다.
|
||||||
|
builder: (context, child) => ColoredBox(
|
||||||
|
color: AppColors.cosmicVoid,
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
child: AdaptivePlanetEffects(child: child ?? const SizedBox()),
|
||||||
|
),
|
||||||
|
),
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
scaffoldMessengerKey: _messengerKey,
|
scaffoldMessengerKey: _messengerKey,
|
||||||
locale: _locale.locale,
|
locale: _locale.locale,
|
||||||
|
|||||||
@@ -114,7 +114,15 @@ ThemeData buildAppTheme() {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||||
),
|
),
|
||||||
labelStyle: _style(AppType.caption, const Color(0xFFD5D7D9)),
|
// 선택(흰 바탕)이면 검은 글자 — 상태 색을 주지 않으면 선택 칩도 연회색 글자라 읽히지 않는다(10/6 실기기).
|
||||||
|
labelStyle: _style(
|
||||||
|
AppType.caption,
|
||||||
|
WidgetStateColor.resolveWith(
|
||||||
|
(s) => s.contains(WidgetState.selected)
|
||||||
|
? Colors.black
|
||||||
|
: const Color(0xFFD5D7D9),
|
||||||
|
),
|
||||||
|
),
|
||||||
secondaryLabelStyle: _style(AppType.caption, AppColors.onCosmicAccent),
|
secondaryLabelStyle: _style(AppType.caption, AppColors.onCosmicAccent),
|
||||||
checkmarkColor: AppColors.onCosmicAccent,
|
checkmarkColor: AppColors.onCosmicAccent,
|
||||||
showCheckmark: false,
|
showCheckmark: false,
|
||||||
|
|||||||
@@ -73,10 +73,57 @@ class _TermsScreenState extends State<TermsScreen> {
|
|||||||
void _toggle(_Term t) =>
|
void _toggle(_Term t) =>
|
||||||
setState(() => _agreed.contains(t) ? _agreed.remove(t) : _agreed.add(t));
|
setState(() => _agreed.contains(t) ? _agreed.remove(t) : _agreed.add(t));
|
||||||
|
|
||||||
|
Widget _termsButtons(BuildContext context) {
|
||||||
|
final shape = RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||||
|
);
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: widget.onBack ?? () => Navigator.of(context).pop(),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(56),
|
||||||
|
shape: shape,
|
||||||
|
side: const BorderSide(color: _stroke),
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
foregroundColor: AppColors.onCosmic,
|
||||||
|
),
|
||||||
|
child: Text(context.l10n.commonPrev),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: _ready ? _done : null,
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(56),
|
||||||
|
shape: shape,
|
||||||
|
side: const BorderSide(color: _stroke),
|
||||||
|
backgroundColor: AppColors.cosmicAccent,
|
||||||
|
foregroundColor: AppColors.onCosmicAccent,
|
||||||
|
disabledBackgroundColor: const Color(0xFF333333),
|
||||||
|
disabledForegroundColor: const Color(0xFF777777),
|
||||||
|
),
|
||||||
|
child: Text(context.l10n.commonDone),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(context.l10n.termsTitle)),
|
appBar: AppBar(title: Text(context.l10n.termsTitle)),
|
||||||
|
// Figma `179:24230` — 「이전 · 완료」는 목록 끝이 아니라 화면 하단 고정(56 높이 · 간격 10 · 아래 16).
|
||||||
|
// 시스템 내비게이션 바 위로 올린다(SafeArea) — 10/6 실기기에서 첫 탭이 바에 먹혔다.
|
||||||
|
bottomNavigationBar: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(AppSpace.s4),
|
||||||
|
child: _termsButtons(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
padding: const EdgeInsets.all(AppSpace.s4),
|
padding: const EdgeInsets.all(AppSpace.s4),
|
||||||
children: [
|
children: [
|
||||||
@@ -137,34 +184,6 @@ class _TermsScreenState extends State<TermsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: AppSpace.s6),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: widget.onBack ?? () => Navigator.of(context).pop(),
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
foregroundColor: AppColors.onCosmic,
|
|
||||||
),
|
|
||||||
child: Text(context.l10n.commonPrev),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: _ready ? _done : null,
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
backgroundColor: AppColors.cosmicAccent,
|
|
||||||
foregroundColor: AppColors.onCosmicAccent,
|
|
||||||
disabledBackgroundColor: const Color(0xFF333333),
|
|
||||||
disabledForegroundColor: const Color(0xFF777777),
|
|
||||||
),
|
|
||||||
child: Text(context.l10n.commonDone),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -420,6 +420,9 @@ class _EclipseDefendScreenState extends State<EclipseDefendScreen>
|
|||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 사본 `373:445267` 「…행성 운석 충돌을 막으세요!」와 다르게 「이클립스를 막으세요!」다 —
|
||||||
|
// 발주사 상세정의 v1.6.1 3장 · 1주차 공식 검토 회신서 17항 「운석 충돌은 Figma 에 남은 임시
|
||||||
|
// 카피, Eclipse 와 별도 이벤트로 구현하지 않고 Eclipse 문구로 일원화」(해석확인 영역A N-A05).
|
||||||
TextSpan(text: context.l10n.eclipseShakeTimed),
|
TextSpan(text: context.l10n.eclipseShakeTimed),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:geocoding/geocoding.dart';
|
import 'package:geocoding/geocoding.dart';
|
||||||
@@ -62,11 +64,20 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
|||||||
Future<void> _drop(LatLng point) async {
|
Future<void> _drop(LatLng point) async {
|
||||||
final coords =
|
final coords =
|
||||||
'${point.latitude.toStringAsFixed(5)}, ${point.longitude.toStringAsFixed(5)}';
|
'${point.latitude.toStringAsFixed(5)}, ${point.longitude.toStringAsFixed(5)}';
|
||||||
setState(() => _selected = QuestPlace(
|
setState(
|
||||||
id: '', name: coords, lat: point.latitude, lng: point.longitude));
|
() => _selected = QuestPlace(
|
||||||
|
id: '',
|
||||||
|
name: coords,
|
||||||
|
lat: point.latitude,
|
||||||
|
lng: point.longitude,
|
||||||
|
),
|
||||||
|
);
|
||||||
var address = coords;
|
var address = coords;
|
||||||
try {
|
try {
|
||||||
final marks = await Geocoding().placemarkFromCoordinates(point.latitude, point.longitude);
|
final marks = await Geocoding().placemarkFromCoordinates(
|
||||||
|
point.latitude,
|
||||||
|
point.longitude,
|
||||||
|
);
|
||||||
if (marks.isNotEmpty) {
|
if (marks.isNotEmpty) {
|
||||||
final line = placeAddress(marks.first);
|
final line = placeAddress(marks.first);
|
||||||
if (line.isNotEmpty) address = line;
|
if (line.isNotEmpty) address = line;
|
||||||
@@ -75,8 +86,14 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
|||||||
// 지오코더가 없거나(에뮬레이터 · 테스트 — 플랫폼 구현 없음) 실패 — 좌표 표시로 둔다.
|
// 지오코더가 없거나(에뮬레이터 · 테스트 — 플랫폼 구현 없음) 실패 — 좌표 표시로 둔다.
|
||||||
}
|
}
|
||||||
if (!mounted || _selected?.lat != point.latitude) return;
|
if (!mounted || _selected?.lat != point.latitude) return;
|
||||||
setState(() => _selected = QuestPlace(
|
setState(
|
||||||
id: '', name: address, lat: point.latitude, lng: point.longitude));
|
() => _selected = QuestPlace(
|
||||||
|
id: '',
|
||||||
|
name: address,
|
||||||
|
lat: point.latitude,
|
||||||
|
lng: point.longitude,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -129,6 +146,8 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
|||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
userAgentPackageName: 'kr.co.fanuzu.app',
|
userAgentPackageName: 'kr.co.fanuzu.app',
|
||||||
|
// Figma `371:356705` 은 어두운 지도다 — 같은 OSM 타일을 어둡게 칠한다(flutter_map 내장).
|
||||||
|
tileBuilder: darkModeTileBuilder,
|
||||||
),
|
),
|
||||||
MarkerLayer(
|
MarkerLayer(
|
||||||
markers: [
|
markers: [
|
||||||
@@ -138,14 +157,21 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
|||||||
point: LatLng(p.lat!, p.lng!),
|
point: LatLng(p.lat!, p.lng!),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => _choose(p),
|
onTap: () => _choose(p),
|
||||||
child: const Icon(Icons.place, color: AppColors.cosmicAccent),
|
child: const Icon(
|
||||||
|
Icons.place,
|
||||||
|
color: AppColors.cosmicAccent,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (pinned != null && pinned.lat != null && pinned.lng != null)
|
if (pinned != null &&
|
||||||
|
pinned.lat != null &&
|
||||||
|
pinned.lng != null)
|
||||||
Marker(
|
Marker(
|
||||||
point: LatLng(pinned.lat!, pinned.lng!),
|
point: LatLng(pinned.lat!, pinned.lng!),
|
||||||
|
width: 38,
|
||||||
|
height: 44,
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: const Icon(Icons.location_pin, size: 40, color: Colors.redAccent),
|
child: const CustomPaint(painter: _PinPainter()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -154,7 +180,7 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
|||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
child: Text(
|
child: Text(
|
||||||
'© OpenStreetMap',
|
'© OpenStreetMap',
|
||||||
style: TextStyle(fontSize: 10, color: Colors.black54),
|
style: TextStyle(fontSize: 10, color: Colors.white54),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -181,7 +207,8 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
|
|||||||
// 디폴트 미선택 · 단수 선택.
|
// 디폴트 미선택 · 단수 선택.
|
||||||
: RadioGroup<String>(
|
: RadioGroup<String>(
|
||||||
groupValue: pinned?.id,
|
groupValue: pinned?.id,
|
||||||
onChanged: (v) => _choose(widget.places.firstWhere((p) => p.id == v)),
|
onChanged: (v) =>
|
||||||
|
_choose(widget.places.firstWhere((p) => p.id == v)),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: _visible.length,
|
itemCount: _visible.length,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
@@ -243,9 +270,46 @@ String placeAddress(Placemark m) {
|
|||||||
}
|
}
|
||||||
final road = street.isNotEmpty
|
final road = street.isNotEmpty
|
||||||
? street
|
? street
|
||||||
: [t(m.thoroughfare), t(m.subThoroughfare)].where((s) => s.isNotEmpty).join(' ');
|
: [
|
||||||
return [area, t(m.locality), t(m.subLocality), road]
|
t(m.thoroughfare),
|
||||||
.where((s) => s.isNotEmpty)
|
t(m.subThoroughfare),
|
||||||
.toSet()
|
].where((s) => s.isNotEmpty).join(' ');
|
||||||
.join(' ');
|
return [
|
||||||
|
area,
|
||||||
|
t(m.locality),
|
||||||
|
t(m.subLocality),
|
||||||
|
road,
|
||||||
|
].where((s) => s.isNotEmpty).toSet().join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Figma `372:356854` 핀 — 38×44 분홍(#FF7DDD) 물방울 · 흰 테두리 · 가운데 흰 점 12.
|
||||||
|
class _PinPainter extends CustomPainter {
|
||||||
|
const _PinPainter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final r = size.width / 2;
|
||||||
|
final drop = ui.Path.combine(
|
||||||
|
PathOperation.union,
|
||||||
|
ui.Path()..addOval(Rect.fromCircle(center: Offset(r, r), radius: r)),
|
||||||
|
ui.Path()
|
||||||
|
..moveTo(r * 0.16, r * 1.5)
|
||||||
|
..lineTo(size.width - r * 0.16, r * 1.5)
|
||||||
|
..lineTo(r, size.height)
|
||||||
|
..close(),
|
||||||
|
);
|
||||||
|
canvas
|
||||||
|
..drawPath(drop, Paint()..color = AppColors.cosmicAccent)
|
||||||
|
..drawPath(
|
||||||
|
drop,
|
||||||
|
Paint()
|
||||||
|
..color = Colors.white
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 1.5,
|
||||||
|
)
|
||||||
|
..drawCircle(Offset(r, r), 6, Paint()..color = Colors.white);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
|
|||||||
context.l10n.questNoCreatable,
|
context.l10n.questNoCreatable,
|
||||||
style: const TextStyle(color: AppColors.onCosmicVariant),
|
style: const TextStyle(color: AppColors.onCosmicVariant),
|
||||||
),
|
),
|
||||||
|
// Figma `254:164081` 은 375 폭에서 3+1 줄(칩 88 · 114 · 111 · 간격 8 = 329)이다. 360 폭 기기(본문
|
||||||
|
// 328)에서는 1 이 모자라 2+2 로 접힌다 — 칩 치수는 사본 그대로 두고 기기 폭대로 줄을 바꾼다.
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
@@ -309,17 +311,23 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
|
|||||||
Text(context.l10n.questExtraVerify, style: Theme.of(context).textTheme.bodySmall),
|
Text(context.l10n.questExtraVerify, style: Theme.of(context).textTheme.bodySmall),
|
||||||
const SizedBox(height: AppSpace.s2),
|
const SizedBox(height: AppSpace.s2),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: AppSpace.s2,
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
for (final (key, label) in [
|
for (final (key, label) in [
|
||||||
('photo', context.l10n.questVerifyPhoto),
|
('photo', context.l10n.questVerifyPhoto),
|
||||||
('qr', context.l10n.questVerifyQr),
|
('qr', context.l10n.questVerifyQr),
|
||||||
])
|
])
|
||||||
FilterChip(
|
// 칩 모양은 Figma 카테고리 칩(`259:46062` 선택 = 흰 바탕 검은 글자) — 기본 FilterChip 은
|
||||||
label: Text(label),
|
// 선택 시 흰 바탕에 연회색 글자라 읽히지 않았다(10/6 실기기).
|
||||||
|
_CategoryChip(
|
||||||
|
label: label,
|
||||||
|
icon: null,
|
||||||
selected: _extraVerify.contains(key),
|
selected: _extraVerify.contains(key),
|
||||||
onSelected: (on) => setState(
|
onTap: () => setState(
|
||||||
() => on ? _extraVerify.add(key) : _extraVerify.remove(key),
|
() => _extraVerify.contains(key)
|
||||||
|
? _extraVerify.remove(key)
|
||||||
|
: _extraVerify.add(key),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -327,7 +327,10 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
|||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withValues(alpha: 0.3),
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
borderRadius: BorderRadius.circular(9999),
|
borderRadius: BorderRadius.circular(9999),
|
||||||
@@ -335,7 +338,11 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.music_note_rounded, size: 18, color: AppColors.onCosmic),
|
const Icon(
|
||||||
|
Icons.music_note_rounded,
|
||||||
|
size: 18,
|
||||||
|
color: AppColors.onCosmic,
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -790,14 +797,15 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
|||||||
}
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _stage = QuestStage.inProgress);
|
setState(() => _stage = QuestStage.inProgress);
|
||||||
final run = await Navigator.of(context, rootNavigator: true).push<ShakeJudge>(
|
final run = await Navigator.of(context, rootNavigator: true)
|
||||||
MaterialPageRoute(
|
.push<ShakeJudge>(
|
||||||
builder: (_) => MusicShakePlayScreen(
|
MaterialPageRoute(
|
||||||
setup: setup,
|
builder: (_) => MusicShakePlayScreen(
|
||||||
shakes: widget.repository.shakeEvents(),
|
setup: setup,
|
||||||
),
|
shakes: widget.repository.shakeEvents(),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (run != null) widget.repository.recordShake(run);
|
if (run != null) widget.repository.recordShake(run);
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -864,9 +872,10 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
|||||||
Future<PlaceProof?> _placeProof(List<String> methods) async {
|
Future<PlaceProof?> _placeProof(List<String> methods) async {
|
||||||
String? qr;
|
String? qr;
|
||||||
if (methods.contains('qr')) {
|
if (methods.contains('qr')) {
|
||||||
qr = await Navigator.of(context, rootNavigator: true).push<String>(
|
qr = await Navigator.of(
|
||||||
MaterialPageRoute(builder: (_) => const QrScanScreen()),
|
context,
|
||||||
);
|
rootNavigator: true,
|
||||||
|
).push<String>(MaterialPageRoute(builder: (_) => const QrScanScreen()));
|
||||||
if (qr == null) return null;
|
if (qr == null) return null;
|
||||||
}
|
}
|
||||||
Uint8List? photo;
|
Uint8List? photo;
|
||||||
@@ -913,7 +922,8 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_busy = false;
|
_busy = false;
|
||||||
// Music Shake 판정 미달(B-24 — 보상 0) — 판정표는 소진됐으니 다시 하게 시작 전으로.
|
// Music Shake 판정 미달(B-24 — 보상 0) — 판정표는 소진됐으니 다시 하게 시작 전으로.
|
||||||
if (detail.card.type == QuestType.musicShake) _stage = QuestStage.notStarted;
|
if (detail.card.type == QuestType.musicShake)
|
||||||
|
_stage = QuestStage.notStarted;
|
||||||
});
|
});
|
||||||
_fail(error);
|
_fail(error);
|
||||||
// 타임어택 창 밖(E-QST-01) — 서버가 본 상태로 다시 읽어 마감 · 완료를 반영한다.
|
// 타임어택 창 밖(E-QST-01) — 서버가 본 상태로 다시 읽어 마감 · 완료를 반영한다.
|
||||||
@@ -996,12 +1006,14 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
|||||||
Future<String?> _watchAd(QuestReward reward) async {
|
Future<String?> _watchAd(QuestReward reward) async {
|
||||||
RewardedAdHandle? ad;
|
RewardedAdHandle? ad;
|
||||||
BackendException? failure;
|
BackendException? failure;
|
||||||
final loading = widget.repository.loadRewardedAd(reward).then<void>(
|
final loading = widget.repository
|
||||||
(handle) => ad = handle,
|
.loadRewardedAd(reward)
|
||||||
onError: (Object error) => failure = error is BackendException
|
.then<void>(
|
||||||
? error
|
(handle) => ad = handle,
|
||||||
: BackendException('E-AD-01', context.l10n.adLoadFailed),
|
onError: (Object error) => failure = error is BackendException
|
||||||
);
|
? error
|
||||||
|
: BackendException('E-AD-01', context.l10n.adLoadFailed),
|
||||||
|
);
|
||||||
await _warp(QuestAdWarpScreen.before(until: loading));
|
await _warp(QuestAdWarpScreen.before(until: loading));
|
||||||
final loaded = ad;
|
final loaded = ad;
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
@@ -1095,35 +1107,47 @@ class _Row extends StatelessWidget {
|
|||||||
color: AppColors.cosmicSurfaceContainer,
|
color: AppColors.cosmicSurfaceContainer,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
),
|
),
|
||||||
|
// Figma `264:48835` — 라벨은 왼쪽, 값(번개 포함)은 오른쪽 끝. 예전에는 라벨 Expanded 와 값 Flexible 이
|
||||||
|
// 폭을 반씩 나눠 값이 가운데 놓였다(10/6 실기기).
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Text(
|
||||||
child: Text(
|
label,
|
||||||
label,
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
fontSize: 14,
|
||||||
fontSize: 14,
|
fontWeight: FontWeight.w500,
|
||||||
fontWeight: FontWeight.w500,
|
height: 20 / 14,
|
||||||
height: 20 / 14,
|
color: AppColors.onCosmicVariant,
|
||||||
color: AppColors.onCosmicVariant,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (bolt) ...[
|
const SizedBox(width: 12),
|
||||||
Image.asset('assets/figma/icon-bolt.png', width: 16, height: 16.7),
|
Expanded(
|
||||||
const SizedBox(width: 4),
|
child: Row(
|
||||||
],
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
// 마감 줄처럼 값이 길면 줄을 바꾼다 — 좁은 화면에서 넘치지 않게.
|
children: [
|
||||||
Flexible(
|
if (bolt) ...[
|
||||||
child: Text(
|
Image.asset(
|
||||||
value,
|
'assets/figma/icon-bolt.png',
|
||||||
textAlign: TextAlign.end,
|
width: 16,
|
||||||
style: const TextStyle(
|
height: 16.7,
|
||||||
fontSize: 16,
|
),
|
||||||
fontWeight: FontWeight.w600,
|
const SizedBox(width: 4),
|
||||||
height: 1.4,
|
],
|
||||||
letterSpacing: -0.4,
|
// 마감 줄처럼 값이 길면 줄을 바꾼다 — 좁은 화면에서 넘치지 않게.
|
||||||
color: AppColors.onCosmic,
|
Flexible(
|
||||||
),
|
child: Text(
|
||||||
|
value,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.4,
|
||||||
|
letterSpacing: -0.4,
|
||||||
|
color: AppColors.onCosmic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:fancake/core/theme/app_theme.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// 10/6 실기기 — 선택한 칩(흰 바탕)의 글자가 연회색이라 읽히지 않았다. 선택이면 검은 글자.
|
||||||
|
void main() {
|
||||||
|
Color? labelColor(WidgetTester t) => t
|
||||||
|
.widget<DefaultTextStyle>(
|
||||||
|
find.ancestor(of: find.text('A'), matching: find.byType(DefaultTextStyle)).first,
|
||||||
|
)
|
||||||
|
.style
|
||||||
|
.color;
|
||||||
|
|
||||||
|
Future<void> pump(WidgetTester t, bool selected) => t.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: buildAppTheme(),
|
||||||
|
home: Scaffold(
|
||||||
|
body: FilterChip(label: const Text('A'), selected: selected, onSelected: (_) {}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('선택 칩은 검은 글자 · 미선택은 연회색', (t) async {
|
||||||
|
await pump(t, true);
|
||||||
|
expect(labelColor(t), Colors.black);
|
||||||
|
await pump(t, false);
|
||||||
|
expect(labelColor(t), const Color(0xFFD5D7D9));
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user