import 'package:friendly_map/modules/platform/platform.dart'; import 'package:friendly_map/modules/storage/storage.dart'; import 'change_bus.dart'; import 'friend_service.dart'; import 'group_service.dart'; /// 주소록 한 사람을 가져올 수 있는지 살핀 결과. class ContactCandidate { const ContactCandidate(this.contact, this.phone, this.known, {this.isMe = false}); final DeviceContact contact; /// 가져올 번호 (표준 형태). 쓸 수 있는 번호가 없으면 null. final String? phone; /// 이미 인맥에 있는 사람. final Person? known; final bool isMe; String get name => contact.name; bool get importable => phone != null && !isMe; } /// 주소록 거르기 조건. class ContactFilter { const ContactFilter({this.query = '', this.label, this.hideKnown = false}); /// 이름 일부, 초성(예: ㄱㅁㅅ), 번호 일부. final String query; /// 주소록 그룹(라벨). null 이면 전부. final String? label; /// 이미 인맥에 있는 사람은 빼고 보기. final bool hideKnown; } /// 가져오기 결과. class ImportSummary { const ImportSummary({this.added = 0, this.existing = 0, this.skipped = 0, this.grouped = 0, this.requested = 0}); final int added; // 새로 인맥에 넣은 사람 final int existing; // 이미 있던 사람 (그룹에만 넣음) final int skipped; // 번호가 없거나 내 번호라 건너뜀 final int grouped; // 그룹에 새로 들어간 횟수 final int requested; // 친구 요청을 보낸 사람 } const _choseong = 'ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ'; /// 한글 이름의 초성 ("김민수" → "ㄱㅁㅅ"). 한글이 아닌 글자는 그대로. String choseongOf(String s) { final b = StringBuffer(); for (final r in s.runes) { if (r >= 0xAC00 && r <= 0xD7A3) { b.write(_choseong[(r - 0xAC00) ~/ 588]); } else { b.writeCharCode(r); } } return b.toString(); } bool _onlyChoseong(String s) => s.isNotEmpty && s.runes.every((r) => _choseong.contains(String.fromCharCode(r))); /// 거르기 조건에 맞는지 (순수 계산). bool matchesContact(ContactCandidate c, ContactFilter f) { if (f.hideKnown && c.known != null) return false; if (f.label != null && !c.contact.labels.contains(f.label)) return false; final q = f.query.replaceAll(' ', '').toLowerCase(); if (q.isEmpty) return true; final name = c.name.replaceAll(' ', '').toLowerCase(); if (name.contains(q)) return true; if (_onlyChoseong(q) && choseongOf(name).contains(q)) return true; final digits = q.replaceAll(RegExp(r'[^0-9]'), ''); if (digits.length >= 3 && digits.length == q.replaceAll('-', '').length) { for (final p in c.contact.phones) { if (p.replaceAll(RegExp(r'[^0-9]'), '').contains(digits)) return true; } } return false; } /// 여러 번호 중 가져올 번호: 휴대폰(+8210)을 먼저, 없으면 처음으로 읽히는 번호. String? bestPhone(List raw) { String? first; for (final r in raw) { final n = normalizePhone(r); if (n == null) continue; if (n.startsWith('+8210')) return n; first ??= n; } return first; } /// 기기 주소록에서 사람을 골라 인맥과 그룹에 넣는다. 주소록은 읽기만 하고 고치지 않는다. /// /// 가져온 사람은 이 기기 인맥에만 들어가고, [import] 의 sendRequest 를 켰을 때만 친구 요청이 간다. class ContactImportService { ContactImportService(this._storage, this._clock, this._friends, this._groups, this._bus, this._provider); final Storage _storage; final Clock _clock; final FriendService _friends; final GroupService _groups; final ChangeBus _bus; final ContactsProvider? _provider; /// 이 기기에서 주소록을 읽을 수 있는지. bool get available => _provider != null; Future requestPermission() async => await _provider?.requestPermission() ?? false; /// 주소록 전부를 살펴 가져올 수 있는지 표시한다 (이름순). Future> load() async { final p = _provider; if (p == null) return const []; final me = (await _storage.settings.loadProfile())?.phone; final out = []; for (final c in await p.all()) { final phone = bestPhone(c.phones); final known = phone == null ? null : await _storage.people.byPhone(phone); out.add(ContactCandidate(c, phone, known, isMe: phone != null && phone == me)); } out.sort((a, b) => a.name.compareTo(b.name)); return out; } /// 조건에 맞는 사람만 (순서 유지). static List filter(List all, ContactFilter f) => [for (final c in all) if (matchesContact(c, f)) c]; /// 주소록 그룹(라벨) 이름들 (가나다순). static List labels(List all) => {for (final c in all) ...c.contact.labels}.toList()..sort(); /// 고른 사람들을 가져와 [groupIds] 그룹에 넣는다. 같은 번호는 한 번만 넣는다. Future import(List picked, {Set groupIds = const {}, bool sendRequest = false}) async { var added = 0, existing = 0, skipped = 0, grouped = 0, requested = 0; final ids = []; final done = {}; for (final c in picked) { final phone = c.phone; if (!c.importable || phone == null) { skipped++; continue; } if (!done.add(phone)) continue; final known = await _storage.people.byPhone(phone); Person? p; if (sendRequest && known?.publicKey == null) { p = await _friends.add(phone, known?.name ?? c.name); if (p != null) requested++; } else { p = known ?? await _storage.people.register(phone, c.name.trim(), _clock.now()); } if (p == null) { skipped++; continue; } if (known == null) { added++; } else { existing++; } ids.add(p.id); } for (final g in groupIds) { grouped += await _groups.addMembers(g, ids); } _bus.notify(); return ImportSummary(added: added, existing: existing, skipped: skipped, grouped: grouped, requested: requested); } }