Files
Lucas Orth 1e8918e0cd
All checks were successful
Build & Deploy / deploy (push) Successful in 1m29s
Bildeditor, klickbare Felder, Einstellungen; Kopfzeile und Namensfeld raus
Bildbearbeitung im Nachgang: Drehen in 90-Grad-Schritten, Ausrichten per
Schieberegler, Zuschneiden per Rahmen mit anfassbaren Ecken. Der Server
rechnet jede Korrektur vom unbearbeiteten Original, das beim ersten
Eingriff daneben abgelegt wird - sonst summieren sich die Verluste über
mehrere Korrekturen, und Zurücksetzen wäre nicht möglich. Zwei
Zuschnitte hintereinander werden ineinander verrechnet.

Der Ort ließ sich nicht übernehmen, weil beide Quellen versagen können:
iOS entfernt beim Weitergeben an eine Webseite oft die GPS-Daten aus dem
Foto, und der Browserstandort scheiterte bisher stumm. Jetzt meldet er
seinen Grund ("Standortfreigabe fehlt"), die Detailansicht zeigt die
Herkunft des Ortes, und der Scan-Ort ist von Hand nachtragbar.

E-Mail, Web, Telefon und Mobil sind in der Tabelle jetzt Links; Straße,
PLZ, Ort und Land stehen als eine Adresszeile, die die eingestellte
Navigations-App öffnet.

Einstellungen als vierter Reiter: Navigations-App (Apple Karten, Google
Maps, OpenStreetMap), Statusübersicht und Abmelden - der Knopf saß
vorher in der Kopfzeile.

Die Kopfzeile ist weg. Sie kostete auf dem Telefon eine Bildschirmzeile
und wiederholte nur, was die Tableiste schon sagt.

Kein full_name mehr, nur Vor- und Nachname; akademische Titel gehören in
den Vornamen. Bestehende Datensätze werden bei der Migration am letzten
Leerzeichen aufgeteilt. Die Spalte bleibt ungenutzt stehen: SQLite baut
zum Löschen die ganze Tabelle neu, der Gewinn wären ein paar Byte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 15:19:10 +02:00

85 lines
2.8 KiB
Python

"""vCard 3.0 erzeugen - das Format, das iOS beim Antippen zuverlaessig oeffnet."""
from datetime import datetime
_ESCAPES = (("\\", "\\\\"), (";", "\\;"), (",", "\\,"), ("\n", "\\n"))
def _escape(value) -> str:
text = str(value)
for char, replacement in _ESCAPES:
text = text.replace(char, replacement)
return text
def _line(name: str, value) -> list:
return [name + ":" + _escape(value)] if value else []
def _display_name(card: dict) -> str:
joined = " ".join(filter(None, (card.get("first_name"), card.get("last_name"))))
return joined or card.get("company") or "Unbenannt"
def _note(card: dict) -> str:
parts = []
if card.get("notes"):
parts.append(card["notes"])
stamp = card.get("captured_at") or card.get("created_at")
scanned = ""
if stamp:
try:
when = datetime.fromisoformat(stamp).strftime("%d.%m.%Y um %H:%M")
scanned = "Gescannt am " + when
except ValueError:
scanned = "Gescannt am " + str(stamp)
if card.get("place"):
scanned = (scanned + " in " if scanned else "Gescannt in ") + card["place"]
if scanned:
parts.append(scanned)
return "\n".join(parts)
def build(card: dict) -> str:
lines = ["BEGIN:VCARD", "VERSION:3.0"]
lines.append(
"N:{};{};;;".format(
_escape(card.get("last_name") or ""), _escape(card.get("first_name") or "")
)
)
lines.append("FN:" + _escape(_display_name(card)))
lines += _line("ORG", card.get("company"))
lines += _line("TITLE", card.get("job_title"))
if card.get("email"):
lines.append("EMAIL;TYPE=INTERNET,WORK:" + _escape(card["email"]))
if card.get("phone"):
lines.append("TEL;TYPE=WORK,VOICE:" + _escape(card["phone"]))
if card.get("mobile"):
lines.append("TEL;TYPE=CELL:" + _escape(card["mobile"]))
if card.get("fax"):
lines.append("TEL;TYPE=WORK,FAX:" + _escape(card["fax"]))
if card.get("website"):
lines.append("URL:" + _escape(card["website"]))
if any(card.get(k) for k in ("street", "postal_code", "city", "country")):
lines.append(
"ADR;TYPE=WORK:;;{};{};;{};{}".format(
_escape(card.get("street") or ""),
_escape(card.get("city") or ""),
_escape(card.get("postal_code") or ""),
_escape(card.get("country") or ""),
)
)
if card.get("lat") is not None and card.get("lon") is not None:
lines.append("GEO:{};{}".format(card["lat"], card["lon"]))
note = _note(card)
if note:
lines.append("NOTE:" + _escape(note))
lines.append("END:VCARD")
return "\r\n".join(lines) + "\r\n"
def filename(card: dict) -> str:
base = _display_name(card).replace(" ", "-")
safe = "".join(c for c in base if c.isalnum() or c in "-_")
return (safe or "kontakt") + ".vcf"