200 lines
8.2 KiB
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)),
|
|
]),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
}
|