import 'package:flutter/material.dart'; import 'package:friendly_map/modules/app/app.dart'; import 'package:friendly_map/modules/inference/inference.dart'; import 'package:friendly_map/modules/storage/storage.dart'; import '../app_scope.dart'; import '../theme.dart'; import '../widgets/async_view.dart'; import '../widgets/common.dart'; class _InferData { const _InferData(this.person, this.guesses, this.links); final Person? person; final List guesses; final List links; } const _manualRelations = ['동창', '동문', '직장 동료', '전 직장 동료', '같은 모임', '같은 여행', '친척', '이웃', '기타']; /// 어디서 봤지? 주고받은 명함을 견주어 추측하고, 맞으면 연결 고리로 확정한다. 직접 적을 수도 있다. class InferScreen extends StatefulWidget { const InferScreen({super.key, required this.personId}); final String personId; @override State createState() => _InferScreenState(); } class _InferScreenState extends State { final _detail = TextEditingController(); String _relation = _manualRelations.first; @override void dispose() { _detail.dispose(); super.dispose(); } Future<_InferData> _load(FriendlyApp app) async => _InferData( await app.friends.person(widget.personId), await app.network.guessesFor(widget.personId), await app.network.links(widget.personId), ); @override Widget build(BuildContext context) { final app = AppScope.of(context).app; return AsyncView<_InferData>( load: _load, builder: (context, d) { final name = d.person?.name ?? '이 사람'; return Scaffold( appBar: AppBar(title: const Text('어디서 봤지?')), body: ListView(padding: const EdgeInsets.only(bottom: 32), children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 4, 20, 12), child: Text('$name님과 주고받은 명함을 내 명함과 견주어 봤어요.', style: const TextStyle(color: Pal.muted, height: 1.5)), ), if (d.guesses.isEmpty) const EmptyNote('겹치는 곳을 아직 찾지 못했어요.\n명함을 더 주고받거나 아래에서 직접 적어 보세요.'), for (final g in d.guesses) Panel( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Text(g.relation, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700)), const SizedBox(width: 8), Expanded(child: Text(g.detail, style: const TextStyle(color: Pal.muted), overflow: TextOverflow.ellipsis)), Text('${g.confidence}%', style: const TextStyle(color: Pal.accent, fontWeight: FontWeight.w700)), ]), const SizedBox(height: 6), Text(g.reason, style: const TextStyle(fontSize: 13, height: 1.4)), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: FilledButton.tonal( onPressed: () async { await app.network.addLink(widget.personId, g.relation, g.detail); if (context.mounted) showNote(context, '연결 고리로 확정했어요.'); }, child: const Text('맞아요'), ), ), ]), ), const SectionHeader('직접 적기'), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Wrap(spacing: 8, runSpacing: 8, children: [ for (final r in _manualRelations) ChoiceChip(label: Text(r), selected: _relation == r, onSelected: (_) => setState(() => _relation = r)), ]), ), Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: TextField(controller: _detail, decoration: const InputDecoration(labelText: '어디서? (예: 가람초 27회, 제주 여행)')), ), Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: SizedBox( height: 48, width: double.infinity, child: FilledButton( onPressed: () async { await app.network.addLink(widget.personId, _relation, _detail.text.trim()); _detail.clear(); if (context.mounted) showNote(context, '연결 고리를 남겼어요.'); }, child: const Text('연결 고리 남기기'), ), ), ), if (d.links.isNotEmpty) const SectionHeader('남겨 둔 연결 고리'), for (final l in d.links) Panel( child: Row(children: [ Expanded(child: Text(l.detail.isEmpty ? l.relation : '${l.relation} · ${l.detail}')), IconButton( tooltip: '지우기', onPressed: () => app.network.removeLink(l.id), icon: const Icon(Icons.close, color: Pal.muted), ), ]), ), ]), ); }, ); } }