📷
Willkommen bei deiner Kamera
Diese App verwandelt deine Fotos mithilfe von KI — wähle einen Stil, und dein Bild wird neu erschaffen.
- Kamerazugriff — um Fotos aufzunehmen
- Galerie-Zugriff — um Ergebnisse zu speichern
iOS fragt dich gleich nach beiden Berechtigungen — einfach „Erlauben" tippen.
Verbindung unterbrochen.
';
}
}
}
let lastServerData = null;
function _ensureQueueList() {
const content = document.getElementById('queue-content');
if (!_qList || !_qList.isConnected) {
content.innerHTML = '';
_qList = document.createElement('div'); _qList.className = 'queue-list';
content.appendChild(_qList); _qMap.clear();
}
}
function renderQueue(d) {
lastServerData = d;
const active = (d.active || []).map(function(j){ return Object.assign({}, j, {_badge:'active'}); });
const pending = (d.pending || []).map(function(j){ return Object.assign({}, j, {_badge:'pending'}); });
const doneAll = [...(d.done || [])].reverse().map(function(j){ return Object.assign({}, j, {_badge:'done'}); });
const failAll = [...(d.failed || [])].reverse().map(function(j){ return Object.assign({}, j, {_badge:'failed'}); });
const ap = [...active, ...pending];
const doneAndFailed = [...doneAll, ...failAll];
if (!ap.length && !doneAndFailed.length) {
document.getElementById('queue-content').innerHTML = 'Keine Jobs — Motor wartet.
';
_qMap.clear(); _qList = null; return;
}
buildLibrary(doneAndFailed);
_ensureQueueList();
const currentIds = new Set([...ap, ...doneAndFailed.slice(0, _qDonePage*7)].map(function(j){ return j.id; }));
_qMap.forEach(function(el, id) { if (!currentIds.has(id)) { el.remove(); _qMap.delete(id); } });
ap.forEach(function(job, i) {
let card = _qMap.get(job.id);
if (!card) { card = createJobCard(job); _qMap.set(job.id, card); }
else if (card.classList.contains('job-done')) { const nc = createJobCard(job); _qList.replaceChild(nc, card); _qMap.set(job.id, nc); card = nc; }
const atPos = _qList.children[i];
if (atPos !== card) _qList.insertBefore(card, atPos || null);
});
const visibleDone = doneAndFailed.slice(0, _qDonePage*7);
const apCount = ap.length;
visibleDone.forEach(function(job, i) {
let card = _qMap.get(job.id);
if (!card) { card = createJobCard(job); _qMap.set(job.id, card); _qList.appendChild(card); }
else if (!card.classList.contains('job-done')) { const nc = createJobCard(job); _qList.replaceChild(nc, card); _qMap.set(job.id, nc); card = nc; }
const targetIdx = apCount + i;
const atPos = _qList.children[targetIdx];
if (atPos !== card) _qList.insertBefore(card, atPos || null);
});
_qList.querySelectorAll('.load-more-row').forEach(function(el){ el.remove(); });
const total = doneAndFailed.length;
if (total > _qDonePage*7) {
const row = document.createElement('div'); row.className = 'load-more-row';
const btn = document.createElement('button'); btn.className = 'load-more-btn';
const remaining = total - _qDonePage*7;
btn.textContent = '+ ' + Math.min(7, remaining) + ' ältere';
btn.addEventListener('click', function(){ _qDonePage++; renderQueue(lastServerData); });
row.appendChild(btn); _qList.appendChild(row);
}
}
/* ── Variation modal ── */
let variationContext = null;
function openVariation(job, idx) { const seed = job.seeds?.[idx], dl = job.downloaded?.[idx]; if (seed == null || !dl?.outPath) return; variationContext = {job, idx, seed}; document.getElementById('variation-seed').replaceChildren(String(seed)); document.getElementById('variation-preview').src = thumbUrl(dl) || ''; let prompt = job.prompt || ''; if (!prompt && job.prompt_file) prompt = '[Filter: ' + job.prompt_file.split('/').pop().split(' — ')[0] + ']\n\n'; document.getElementById('variation-prompt').value = prompt; document.getElementById('variation-modal').classList.add('visible'); setTimeout(function(){ document.getElementById('variation-prompt')?.focus(); }, 50); }
function closeVariation() { document.getElementById('variation-modal').classList.remove('visible'); variationContext = null; }
async function submitVariation() {
if (!variationContext) return;
const {job, seed} = variationContext;
const newPrompt = document.getElementById('variation-prompt').value.trim();
if (!newPrompt) { alert('Bitte Prompt eingeben.'); return; }
const body = {
ratio: job.ratio || 'auto', num_images: 1,
resolution: job.resolution || '4k', thinking_level: job.thinking_level || 'high',
mode: 'imagen-nano-banana-2-flash',
stem: (job.stem || 'Variation') + ' - Seed ' + seed,
prompt: newPrompt, seeds: [seed]
};
if (job.image) body.image = job.image;
try {
const r = await fetch(fApiUrl('/api/job'), {method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(body)});
const d = await r.json();
if (d.ok) { closeVariation(); loadStatus(); } else alert('Fehler: ' + (d.error || 'unbekannt'));
} catch(e) { alert('Fehler: ' + e.message); }
}
/* ── Live ticking of active cards ── */
function tickActiveBars() {
if (!lastServerData) return;
function tick(j) {
const card = _qMap.get(j.id); if (!card) return;
const prog = progressForJob(j);
const bar = card.querySelector('[data-bar-for="' + j.id + '"]');
if (bar) { bar.style.width = prog.pct + '%'; bar.className = 'progress-bar ' + prog.phase; }
const lamp = card.querySelector('[data-lamp-for="' + j.id + '"]');
if (lamp) { if (prog.phase === 'phase1') lamp.className = 'job-lamp phase1'; else if (prog.phase === 'active') lamp.className = 'job-lamp phase2'; }
const te = card.querySelector('[data-title-for="' + j.id + '"]');
if (te && j.original_name && j.original_name.title && !te.classList.contains('visible')) { te.textContent = j.original_name.title; te.classList.add('visible'); }
}
(lastServerData.active || []).forEach(tick);
(lastServerData.pending || []).forEach(tick);
}
let pollTimer = null;
function schedulePoll() {
clearTimeout(pollTimer);
const n = (lastServerData?.active?.length || 0) + (lastServerData?.pending?.length || 0);
pollTimer = setTimeout(async function(){ await loadStatus(); schedulePoll(); }, n > 0 ? 1500 : 5000);
}
Fullscreen.init();
/* ── Eigene Kamera (getUserMedia, 16:9, EXIF-frei dank Canvas-Capture) ──
* Liefert ein File-Objekt zurück, das im selben Pfad wie der iOS-Picker landet.
* Kein 90°-Bug, weil Canvas direkt vom Video-Frame zieht (keine EXIF involviert).
*/
const Cam = (function() {
let overlay, video, viewfinder, flashEl, shutterBtn, statusEl;
let stream = null, facing = 'environment', resolver = null;
function init() {
overlay = document.getElementById('cc-overlay');
video = document.getElementById('cc-video');
viewfinder = document.getElementById('cc-viewfinder');
flashEl = document.getElementById('cc-flash');
shutterBtn = document.getElementById('cc-shutter');
statusEl = document.getElementById('cc-status');
document.getElementById('cc-close-btn').addEventListener('click', function(){ Cam.close(null); });
document.getElementById('cc-flip-btn').addEventListener('click', function(){ Cam.flip(); });
shutterBtn.addEventListener('click', function(){ Cam.capture(); });
}
async function startStream() {
stopStream();
statusEl.firstChild ? (statusEl.firstChild.nodeValue = 'Kamera startet…') : statusEl.appendChild(document.createTextNode('Kamera startet…'));
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: facing, width: { ideal: 3840 }, height: { ideal: 2160 } },
audio: false
});
video.srcObject = stream;
video.classList.toggle('mirror', facing === 'user');
statusEl.firstChild.nodeValue = 'Sucher aktiv';
} catch (e) {
statusEl.firstChild.nodeValue = 'Fehler: ' + (e.name || 'unbekannt');
}
}
function stopStream() {
if (stream) { stream.getTracks().forEach(function(t){ t.stop(); }); stream = null; }
video.srcObject = null;
}
async function open() {
return new Promise(function(resolve) {
resolver = resolve;
overlay.classList.add('visible');
startStream();
});
}
function close(result) {
stopStream();
overlay.classList.remove('visible');
const r = resolver; resolver = null;
if (r) r(result || null);
}
function flip() {
facing = (facing === 'environment') ? 'user' : 'environment';
startStream();
}
function capture() {
if (!stream || shutterBtn.classList.contains('busy')) return;
shutterBtn.classList.add('busy');
flashEl.classList.add('active');
setTimeout(function(){ flashEl.classList.remove('active'); }, 160);
const vw = video.videoWidth, vh = video.videoHeight;
const target = 16/9;
let cw, ch, ox, oy;
if (vw / vh > target) { ch = vh; cw = Math.round(vh * target); ox = Math.round((vw - cw)/2); oy = 0; }
else { cw = vw; ch = Math.round(vw / target); ox = 0; oy = Math.round((vh - ch)/2); }
const canvas = document.createElement('canvas');
canvas.width = cw; canvas.height = ch;
const ctx = canvas.getContext('2d');
if (facing === 'user') { ctx.translate(cw, 0); ctx.scale(-1, 1); }
ctx.drawImage(video, ox, oy, cw, ch, 0, 0, cw, ch);
canvas.toBlob(function(blob) {
shutterBtn.classList.remove('busy');
if (!blob) { close(null); return; }
const file = new File([blob], 'aufnahme_' + Date.now() + '.jpg', { type: 'image/jpeg' });
close(file);
}, 'image/jpeg', 0.92);
}
return { init: init, open: open, close: close, flip: flip, capture: capture };
})();
Cam.init();
async function openOwnCamera() {
const file = await Cam.open();
if (!file) return;
/* Same path as iOS picker: set pendingFile + preview */
pendingFile = file;
const url = URL.createObjectURL(file);
document.getElementById('preview-img').src = url;
document.getElementById('preview-frame').classList.add('visible');
updateDevelopBtn();
scheduleSettingsSave();
}
restoreQueueFromCache();
loadFilters(); loadSettings(); loadStatus().then(schedulePoll);
/* ── Update-Badge ── */
(function initUpdateBadge() {
var knownVersion = null;
function checkVersion() {
fetch('/api/page_version?page=friends_thecamera.html&_=' + Date.now())
.then(function(r){ return r.json(); })
.then(function(d) {
if (!d.version) return;
if (knownVersion === null) { knownVersion = d.version; return; }
if (d.version !== knownVersion) {
document.getElementById('update-badge').style.display = 'block';
}
}).catch(function(){});
}
checkVersion();
setInterval(checkVersion, 20000);
})();
/* Kein SSE vom Friends-Server — Polling reicht */
setInterval(tickActiveBars, 500);
setInterval(function(){ loadFilters(true); }, 20000);
/* ── App-Titel + Wordmark via FRIENDS_USER setzen ── */
(function applyFriendsIdentity() {
const appName = FRIENDS_USER.app_name || 'Meine Kamera';
document.title = appName;
const wm = document.getElementById('app-wordmark');
if (wm) wm.textContent = appName;
/* apple-mobile-web-app-title */
const metaTitle = document.querySelector('meta[name="apple-mobile-web-app-title"]');
if (metaTitle) metaTitle.setAttribute('content', appName);
})();
/* ── Limits laden und anzeigen ── */
async function loadLimits() {
try {
const r = await fetch(fApiUrl('/api/limits'));
const d = await r.json();
if (d.ok) {
const el = document.getElementById('limit-info');
if (el) el.textContent = d.daily_used + ' / ' + d.daily_limit + ' heute';
}
} catch(e) {}
}
loadLimits();