import 'package:flutter/material.dart'; import 'package:friendly_map/modules/app/app.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'; import '../widgets/group_dialogs.dart'; import '../widgets/group_menu.dart'; import 'add_friend_screen.dart'; import 'contact_import_screen.dart'; import 'family_tree_screen.dart'; import 'group_detail_screen.dart'; import 'people_screen.dart'; import 'person_screen.dart'; import 'requests_screen.dart'; import 'shake_screen.dart'; class _Data { const _Data(this.groups, this.people, this.hidden, this.requests, this.suggestions); final List groups; final List people; // 보이는 사람 (닫힌 잠긴 그룹 사람 제외) final int hidden; final int requests; final List suggestions; } /// 관계: 사람 찾기, 빠른 추가, 그룹(가족·학교·회사·모임과 하위 그룹), 이런 사이 아닐까요. class RelationsScreen extends StatefulWidget { const RelationsScreen({super.key}); @override State createState() => _RelationsScreenState(); } class _RelationsScreenState extends State { String _q = ''; Future<_Data> _load(FriendlyApp a) async { final hidden = await a.groups.hiddenPeople(); final all = await a.friends.people(); final shown = [for (final p in all) if (!hidden.contains(p.id)) p]; return _Data(await a.groups.all(), shown, all.length - shown.length, (await a.blocks.incoming()).length, await a.network.suggestions()); } Future _addGroup() async { final app = AppScope.of(context).app; final r = await askGroupInfo(context); if (r == null || !mounted) return; if (await app.groups.add(r.$1, r.$2) == null && mounted) showNote(context, '이름이 비었거나 같은 이름의 그룹이 있어요'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('관계'), actions: [IconButton(tooltip: '친구 추가', onPressed: () => pushPage(context, const AddFriendScreen()), icon: const Icon(Icons.person_add_alt_1))], ), body: AsyncView<_Data>( load: _load, builder: (context, d) { final q = _q.trim(); return ListView(padding: const EdgeInsets.only(bottom: 32), children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), child: TextField( decoration: InputDecoration(hintText: '사람 찾기 (${d.people.length}명)', prefixIcon: const Icon(Icons.search), isDense: true), onChanged: (v) => setState(() => _q = v), ), ), if (q.isNotEmpty) ..._results(context, d, q) else ..._home(context, d), ]); }, ), ); } List _results(BuildContext context, _Data d, String q) { final hit = [for (final p in d.people) if (p.name.contains(q) || p.phone.contains(q) || displayPhone(p.phone).contains(q)) p]; if (hit.isEmpty) return const [EmptyNote('맞는 사람이 없어요.')]; return [ for (final p in hit) ListTile(leading: Avatar(p.name, size: 40), title: Text(p.name), subtitle: Text(displayPhone(p.phone)), onTap: () => pushPage(context, PersonScreen(personId: p.id))), ]; } List _home(BuildContext context, _Data d) { final roots = [for (final v in d.groups) if (v.depth == 1) v]; int kids(GroupView v) => d.groups.where((c) => c.group.parent == v.group.id).length; return [ if (d.requests > 0) Panel( onTap: () => pushPage(context, const RequestsScreen()), child: Row(children: [ const Icon(Icons.mark_email_unread_outlined, color: Pal.accent), const SizedBox(width: 12), Expanded(child: Text('받은 요청 ${d.requests}건', style: const TextStyle(fontWeight: FontWeight.w700))), const Icon(Icons.chevron_right, color: Pal.muted), ]), ), Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 4), child: Row(children: [ _Quick(Icons.people_alt_outlined, '모든 사람', '${d.people.length}명', () => pushPage(context, const PeopleScreen())), _Quick(Icons.account_tree_outlined, '가계도', '촌수', () => pushPage(context, const FamilyTreeScreen())), _Quick(Icons.contact_phone_outlined, '주소록', '불러오기', () => pushPage(context, const ContactImportScreen())), _Quick(Icons.vibration, '흔들기', '명함 교환', () => pushPage(context, const ShakeScreen())), ]), ), SectionHeader('그룹', trailing: TextButton.icon(onPressed: _addGroup, icon: const Icon(Icons.add, size: 18), label: const Text('새 그룹'))), for (final v in roots) GroupTile(v, children: kids(v), onOpen: () => pushPage(context, GroupDetailScreen(groupId: v.group.id))), if (d.hidden > 0) Padding( padding: const EdgeInsets.fromLTRB(20, 4, 20, 0), child: Row(children: [ const Icon(Icons.lock, size: 14, color: Pal.muted), const SizedBox(width: 6), Text('잠긴 그룹의 ${d.hidden}명은 숨겨져 있어요', style: const TextStyle(fontSize: 12, color: Pal.muted)), ]), ), if (d.suggestions.isNotEmpty) ...[ const SectionHeader('이런 사이 아닐까요?'), for (final s in d.suggestions.take(5)) ListTile( leading: Avatar(s.person.name, size: 40), title: Text(s.person.name), subtitle: Text('${s.guess.relation} · ${s.guess.detail}', maxLines: 1, overflow: TextOverflow.ellipsis), onTap: () => pushPage(context, PersonScreen(personId: s.person.id)), trailing: TextButton( onPressed: () => AppScope.of(context).app.network.addLink(s.person.id, s.guess.relation, s.guess.detail), child: const Text('맞아요'), ), ), ], ]; } } class _Quick extends StatelessWidget { const _Quick(this.icon, this.title, this.sub, this.onTap); final IconData icon; final String title; final String sub; final VoidCallback onTap; @override Widget build(BuildContext context) => Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Material( color: Pal.surface, borderRadius: BorderRadius.circular(14), child: InkWell( borderRadius: BorderRadius.circular(14), onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Column(children: [ Icon(icon, color: Pal.accent), const SizedBox(height: 6), Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), Text(sub, style: const TextStyle(fontSize: 11, color: Pal.muted)), ]), ), ), ), ), ); }