frindlymap/lib/modules/ui/src/screens/person_screen.dart

200 lines
8.2 KiB
Dart

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<ReceivedCard> cards;
final List<Link> links;
final Map<String, String> 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<void> _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<void> _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<String, String> 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)),
]),
),
]),
);
}
}