Visitenkarten-Scanner: Stapelscan, Extraktion, Übersicht, vCard-Export
Selbst gehostete PWA, die Visitenkarten von einem Foto freistellt, ausliest und als Kontakt bereitstellt. - Stapelscan: OpenCV findet die Kartenrechtecke über mehrere Binärmasken, entzerrt sie perspektivisch und schneidet sie einzeln aus. Ohne Fund gilt das ganze Foto als eine Karte. - Extraktion: ein Aufruf je Zuschnitt an das Vision-Modell mit JSON-Schema. Kein vorgeschaltetes OCR - das würde Layout und Schriftgrößen wegwerfen, aus denen die Feldzuordnung entsteht. - Metadaten: Aufnahmezeit und GPS aus den EXIF-Daten des Fotos, Ortsname über Nominatim, Browserstandort nur als Rückfallebene. - Übersicht mit Volltextsuche und Filtern, Detailansicht mit Korrekturmaske. - Notizfeld je Karte, Erinnerungen per Mail inklusive Nachholen verpasster Termine nach einem Neustart. - vCard 3.0 einzeln und als Sammeldatei, Karte gilt danach als exportiert. - Anmeldung über ein Passwort, Sitzung als signiertes Cookie. - Deployment per Tag-Push nach den Konventionen in DEPLOY.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
136
static/index.html
Normal file
136
static/index.html
Normal file
@@ -0,0 +1,136 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Karten">
|
||||
<title>Visitenkarten Scanner</title>
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
|
||||
<link rel="icon" href="/static/icons/icon-192.png">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="app">
|
||||
<header class="topbar">
|
||||
<span class="mark" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2">
|
||||
<rect x="2" y="5" width="20" height="14"></rect>
|
||||
<path d="M6 10h5M6 14h8"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="wordmark">
|
||||
<span class="display d-md">Visitenkarten</span>
|
||||
<span class="label">Scanner</span>
|
||||
</span>
|
||||
<span class="spacer"></span>
|
||||
<button class="chip" id="logout-button" hidden>Abmelden</button>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- ------------------------------------------------------------- Scan -->
|
||||
<section class="screen stack" id="screen-scan">
|
||||
<div class="block-invert">
|
||||
<p class="label">Stapel oder Einzelkarte</p>
|
||||
<h1 class="display d-xl" style="margin-top:8px">Karten<br>erfassen</h1>
|
||||
<p class="muted" style="margin:12px 0 0">
|
||||
Karten nebeneinander auf einen dunklen, einfarbigen Untergrund legen,
|
||||
Handy parallel darüber halten. Ein Foto genügt für den ganzen Tisch.
|
||||
</p>
|
||||
<div class="figures">
|
||||
<span class="figure">
|
||||
<span class="label">Erfasst</span>
|
||||
<span class="display d-lg mono-num" id="figure-total">0</span>
|
||||
</span>
|
||||
<span class="figure">
|
||||
<span class="label">Nicht exportiert</span>
|
||||
<span class="display d-lg mono-num" id="figure-open">0</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="btn btn-primary" for="input-camera">
|
||||
Foto aufnehmen <span class="arrow">→</span>
|
||||
<input id="input-camera" type="file" accept="image/*" capture="environment" hidden>
|
||||
</label>
|
||||
<label class="btn" for="input-library">
|
||||
Aus Fotos wählen <span class="arrow">↗</span>
|
||||
<input id="input-library" type="file" accept="image/*" hidden>
|
||||
</label>
|
||||
|
||||
<p class="hint">
|
||||
Die Kamera liefert die volle Auflösung – bei zwanzig Karten auf einem Foto
|
||||
ist das der Unterschied zwischen lesbar und nicht lesbar.
|
||||
</p>
|
||||
|
||||
<div id="scan-result"></div>
|
||||
</section>
|
||||
|
||||
<!-- ------------------------------------------------------------ Karten -->
|
||||
<section class="screen stack" id="screen-cards" hidden>
|
||||
<div class="search">
|
||||
<input id="search-input" type="search" placeholder="Name, Firma, Ort, Notiz …"
|
||||
autocomplete="off" enterkeyhint="search">
|
||||
<button id="search-clear" type="button" aria-label="Suche leeren">×</button>
|
||||
</div>
|
||||
<div class="chips" id="filter-chips">
|
||||
<button class="chip" data-filter="alle" aria-pressed="true">Alle</button>
|
||||
<button class="chip" data-filter="offen" aria-pressed="false">Nicht exportiert</button>
|
||||
<button class="chip" data-filter="unvollstaendig" aria-pressed="false">Unvollständig</button>
|
||||
<button class="chip" data-filter="erinnerung" aria-pressed="false">Mit Erinnerung</button>
|
||||
</div>
|
||||
<div id="cards-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- ------------------------------------------------------------ Detail -->
|
||||
<section class="screen stack" id="screen-detail" hidden></section>
|
||||
|
||||
<!-- ------------------------------------------------------ Erinnerungen -->
|
||||
<section class="screen stack" id="screen-reminders" hidden>
|
||||
<div class="section-label"><span class="label">Erinnerungen</span></div>
|
||||
<div id="reminders-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<nav class="tabs" role="tablist">
|
||||
<button class="tab" role="tab" data-route="scan" aria-selected="true">Scan</button>
|
||||
<button class="tab" role="tab" data-route="karten" aria-selected="false">
|
||||
<span class="count mono-num" id="tab-count-cards">0</span>Karten
|
||||
</button>
|
||||
<button class="tab" role="tab" data-route="erinnerungen" aria-selected="false">
|
||||
<span class="count mono-num" id="tab-count-reminders">0</span>Termine
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- ------------------------------------------------------------------ Login -->
|
||||
<div class="overlay" id="login-overlay" hidden>
|
||||
<form class="block stack" id="login-form">
|
||||
<p class="label muted">Zugang</p>
|
||||
<h2 class="display d-lg">Anmelden</h2>
|
||||
<label class="field">
|
||||
<span class="label">Passwort</span>
|
||||
<input type="password" id="login-password" autocomplete="current-password" required>
|
||||
</label>
|
||||
<button class="btn btn-primary" type="submit">Weiter <span class="arrow">→</span></button>
|
||||
<p class="hint" id="login-error" hidden></p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- -------------------------------------------------------------- Fortschritt -->
|
||||
<div class="overlay" id="busy-overlay" hidden>
|
||||
<div class="block-invert stack" style="text-align:center">
|
||||
<div class="spinner" style="margin:0 auto"></div>
|
||||
<p class="label" id="busy-text">Karten werden gelesen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast" hidden></div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user