316 lines
8.6 KiB
JavaScript
316 lines
8.6 KiB
JavaScript
// Journal UI (Figma skin) + Prometheus HTR extraction wiring
|
||
// - Camera button => opens system image chooser, posts to /api/extract
|
||
// - Clear => clears text + resets page counter
|
||
// - Save => saves to localStorage
|
||
// - Share => native share or clipboard
|
||
//
|
||
// Assumes your server accepts JSON: { image: "<base64>" } at POST /api/extract
|
||
// (matches your original working prototype).
|
||
|
||
// -------------------------
|
||
// Config
|
||
// -------------------------
|
||
const API_BASE = window.location.origin;
|
||
const MAX_IMAGE_DIMENSION = 1600;
|
||
|
||
// -------------------------
|
||
// DOM
|
||
// -------------------------
|
||
const noteTextarea = document.getElementById('noteTextarea');
|
||
const shareBtn = document.getElementById('shareBtn');
|
||
const menuBtn = document.getElementById('menuBtn');
|
||
const cameraBtn = document.getElementById('cameraBtn');
|
||
const clearBtn = document.getElementById('clearBtn');
|
||
const saveBtn = document.getElementById('saveBtn');
|
||
const toastContainer = document.getElementById('toastContainer');
|
||
const fileInput = document.getElementById('fileInput');
|
||
|
||
// -------------------------
|
||
// State
|
||
// -------------------------
|
||
let pageCount = 0;
|
||
let isProcessing = false;
|
||
|
||
// -------------------------
|
||
// Toasts
|
||
// -------------------------
|
||
function showToast(message, type = 'info') {
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast toast-${type}`;
|
||
toast.textContent = message;
|
||
|
||
toastContainer.appendChild(toast);
|
||
|
||
// Trigger animation
|
||
setTimeout(() => toast.classList.add('show'), 10);
|
||
|
||
// Remove toast after 3 seconds
|
||
setTimeout(() => {
|
||
toast.classList.remove('show');
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, 3000);
|
||
}
|
||
|
||
// -------------------------
|
||
// Helpers
|
||
// -------------------------
|
||
function setBusy(busy) {
|
||
isProcessing = busy;
|
||
cameraBtn.disabled = busy;
|
||
clearBtn.disabled = busy;
|
||
saveBtn.disabled = busy;
|
||
shareBtn.disabled = busy;
|
||
cameraBtn.style.opacity = busy ? '0.6' : '';
|
||
}
|
||
|
||
function appendExtractedText(newText) {
|
||
const text = (newText || '').trim();
|
||
if (!text) {
|
||
showToast('No text returned', 'info');
|
||
return;
|
||
}
|
||
|
||
pageCount += 1;
|
||
|
||
const current = noteTextarea.value.trim();
|
||
if (current) {
|
||
noteTextarea.value = current + `\n\n--- Page ${pageCount} ---\n\n` + text;
|
||
} else {
|
||
noteTextarea.value = text;
|
||
}
|
||
|
||
// Keep cursor at end
|
||
noteTextarea.scrollTop = noteTextarea.scrollHeight;
|
||
}
|
||
|
||
// Resize image -> base64 (no data: prefix)
|
||
function resizeImageToBase64(file) {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
|
||
reader.onload = (e) => {
|
||
const img = new Image();
|
||
|
||
img.onload = () => {
|
||
const canvas = document.createElement('canvas');
|
||
let { width, height } = img;
|
||
|
||
if (width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) {
|
||
if (width > height) {
|
||
height = Math.round((height * MAX_IMAGE_DIMENSION) / width);
|
||
width = MAX_IMAGE_DIMENSION;
|
||
} else {
|
||
width = Math.round((width * MAX_IMAGE_DIMENSION) / height);
|
||
height = MAX_IMAGE_DIMENSION;
|
||
}
|
||
}
|
||
|
||
canvas.width = width;
|
||
canvas.height = height;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0, width, height);
|
||
|
||
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
|
||
const base64 = dataUrl.split(',')[1];
|
||
resolve(base64);
|
||
};
|
||
|
||
img.onerror = () => reject(new Error('Failed to load image'));
|
||
img.src = e.target.result;
|
||
};
|
||
|
||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
// -------------------------
|
||
// Extraction pipeline
|
||
// -------------------------
|
||
async function extractFromImageFile(file) {
|
||
if (isProcessing) return;
|
||
|
||
setBusy(true);
|
||
showToast('Extracting…', 'info');
|
||
|
||
try {
|
||
const resizedBase64 = await resizeImageToBase64(file);
|
||
|
||
const response = await fetch(`${API_BASE}/api/extract`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ image: resizedBase64 })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
let msg = `HTTP ${response.status}`;
|
||
try {
|
||
const err = await response.json();
|
||
msg = err?.detail?.message || err?.detail || err?.message || msg;
|
||
} catch (_) {
|
||
// ignore json parse
|
||
try { msg = await response.text(); } catch (_) {}
|
||
}
|
||
throw new Error(msg || 'Extraction failed');
|
||
}
|
||
|
||
const result = await response.json();
|
||
appendExtractedText(result.text);
|
||
showToast(`Page extracted (${pageCount})`, 'success');
|
||
} catch (err) {
|
||
console.error(err);
|
||
showToast(err?.message ? `Extract failed: ${err.message}` : 'Extract failed', 'error');
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
// -------------------------
|
||
// Button wiring
|
||
// -------------------------
|
||
|
||
// Share
|
||
shareBtn.addEventListener('click', async () => {
|
||
const text = noteTextarea.value.trim();
|
||
|
||
if (!text) {
|
||
showToast('Nothing to share', 'error');
|
||
return;
|
||
}
|
||
|
||
if (navigator.share) {
|
||
try {
|
||
await navigator.share({ text });
|
||
// no toast needed; iOS share sheet is the feedback
|
||
} catch (err) {
|
||
// user cancelled is fine; only show for real failures
|
||
if (err?.name !== 'AbortError') showToast('Failed to share', 'error');
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
showToast('Copied to clipboard', 'success');
|
||
} catch (err) {
|
||
showToast('Clipboard blocked (long-press to copy)', 'error');
|
||
}
|
||
});
|
||
|
||
// Menu (Hamburger Menu - potential future addition skin-picker?)
|
||
menuBtn.addEventListener('click', () => {
|
||
window.location.href = 'menu.html';
|
||
});
|
||
|
||
// Extract from image
|
||
cameraBtn.addEventListener('click', () => {
|
||
if (isProcessing) return;
|
||
// Allow selecting same photo twice
|
||
fileInput.value = '';
|
||
fileInput.click();
|
||
});
|
||
|
||
fileInput.addEventListener('change', async (e) => {
|
||
const file = e.target.files?.[0];
|
||
if (!file) return;
|
||
// reset input so selecting the same file again works
|
||
e.target.value = '';
|
||
await extractFromImageFile(file);
|
||
});
|
||
|
||
// Clear
|
||
clearBtn.addEventListener('click', () => {
|
||
const text = noteTextarea.value.trim();
|
||
if (!text) {
|
||
// Nothing to clear; no need to warn.
|
||
return;
|
||
}
|
||
|
||
const ok = window.confirm(
|
||
'Clear all text?\n\nThis cannot be undone. Make sure you have saved or shared anything important first.'
|
||
);
|
||
|
||
if (!ok) {
|
||
return;
|
||
}
|
||
|
||
noteTextarea.value = '';
|
||
pageCount = 0;
|
||
localStorage.removeItem('savedNote');
|
||
localStorage.removeItem('savedNotePageCount');
|
||
showToast('Cleared', 'success');
|
||
});
|
||
|
||
// Save - localStorage
|
||
saveBtn.addEventListener('click', () => {
|
||
const text = noteTextarea.value.trim();
|
||
if (!text) {
|
||
showToast('Nothing to save', 'error');
|
||
return;
|
||
}
|
||
localStorage.setItem('savedNote', text);
|
||
localStorage.setItem('savedNotePageCount', String(pageCount));
|
||
showToast('Saved', 'success');
|
||
});
|
||
|
||
// Load saved note on page load
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
const savedNote = localStorage.getItem('savedNote');
|
||
const savedCount = parseInt(localStorage.getItem('savedNotePageCount') || '0', 10);
|
||
|
||
if (savedNote) {
|
||
noteTextarea.value = savedNote;
|
||
pageCount = Number.isFinite(savedCount) ? savedCount : 0;
|
||
showToast('Loaded saved note', 'info');
|
||
}
|
||
});
|
||
|
||
// Navigation back to main app
|
||
function goBackToApp() {
|
||
window.location.href = "index.html"; // change if your main file has a different name/path
|
||
}
|
||
|
||
// Credits & Rewards actions
|
||
function onWatchAd() {
|
||
// TODO: integrate with your AdMob rewarded flow
|
||
// Example: AdMob.showRewardedAd().then(refreshCredits);
|
||
console.log("Watch-ad clicked – call AdMob flow here.");
|
||
}
|
||
|
||
function onBuyPack(packId) {
|
||
// TODO: integrate with your in-app purchase / RevenueCat / store kit flow
|
||
console.log("Buy-pack clicked:", packId);
|
||
// Example: Purchases.purchasePackage(packId);
|
||
}
|
||
|
||
// Feedback handling
|
||
function submitFeedback() {
|
||
const messageEl = document.getElementById('feedbackMessage');
|
||
const emailEl = document.getElementById('feedbackEmail');
|
||
|
||
if (!messageEl) return;
|
||
|
||
const message = messageEl.value.trim();
|
||
const email = emailEl ? emailEl.value.trim() : '';
|
||
|
||
if (!message) {
|
||
showToast('Please add a short message before sending.', 'error');
|
||
return;
|
||
}
|
||
|
||
// For MVP: open mail client with a prefilled email.
|
||
const to = 'support@prometheus.cafe'; // change to your actual inbox
|
||
const subject = encodeURIComponent('Pen2Post feedback');
|
||
const bodyLines = [
|
||
message,
|
||
'',
|
||
email ? `Reply email (optional): ${email}` : '',
|
||
];
|
||
const body = encodeURIComponent(bodyLines.join('\n'));
|
||
|
||
window.location.href = `mailto:${to}?subject=${subject}&body=${body}`;
|
||
|
||
showToast('Opening your mail app to send feedback…', 'info');
|
||
}
|