ui: echte Mobile-First-Variante — Recording-Hero, Job-Cards, Admin-Klappe
Mobile-Layout ab <700px komplett umgebaut, nicht mehr nur Desktop-Verkleinerung: - Header: nur Logo + User + Profil-Mini-Dropdown + Logout - Upload-Card: großer roter Aufnahme-Button als Hero-Element - Profil und Titel darunter, kompakt - Datei-Upload in <details>-Klappe (auf Desktop offen, Mobile zu) - Jobs: Card-Liste statt Tabelle (Tabelle nur Desktop, Cards nur Mobile) - Admin: H2 als Toggle, Body kollabiert (auf Desktop immer offen) - Inputs auf Mobile mit font-size:16px → kein iOS-Zoom-on-Focus - Recording-Pane: Timer 64px, Buttons full-width - safe-area-inset für iPhone-Notch + Bottom-Bar Ohne JS-Mode-Switch — alles via CSS Media Query + .desktop-only / .mobile-only Helfer-Klassen. JS rendert beide Job-Repräsentationen, CSS schaltet die richtige sichtbar.
This commit is contained in:
parent
e75bc05421
commit
24ac3ac058
@ -37,6 +37,10 @@ const defaultProfileName = document.getElementById("default-profile-name");
|
||||
const headerProfileWrap = document.getElementById("header-profile-wrap");
|
||||
const headerProfile = document.getElementById("header-profile");
|
||||
|
||||
const jobsCards = document.getElementById("jobs-cards");
|
||||
const adminToggle = document.getElementById("admin-toggle");
|
||||
const adminCollapsible = document.getElementById("admin-card");
|
||||
|
||||
const recordToggle = document.getElementById("record-toggle");
|
||||
const recordToggleLabel = document.getElementById("record-toggle-label");
|
||||
const recordPane = document.getElementById("record-pane");
|
||||
@ -302,6 +306,43 @@ function row(job) {
|
||||
return tr;
|
||||
}
|
||||
|
||||
function card(job) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "jc";
|
||||
const title = job.title || job.original_name;
|
||||
const showProfileTag = availableProfiles.length > 1 || (job.profile && job.profile !== "meeting");
|
||||
const profileTag = showProfileTag
|
||||
? `<span class="tag-profile">${escapeHtml(profileLabelOf(job.profile || "meeting"))}</span>`
|
||||
: "";
|
||||
const ownerLine = currentUser?.is_admin && job.owner_username
|
||||
? `<div class="jc-sub">von ${escapeHtml(job.owner_username)}</div>` : "";
|
||||
|
||||
const dlButtons = DOWNLOADS.map(({ kind, label, title: tip }) => {
|
||||
const dis = !job.has[kind];
|
||||
const href = dis ? "" : `href="/api/jobs/${job.id}/download/${kind}"`;
|
||||
return `<a class="btn-dl ${dis ? "disabled" : ""}" ${href} title="${escapeHtml(tip)}">${label}</a>`;
|
||||
}).join("");
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="jc-head">
|
||||
<div>
|
||||
<div class="jc-title">${escapeHtml(title)} ${profileTag}</div>
|
||||
<div class="jc-sub">${escapeHtml(job.original_name)} · <span class="jc-id">#${job.id}</span></div>
|
||||
${ownerLine}
|
||||
</div>
|
||||
<span class="status-pill status-${job.status}">${escapeHtml(job.status)}</span>
|
||||
</div>
|
||||
<div class="jc-status-row">
|
||||
<div class="jc-progress"><div class="bar"><div class="bar-fill" style="width:${job.progress}%"></div></div></div>
|
||||
<small class="muted">${job.progress}%</small>
|
||||
</div>
|
||||
${job.error ? `<div class="jc-error" title="${escapeHtml(job.error)}">${escapeHtml(job.error)}</div>` : ""}
|
||||
<div class="jc-sub">aktualisiert: ${escapeHtml(fmtDate(job.updated_at))}</div>
|
||||
<div class="jc-dl">${dlButtons}</div>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
async function loadJobs() {
|
||||
if (!currentUser) return;
|
||||
try {
|
||||
@ -309,11 +350,16 @@ async function loadJobs() {
|
||||
if (r.status === 401) { showAuth("login"); return; }
|
||||
const jobs = await r.json();
|
||||
jobsBody.innerHTML = "";
|
||||
jobsCards.innerHTML = "";
|
||||
if (!jobs.length) {
|
||||
jobsBody.innerHTML = '<tr><td colspan="6" class="muted">Noch keine Jobs.</td></tr>';
|
||||
jobsCards.innerHTML = '<div class="muted">Noch keine Jobs.</div>';
|
||||
return;
|
||||
}
|
||||
jobs.forEach((j) => jobsBody.appendChild(row(j)));
|
||||
jobs.forEach((j) => {
|
||||
jobsBody.appendChild(row(j));
|
||||
jobsCards.appendChild(card(j));
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@ -613,6 +659,13 @@ recPauseBtn.addEventListener("click", pauseRecording);
|
||||
recStopBtn.addEventListener("click", stopRecording);
|
||||
recordDiscardBtn.addEventListener("click", discardRecording);
|
||||
|
||||
// Admin-Toggle (klappbar auf Mobile, immer offen auf Desktop)
|
||||
adminToggle.addEventListener("click", () => {
|
||||
if (window.matchMedia("(max-width: 700px)").matches) {
|
||||
adminCollapsible.classList.toggle("open");
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Init ──────────────────────────────────────────────────────────────────
|
||||
bootstrap();
|
||||
checkMac();
|
||||
|
||||
@ -19,17 +19,17 @@
|
||||
<div class="header-row">
|
||||
<div class="header-left">
|
||||
<h1>Voice-Agent</h1>
|
||||
<p class="sub">Sitzungsaufnahmen automatisch transkribieren und protokollieren</p>
|
||||
<p class="sub desktop-only">Sitzungsaufnahmen automatisch transkribieren und protokollieren</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div id="mac-status" class="badge">Mac-Backend: prüfe …</div>
|
||||
<div id="mac-status" class="badge desktop-only">Mac-Backend: prüfe …</div>
|
||||
<div id="user-bar" class="user-bar hidden">
|
||||
<span class="muted">Angemeldet als</span>
|
||||
<span class="muted desktop-only">Angemeldet als</span>
|
||||
<strong id="user-name"></strong>
|
||||
<span id="user-admin-badge" class="badge admin-badge hidden">Admin</span>
|
||||
<span id="header-profile-wrap" class="header-profile-wrap hidden">
|
||||
<span class="muted">·</span>
|
||||
<span class="muted">Profil:</span>
|
||||
<span class="muted desktop-only">·</span>
|
||||
<span class="muted desktop-only">Profil:</span>
|
||||
<select id="header-profile" class="header-profile" title="Standard-Profil — wird sofort gespeichert"></select>
|
||||
</span>
|
||||
<button type="button" id="logout-btn" class="btn-link">Abmelden</button>
|
||||
@ -59,30 +59,16 @@
|
||||
</div>
|
||||
|
||||
<main id="app" class="hidden">
|
||||
<section class="card">
|
||||
<h2>Neue Aufnahme hochladen</h2>
|
||||
<section class="card upload-card">
|
||||
<h2 class="desktop-only">Neue Aufnahme hochladen</h2>
|
||||
<form id="upload-form">
|
||||
<label>
|
||||
Titel (optional)
|
||||
<input type="text" name="title" id="title" placeholder="z. B. Monatsbesprechung" />
|
||||
</label>
|
||||
<label id="profile-label" class="hidden">
|
||||
Verarbeitungs-Profil
|
||||
<select id="profile-select"></select>
|
||||
<small class="muted">Standard: <span id="default-profile-name">—</span> (in Einstellungen änderbar)</small>
|
||||
</label>
|
||||
<div class="audio-source">
|
||||
<label class="audio-file-label">
|
||||
<span>Audiodatei wählen <small class="muted">(MP3, WAV, M4A, MP4, OGG, FLAC, WebM)</small></span>
|
||||
<input type="file" name="audio" id="audio" accept="audio/*,.mp3,.wav,.m4a,.mp4,.ogg,.flac,.webm" />
|
||||
</label>
|
||||
<div class="audio-or muted">— oder —</div>
|
||||
<!-- HERO: Aufnahme-Button (auf Mobile groß und zentriert) -->
|
||||
<button type="button" id="record-toggle" class="btn-record">
|
||||
<span class="rec-dot"></span>
|
||||
<span id="record-toggle-label">Aufnahme starten</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Aufnahme-Bühne -->
|
||||
<div id="record-pane" class="record-pane hidden">
|
||||
<div class="record-timer" id="record-timer">00:00</div>
|
||||
<div class="record-status" id="record-status">Aufnahme läuft</div>
|
||||
@ -92,12 +78,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aufnahme bereit -->
|
||||
<div id="record-result" class="record-result hidden">
|
||||
<span class="muted">Aufnahme bereit:</span>
|
||||
<span id="record-filename"></span>
|
||||
<button type="button" id="record-discard" class="btn-link btn-danger">Verwerfen</button>
|
||||
</div>
|
||||
|
||||
<!-- Profil + Titel -->
|
||||
<div class="upload-meta">
|
||||
<label id="profile-label" class="hidden">
|
||||
Verarbeitungs-Profil
|
||||
<select id="profile-select"></select>
|
||||
<small class="muted desktop-only">Standard: <span id="default-profile-name">—</span></small>
|
||||
</label>
|
||||
<label>
|
||||
Titel <span class="muted">(optional)</span>
|
||||
<input type="text" name="title" id="title" placeholder="z. B. Monatsbesprechung" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Datei-Upload als Klappe (auf Mobile collapsed, auf Desktop offen) -->
|
||||
<details class="audio-file-details" id="audio-file-details">
|
||||
<summary>Lieber eine vorhandene Audio-Datei hochladen?</summary>
|
||||
<label class="audio-file-label">
|
||||
<span class="muted">MP3, WAV, M4A, MP4, OGG, FLAC, WebM</span>
|
||||
<input type="file" name="audio" id="audio" accept="audio/*,.mp3,.wav,.m4a,.mp4,.ogg,.flac,.webm" />
|
||||
</label>
|
||||
</details>
|
||||
|
||||
<button type="submit" id="submit-btn">Hochladen & verarbeiten</button>
|
||||
</form>
|
||||
<div id="upload-progress" class="hidden">
|
||||
@ -107,9 +116,10 @@
|
||||
<p id="upload-msg" class="msg"></p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<section class="card jobs-card">
|
||||
<h2>Jobs</h2>
|
||||
<table id="jobs-table">
|
||||
<!-- Desktop: Tabelle -->
|
||||
<table id="jobs-table" class="desktop-only">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-id">#</th>
|
||||
@ -124,11 +134,16 @@
|
||||
<tr><td colspan="6" class="muted">Noch keine Jobs.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Mobile: Card-Liste -->
|
||||
<div id="jobs-cards" class="jobs-cards mobile-only">
|
||||
<div class="muted">Noch keine Jobs.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Nur sichtbar für Admins -->
|
||||
<section id="admin-card" class="card hidden">
|
||||
<h2>Benutzer verwalten</h2>
|
||||
<section id="admin-card" class="card hidden admin-collapsible">
|
||||
<h2 class="admin-toggle" id="admin-toggle">Benutzer verwalten <span class="admin-toggle-icon">▾</span></h2>
|
||||
<div class="admin-body">
|
||||
<form id="user-form" class="user-form">
|
||||
<label>
|
||||
Benutzername
|
||||
@ -157,6 +172,7 @@
|
||||
</thead>
|
||||
<tbody id="users-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
@ -281,24 +281,6 @@ select {
|
||||
}
|
||||
.header-profile:disabled { opacity: .5; }
|
||||
|
||||
/* ── Audio-Quelle: Datei-Upload vs. Aufnahme ───────────────────── */
|
||||
.audio-source {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.audio-file-label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.audio-or {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Aufnahme-Button (groß, rot, touch-tauglich) */
|
||||
.btn-record {
|
||||
display: inline-flex;
|
||||
@ -378,36 +360,174 @@ select {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Mobile-Optimierung ────────────────────────────────────────── */
|
||||
/* ── Sichtbarkeit-Helfer ───────────────────────────────────────── */
|
||||
.mobile-only { display: none; }
|
||||
|
||||
/* Upload-Form Layout (Desktop) */
|
||||
.upload-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
.audio-file-details summary {
|
||||
cursor: pointer;
|
||||
padding: 8px 4px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
}
|
||||
.audio-file-details summary:hover { color: var(--text); }
|
||||
.audio-file-details[open] summary { color: var(--text); margin-bottom: 8px; }
|
||||
.audio-file-label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Admin-Toggle (auf Desktop deaktiviert) */
|
||||
.admin-toggle { cursor: default; }
|
||||
.admin-toggle-icon { display: none; }
|
||||
|
||||
/* ── Mobile-Optimierung (echte Mobile-First-UI) ────────────────── */
|
||||
@media (max-width: 700px) {
|
||||
header { padding: 16px 12px 12px; }
|
||||
header h1 { font-size: 22px; }
|
||||
.sub { font-size: 13px; }
|
||||
main { padding: 0 10px; gap: 14px; }
|
||||
.card { padding: 14px 16px; border-radius: 10px; }
|
||||
.card h2 { font-size: 16px; }
|
||||
.desktop-only { display: none !important; }
|
||||
.mobile-only { display: block; }
|
||||
|
||||
.header-row { flex-direction: column; align-items: stretch; gap: 10px; }
|
||||
.header-right { align-items: stretch; }
|
||||
.user-bar { flex-wrap: wrap; justify-content: flex-start; gap: 6px; font-size: 12px; }
|
||||
.header-profile { max-width: 140px; }
|
||||
body { font-size: 15px; }
|
||||
|
||||
.audio-source { grid-template-columns: 1fr; gap: 8px; }
|
||||
.audio-or { padding: 4px 0; }
|
||||
.btn-record { padding: 18px; font-size: 17px; }
|
||||
/* HEADER: nur Logo + User */
|
||||
header {
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
header h1 { font-size: 18px; margin: 0; }
|
||||
.header-row {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.header-left, .header-right { align-items: center; }
|
||||
.header-right { flex-direction: row; gap: 8px; flex: 1; justify-content: flex-end; }
|
||||
.user-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.user-bar strong { font-size: 14px; max-width: 90px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.header-profile { max-width: 130px; padding: 4px 6px; font-size: 12px; }
|
||||
.btn-link { font-size: 12px; padding: 4px 6px; }
|
||||
|
||||
/* Tabelle horizontal scrollbar statt zerquetscht */
|
||||
.card section, .card { overflow-x: auto; }
|
||||
table { min-width: 720px; font-size: 13px; }
|
||||
button[type="submit"] { padding: 14px 20px; font-size: 15px; min-height: 48px; }
|
||||
main { padding: 14px 12px 90px; gap: 14px; }
|
||||
.card { padding: 16px; border-radius: 12px; }
|
||||
.card h2 { font-size: 16px; margin-bottom: 10px; }
|
||||
|
||||
.record-timer { font-size: 56px; }
|
||||
.btn-record-ctrl { padding: 16px 24px; font-size: 17px; }
|
||||
/* UPLOAD-CARD: Recording-Hero-Style */
|
||||
.upload-card form { display: flex; flex-direction: column; gap: 14px; }
|
||||
|
||||
.btn-record {
|
||||
/* HERO: groß und auffällig */
|
||||
padding: 26px;
|
||||
font-size: 19px;
|
||||
min-height: 90px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(231,76,60,.15);
|
||||
}
|
||||
.btn-record .rec-dot { width: 16px; height: 16px; animation: pulse 1.5s ease-in-out infinite; }
|
||||
|
||||
.upload-meta { grid-template-columns: 1fr; gap: 10px; }
|
||||
.upload-meta select,
|
||||
.upload-meta input[type=text] {
|
||||
padding: 14px 12px;
|
||||
font-size: 16px; /* iOS: ≥16px verhindert Zoom-on-Focus */
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.audio-file-details summary { padding: 12px 4px; font-size: 14px; }
|
||||
|
||||
button[type="submit"] {
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
min-height: 52px;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Aufnahme-Bühne: noch größer auf Mobile */
|
||||
.record-pane { padding: 24px 16px; }
|
||||
.record-timer { font-size: 64px; line-height: 1; }
|
||||
.record-controls { width: 100%; gap: 12px; }
|
||||
.btn-record-ctrl {
|
||||
flex: 1;
|
||||
padding: 18px;
|
||||
font-size: 17px;
|
||||
min-height: 56px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* JOBS-CARDS statt Tabelle */
|
||||
.jobs-cards { display: flex; flex-direction: column; gap: 10px; }
|
||||
.jc {
|
||||
background: #0f1219;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.jc-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.jc-id { color: var(--muted); font-size: 12px; }
|
||||
.jc-title { font-weight: 600; font-size: 15px; line-height: 1.3; word-break: break-word; }
|
||||
.jc-sub { color: var(--muted); font-size: 12px; margin-top: 2px; }
|
||||
.jc-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 8px 0 6px;
|
||||
}
|
||||
.jc-progress { flex: 1; }
|
||||
.jc-error {
|
||||
color: var(--err);
|
||||
font-size: 12px;
|
||||
margin: 4px 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
.jc-dl {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.jc-dl .btn-dl { padding: 8px; font-size: 12px; min-height: 36px; }
|
||||
|
||||
/* Admin-Bereich: collapsible */
|
||||
.admin-collapsible .admin-toggle {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
.admin-toggle-icon { display: inline; transition: transform .2s; }
|
||||
.admin-collapsible:not(.open) .admin-body { display: none; }
|
||||
.admin-collapsible.open .admin-toggle-icon { transform: rotate(180deg); }
|
||||
|
||||
.user-form { grid-template-columns: 1fr; }
|
||||
.user-form input { font-size: 16px; }
|
||||
}
|
||||
|
||||
/* iOS PWA: oberer Sicherheitsbereich (Notch) respektieren */
|
||||
@supports (padding: max(0px)) {
|
||||
header { padding-top: max(16px, env(safe-area-inset-top)); }
|
||||
header { padding-top: max(10px, env(safe-area-inset-top)); }
|
||||
main { padding-bottom: max(20px, env(safe-area-inset-bottom)); }
|
||||
}
|
||||
|
||||
/* Profile-Tag in der Job-Zeile */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user