Commit 660b6ec5b109ad84d519be320cc0f78d9ae68e91

Authored by yibo
1 parent 398bc940

feat(lifelog/photolog): 手机通讯录改独立多选页(读系统联系人→勾选→导入)

之前「📱手机通讯录」一点就全量导进池子,改为跟微信导入一致的列表+多选体验:
- 新增 PhoneContactPickerScreen:READ_CONTACTS 后读系统联系人成内存列表(不入库),
  按拼音 A–Z 分组+搜索,每行显示 人名+手机号 带复选框,支持全选/取消全选
- 勾选「导入选中」才入库,直接进「我管理的人物」(managed=true,不用再去池子勾第二遍)
- VM: importPhoneContacts(全量) 拆成 loadPhoneContacts(读列表)+importSelectedPhoneContacts(导选中)
- 保留按手机号/姓名跨源自动合并去重
- 「通讯录」池页的手机入口改为跳转多选页(onOpenPhonePicker)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/PhotologScreen.kt
@@ -53,8 +53,10 @@ import androidx.compose.material3.Text @@ -53,8 +53,10 @@ import androidx.compose.material3.Text
53 import androidx.compose.material3.TextButton 53 import androidx.compose.material3.TextButton
54 import androidx.compose.material3.rememberModalBottomSheetState 54 import androidx.compose.material3.rememberModalBottomSheetState
55 import androidx.compose.runtime.Composable 55 import androidx.compose.runtime.Composable
  56 +import androidx.compose.runtime.LaunchedEffect
56 import androidx.compose.runtime.collectAsState 57 import androidx.compose.runtime.collectAsState
57 import androidx.compose.runtime.getValue 58 import androidx.compose.runtime.getValue
  59 +import androidx.compose.runtime.mutableStateMapOf
