Files
pen2post/journal.js
2026-01-28 20:39:59 -08:00

316 lines
8.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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');
}