172 lines
7.0 KiB
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)),
|
|
]),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|