813 lines
28 KiB
JavaScript
813 lines
28 KiB
JavaScript
// CrowdProof Observe - Chrome Side Panel Script
|
|
|
|
let serverUrl = null;
|
|
let pendingImageFile = null; // Stores image file from context menu
|
|
|
|
// Screen management
|
|
function showScreen(screenId) {
|
|
document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden'));
|
|
document.getElementById(screenId).classList.remove('hidden');
|
|
}
|
|
|
|
function showStatus(message, isError = false) {
|
|
const status = document.getElementById('status');
|
|
status.textContent = message;
|
|
status.className = `status ${isError ? 'error' : 'success'}`;
|
|
setTimeout(() => {
|
|
status.className = 'status hidden';
|
|
}, 4000);
|
|
}
|
|
|
|
// API helper
|
|
async function apiRequest(endpoint, options = {}) {
|
|
if (!serverUrl) {
|
|
throw new Error('Server URL not configured');
|
|
}
|
|
|
|
const url = `${serverUrl}${endpoint}`;
|
|
const fetchOptions = {
|
|
credentials: 'include',
|
|
...options
|
|
};
|
|
|
|
return fetch(url, fetchOptions);
|
|
}
|
|
|
|
// Form submission handler
|
|
async function handleFormSubmit(e, type) {
|
|
e.preventDefault();
|
|
console.log('=== FORM SUBMIT START ===');
|
|
console.log('Type:', type);
|
|
console.log('Server URL:', serverUrl);
|
|
|
|
const form = e.target;
|
|
const formData = new FormData(form);
|
|
|
|
// For photo submissions, use pendingImageFile if available
|
|
if (type === 'photo' && pendingImageFile) {
|
|
formData.set('file', pendingImageFile);
|
|
pendingImageFile = null;
|
|
hideImagePreview();
|
|
}
|
|
|
|
console.log('Form data entries:');
|
|
for (let [key, value] of formData.entries()) {
|
|
console.log(' ', key, '=', value);
|
|
}
|
|
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const originalText = submitBtn.textContent;
|
|
submitBtn.textContent = 'Creating...';
|
|
submitBtn.disabled = true;
|
|
|
|
const url = `${serverUrl}/create_observation/${type}`;
|
|
console.log('Fetching URL:', url);
|
|
|
|
try {
|
|
console.log('About to fetch...');
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
body: formData,
|
|
credentials: 'include',
|
|
headers: {
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
console.log('Fetch completed');
|
|
console.log('Response status:', response.status);
|
|
|
|
const contentType = response.headers.get('content-type');
|
|
if (contentType && contentType.includes('application/json')) {
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
showStatus('Observation created successfully!');
|
|
form.reset();
|
|
if (type === 'photo') hideImagePreview();
|
|
} else if (result.error === 'login_required') {
|
|
showStatus('Session expired. Please login again.', true);
|
|
showScreen('login');
|
|
} else {
|
|
showStatus(result.error || 'Failed to create observation', true);
|
|
}
|
|
} else {
|
|
// Fallback to old redirect-based detection
|
|
if (response.url.includes('/login')) {
|
|
showStatus('Session expired. Please login again.', true);
|
|
showScreen('login');
|
|
} else if (response.url.includes('/observations')) {
|
|
showStatus('Observation created successfully!');
|
|
form.reset();
|
|
if (type === 'photo') hideImagePreview();
|
|
} else {
|
|
showStatus('Failed to create observation', true);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Fetch error:', error);
|
|
showStatus('Error: ' + error.message, true);
|
|
} finally {
|
|
submitBtn.textContent = originalText;
|
|
submitBtn.disabled = false;
|
|
console.log('=== FORM SUBMIT END ===');
|
|
}
|
|
}
|
|
|
|
// Check for pending URL from context menu
|
|
async function checkPendingUrl() {
|
|
const result = await chrome.storage.local.get('pendingUrl');
|
|
if (result.pendingUrl) {
|
|
// Clear it
|
|
await chrome.storage.local.remove('pendingUrl');
|
|
// Switch to URL tab
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelector('.tab[data-type="url"]').classList.add('active');
|
|
document.querySelectorAll('.obs-form').forEach(f => f.classList.add('hidden'));
|
|
document.getElementById('form-url').classList.remove('hidden');
|
|
// Populate URL field
|
|
document.getElementById('url-reference_url').value = result.pendingUrl;
|
|
}
|
|
}
|
|
|
|
// Check for pending image from context menu
|
|
async function checkPendingImage() {
|
|
const result = await chrome.storage.local.get('pendingImage');
|
|
if (result.pendingImage) {
|
|
const { sourceUrl } = result.pendingImage;
|
|
// Clear it
|
|
await chrome.storage.local.remove('pendingImage');
|
|
// Switch to Photo tab
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelector('.tab[data-type="photo"]').classList.add('active');
|
|
document.querySelectorAll('.obs-form').forEach(f => f.classList.add('hidden'));
|
|
document.getElementById('form-photo').classList.remove('hidden');
|
|
// Populate the reference URL field
|
|
if (sourceUrl) {
|
|
document.getElementById('photo-photo_url').value = sourceUrl;
|
|
// Fetch image from source URL and convert to blob
|
|
try {
|
|
const response = await fetch(sourceUrl);
|
|
const blob = await response.blob();
|
|
pendingImageFile = new File([blob], `image-${Date.now()}.png`, { type: blob.type || 'image/png' });
|
|
// Show preview
|
|
document.getElementById('image-preview-img').src = sourceUrl;
|
|
document.getElementById('image-preview').classList.remove('hidden');
|
|
showStatus('Image captured and ready to submit!');
|
|
} catch (e) {
|
|
console.error('Could not fetch image:', e);
|
|
showStatus('Could not fetch image. You can still submit with the URL.', true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show image preview
|
|
function showImagePreview(src) {
|
|
document.getElementById('image-preview-img').src = src;
|
|
document.getElementById('image-preview').classList.remove('hidden');
|
|
}
|
|
|
|
// Hide image preview
|
|
function hideImagePreview() {
|
|
document.getElementById('image-preview').classList.add('hidden');
|
|
document.getElementById('image-preview-img').src = '';
|
|
}
|
|
|
|
// Update logos and instance name from server
|
|
async function updateLogos() {
|
|
if (!serverUrl) return;
|
|
|
|
const loginLogo = document.getElementById('login-logo');
|
|
const mainLogo = document.getElementById('main-logo');
|
|
const loginTitle = document.getElementById('login-title');
|
|
const mainTitle = document.getElementById('main-title');
|
|
const loginInstanceName = document.getElementById('login-instance-name');
|
|
const mainInstanceName = document.getElementById('main-instance-name');
|
|
|
|
// Fetch instance name from login page
|
|
try {
|
|
const pageResponse = await fetch(`${serverUrl}/login`, {
|
|
credentials: 'include'
|
|
});
|
|
if (pageResponse.ok) {
|
|
const html = await pageResponse.text();
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
const brand = doc.querySelector('.navbar-brand');
|
|
if (brand) {
|
|
const instanceName = brand.textContent.trim();
|
|
loginInstanceName.textContent = instanceName;
|
|
mainInstanceName.textContent = instanceName;
|
|
} else {
|
|
throw new Error('Brand not found');
|
|
}
|
|
} else {
|
|
throw new Error('Page fetch failed');
|
|
}
|
|
} catch (e) {
|
|
loginInstanceName.textContent = 'CrowdProof (Error)';
|
|
mainInstanceName.textContent = 'CrowdProof (Error)';
|
|
}
|
|
|
|
// Fetch logo
|
|
try {
|
|
const response = await fetch(`${serverUrl}/static/logo.svg`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (response.ok) {
|
|
const svgText = await response.text();
|
|
const dataUrl = 'data:image/svg+xml;base64,' + btoa(svgText);
|
|
|
|
loginLogo.src = dataUrl;
|
|
mainLogo.src = dataUrl;
|
|
loginLogo.classList.remove('hidden');
|
|
mainLogo.classList.remove('hidden');
|
|
loginTitle.classList.add('hidden');
|
|
mainTitle.classList.add('hidden');
|
|
} else {
|
|
throw new Error('Logo not found');
|
|
}
|
|
} catch (e) {
|
|
// Fallback to text
|
|
loginLogo.classList.add('hidden');
|
|
mainLogo.classList.add('hidden');
|
|
loginTitle.classList.remove('hidden');
|
|
mainTitle.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
// Load observations list for access screen
|
|
async function loadObservations() {
|
|
const listEl = document.getElementById('observations-list');
|
|
listEl.innerHTML = '<p class="loading-text">Loading...</p>';
|
|
|
|
try {
|
|
const response = await fetch(`${serverUrl}/observations`, {
|
|
credentials: 'include',
|
|
headers: {
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (response.url.includes('/login')) {
|
|
showScreen('login');
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.success && data.observations) {
|
|
renderObservationsList(data.observations);
|
|
} else {
|
|
listEl.innerHTML = '<p class="no-observations">Failed to load observations</p>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Load observations error:', error);
|
|
listEl.innerHTML = '<p class="no-observations">Error loading observations</p>';
|
|
}
|
|
}
|
|
|
|
// Render observations list with access toggles
|
|
function renderObservationsList(observations) {
|
|
const listEl = document.getElementById('observations-list');
|
|
|
|
if (!observations || observations.length === 0) {
|
|
listEl.innerHTML = '<p class="no-observations">No observations found</p>';
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = observations.map(obs => {
|
|
const isOwner = obs.is_owner;
|
|
const disabledClass = isOwner ? '' : 'disabled';
|
|
const disabledAttr = isOwner ? '' : 'disabled';
|
|
const details = obs.details || {};
|
|
|
|
// Determine which button is active based on public_access value
|
|
const noneActive = obs.public_access === 0 ? 'active-none' : '';
|
|
const editorActive = obs.public_access === 1 ? 'active-editor' : '';
|
|
const viewerActive = obs.public_access === 2 ? 'active-viewer' : '';
|
|
|
|
// Build thumbnail for photos
|
|
let thumbHtml = '';
|
|
if (obs.observation_type === 'photo' && details.image_url) {
|
|
thumbHtml = `<img class="observation-thumb" src="${serverUrl}${details.image_url}" alt="">`;
|
|
}
|
|
|
|
// Build type-specific details
|
|
let detailsHtml = '';
|
|
if (obs.observation_type === 'photo') {
|
|
if (details.description) detailsHtml += `<div class="observation-detail">${escapeHtml(details.description)}</div>`;
|
|
if (details.date) detailsHtml += `<div class="observation-detail"><strong>Date:</strong> ${escapeHtml(details.date)}</div>`;
|
|
} else if (obs.observation_type === 'email') {
|
|
if (details.subject) detailsHtml += `<div class="observation-detail"><strong>Subject:</strong> ${escapeHtml(details.subject)}</div>`;
|
|
if (details.sender) detailsHtml += `<div class="observation-detail"><strong>From:</strong> ${escapeHtml(details.sender)}</div>`;
|
|
if (details.date) detailsHtml += `<div class="observation-detail"><strong>Date:</strong> ${escapeHtml(details.date)}</div>`;
|
|
} else if (obs.observation_type === 'url') {
|
|
if (details.url) detailsHtml += `<div class="observation-detail">${escapeHtml(details.url)}</div>`;
|
|
if (details.date) detailsHtml += `<div class="observation-detail"><strong>Date:</strong> ${escapeHtml(details.date)}</div>`;
|
|
} else if (obs.observation_type === 'log') {
|
|
if (details.source) detailsHtml += `<div class="observation-detail"><strong>Source:</strong> ${escapeHtml(details.source)}</div>`;
|
|
if (details.date) detailsHtml += `<div class="observation-detail"><strong>Date:</strong> ${escapeHtml(details.date)}</div>`;
|
|
} else if (obs.observation_type === 'statement') {
|
|
if (details.issuer) detailsHtml += `<div class="observation-detail"><strong>Issuer:</strong> ${escapeHtml(details.issuer)}</div>`;
|
|
if (details.recipient) detailsHtml += `<div class="observation-detail"><strong>To:</strong> ${escapeHtml(details.recipient)}</div>`;
|
|
if (details.date) detailsHtml += `<div class="observation-detail"><strong>Date:</strong> ${escapeHtml(details.date)}</div>`;
|
|
}
|
|
|
|
return `
|
|
<div class="observation-item ${disabledClass}" data-uuid="${obs.uuid}">
|
|
<div class="observation-header">
|
|
${thumbHtml}
|
|
<div class="observation-info">
|
|
<a href="${serverUrl}/observation/${obs.uuid}" target="_blank" class="observation-name" title="${escapeHtml(obs.display_name)}">${escapeHtml(obs.display_name)}</a>
|
|
<div class="observation-type">${obs.observation_type}</div>
|
|
${detailsHtml}
|
|
</div>
|
|
</div>
|
|
<div class="access-toggle">
|
|
<button type="button" class="${noneActive}" data-level="0" ${disabledAttr}>None</button>
|
|
<button type="button" class="${editorActive}" data-level="1" ${disabledAttr}>Editor</button>
|
|
<button type="button" class="${viewerActive}" data-level="2" ${disabledAttr}>Viewer</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
// Add click handlers for toggle buttons
|
|
listEl.querySelectorAll('.access-toggle button:not([disabled])').forEach(btn => {
|
|
btn.addEventListener('click', async (e) => {
|
|
const item = e.target.closest('.observation-item');
|
|
const uuid = item.dataset.uuid;
|
|
const level = parseInt(e.target.dataset.level);
|
|
await setPublicAccess(uuid, level, item);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Escape HTML to prevent XSS
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Set public access for an observation
|
|
async function setPublicAccess(uuid, level, itemEl) {
|
|
const buttons = itemEl.querySelectorAll('.access-toggle button');
|
|
buttons.forEach(btn => btn.disabled = true);
|
|
|
|
try {
|
|
const response = await fetch(`${serverUrl}/set_public_access/observation/${uuid}/${level}`, {
|
|
method: 'POST',
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Update button states
|
|
buttons.forEach(btn => {
|
|
btn.classList.remove('active-none', 'active-editor', 'active-viewer');
|
|
const btnLevel = parseInt(btn.dataset.level);
|
|
if (btnLevel === level) {
|
|
if (level === 0) btn.classList.add('active-none');
|
|
else if (level === 1) btn.classList.add('active-editor');
|
|
else if (level === 2) btn.classList.add('active-viewer');
|
|
}
|
|
});
|
|
} else {
|
|
showStatus('Failed to update access', true);
|
|
}
|
|
} catch (error) {
|
|
console.error('Set public access error:', error);
|
|
showStatus('Error updating access', true);
|
|
} finally {
|
|
buttons.forEach(btn => btn.disabled = false);
|
|
}
|
|
}
|
|
|
|
// Listen for storage changes to catch pending URL/image even when sidepanel is already open
|
|
chrome.storage.onChanged.addListener((changes, areaName) => {
|
|
if (areaName === 'local') {
|
|
if (changes.pendingUrl && changes.pendingUrl.newValue) {
|
|
checkPendingUrl();
|
|
}
|
|
if (changes.pendingImage && changes.pendingImage.newValue) {
|
|
checkPendingImage();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Initialize
|
|
async function init() {
|
|
console.log('Init starting...');
|
|
const result = await chrome.storage.local.get('serverUrl');
|
|
serverUrl = result.serverUrl || 'https://crowdproof.silogroup.org';
|
|
console.log('Server URL:', serverUrl);
|
|
|
|
// Set create account link
|
|
document.getElementById('createAccountLink').href = `${serverUrl}/register`;
|
|
|
|
updateLogos();
|
|
|
|
try {
|
|
const response = await apiRequest('/profile');
|
|
if (response.url.includes('/login')) {
|
|
showScreen('login');
|
|
} else {
|
|
showScreen('main');
|
|
// Check for pending URL/image after showing main screen
|
|
await checkPendingUrl();
|
|
await checkPendingImage();
|
|
}
|
|
} catch (error) {
|
|
console.error('Auth check failed:', error);
|
|
showScreen('login');
|
|
}
|
|
}
|
|
|
|
// Setup all event handlers when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
console.log('=== DOM READY ===');
|
|
|
|
// Open settings
|
|
document.getElementById('openSettings').addEventListener('click', () => {
|
|
showScreen('settings');
|
|
});
|
|
|
|
// Login form
|
|
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
console.log('Login form submitted');
|
|
|
|
const username = document.getElementById('username').value;
|
|
const password = document.getElementById('password').value;
|
|
const errorEl = document.getElementById('loginError');
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('username', username);
|
|
formData.append('password', password);
|
|
|
|
const response = await apiRequest('/login', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (!response.url.includes('/login')) {
|
|
showScreen('main');
|
|
} else {
|
|
errorEl.textContent = 'Invalid username or password';
|
|
errorEl.classList.remove('hidden');
|
|
}
|
|
} catch (error) {
|
|
errorEl.textContent = error.message;
|
|
errorEl.classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
// Login settings button
|
|
document.getElementById('loginSettingsBtn').addEventListener('click', async () => {
|
|
const result = await chrome.storage.local.get('serverUrl');
|
|
document.getElementById('settings-serverUrl').value = result.serverUrl || 'https://crowdproof.silogroup.org';
|
|
showScreen('settings');
|
|
});
|
|
|
|
// Logout
|
|
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
|
try {
|
|
await apiRequest('/logout');
|
|
showScreen('login');
|
|
} catch (error) {
|
|
showStatus('Logout failed: ' + error.message, true);
|
|
}
|
|
});
|
|
|
|
// Settings button on main screen
|
|
document.getElementById('mainSettingsBtn').addEventListener('click', async () => {
|
|
const result = await chrome.storage.local.get('serverUrl');
|
|
document.getElementById('settings-serverUrl').value = result.serverUrl || '';
|
|
showScreen('settings');
|
|
});
|
|
|
|
// Settings back button
|
|
document.getElementById('settingsBackBtn').addEventListener('click', async () => {
|
|
// Go back to appropriate screen
|
|
if (serverUrl) {
|
|
try {
|
|
const response = await apiRequest('/profile');
|
|
if (response.url.includes('/login')) {
|
|
showScreen('login');
|
|
} else {
|
|
showScreen('main');
|
|
}
|
|
} catch {
|
|
showScreen('login');
|
|
}
|
|
} else {
|
|
showScreen('setup');
|
|
}
|
|
});
|
|
|
|
// Access button
|
|
document.getElementById('accessBtn').addEventListener('click', () => {
|
|
showScreen('access');
|
|
loadObservations();
|
|
});
|
|
|
|
// Access back button
|
|
document.getElementById('accessBackBtn').addEventListener('click', () => {
|
|
showScreen('main');
|
|
});
|
|
|
|
// Settings form
|
|
document.getElementById('settingsForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const newUrl = document.getElementById('settings-serverUrl').value.replace(/\/+$/, '');
|
|
await chrome.storage.local.set({ serverUrl: newUrl });
|
|
serverUrl = newUrl;
|
|
document.getElementById('createAccountLink').href = `${serverUrl}/register`;
|
|
updateLogos();
|
|
|
|
const statusEl = document.getElementById('settingsStatus');
|
|
statusEl.textContent = 'Settings saved';
|
|
statusEl.className = 'status success';
|
|
statusEl.classList.remove('hidden');
|
|
|
|
setTimeout(() => {
|
|
statusEl.classList.add('hidden');
|
|
}, 2000);
|
|
});
|
|
|
|
// Tab switching
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
const type = tab.dataset.type;
|
|
console.log('Tab clicked:', type);
|
|
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
|
|
document.querySelectorAll('.obs-form').forEach(f => f.classList.add('hidden'));
|
|
document.getElementById(`form-${type}`).classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
// Capture URL button
|
|
document.getElementById('captureUrl').addEventListener('click', async () => {
|
|
console.log('Capture URL clicked');
|
|
try {
|
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
if (tabs[0]) {
|
|
document.getElementById('url-reference_url').value = tabs[0].url;
|
|
if (!document.getElementById('url-observation_name').value) {
|
|
document.getElementById('url-observation_name').value = tabs[0].title;
|
|
}
|
|
showStatus('URL captured!');
|
|
}
|
|
} catch (error) {
|
|
console.error('Capture URL error:', error);
|
|
showStatus('Could not capture URL', true);
|
|
}
|
|
});
|
|
|
|
// Log file import
|
|
document.getElementById('log-file-import').addEventListener('change', async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
try {
|
|
const text = await file.text();
|
|
document.getElementById('log-log_content').value = text;
|
|
showStatus('File imported');
|
|
} catch (error) {
|
|
showStatus('Could not read file', true);
|
|
}
|
|
});
|
|
|
|
// Photo file input - show preview
|
|
document.getElementById('photo-file').addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (file && file.type.startsWith('image/')) {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
showImagePreview(event.target.result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
// Clear any pending context menu image since user selected a file
|
|
pendingImageFile = null;
|
|
} else {
|
|
hideImagePreview();
|
|
}
|
|
});
|
|
|
|
// Form submissions
|
|
const forms = ['url', 'statement', 'log', 'photo', 'email'];
|
|
forms.forEach(type => {
|
|
const form = document.getElementById(`form-${type}`);
|
|
console.log(`Attaching handler for form-${type}:`, form);
|
|
form.addEventListener('submit', (e) => {
|
|
console.log(`Form ${type} submit event fired`);
|
|
handleFormSubmit(e, type);
|
|
});
|
|
});
|
|
|
|
// Paste target for photo form - click to enable paste
|
|
const pasteTarget = document.getElementById('paste-target');
|
|
console.log('Paste target element:', pasteTarget);
|
|
|
|
// Click to focus (enables paste)
|
|
pasteTarget.addEventListener('click', () => {
|
|
console.log('Paste target clicked, focusing...');
|
|
pasteTarget.focus();
|
|
});
|
|
|
|
// Update text on focus/blur
|
|
pasteTarget.addEventListener('focus', () => {
|
|
console.log('Paste target focused');
|
|
pasteTarget.querySelector('span').textContent = 'Ready - paste now (Ctrl+V)';
|
|
});
|
|
|
|
pasteTarget.addEventListener('blur', () => {
|
|
console.log('Paste target blurred');
|
|
pasteTarget.querySelector('span').textContent = 'Click to enable paste';
|
|
});
|
|
|
|
// Handle paste when paste target is focused (document-level since divs don't receive paste)
|
|
document.addEventListener('paste', async (e) => {
|
|
// Only handle if paste target is focused
|
|
if (document.activeElement !== pasteTarget) return;
|
|
console.log('Paste event received, paste target is focused');
|
|
const photoForm = document.getElementById('form-photo');
|
|
if (photoForm.classList.contains('hidden')) return;
|
|
console.log('Clipboard data:', e.clipboardData);
|
|
e.preventDefault();
|
|
|
|
const items = e.clipboardData?.items;
|
|
if (!items) return;
|
|
|
|
console.log('Items:', items.length);
|
|
for (const item of items) {
|
|
console.log('Item type:', item.type);
|
|
if (item.type.startsWith('image/')) {
|
|
const blob = item.getAsFile();
|
|
console.log('Blob:', blob);
|
|
if (!blob) continue;
|
|
|
|
// Create a File from the blob
|
|
const file = new File([blob], `screenshot-${Date.now()}.png`, { type: blob.type });
|
|
console.log('File created:', file.name, file.size);
|
|
|
|
// Build FormData from the photo form
|
|
const form = document.getElementById('form-photo');
|
|
const formData = new FormData();
|
|
|
|
formData.append('file', file);
|
|
formData.append('observation_name', form.querySelector('[name="observation_name"]').value);
|
|
formData.append('photo_description', form.querySelector('[name="photo_description"]').value);
|
|
formData.append('photo_url', form.querySelector('[name="photo_url"]').value);
|
|
formData.append('photo_date', form.querySelector('[name="photo_date"]').value);
|
|
|
|
// Submit
|
|
pasteTarget.classList.add('ready');
|
|
pasteTarget.querySelector('span').textContent = 'Submitting...';
|
|
|
|
try {
|
|
const response = await fetch(`${serverUrl}/create_observation/photo`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
credentials: 'include',
|
|
headers: {
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
const contentType = response.headers.get('content-type');
|
|
if (contentType && contentType.includes('application/json')) {
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
showStatus('Photo observation created!');
|
|
form.reset();
|
|
hideImagePreview();
|
|
pendingImageFile = null;
|
|
} else if (result.error === 'login_required') {
|
|
showStatus('Session expired. Please login again.', true);
|
|
showScreen('login');
|
|
} else {
|
|
showStatus(result.error || 'Failed to create observation', true);
|
|
}
|
|
} else {
|
|
// Fallback
|
|
if (response.url.includes('/login')) {
|
|
showStatus('Session expired. Please login again.', true);
|
|
showScreen('login');
|
|
} else if (response.url.includes('/observations')) {
|
|
showStatus('Photo observation created!');
|
|
form.reset();
|
|
hideImagePreview();
|
|
pendingImageFile = null;
|
|
} else {
|
|
showStatus('Failed to create observation', true);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
showStatus('Error: ' + error.message, true);
|
|
} finally {
|
|
pasteTarget.classList.remove('ready');
|
|
pasteTarget.querySelector('span').textContent = 'Click to enable paste';
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
showStatus('No image found in clipboard', true);
|
|
});
|
|
|
|
// Email drop target
|
|
const emailDropTarget = document.getElementById('email-drop-target');
|
|
|
|
emailDropTarget.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
emailDropTarget.classList.add('dragover');
|
|
});
|
|
|
|
emailDropTarget.addEventListener('dragleave', () => {
|
|
emailDropTarget.classList.remove('dragover');
|
|
});
|
|
|
|
emailDropTarget.addEventListener('drop', async (e) => {
|
|
e.preventDefault();
|
|
emailDropTarget.classList.remove('dragover');
|
|
|
|
const files = e.dataTransfer?.files;
|
|
if (!files || files.length === 0) return;
|
|
|
|
const file = files[0];
|
|
const validExtensions = ['.msg', '.eml'];
|
|
const hasValidExt = validExtensions.some(ext => file.name.toLowerCase().endsWith(ext));
|
|
|
|
if (!hasValidExt) {
|
|
showStatus('Please drop a .msg or .eml file', true);
|
|
return;
|
|
}
|
|
|
|
// Build FormData from the email form
|
|
const form = document.getElementById('form-email');
|
|
const formData = new FormData();
|
|
|
|
formData.append('msg_file', file);
|
|
formData.append('observation_name', form.querySelector('[name="observation_name"]').value);
|
|
formData.append('email_description', form.querySelector('[name="email_description"]').value);
|
|
|
|
// Submit
|
|
emailDropTarget.classList.add('ready');
|
|
emailDropTarget.querySelector('span').textContent = 'Submitting...';
|
|
|
|
try {
|
|
const response = await fetch(`${serverUrl}/create_observation/email`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
credentials: 'include',
|
|
headers: {
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
const contentType = response.headers.get('content-type');
|
|
if (contentType && contentType.includes('application/json')) {
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
showStatus('Email observation created!');
|
|
form.reset();
|
|
} else if (result.error === 'login_required') {
|
|
showStatus('Session expired. Please login again.', true);
|
|
showScreen('login');
|
|
} else {
|
|
showStatus(result.error || 'Failed to create observation', true);
|
|
}
|
|
} else {
|
|
// Fallback
|
|
if (response.url.includes('/login')) {
|
|
showStatus('Session expired. Please login again.', true);
|
|
showScreen('login');
|
|
} else if (response.url.includes('/observations')) {
|
|
showStatus('Email observation created!');
|
|
form.reset();
|
|
} else {
|
|
showStatus('Failed to create observation', true);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
showStatus('Error: ' + error.message, true);
|
|
} finally {
|
|
emailDropTarget.classList.remove('ready');
|
|
emailDropTarget.querySelector('span').textContent = 'Drop .msg or .eml file to submit';
|
|
}
|
|
});
|
|
|
|
console.log('=== ALL HANDLERS ATTACHED ===');
|
|
|
|
// Initialize
|
|
init();
|
|
});
|