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

172 lines
7.0 KiB
Dart

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<GroupView> groups;
final List<Person> people; // 보이는 사람 (닫힌 잠긴 그룹 사람 제외)
final int hidden;
final int requests;
final List<Suggestion> suggestions;
}
/// 관계: 사람 찾기, 빠른 추가, 그룹(가족·학교·회사·모임과 하위 그룹), 이런 사이 아닐까요.
class RelationsScreen extends StatefulWidget {
const RelationsScreen({super.key});
@override
State<RelationsScreen> createState() => _RelationsScreenState();
}
class _RelationsScreenState extends State<RelationsScreen> {
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<void> _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<Widget> _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<Widget> _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)),
]),
),
),
),
),
);
}