use std::fmt::Write as _;
use ipc::{CardEntryDto, ContactDto, SyncReportDto};
use pbap_core::{CardEntry, Contact};
use session::contacts::SyncReport;
use store::{ContactEntryRow, ContactRow, PhoneField};
struct PhoneRef<'a> {
raw: &'a str,
display: &'a str,
}
impl<'a> From<&'a PhoneField> for PhoneRef<'a> {
fn from(p: &'a PhoneField) -> Self {
Self { raw: p.raw(), display: p.display() }
}
}
impl<'a> From<&'a ipc::PhoneDto> for PhoneRef<'a> {
fn from(p: &'a ipc::PhoneDto) -> Self {
Self { raw: &p.raw, display: p.e164.as_deref().unwrap_or(&p.raw) }
}
}
pub(super) struct ContactView<'a> {
name: Option<&'a str>,
phones: Vec<PhoneRef<'a>>,
}
impl<'a> ContactView<'a> {
pub(super) fn from_contact(c: &'a Contact) -> Self {
Self {
name: c.display_name.as_deref(),
phones: c.phones().iter().map(Into::into).collect(),
}
}
pub(super) fn from_dto(c: &'a ContactDto) -> Self {
Self { name: c.display_name.as_deref(), phones: c.phones.iter().map(Into::into).collect() }
}
pub(super) fn from_row(c: &'a ContactRow) -> Self {
Self { name: c.display_name.as_deref(), phones: c.phones.iter().map(Into::into).collect() }
}
}
pub(super) struct EntryView<'a> {
key: &'a str,
name: Option<&'a str>,
}
impl<'a> EntryView<'a> {
pub(super) fn from_entry(e: &'a CardEntry) -> Self {
Self { key: e.handle(), name: e.name() }
}
pub(super) fn from_dto(e: &'a CardEntryDto) -> Self {
Self { key: &e.handle, name: e.name.as_deref() }
}
pub(super) fn from_row(e: &'a ContactEntryRow) -> Self {
Self { key: &e.uid, name: e.display_name.as_deref() }
}
}
pub(super) fn render_contact_view(v: &ContactView, raw: bool) -> String {
let name = v.name.unwrap_or("(unknown)");
let mut out =
String::with_capacity(name.len().saturating_add(v.phones.len().saturating_mul(20)));
out.push_str(name);
for tel in &v.phones {
let number = if raw { tel.raw } else { tel.display };
let _ = write!(out, "\n {number}");
}
out
}
pub(super) fn render_contacts_view(views: &[ContactView], raw: bool) -> String {
views.iter().map(|v| render_contact_view(v, raw)).collect::<Vec<_>>().join("\n\n")
}
pub(super) fn render_entry_view(v: &EntryView) -> String {
v.name.map_or_else(|| v.key.to_owned(), |n| format!("{} {n}", v.key))
}
pub(super) fn render_entries_view(views: &[EntryView]) -> String {
views.iter().map(render_entry_view).collect::<Vec<_>>().join("\n")
}
pub(super) fn render_sync_dto(r: &SyncReportDto) -> String {
match *r {
SyncReportDto::UpToDate => up_to_date(),
SyncReportDto::Refreshed(f) => render_refresh(f.pull_failed, f.no_uid, f.written, f.wiped),
}
}
pub(super) fn render_sync_report(r: &SyncReport) -> String {
match *r {
SyncReport::UpToDate => up_to_date(),
SyncReport::Refreshed(f) => render_refresh(f.pull_failed, f.no_uid, f.written, f.wiped),
}
}
fn up_to_date() -> String {
"contacts already up to date".to_owned()
}
fn render_refresh(pull_failed: usize, no_uid: usize, written: usize, wiped: bool) -> String {
let mut out = format!("synced {written} {}", plural(written, "contact"));
let mut notes: Vec<String> = Vec::new();
if wiped {
notes.push("cache rebuilt".to_owned());
}
if pull_failed > 0 {
notes.push(format!("{pull_failed} failed to fetch"));
}
if no_uid > 0 {
notes.push(format!("{no_uid} without a UID"));
}
if !notes.is_empty() {
let _ = write!(out, " ({})", notes.join(", "));
}
out
}
fn plural(n: usize, word: &str) -> String {
if n == 1 {
word.to_owned()
} else {
format!("{word}s")
}
}
pub(super) fn offset_of(limit: Option<u16>, page: Option<u16>) -> u16 {
let Some(n) = limit else { return 0 };
if n == 0 {
return 0;
}
let p = page.unwrap_or(1).max(1);
p.saturating_sub(1).saturating_mul(n)
}
#[cfg(test)]
mod tests;