58 import androidx.compose.runtime.mutableStateOf 60 import androidx.compose.runtime.mutableStateOf
59 import androidx.compose.runtime.produceState 61 import androidx.compose.runtime.produceState
60 import androidx.compose.runtime.remember 62 import androidx.compose.runtime.remember
@@ -88,6 +90,7 @@ internal fun ContactsTab(repo: Repository) { @@ -88,6 +90,7 @@ internal fun ContactsTab(repo: Repository) {
88 var profilePersonId by remember { mutableStateOf<Long?>(null) } 90 var profilePersonId by remember { mutableStateOf<Long?>(null) }
89 var detailPhotoId by remember { mutableStateOf<Long?>(null) } 91 var detailPhotoId by remember { mutableStateOf<Long?>(null) }
90 var showPicker by remember { mutableStateOf(false) } 92 var showPicker by remember { mutableStateOf(false) }
  93 + var showPhonePicker by remember { mutableStateOf(false) }
91 // 人物 tab 只显示「我管理的人物」;全部微信联系人是池子(在选人页里)。 94 // 人物 tab 只显示「我管理的人物」;全部微信联系人是池子(在选人页里)。
92 val managed by vm.repo.persons.observeManaged().collectAsState(initial = emptyList()) 95 val managed by vm.repo.persons.observeManaged().collectAsState(initial = emptyList())
93 96
@@ -98,7 +101,12 @@ internal fun ContactsTab(repo: Repository) { @@ -98,7 +101,12 @@ internal fun ContactsTab(repo: Repository) {
98 onBack = { profilePersonId = null }, 101 onBack = { profilePersonId = null },
99 onOpenPhoto = { detailPhotoId = it }, 102 onOpenPhoto = { detailPhotoId = it },
100 ) 103 )
101 - showPicker -> WeChatPickerScreen(vm = vm, onBack = { showPicker = false }) 104 + showPhonePicker -> PhoneContactPickerScreen(vm = vm, onBack = { showPhonePicker = false })
  105 + showPicker -> WeChatPickerScreen(
  106 + vm = vm,
  107 + onBack = { showPicker = false },
  108 + onOpenPhonePicker = { showPhonePicker = true },
  109 + )
102 else -> ContactList( 110 else -> ContactList(
103 vm = vm, persons = managed, 111 vm = vm, persons = managed,
104 onOpen = { vm.touchPerson(it); profilePersonId = it }, 112 onOpen = { vm.touchPerson(it); profilePersonId = it },
@@ -420,30 +428,13 @@ private fun ContactRow(p: Person, onClick: () -&gt; Unit) { @@ -420,30 +428,13 @@ private fun ContactRow(p: Person, onClick: () -&gt; Unit) {
420 /** 通讯录(微信 + 手机两源合并的全部联系人池):勾选 → 纳入「我管理的人物」。搜索 + 拼音 A–Z。 */ 428 /** 通讯录(微信 + 手机两源合并的全部联系人池):勾选 → 纳入「我管理的人物」。搜索 + 拼音 A–Z。 */
421 @OptIn(ExperimentalFoundationApi::class) 429 @OptIn(ExperimentalFoundationApi::class)
422 @Composable 430 @Composable
423 -private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -> Unit) { 431 +private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -> Unit, onOpenPhonePicker: () -> Unit) {
424 val all by vm.repo.persons.observeAll().collectAsState(initial = emptyList()) 432 val all by vm.repo.persons.observeAll().collectAsState(initial = emptyList())
425 val enrollReady = remember { vm.wechatEnrollReady() } 433 val enrollReady = remember { vm.wechatEnrollReady() }
426 var enrollTapped by remember { mutableStateOf(false) } 434 var enrollTapped by remember { mutableStateOf(false) }
427 var query by remember { mutableStateOf("") } 435 var query by remember { mutableStateOf("") }
428 val managedCount = all.count { it.managed } 436 val managedCount = all.count { it.managed }
429 437
430 - val ctx = LocalContext.current  
431 - var phoneStatus by remember { mutableStateOf<String?>(null) }  
432 - val runPhoneImport = {  
433 - phoneStatus = "导入中…"  
434 - vm.importPhoneContacts { n -> phoneStatus = "已导入手机通讯录(新增 $n)" }  
435 - }  
436 - val contactsLauncher = rememberLauncherForActivityResult(  
437 - ActivityResultContracts.RequestPermission(),  
438 - ) { granted -> if (granted) runPhoneImport() else phoneStatus = "未授予通讯录权限" }  
439 - val importPhone = {  
440 - if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {  
441 - runPhoneImport()  
442 - } else {  
443 - contactsLauncher.launch(Manifest.permission.READ_CONTACTS)  
444 - }  
445 - }  
446 -  
447 val sections = remember(all, query) { 438 val sections = remember(all, query) {
448 val q = query.trim() 439 val q = query.trim()
449 val filtered = if (q.isBlank()) all else all.filter { p -> 440 val filtered = if (q.isBlank()) all else all.filter { p ->
@@ -469,7 +460,7 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -&gt; Unit) { @@ -469,7 +460,7 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -&gt; Unit) {
469 placeholder = { Text("搜索(共 ${all.size},已管理 $managedCount)") }, singleLine = true, 460 placeholder = { Text("搜索(共 ${all.size},已管理 $managedCount)") }, singleLine = true,
470 modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp), 461 modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp),
471 ) 462 )
472 - // 两源导入:微信(Mac 推过来的 enroll 数据)+ 手机系统通讯录。导入进池子,下面勾选。 463 + // 两源导入:微信(Mac 推过来的 enroll 数据,导入进池子后下面勾选)+ 手机系统通讯录(进多选页勾选导入)。
473 Row( 464 Row(
474 modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).padding(bottom = 4.dp), 465 modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).padding(bottom = 4.dp),
475 verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), 466 verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -477,10 +468,9 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -&gt; Unit) { @@ -477,10 +468,9 @@ private fun WeChatPickerScreen(vm: PhotologViewModel, onBack: () -&gt; Unit) {
477 if (enrollReady) { 468 if (enrollReady) {
478 Button(onClick = { vm.enrollWeChat(); enrollTapped = true }) { Text("💬 微信导入") } 469 Button(onClick = { vm.enrollWeChat(); enrollTapped = true }) { Text("💬 微信导入") }
479 } 470 }
480 - OutlinedButton(onClick = importPhone) { Text("📱 手机通讯录") } 471 + OutlinedButton(onClick = onOpenPhonePicker) { Text("📱 手机通讯录") }
481 } 472 }
482 - val hint = phoneStatus  
483 - ?: if (enrollTapped) "微信后台导入中,联系人会陆续出现…" 473 + val hint = if (enrollTapped) "微信后台导入中,联系人会陆续出现…"
484 else "勾选要纳入「我的人物」的联系人" 474 else "勾选要纳入「我的人物」的联系人"
485 Text(hint, style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(horizontal = 16.dp, vertical = 2.dp)) 475 Text(hint, style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(horizontal = 16.dp, vertical = 2.dp))
486 HorizontalDivider() 476 HorizontalDivider()
@@ -528,3 +518,132 @@ private fun PickerRow(p: Person, onToggle: () -&gt; Unit) { @@ -528,3 +518,132 @@ private fun PickerRow(p: Person, onToggle: () -&gt; Unit) {
528 } 518 }
529 } 519 }
530 } 520 }
  521 +
  522 +/**
  523 + * 手机系统通讯录多选页:读系统联系人 → 列表(姓名 + 手机号) → 勾选 → 「导入选中」进「我的人物」。
  524 + * 跟微信导入一致的列表 + 多选体验,但选择发生在系统通讯录这一层(选了才入库)。
  525 + */
  526 +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
  527 +@Composable
  528 +private fun PhoneContactPickerScreen(vm: PhotologViewModel, onBack: () -> Unit) {
  529 + val ctx = LocalContext.current
  530 + var granted by remember {
  531 + mutableStateOf(
  532 + ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED,
  533 + )
  534 + }
  535 + var contacts by remember { mutableStateOf<List<PhoneContact>?>(null) }
  536 + val selected = remember { mutableStateMapOf<Long, Boolean>() }
  537 + var query by remember { mutableStateOf("") }
  538 + var status by remember { mutableStateOf<String?>(null) }
  539 +
  540 + val permLauncher = rememberLauncherForActivityResult(
  541 + ActivityResultContracts.RequestPermission(),
  542 + ) { g -> granted = g }
  543 +
  544 + LaunchedEffect(granted) {
  545 + if (granted) {
  546 + if (contacts == null) vm.loadPhoneContacts { contacts = it }
  547 + } else {
  548 + permLauncher.launch(Manifest.permission.READ_CONTACTS)
  549 + }
  550 + }
  551 +
  552 + val list = contacts.orEmpty()
  553 + val sections = remember(list, query) {
  554 + val q = query.trim()
  555 + val filtered = if (q.isBlank()) list else list.filter {
  556 + it.name.contains(q, true) || it.phoneDisplay?.contains(q) == true || it.pinyin?.contains(q, true) == true
  557 + }
  558 + filtered.sortedWith(compareBy({ phoneLetter(it) }, { it.pinyin ?: it.name }))
  559 + .groupBy { phoneLetter(it) }
  560 + .toSortedMap(compareBy { if (it == "#") "[" else it })
  561 + }
  562 + val selCount = selected.count { it.value }
  563 +
  564 + Column(modifier = Modifier.fillMaxSize()) {
  565 + Row(
  566 + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
  567 + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp),
  568 + ) {
  569 + TextButton(onClick = onBack) { Text("← 返回") }
  570 + Text("手机通讯录", style = MaterialTheme.typography.titleMedium)
  571 + }
  572 + OutlinedTextField(
  573 + value = query, onValueChange = { query = it },
  574 + placeholder = { Text("搜索(共 ${list.size})") }, singleLine = true,
  575 + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp),
  576 + )
  577 + Row(
  578 + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).padding(bottom = 4.dp),
  579 + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp),
  580 + ) {
  581 + Button(
  582 + enabled = selCount > 0,
  583 + onClick = {
  584 + val sel = list.filter { selected[it.id] == true }
  585 + status = "导入中…"
  586 + vm.importSelectedPhoneContacts(sel) { n ->
  587 + status = "已导入 ${sel.size} 人(新增 $n)到「我的人物」"
  588 + selected.clear()
  589 + }
  590 + },
  591 + ) { Text("导入选中($selCount)") }
  592 + if (list.isNotEmpty()) {
  593 + TextButton(onClick = {
  594 + if (selCount == list.size) selected.clear()
  595 + else list.forEach { selected[it.id] = true }
  596 + }) { Text(if (selCount == list.size) "取消全选" else "全选") }
  597 + }
  598 + }
  599 + status?.let { Text(it, style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(horizontal = 16.dp, vertical = 2.dp)) }
  600 + HorizontalDivider()
  601 + when {
  602 + !granted -> Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
  603 + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) {
  604 + Text("需要通讯录权限才能读取手机联系人。", style = MaterialTheme.typography.bodyMedium)
  605 + Button(onClick = { permLauncher.launch(Manifest.permission.READ_CONTACTS) }) { Text("去授权") }
  606 + }
  607 + }
  608 + contacts == null -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() }
  609 + list.isEmpty() -> Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
  610 + Text("手机通讯录里没有带号码的联系人。", style = MaterialTheme.typography.bodyMedium)
  611 + }
  612 + else -> LazyColumn(modifier = Modifier.fillMaxSize()) {
  613 + sections.forEach { (letter, group) ->
  614 + stickyHeader {
  615 + Surface(color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier.fillMaxWidth()) {
  616 + Text(letter, style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 3.dp))
  617 + }
  618 + }
  619 + items(group, key = { it.id }) { pc ->
  620 + PhoneContactRow(pc, checked = selected[pc.id] == true) {
  621 + selected[pc.id] = !(selected[pc.id] ?: false)
  622 + }
  623 + }
  624 + }
  625 + }
  626 + }
  627 + }
  628 +}
  629 +
  630 +private fun phoneLetter(pc: PhoneContact): String {
  631 + val c = (pc.pyInitial?.firstOrNull() ?: pc.name.firstOrNull())?.uppercaseChar()
  632 + return if (c != null && c in 'A'..'Z') c.toString() else "#"
  633 +}
  634 +
  635 +@Composable
  636 +private fun PhoneContactRow(pc: PhoneContact, checked: Boolean, onToggle: () -> Unit) {
  637 + Row(
  638 + modifier = Modifier.fillMaxWidth().clickable(onClick = onToggle).padding(horizontal = 12.dp, vertical = 5.dp),
  639 + verticalAlignment = Alignment.CenterVertically,
  640 + horizontalArrangement = Arrangement.spacedBy(10.dp),
  641 + ) {
  642 + Checkbox(checked = checked, onCheckedChange = { onToggle() })
  643 + Box(modifier = Modifier.size(40.dp).clip(RoundedCornerShape(6.dp)).background(MaterialTheme.colorScheme.surfaceVariant))
  644 + Column(modifier = Modifier.weight(1f)) {
  645 + Text(pc.name, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleSmall)
  646 + pc.phoneDisplay?.let { Text(it, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelSmall) }
  647 + }
  648 + }
  649 +}
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/PhotologViewModel.kt
@@ -109,19 +109,14 @@ class PhotologViewModel(app: Application) : AndroidViewModel(app) { @@ -109,19 +109,14 @@ class PhotologViewModel(app: Application) : AndroidViewModel(app) {
109 } 109 }
110 110
111 /** 111 /**
112 - * 从手机系统通讯录导入联系人到「池子」(managed=false,跟微信一致,用户再勾选)。  
113 - * 跨源去重(用户选择「按手机号/姓名自动合并」):  
114 - * 1) 已按 contactRef="phone:<id>" 建过 → 刷新资料;  
115 - * 2) 否则手机号命中已有人物 → 只补手机号,不另建;  
116 - * 3) 否则姓名精确命中 → 只补手机号,不另建;  
117 - * 4) 都没有 → 新建。  
118 - * 调用方需先拿到 READ_CONTACTS。回调返回本轮新建数。 112 + * 读手机系统通讯录 → 内存列表(不入库),供「手机通讯录」多选页展示。
  113 + * 一个联系人多号码只取第一条;无名字的跳过。调用方需先拿到 READ_CONTACTS。
119 */ 114 */
120 - fun importPhoneContacts(onDone: (Int) -> Unit) { 115 + fun loadPhoneContacts(onResult: (List<PhoneContact>) -> Unit) {
121 val app = getApplication<Application>() 116 val app = getApplication<Application>()
122 viewModelScope.launch { 117 viewModelScope.launch {
123 - val created = withContext(Dispatchers.IO) {  
124 - var count = 0 118 + val list = withContext(Dispatchers.IO) {
  119 + val out = ArrayList<PhoneContact>()
125 try { 120 try {
126 val proj = arrayOf( 121 val proj = arrayOf(
127 android.provider.ContactsContract.CommonDataKinds.Phone.CONTACT_ID, 122 android.provider.ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
@@ -143,17 +138,39 @@ class PhotologViewModel(app: Application) : AndroidViewModel(app) { @@ -143,17 +138,39 @@ class PhotologViewModel(app: Application) : AndroidViewModel(app) {
143 if (!seen.add(cid)) continue // 一个联系人多号码,只取第一条 138 if (!seen.add(cid)) continue // 一个联系人多号码,只取第一条
144 val name = (if (nameIdx >= 0) c.getString(nameIdx) else null)?.trim().orEmpty() 139 val name = (if (nameIdx >= 0) c.getString(nameIdx) else null)?.trim().orEmpty()
145 if (name.isBlank()) continue 140 if (name.isBlank()) continue
146 - val phone = (if (numIdx >= 0) c.getString(numIdx) else null)  
147 - ?.filter { it.isDigit() }?.takeIf { it.isNotBlank() } 141 + val phoneRaw = (if (numIdx >= 0) c.getString(numIdx) else null)?.trim()?.takeIf { it.isNotBlank() }
  142 + val phone = phoneRaw?.filter { it.isDigit() }?.takeIf { it.isNotBlank() }
148 val sortKey = if (sortIdx >= 0) c.getString(sortIdx) else null 143 val sortKey = if (sortIdx >= 0) c.getString(sortIdx) else null
149 val pinyin = sortKey?.lowercase()?.filter { it.isLetter() }?.takeIf { it.isNotBlank() } 144 val pinyin = sortKey?.lowercase()?.filter { it.isLetter() }?.takeIf { it.isNotBlank() }
150 val pyInit = ((sortKey?.firstOrNull() ?: name.firstOrNull())?.uppercaseChar()) 145 val pyInit = ((sortKey?.firstOrNull() ?: name.firstOrNull())?.uppercaseChar())
151 ?.takeIf { it in 'A'..'Z' }?.toString() 146 ?.takeIf { it in 'A'..'Z' }?.toString()
152 - if (upsertPhoneContact(cid, name, phone, pinyin, pyInit)) count++ 147 + out.add(PhoneContact(cid, name, phoneRaw, phone, pinyin, pyInit))
153 } 148 }
154 } 149 }
155 } catch (t: Throwable) { 150 } catch (t: Throwable) {
156 - Log.w("PhotologVM", "phone import failed: ${t.message}") 151 + Log.w("PhotologVM", "load phone contacts failed: ${t.message}")
  152 + }
  153 + out
  154 + }
  155 + onResult(list)
  156 + }
  157 + }
  158 +
  159 + /**
  160 + * 把多选页勾选的手机联系人导入「我管理的人物」(managed=true,因为已是人工勾选)。
  161 + * 跨源去重(用户选择「按手机号/姓名自动合并」):
  162 + * 1) 已按 contactRef="phone:<id>" 建过 → 刷新资料 + 纳入管理;
  163 + * 2) 否则手机号命中已有人物 → 补手机号 + 纳入管理,不另建;
  164 + * 3) 否则姓名精确命中 → 补手机号 + 纳入管理,不另建;
  165 + * 4) 都没有 → 新建(managed=true)。
  166 + * 回调返回本轮新建数。
  167 + */
  168 + fun importSelectedPhoneContacts(selected: List<PhoneContact>, onDone: (Int) -> Unit) {
  169 + viewModelScope.launch {
  170 + val created = withContext(Dispatchers.IO) {
  171 + var count = 0
  172 + for (pc in selected) {
  173 + if (upsertPhoneContact(pc.id, pc.name, pc.phone, pc.pinyin, pc.pyInitial, managed = true)) count++
157 } 174 }
158 count 175 count
159 } 176 }
@@ -163,20 +180,22 @@ class PhotologViewModel(app: Application) : AndroidViewModel(app) { @@ -163,20 +180,22 @@ class PhotologViewModel(app: Application) : AndroidViewModel(app) {
163 180
164 /** 返回 true = 本轮新建了一行(用于计数)。 */ 181 /** 返回 true = 本轮新建了一行(用于计数)。 */
165 private suspend fun upsertPhoneContact( 182 private suspend fun upsertPhoneContact(
166 - cid: Long, name: String, phone: String?, pinyin: String?, pyInit: String?, 183 + cid: Long, name: String, phone: String?, pinyin: String?, pyInit: String?, managed: Boolean,
167 ): Boolean { 184 ): Boolean {
168 val ref = "phone:$cid" 185 val ref = "phone:$cid"
169 repo.persons.getByContactRef(ref)?.let { 186 repo.persons.getByContactRef(ref)?.let {
170 repo.persons.updatePhoneInfo(it.id, name, phone, pinyin, pyInit) 187 repo.persons.updatePhoneInfo(it.id, name, phone, pinyin, pyInit)
  188 + if (managed) repo.persons.setManaged(it.id, true)
171 return false 189 return false
172 } 190 }
173 val merge = (phone?.let { repo.persons.getByPhone(it) }) ?: repo.persons.getByExactName(name) 191 val merge = (phone?.let { repo.persons.getByPhone(it) }) ?: repo.persons.getByExactName(name)
174 if (merge != null) { 192 if (merge != null) {
175 if (merge.phone.isNullOrBlank() && phone != null) repo.persons.setPhone(merge.id, phone) 193 if (merge.phone.isNullOrBlank() && phone != null) repo.persons.setPhone(merge.id, phone)
  194 + if (managed) repo.persons.setManaged(merge.id, true)
176 return false 195 return false
177 } 196 }
178 repo.persons.insert( 197 repo.persons.insert(
179 - Person(name = name, contactRef = ref, phone = phone, pinyin = pinyin, pyInitial = pyInit), 198 + Person(name = name, contactRef = ref, phone = phone, pinyin = pinyin, pyInitial = pyInit, managed = managed),
180 ) 199 )
181 return true 200 return true
182 } 201 }
@@ -308,6 +327,16 @@ class PhotologViewModel(app: Application) : AndroidViewModel(app) { @@ -308,6 +327,16 @@ class PhotologViewModel(app: Application) : AndroidViewModel(app) {
308 } 327 }
309 } 328 }
310 329
  330 +/** 手机系统通讯录里的一条(内存态,未入库)。phoneDisplay 留原格式给 UI,phone 是纯数字去重键。 */
  331 +data class PhoneContact(
  332 + val id: Long,
  333 + val name: String,
  334 + val phoneDisplay: String?,
  335 + val phone: String?,
  336 + val pinyin: String?,
  337 + val pyInitial: String?,
  338 +)
  339 +
311 /** 「录一段绑声纹」对话框的状态机。 */ 340 /** 「录一段绑声纹」对话框的状态机。 */
312 sealed class VoiceEnroll { 341 sealed class VoiceEnroll {
313 object Idle : VoiceEnroll() 342 object Idle : VoiceEnroll()