사용자 화면 문구·표본 데이터 · 퀘스트 참여 흐름 · HOME 진입 구획 · 미니맵 · 공통 확인 창 배치 수정
This commit is contained in:
@@ -115,8 +115,8 @@ class _AccountLinkScreenState extends State<AccountLinkScreen> {
|
||||
switch (outcome) {
|
||||
case AuthFailed(:final code, :final message):
|
||||
_toast('[$code] $message');
|
||||
case AuthNotConfigured(:final missing):
|
||||
_toast(context.l10n.settingsLinkNotReady(_name, missing.join(' · ')));
|
||||
case AuthNotConfigured():
|
||||
_toast(context.l10n.settingsLinkNotReady(_name));
|
||||
case AuthSucceeded() || AuthCancelled():
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,18 @@ enum MyListKind {
|
||||
enum _MySort { latest, pop, solved, achieved }
|
||||
|
||||
class MyListsScreen extends StatefulWidget {
|
||||
const MyListsScreen({super.key, required this.kind, this.campaigns});
|
||||
const MyListsScreen({
|
||||
super.key,
|
||||
required this.kind,
|
||||
this.campaigns,
|
||||
this.insides,
|
||||
});
|
||||
|
||||
final MyListKind kind;
|
||||
|
||||
/// 내 인사이드의 원천. null 이면 표본(목 모드). 저장소 선택은 repositories.dart.
|
||||
final InsideRepository? insides;
|
||||
|
||||
/// 내 캠페인의 원천. null 이면 표본(목 모드). 저장소 선택은 repositories.dart.
|
||||
final CampaignRepository? campaigns;
|
||||
|
||||
@@ -47,44 +55,6 @@ class MyListsScreen extends StatefulWidget {
|
||||
class _MyListsScreenState extends State<MyListsScreen> {
|
||||
_MySort _sort = _MySort.latest;
|
||||
|
||||
/// 표본 — 시안 `370:228289` 문구. 서버가 붙으면 INSIDE 저장소의 내 글로 옮긴다.
|
||||
static final _insides = () {
|
||||
final now = DateTime.now();
|
||||
return <InsideQuestion>[
|
||||
InsideQuestion(
|
||||
id: 'i1',
|
||||
title: 'BTS 컴백 알려주세요!',
|
||||
body: 'BTS 한국 컴백 시기가 궁급합니다!\n아시는 분들 댓글 부탁드려요 ㅠㅠ',
|
||||
authorName: 'BTS_Hong',
|
||||
createdAt: now,
|
||||
bountyPop: 100,
|
||||
isMine: true,
|
||||
),
|
||||
InsideQuestion(
|
||||
id: 'i2',
|
||||
title: '화양연화 세계관 해석해주실 분?',
|
||||
body: '이번 뮤비랑 연결되는 부분이 헷갈리네요. 타임라인 정리해주실 천재 아미 구합니다!',
|
||||
authorName: 'BTS_Hong',
|
||||
createdAt: now.subtract(const Duration(minutes: 10)),
|
||||
bountyPop: 500,
|
||||
viewCount: 3,
|
||||
isMine: true,
|
||||
),
|
||||
InsideQuestion(
|
||||
id: 'i3',
|
||||
title: 'BTS 이 포즈 뭔가요?',
|
||||
body: '아미 분들 이 포즈 뭔지 알려주실 수 있나요?',
|
||||
authorName: 'BTS_Hong',
|
||||
createdAt: now.subtract(const Duration(days: 30)),
|
||||
bountyPop: 800,
|
||||
answerCount: 24,
|
||||
viewCount: 82,
|
||||
isMine: true,
|
||||
isSolved: true,
|
||||
),
|
||||
];
|
||||
}();
|
||||
|
||||
List<_MySort> get _sorts => switch (widget.kind) {
|
||||
MyListKind.inside => const [_MySort.latest, _MySort.pop, _MySort.solved],
|
||||
MyListKind.campaign => const [_MySort.latest, _MySort.achieved],
|
||||
@@ -132,13 +102,27 @@ class _MyListsScreenState extends State<MyListsScreen> {
|
||||
},
|
||||
);
|
||||
|
||||
Widget _insideList(BuildContext context) {
|
||||
/// 내가 쓴 질문 — 화면을 열 때 한 번 읽고, 정렬 칩은 그 목록을 다시 거른다.
|
||||
late final Future<List<InsideQuestion>> _mine =
|
||||
(widget.insides ?? const SampleInsideRepository()).mine();
|
||||
|
||||
Widget _insideList(BuildContext context) =>
|
||||
FutureBuilder<List<InsideQuestion>>(
|
||||
future: _mine,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return _insideItems(context, snap.data ?? const []);
|
||||
},
|
||||
);
|
||||
|
||||
Widget _insideItems(BuildContext context, List<InsideQuestion> mine) {
|
||||
final items = switch (_sort) {
|
||||
_MySort.pop => [
|
||||
..._insides,
|
||||
]..sort((a, b) => b.bountyPop.compareTo(a.bountyPop)),
|
||||
_MySort.solved => _insides.where((q) => q.isSolved).toList(),
|
||||
_ => _insides,
|
||||
_MySort.pop => [...mine]
|
||||
..sort((a, b) => b.bountyPop.compareTo(a.bountyPop)),
|
||||
_MySort.solved => mine.where((q) => q.isSolved).toList(),
|
||||
_ => mine,
|
||||
};
|
||||
if (items.isEmpty) {
|
||||
return Center(
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../core/ui/app_icons.dart';
|
||||
import '../auth/auth.dart';
|
||||
import '../auth/auth_repository.dart';
|
||||
import '../campaign/campaign_data.dart' show CampaignRepository;
|
||||
import '../inside/inside_data.dart' show InsideRepository;
|
||||
import 'my_data.dart';
|
||||
import 'my_lists_screen.dart';
|
||||
import 'profile_edit_screen.dart';
|
||||
@@ -29,6 +30,7 @@ class MyScreen extends StatefulWidget {
|
||||
this.push,
|
||||
this.auth,
|
||||
this.campaigns,
|
||||
this.insides,
|
||||
});
|
||||
|
||||
final MyRepository repository;
|
||||
@@ -36,6 +38,9 @@ class MyScreen extends StatefulWidget {
|
||||
/// 「내 캠페인」이 읽는 저장소. null 이면 표본.
|
||||
final CampaignRepository? campaigns;
|
||||
|
||||
/// 「내 인사이드」가 읽는 저장소. null 이면 표본.
|
||||
final InsideRepository? insides;
|
||||
|
||||
/// 설정 > SNS 연동으로 그대로 내려간다. null 이면 설정 화면이 표본을 쓴다.
|
||||
final AccountLinker? linker;
|
||||
|
||||
@@ -251,7 +256,10 @@ class _MyScreenState extends State<MyScreen> {
|
||||
_Row(
|
||||
label: context.l10n.myInside,
|
||||
onTap: () => _push(
|
||||
const MyListsScreen(kind: MyListKind.inside),
|
||||
MyListsScreen(
|
||||
kind: MyListKind.inside,
|
||||
insides: widget.insides,
|
||||
),
|
||||
),
|
||||
),
|
||||
_Row(
|
||||
@@ -314,16 +322,23 @@ class _MyScreenState extends State<MyScreen> {
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: ProfileCard(
|
||||
profile: profile,
|
||||
onShare: (target) {
|
||||
onShare: (target) async {
|
||||
final l10n = context.l10n;
|
||||
final messenger = ScaffoldMessenger.of(this.context);
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(this.context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.myShareLater(target)),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
try {
|
||||
if (!await shareProfile(profile, target)) return;
|
||||
messenger
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.shareCopied),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
messenger.showSnackBar(SnackBar(content: Text(l10n.commonRequestFailed)));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import '../../core/push/push_messaging.dart';
|
||||
import '../../core/theme/tokens.dart';
|
||||
import '../../core/ui/app_dialog.dart';
|
||||
import '../../core/ui/app_icons.dart';
|
||||
import '../auth/auth.dart';
|
||||
import '../auth/auth_repository.dart';
|
||||
@@ -281,13 +282,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await _notice(context.l10n.settingsSameEmailOnly);
|
||||
case AuthFailed(:final code, :final message):
|
||||
_toast('[$code] $message');
|
||||
case AuthNotConfigured(:final missing):
|
||||
_toast(
|
||||
context.l10n.settingsLinkNotReady(
|
||||
_snsName(context, provider),
|
||||
missing.join(' · '),
|
||||
),
|
||||
);
|
||||
case AuthNotConfigured():
|
||||
_toast(context.l10n.settingsLinkNotReady(_snsName(context, provider)));
|
||||
case AuthSucceeded() || AuthCancelled():
|
||||
break;
|
||||
}
|
||||
@@ -344,19 +340,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
|
||||
/// 안내 팝업(확인 하나) — 시안 `554:23103`.
|
||||
Future<void> _notice(String message) => showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => _MyDialog(
|
||||
title: message,
|
||||
actions: [
|
||||
_DialogButton(
|
||||
label: context.l10n.commonConfirm,
|
||||
filled: true,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
Future<void> _notice(String message) => showAlertDialog(context, message);
|
||||
|
||||
/// 확인 팝업(취소 · 실행) — 시안 `554:24669` · `374:14364` · `554:23257`.
|
||||
Future<bool> _confirm({
|
||||
@@ -366,30 +350,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
bool withdraw = false,
|
||||
Future<void> Function()? onConfirm,
|
||||
}) async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => _MyDialog(
|
||||
title: title,
|
||||
body: body,
|
||||
actions: [
|
||||
_DialogButton(
|
||||
label: context.l10n.commonCancel,
|
||||
onTap: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
_DialogButton(
|
||||
label: action,
|
||||
filled: true,
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
),
|
||||
final ok = await showConfirmDialog(
|
||||
context,
|
||||
title,
|
||||
body: body,
|
||||
confirmLabel: action,
|
||||
);
|
||||
if (ok == true && onConfirm != null) {
|
||||
await onConfirm();
|
||||
} else if (ok == true && withdraw && mounted) {
|
||||
_toast(context.l10n.settingsWithdrawLater);
|
||||
}
|
||||
return ok ?? false;
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,101 +494,6 @@ class _SnsRow extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// 팝업 — 바탕 #2d2d2d · 좌우 31.5 · 안쪽 24/16 · 제목 16/600 가운데 · 버튼 높이 48 간격 8.
|
||||
class _MyDialog extends StatelessWidget {
|
||||
const _MyDialog({required this.title, this.body, required this.actions});
|
||||
|
||||
final String title;
|
||||
final String? body;
|
||||
final List<Widget> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 31.5),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: myText(16, FontWeight.w600),
|
||||
),
|
||||
if (body case final b?) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
b,
|
||||
textAlign: TextAlign.center,
|
||||
style: myText(16, FontWeight.w500),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
for (final (i, a) in actions.indexed) ...[
|
||||
if (i > 0) const SizedBox(width: 8),
|
||||
Expanded(child: a),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _DialogButton extends StatelessWidget {
|
||||
const _DialogButton({
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.filled = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final bool filled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shape = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
);
|
||||
final text = Text(
|
||||
label,
|
||||
style: myText(
|
||||
16,
|
||||
FontWeight.w600,
|
||||
height: 1,
|
||||
color: filled ? AppColors.onCosmicAccent : AppColors.onCosmic,
|
||||
),
|
||||
);
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: filled
|
||||
? FilledButton(
|
||||
onPressed: onTap,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: AppColors.cosmicAccent,
|
||||
shape: shape,
|
||||
),
|
||||
child: text,
|
||||
)
|
||||
: OutlinedButton(
|
||||
onPressed: onTap,
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
padding: EdgeInsets.zero,
|
||||
side: const BorderSide(color: AppColors.onCosmic),
|
||||
shape: shape,
|
||||
),
|
||||
child: text,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 언어 설정 시트 — 시안 `357:224379`: 제목 + 닫기 · 언어 목록(고른 것에 분홍 체크) · 「선택」.
|
||||
class _LanguageSheet extends StatefulWidget {
|
||||
const _LanguageSheet({required this.current});
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../../core/format.dart';
|
||||
import '../../../core/theme/tokens.dart';
|
||||
import '../../../core/ui/app_icons.dart';
|
||||
import '../my_data.dart';
|
||||
import 'my_cards.dart';
|
||||
import '../../../core/i18n/app_locale.dart';
|
||||
import '../../campaign/campaign_share.dart' show shareHashtags;
|
||||
|
||||
/// 프로필 카드 오버레이 — 시안 `374:15738`(명세 `549:22058`).
|
||||
///
|
||||
@@ -17,11 +21,12 @@ class ProfileCard extends StatelessWidget {
|
||||
final void Function(String target) onShare;
|
||||
|
||||
/// 공유 대상과 시안 아이콘. 명세가 적은 순서 그대로다.
|
||||
/// 프로필 웹 주소가 아직 없어 마지막 칸은 「링크 복사」가 아니라 「문구 복사」다.
|
||||
static List<String> get shareTargets => [
|
||||
appL10n.shareInstagram,
|
||||
appL10n.shareTiktok,
|
||||
'X',
|
||||
appL10n.shareCopyLink,
|
||||
appL10n.shareCopyText,
|
||||
];
|
||||
static const _shareIcons = [
|
||||
'assets/figma/share-instagram.png',
|
||||
@@ -106,3 +111,29 @@ class ProfileCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 프로필 공유 문구 — 공유 시트와 복사가 같은 문구를 쓴다.
|
||||
String profileShareText(MyProfile profile, [AppLocalizations? l10n]) => [
|
||||
(l10n ?? appL10n).myShareText(
|
||||
profile.nickname,
|
||||
'${profile.userLevel}',
|
||||
formatPop(profile.contributedPop),
|
||||
),
|
||||
shareHashtags().join(' '),
|
||||
].join('\n');
|
||||
|
||||
/// 프로필 카드 공유. 앱별 전용 SDK 없이 OS 공유 시트로 보내고(사용자가 그 안에서
|
||||
/// 인스타 · 틱톡 · X 를 고른다), 마지막 칸은 문구를 복사한다. 복사했으면 true.
|
||||
Future<bool> shareProfile(
|
||||
MyProfile profile,
|
||||
String target, {
|
||||
Future<ShareResult> Function(ShareParams)? share,
|
||||
}) async {
|
||||
final text = profileShareText(profile);
|
||||
if (target == ProfileCard.shareTargets.last) {
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
return true;
|
||||
}
|
||||
await (share ?? SharePlus.instance.share)(ShareParams(text: text));
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user