import 'package:flutter/material.dart'; import 'package:friendly_map/modules/app/app.dart'; import 'package:friendly_map/modules/core/core.dart'; import 'package:friendly_map/modules/storage/storage.dart'; import '../app_scope.dart'; import '../theme.dart'; import '../widgets/async_view.dart'; import '../widgets/career_panel.dart'; import '../widgets/common.dart'; import '../widgets/labels.dart'; import '../widgets/person_summary.dart'; import '../widgets/relate_sheet.dart'; import 'chat_room_screen.dart'; import 'person_facts_screen.dart'; import 'infer_screen.dart'; class _PersonData { const _PersonData(this.person, this.cards, this.links, this.labels, this.info); final Person? person; final List cards; final List links; final Map labels; final PersonInfo info; } /// 사람 상세: 받은 명함들, 연결 고리, "어디서 봤지?"로 가는 길. class PersonScreen extends StatelessWidget { const PersonScreen({super.key, required this.personId}); final String personId; Future<_PersonData> _load(FriendlyApp app) async => _PersonData( await app.friends.person(personId), await app.network.receivedCards(personId), await app.network.links(personId), await fieldLabels(app), await PersonInfo.load(app, personId), ); Future _chat(BuildContext context, String kind) async { final app = AppScope.of(context).app; final room = await app.chats.openDirect(personId, kind); if (!context.mounted) return; if (room == null) { showNote(context, '${CardKinds.label(kind)} 대화는 ${CardKinds.label(kind)} 명함을 서로 공유한 뒤에 할 수 있어요.'); return; } pushPage(context, ChatRoomScreen(roomId: room.id)); } Future _call(BuildContext context, bool sms) async { final app = AppScope.of(context).app; final ok = sms ? await app.calls.sms(personId) : await app.calls.call(personId); if (!ok && context.mounted) showNote(context, '이 번호로는 ${sms ? '문자를' : '전화를'} 걸 수 없어요.'); } @override Widget build(BuildContext context) { return AsyncView<_PersonData>( load: _load, builder: (context, d) { final p = d.person; if (p == null) return Scaffold(appBar: AppBar(), body: const EmptyNote('찾을 수 없는 사람이에요.')); return Scaffold( appBar: AppBar(title: Text(p.name), actions: [ IconButton(tooltip: '관계 정하기', onPressed: () => showRelateSheet(context, p.id, p.name), icon: const Icon(Icons.hub_outlined)), IconButton( tooltip: '정보 고치기', onPressed: () => pushPage(context, PersonFactsScreen(personId: p.id)), icon: const Icon(Icons.edit_outlined), ), ]), body: ListView(padding: const EdgeInsets.only(bottom: 32), children: [ if (d.info.markDeceased) DeceasedBanner(d.info.facts), CareerNoticesPanel(personId: p.id), Padding( padding: const EdgeInsets.all(16), child: Row(children: [ Avatar(p.name, size: 56), const SizedBox(width: 14), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(p.name, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700)), const SizedBox(height: 2), if (d.info.reachable) Text(p.phone, style: const TextStyle(color: Pal.muted)), if (p.publicKey == null && d.info.reachable) const Text('아직 수락 전이에요', style: TextStyle(color: Pal.accent, fontSize: 12)), ]), ), ]), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row(children: [ Expanded( child: FilledButton.icon( onPressed: () => _chat(context, CardKinds.personal), icon: const Icon(Icons.chat_bubble_outline), label: const Text('대화'), ), ), const SizedBox(width: 10), Expanded( child: OutlinedButton.icon( onPressed: () => pushPage(context, InferScreen(personId: p.id)), icon: const Icon(Icons.travel_explore), label: const Text('어디서 봤지?'), ), ), ]), ), if (d.info.reachable && d.info.canCall) Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 0), child: Row(children: [ Expanded( child: FilledButton.tonalIcon( onPressed: () => _call(context, false), icon: const Icon(Icons.call), label: const Text('전화'), ), ), const SizedBox(width: 10), Expanded( child: FilledButton.tonalIcon( onPressed: () => _call(context, true), icon: const Icon(Icons.sms_outlined), label: const Text('문자'), ), ), ]), ), const SizedBox(height: 10), PlacesPanel(personId: p.id, name: p.name), RelationPanel(d.info.relations, onEdit: () => pushPage(context, PersonFactsScreen(personId: p.id))), CallPanel(d.info), EventsPanel(d.info.events), if (d.links.isNotEmpty) ...[ const SectionHeader('연결 고리'), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Wrap(spacing: 8, runSpacing: 8, children: [ for (final l in d.links) Chip(label: Text(l.detail.isEmpty ? l.relation : '${l.relation} · ${l.detail}')), ]), ), ], const SectionHeader('받은 명함'), if (d.cards.isEmpty) const EmptyNote('받은 명함이 아직 없어요.'), for (final c in d.cards) _ReceivedCardTile(c, d.labels), const SectionHeader('이 사람과 대화하기'), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Wrap(spacing: 8, runSpacing: 8, children: [ for (final k in CardKinds.all.where((k) => k != CardKinds.personal)) ActionChip(label: Text('${CardKinds.label(k)} 대화'), onPressed: () => _chat(context, k)), ]), ), ]), ); }, ); } } class _ReceivedCardTile extends StatelessWidget { const _ReceivedCardTile(this.c, this.labels); final ReceivedCard c; final Map labels; @override Widget build(BuildContext context) { final lines = filledFields(decodeFields(c.fieldsJson), labels); return Panel( onLongPress: () async { final ok = await askConfirm(context, '이 명함을 지울까요?', '내 기기에서만 지워져요. 상대의 명함은 그대로예요.', ok: '지우기'); if (ok && context.mounted) await AppScope.of(context).app.network.deleteReceivedCard(c.id); }, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ KindChip(c.kind, CardKinds.label(c.kind)), const Spacer(), Text(shortTime(c.updatedAt), style: const TextStyle(fontSize: 11, color: Pal.muted)), ]), const SizedBox(height: 8), if (lines.isEmpty) const Text('비어 있는 명함', style: TextStyle(color: Pal.muted)), for (final e in lines) Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: 72, child: Text(e.key, style: const TextStyle(fontSize: 12, color: Pal.muted))), Expanded(child: Text(e.value)), ]), ), ]), ); } }