first commit
commit
637b67d971
|
|
@ -0,0 +1,111 @@
|
|||
# CrowdProof Observe - Firefox Add-on Submission
|
||||
|
||||
## Extension Details
|
||||
|
||||
**Name:** CrowdProof Observe
|
||||
|
||||
**Summary:**
|
||||
Quick-add observations to your CrowdProof instance. Submit photos, URLs, emails, logs, and statements directly from your browser sidebar.
|
||||
|
||||
**Category:** Privacy & Security
|
||||
|
||||
**Tags:** productivity, evidence, documentation, observation, crowdproof
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
CrowdProof Observe is the official browser companion for CrowdProof, a collaborative evidence, observation management, intelligence collection, and publication platform.
|
||||
|
||||
### Features
|
||||
|
||||
- **Sidebar Interface** - Submit observations without leaving your current page
|
||||
- **Multiple Observation Types** - Photos, URLs, emails (.msg/.eml), logs, and statements
|
||||
- **Quick Capture** - Right-click images or links to send directly to CrowdProof
|
||||
- **Paste to Submit** - Paste screenshots directly from clipboard
|
||||
- **Access Management** - Set public access levels for your observations
|
||||
- **Server Flexibility** - Connect to any CrowdProof instance
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Configure your CrowdProof server URL in settings
|
||||
2. Log in with your CrowdProof credentials
|
||||
3. Use the sidebar or right-click context menus to submit observations
|
||||
4. Manage public access settings for your observations
|
||||
|
||||
Note: This extension defaults to use the public instance of CrowdProof by design and intent, however, requires the configurability to point to other instances for SaaS and self-hosted clients and their users.
|
||||
|
||||
---
|
||||
|
||||
## Permissions Justification
|
||||
|
||||
| Permission | Reason |
|
||||
|------------|--------|
|
||||
| `storage` | Stores the user's configured server URL locally in browser storage. |
|
||||
| `activeTab` | Captures the current page's URL and title when user clicks "Capture Current Page" for URL observations. |
|
||||
| `cookies` | Maintains authenticated session cookies with the user's CrowdProof server. |
|
||||
| `theme` | Adapts the sidebar appearance to match the user's Firefox theme. |
|
||||
| `contextMenus` | Provides right-click menu options ("Send Image to CrowdProof", "Send Link to CrowdProof"). |
|
||||
| `menus` | Required for `getTargetElement()` to access the clicked image element for capture. |
|
||||
| `<all_urls>` | Necessary to communicate with user-configured CrowdProof server instances hosted at arbitrary domains. The extension does not access or modify page content on other sites. |
|
||||
|
||||
---
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
**Last Updated:** 2025-12-30
|
||||
|
||||
### Data Collection
|
||||
|
||||
CrowdProof Observe does not collect, store, or transmit any user data to third parties.
|
||||
|
||||
### Server Communication
|
||||
|
||||
The extension communicates exclusively with the CrowdProof server URL configured by the user. All data submitted through the extension (observations, images, credentials) is sent only to this user-specified server.
|
||||
|
||||
### Local Storage
|
||||
|
||||
The extension stores only the following data locally in browser storage:
|
||||
- The configured server URL
|
||||
- Temporary pending image/URL data for context menu actions (cleared after use)
|
||||
|
||||
### Cookies
|
||||
|
||||
Session cookies are used to maintain authentication with the configured CrowdProof server. These cookies are managed by the browser and scoped to the server domain.
|
||||
|
||||
### No Tracking
|
||||
|
||||
This extension does not include any analytics, telemetry, or tracking functionality.
|
||||
|
||||
### Contact
|
||||
|
||||
For privacy inquiries, contact SILO GROUP at `pr@silogroup.org`
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
Recommended screenshots for submission:
|
||||
|
||||
1. **Main sidebar** - Showing the photo observation form with tabs visible
|
||||
2. **Access management** - Showing the observation list with access toggles
|
||||
3. **Context menu** - Showing right-click "Send Image to CrowdProof" option
|
||||
4. **Login screen** - Showing the branded login interface
|
||||
|
||||
---
|
||||
|
||||
## Technical Notes for Reviewers
|
||||
|
||||
- Source code is unobfuscated and readable
|
||||
- No external scripts or CDN dependencies
|
||||
- No data exfiltration - only communicates with user-configured server
|
||||
- Standard WebExtension APIs only
|
||||
- Server URL defaults to `https://crowdproof.silogroup.org` but is user-configurable
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- **Homepage:** https://crowdproof.silogroup.org
|
||||
- **Support:** `crowdproof-support@silogroup.org`
|
||||
- **Privacy Policy:** https://www.silogroup.org/legal/privacy/
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
# CrowdProof Observe
|
||||
|
||||
A Firefox browser extension for quickly submitting observations to your CrowdProof instance.
|
||||
|
||||
## Features
|
||||
|
||||
- **Sidebar Interface** - Submit observations without leaving your current page
|
||||
- **Multiple Observation Types** - Photos, URLs, emails (.msg/.eml), logs, and statements
|
||||
- **Quick Capture** - Right-click images or links to send directly to CrowdProof
|
||||
- **Paste to Submit** - Paste screenshots directly from clipboard
|
||||
- **Access Management** - Set public access levels for your observations
|
||||
- **Theme Integration** - Adapts to your Firefox theme
|
||||
|
||||
## Installation
|
||||
|
||||
### From Firefox Add-ons (Recommended)
|
||||
Visit the [Firefox Add-ons page](#) and click "Add to Firefox".
|
||||
|
||||
### Manual Installation (Development)
|
||||
1. Open Firefox and navigate to `about:debugging`
|
||||
2. Click "This Firefox" in the sidebar
|
||||
3. Click "Load Temporary Add-on"
|
||||
4. Select the `manifest.json` file from this directory
|
||||
|
||||
## Usage
|
||||
|
||||
### Initial Setup
|
||||
1. Click the CrowdProof Observe icon in the toolbar to open the sidebar
|
||||
2. The extension defaults to `https://crowdproof.silogroup.org`
|
||||
3. To use a different instance, click "Settings" and enter your server URL
|
||||
4. Log in with your CrowdProof credentials
|
||||
|
||||
### Submitting Observations
|
||||
|
||||
**Photos:**
|
||||
- Select a file using the file picker, OR
|
||||
- Click the paste zone and press Ctrl+V to paste from clipboard, OR
|
||||
- Right-click any image on a webpage and select "Send Image to CrowdProof"
|
||||
|
||||
**URLs:**
|
||||
- Click "Capture Current Page" to grab the active tab's URL, OR
|
||||
- Enter a URL manually, OR
|
||||
- Right-click any link and select "Send Link to CrowdProof"
|
||||
|
||||
**Emails:**
|
||||
- Select a .msg or .eml file, OR
|
||||
- Drag and drop an email file onto the drop zone
|
||||
|
||||
**Logs & Statements:**
|
||||
- Fill in the form fields and submit
|
||||
|
||||
### Managing Access
|
||||
Click the "Access" button to view your observations and set public access levels:
|
||||
- **None** - Only explicitly added users can access
|
||||
- **Editor** - All active CrowdProof users can view and edit
|
||||
- **Viewer** - All active CrowdProof users can view only
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
crowdproof-observe/
|
||||
├── manifest.json # Extension manifest
|
||||
├── background/
|
||||
│ └── background.js # Context menu and background tasks
|
||||
├── sidebar/
|
||||
│ ├── sidebar.html # Sidebar UI structure
|
||||
│ ├── sidebar.css # Sidebar styles
|
||||
│ └── sidebar.js # Sidebar logic
|
||||
├── options/
|
||||
│ ├── options.html # Options page
|
||||
│ └── options.js # Options logic
|
||||
├── icons/ # Extension icons
|
||||
├── README.md # This file
|
||||
└── AMO.md # Firefox Add-ons submission info
|
||||
```
|
||||
|
||||
### Building
|
||||
No build step required. The extension runs directly from source.
|
||||
|
||||
### Testing
|
||||
1. Load the extension temporarily via `about:debugging`
|
||||
2. Make changes to source files
|
||||
3. Click "Reload" in `about:debugging` to apply changes
|
||||
|
||||
### Packaging
|
||||
```bash
|
||||
cd crowdproof-observe
|
||||
zip -r crowdproof-observe.zip . -x "*.git*" -x "*.md"
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Purpose |
|
||||
|------------|---------|
|
||||
| `storage` | Store configured server URL |
|
||||
| `activeTab` | Capture current page URL/title |
|
||||
| `cookies` | Maintain server authentication |
|
||||
| `theme` | Match Firefox theme colors |
|
||||
| `contextMenus` | Right-click menu integration |
|
||||
| `menus` | Access clicked elements for capture |
|
||||
| `<all_urls>` | Connect to user-configured servers |
|
||||
|
||||
## Privacy
|
||||
|
||||
- No data collection or third-party transmission
|
||||
- Communicates only with your configured CrowdProof server
|
||||
- No analytics or tracking
|
||||
|
||||
See [AMO.md](AMO.md) for the full privacy policy.
|
||||
|
||||
## Support
|
||||
|
||||
- **Email:** crowdproof-support@silogroup.org
|
||||
- **Homepage:** https://crowdproof.silogroup.org
|
||||
|
||||
## License
|
||||
|
||||
Copyright SILO GROUP. All rights reserved.
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
// CrowdProof Observe - Background Script
|
||||
// Handles sidebar toggle and API communication
|
||||
|
||||
// Toggle sidebar when browser action button is clicked
|
||||
browser.browserAction.onClicked.addListener(() => {
|
||||
browser.sidebarAction.toggle();
|
||||
});
|
||||
|
||||
// Create context menu for links
|
||||
browser.contextMenus.create({
|
||||
id: "send-url-to-crowdproof",
|
||||
title: "Send URL to CrowdProof",
|
||||
contexts: ["link"]
|
||||
});
|
||||
|
||||
// Create context menu for images
|
||||
browser.contextMenus.create({
|
||||
id: "send-image-to-crowdproof",
|
||||
title: "Send Image to CrowdProof",
|
||||
contexts: ["image"]
|
||||
});
|
||||
|
||||
// Handle context menu click
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
if (info.menuItemId === "send-url-to-crowdproof") {
|
||||
// Open sidebar immediately (must be synchronous for user action context)
|
||||
browser.sidebarAction.open();
|
||||
// Then store the URL
|
||||
browser.storage.local.set({ pendingUrl: info.linkUrl });
|
||||
} else if (info.menuItemId === "send-image-to-crowdproof") {
|
||||
// Open sidebar immediately
|
||||
browser.sidebarAction.open();
|
||||
// Then capture and store the image
|
||||
const targetElementId = info.targetElementId;
|
||||
browser.tabs.executeScript(tab.id, {
|
||||
code: `
|
||||
(function() {
|
||||
try {
|
||||
const img = browser.menus.getTargetElement(${targetElementId});
|
||||
if (!img || img.tagName !== 'IMG') return null;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
return canvas.toDataURL('image/png');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
`
|
||||
}).then((results) => {
|
||||
const dataUrl = results && results[0];
|
||||
browser.storage.local.set({
|
||||
pendingImage: {
|
||||
dataUrl: dataUrl,
|
||||
sourceUrl: info.srcUrl
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const API = {
|
||||
async getServerUrl() {
|
||||
const result = await browser.storage.local.get('serverUrl');
|
||||
return result.serverUrl || null;
|
||||
},
|
||||
|
||||
async request(endpoint, options = {}) {
|
||||
const serverUrl = await this.getServerUrl();
|
||||
if (!serverUrl) {
|
||||
throw new Error('Server URL not configured. Please set it in extension options.');
|
||||
}
|
||||
|
||||
const url = `${serverUrl}${endpoint}`;
|
||||
const fetchOptions = {
|
||||
credentials: 'include',
|
||||
...options
|
||||
};
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
return response;
|
||||
},
|
||||
|
||||
async login(username, password) {
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await this.request('/login', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
// Login redirects on success, returns to login page on failure
|
||||
// We check if we got redirected to a non-login page
|
||||
return {
|
||||
success: response.ok && !response.url.includes('/login'),
|
||||
response
|
||||
};
|
||||
},
|
||||
|
||||
async logout() {
|
||||
const response = await this.request('/logout', {
|
||||
method: 'GET'
|
||||
});
|
||||
return response.ok;
|
||||
},
|
||||
|
||||
async checkAuth() {
|
||||
// Try to access a protected page to check if we're logged in
|
||||
const response = await this.request('/profile', {
|
||||
method: 'GET'
|
||||
});
|
||||
// If we get redirected to login, we're not authenticated
|
||||
return !response.url.includes('/login');
|
||||
},
|
||||
|
||||
async createObservation(type, formData) {
|
||||
const response = await this.request(`/create_observation/${type}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
return {
|
||||
success: response.ok,
|
||||
response
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Handle messages from popup
|
||||
browser.runtime.onMessage.addListener(async (message, sender) => {
|
||||
try {
|
||||
switch (message.action) {
|
||||
case 'getServerUrl':
|
||||
return { serverUrl: await API.getServerUrl() };
|
||||
|
||||
case 'checkAuth':
|
||||
const isAuthenticated = await API.checkAuth();
|
||||
return { authenticated: isAuthenticated };
|
||||
|
||||
case 'login':
|
||||
const loginResult = await API.login(message.username, message.password);
|
||||
return { success: loginResult.success };
|
||||
|
||||
case 'logout':
|
||||
const logoutSuccess = await API.logout();
|
||||
return { success: logoutSuccess };
|
||||
|
||||
case 'createObservation':
|
||||
const obsResult = await API.createObservation(message.type, message.formData);
|
||||
return { success: obsResult.success };
|
||||
|
||||
default:
|
||||
return { error: 'Unknown action' };
|
||||
}
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 304 B |
Binary file not shown.
|
After Width: | Height: | Size: 599 B |
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "CrowdProof Observe",
|
||||
"version": "1.0.0",
|
||||
"description": "Send observations to CrowdProof from any webpage",
|
||||
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"cookies",
|
||||
"theme",
|
||||
"contextMenus",
|
||||
"menus",
|
||||
"<all_urls>"
|
||||
],
|
||||
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "crowdproof-observe@proofpoint.local"
|
||||
}
|
||||
},
|
||||
|
||||
"icons": {
|
||||
"48": "icons/icon-48.png",
|
||||
"96": "icons/icon-96.png"
|
||||
},
|
||||
|
||||
"sidebar_action": {
|
||||
"default_icon": {
|
||||
"48": "icons/icon-48.png"
|
||||
},
|
||||
"default_title": "CrowdProof Observe",
|
||||
"default_panel": "sidebar/sidebar.html"
|
||||
},
|
||||
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"48": "icons/icon-48.png"
|
||||
},
|
||||
"default_title": "Toggle CrowdProof Observe"
|
||||
},
|
||||
|
||||
"options_ui": {
|
||||
"page": "options/options.html",
|
||||
"browser_style": true
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": ["background/background.js"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>CrowdProof Observe Settings</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
input[type="url"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
input[type="url"]:focus {
|
||||
outline: none;
|
||||
border-color: #0066cc;
|
||||
}
|
||||
.help-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
button {
|
||||
margin-top: 15px;
|
||||
padding: 10px 20px;
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0055aa;
|
||||
}
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
.status.success {
|
||||
display: block;
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status.error {
|
||||
display: block;
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>CrowdProof Observe Settings</h1>
|
||||
|
||||
<div>
|
||||
<label for="serverUrl">Server URL</label>
|
||||
<input type="url" id="serverUrl" placeholder="https://your-crowdproof-server.com">
|
||||
<p class="help-text">Enter the full URL of your CrowdProof server (e.g., https://crowdproof.example.com)</p>
|
||||
</div>
|
||||
|
||||
<button id="save">Save Settings</button>
|
||||
|
||||
<div id="status" class="status"></div>
|
||||
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// Load saved settings on page load
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const result = await browser.storage.local.get('serverUrl');
|
||||
if (result.serverUrl) {
|
||||
document.getElementById('serverUrl').value = result.serverUrl;
|
||||
}
|
||||
});
|
||||
|
||||
// Save settings
|
||||
document.getElementById('save').addEventListener('click', async () => {
|
||||
const serverUrl = document.getElementById('serverUrl').value.trim();
|
||||
const statusEl = document.getElementById('status');
|
||||
|
||||
// Validate URL
|
||||
if (!serverUrl) {
|
||||
statusEl.textContent = 'Please enter a server URL';
|
||||
statusEl.className = 'status error';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(serverUrl);
|
||||
} catch {
|
||||
statusEl.textContent = 'Please enter a valid URL';
|
||||
statusEl.className = 'status error';
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove trailing slash for consistency
|
||||
const normalizedUrl = serverUrl.replace(/\/+$/, '');
|
||||
|
||||
// Save to storage
|
||||
await browser.storage.local.set({ serverUrl: normalizedUrl });
|
||||
|
||||
statusEl.textContent = 'Settings saved!';
|
||||
statusEl.className = 'status success';
|
||||
|
||||
// Hide status after 2 seconds
|
||||
setTimeout(() => {
|
||||
statusEl.className = 'status';
|
||||
}, 2000);
|
||||
});
|
||||
|
|
@ -0,0 +1,530 @@
|
|||
/* Reset */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Base */
|
||||
html, body {
|
||||
height: 100%;
|
||||
background: var(--theme-bg, #1c1b22);
|
||||
color: var(--theme-text, #fbfbfe);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Utility */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Screens */
|
||||
.screen {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--theme-border, #5b5b66);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 80px;
|
||||
max-width: 240px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.header-logo.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.instance-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 20px;
|
||||
padding: 4px;
|
||||
background: rgba(128, 128, 128, 0.15);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 8px 6px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--theme-text, #fbfbfe) !important;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: all 0.15s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
opacity: 1;
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
opacity: 1;
|
||||
background: rgba(10, 132, 255, 0.2);
|
||||
border-color: #0a84ff;
|
||||
color: #0a84ff !important;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Form Groups */
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--theme-text, #fbfbfe);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
input[type="text"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: var(--theme-button-bg, #42414d);
|
||||
color: var(--theme-text, #fbfbfe);
|
||||
border: 1px solid var(--theme-border, #5b5b66);
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="url"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="email"]:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--theme-highlight, #00ddff);
|
||||
box-shadow: 0 0 0 1px var(--theme-highlight, #00ddff);
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--theme-text, #fbfbfe);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 16px;
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
color: inherit;
|
||||
border: 1px solid rgba(128, 128, 128, 0.4);
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
background: rgba(128, 128, 128, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #0a84ff;
|
||||
color: #ffffff;
|
||||
border-color: #0a84ff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0060df;
|
||||
border-color: #0060df;
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
background: #003eaa;
|
||||
border-color: #003eaa;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Submit button specific */
|
||||
.btn-submit {
|
||||
margin-top: 8px;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
#loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
gap: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Setup screen */
|
||||
#setup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
#setup .header-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#setup p {
|
||||
opacity: 0.7;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
/* Login form */
|
||||
#login .header {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Error messages */
|
||||
.error-message {
|
||||
color: #ff6b6b;
|
||||
font-size: 13px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Status messages */
|
||||
.status {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: var(--theme-highlight, #00ddff);
|
||||
color: var(--theme-highlight-text, #1c1b22);
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: rgba(255, 107, 107, 0.15);
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* Observation forms */
|
||||
.obs-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Form divider */
|
||||
.form-divider {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.3);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* Paste target */
|
||||
.paste-target {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 80px;
|
||||
border: 2px dashed rgba(128, 128, 128, 0.5);
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, opacity 0.15s;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.paste-target:hover {
|
||||
border-color: #0a84ff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.paste-target:focus {
|
||||
border-color: #30d158;
|
||||
border-style: solid;
|
||||
background: rgba(48, 209, 88, 0.1);
|
||||
opacity: 1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.paste-target.ready {
|
||||
border-color: #0a84ff;
|
||||
border-style: solid;
|
||||
background: rgba(10, 132, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Drop target */
|
||||
.drop-target {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 80px;
|
||||
border: 2px dashed rgba(128, 128, 128, 0.5);
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, opacity 0.15s;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.drop-target:hover,
|
||||
.drop-target:focus,
|
||||
.drop-target.dragover {
|
||||
border-color: #0a84ff;
|
||||
opacity: 1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.drop-target.ready {
|
||||
border-color: #0a84ff;
|
||||
border-style: solid;
|
||||
background: rgba(10, 132, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Image preview */
|
||||
.image-preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 150px;
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Access screen */
|
||||
.access-hint {
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.observations-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.observation-item {
|
||||
padding: 12px;
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.observation-item.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.observation-header {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.observation-thumb {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.observation-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.observation-name {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #0a84ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.observation-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.observation-type {
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
text-transform: capitalize;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.observation-detail {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.observation-detail strong {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.access-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.access-toggle button {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
border: 1px solid rgba(128, 128, 128, 0.3);
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.access-toggle button:hover:not(:disabled) {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
|
||||
.access-toggle button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.access-toggle button.active-none {
|
||||
background: rgba(255, 179, 0, 0.25);
|
||||
border-color: #ffb300;
|
||||
color: #ffb300;
|
||||
}
|
||||
|
||||
.access-toggle button.active-editor {
|
||||
background: rgba(255, 179, 0, 0.25);
|
||||
border-color: #ffb300;
|
||||
color: #ffb300;
|
||||
}
|
||||
|
||||
.access-toggle button.active-viewer {
|
||||
background: rgba(48, 209, 88, 0.2);
|
||||
border-color: #30d158;
|
||||
color: #30d158;
|
||||
}
|
||||
|
||||
.no-observations {
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
padding: 24px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>CrowdProof Observe</title>
|
||||
<link rel="stylesheet" href="sidebar.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Loading -->
|
||||
<div id="loading" class="screen">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
<!-- Setup -->
|
||||
<div id="setup" class="screen hidden">
|
||||
<h1 class="header-title">CrowdProof Observe</h1>
|
||||
<p>Configure the server URL in extension settings to get started.</p>
|
||||
<button id="openSettings" class="btn btn-primary">Open Settings</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div id="settings" class="screen hidden">
|
||||
<header class="header header-row">
|
||||
<h1 class="header-title">Settings</h1>
|
||||
<button id="settingsBackBtn" class="btn btn-sm">Back</button>
|
||||
</header>
|
||||
<form id="settingsForm" class="form">
|
||||
<div class="form-group">
|
||||
<label for="settings-serverUrl">Server URL</label>
|
||||
<input type="url" id="settings-serverUrl" name="serverUrl" required placeholder="https://your-crowdproof-server.com">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit">Save</button>
|
||||
</form>
|
||||
<div id="settingsStatus" class="status hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- Access -->
|
||||
<div id="access" class="screen hidden">
|
||||
<header class="header header-row">
|
||||
<h1 class="header-title">Manage Access</h1>
|
||||
<button id="accessBackBtn" class="btn btn-sm">Back</button>
|
||||
</header>
|
||||
<p class="access-hint">Set public access for observations you own.</p>
|
||||
<div id="observations-list" class="observations-list">
|
||||
<p class="loading-text">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login -->
|
||||
<div id="login" class="screen hidden">
|
||||
<header class="header">
|
||||
<img id="login-logo" class="header-logo" src="" alt="">
|
||||
<h1 id="login-title" class="header-title hidden">Login</h1>
|
||||
<p id="login-instance-name" class="instance-name"></p>
|
||||
</header>
|
||||
<form id="loginForm" class="form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit">Login</button>
|
||||
<div id="loginError" class="error-message hidden"></div>
|
||||
<a id="createAccountLink" href="#" class="btn btn-block btn-submit" target="_blank">Create Account</a>
|
||||
<button type="button" id="loginSettingsBtn" class="btn btn-block btn-submit">Settings</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Main -->
|
||||
<div id="main" class="screen hidden">
|
||||
<header class="header">
|
||||
<div class="header-actions">
|
||||
<button id="accessBtn" class="btn btn-sm">Access</button>
|
||||
<button id="mainSettingsBtn" class="btn btn-sm">Settings</button>
|
||||
<button id="logoutBtn" class="btn btn-sm">Logout</button>
|
||||
</div>
|
||||
<img id="main-logo" class="header-logo" src="" alt="">
|
||||
<h1 id="main-title" class="header-title hidden">CrowdProof</h1>
|
||||
<p id="main-instance-name" class="instance-name"></p>
|
||||
</header>
|
||||
|
||||
<nav class="tabs">
|
||||
<button type="button" class="tab active" data-type="photo">Photo</button>
|
||||
<button type="button" class="tab" data-type="url">URL</button>
|
||||
<button type="button" class="tab" data-type="statement">Statement</button>
|
||||
<button type="button" class="tab" data-type="log">Log</button>
|
||||
<button type="button" class="tab" data-type="email">Email</button>
|
||||
</nav>
|
||||
|
||||
<!-- URL Form -->
|
||||
<form id="form-url" class="obs-form hidden">
|
||||
<div class="form-group">
|
||||
<button type="button" id="captureUrl" class="btn btn-block">Capture Current Page</button>
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<div class="form-group">
|
||||
<label for="url-reference_url">URL</label>
|
||||
<input type="url" id="url-reference_url" name="reference_url" required>
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<div class="form-group">
|
||||
<label for="url-observation_name">Title (optional)</label>
|
||||
<input type="text" id="url-observation_name" name="observation_name" maxlength="256">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="url-url_description">Description (optional)</label>
|
||||
<textarea id="url-url_description" name="url_description" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="url-url_date">Date (optional)</label>
|
||||
<input type="text" id="url-url_date" name="url_date" placeholder="YYYY-MM-DD">
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit">Send</button>
|
||||
</form>
|
||||
|
||||
<!-- Statement Form -->
|
||||
<form id="form-statement" class="obs-form hidden">
|
||||
<div class="form-group">
|
||||
<label for="statement-statement_issuer">Issuer</label>
|
||||
<input type="text" id="statement-statement_issuer" name="statement_issuer" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="statement-statement_recipient">Recipient (optional)</label>
|
||||
<input type="text" id="statement-statement_recipient" name="statement_recipient">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="statement-statement_date">Date (optional)</label>
|
||||
<input type="text" id="statement-statement_date" name="statement_date" placeholder="YYYY-MM-DD">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="statement-statement_content">Statement</label>
|
||||
<textarea id="statement-statement_content" name="statement_content" rows="4" required></textarea>
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<div class="form-group">
|
||||
<label for="statement-observation_name">Title (optional)</label>
|
||||
<input type="text" id="statement-observation_name" name="observation_name" maxlength="256">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="statement-statement_description">Description (optional)</label>
|
||||
<textarea id="statement-statement_description" name="statement_description" rows="2"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit">Send</button>
|
||||
</form>
|
||||
|
||||
<!-- Log Form -->
|
||||
<form id="form-log" class="obs-form hidden">
|
||||
<div class="form-group">
|
||||
<label for="log-log_source">Source</label>
|
||||
<input type="text" id="log-log_source" name="log_source" required placeholder="Email, IRC, System logs, etc.">
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<div class="form-group">
|
||||
<label>Import from File</label>
|
||||
<input type="file" id="log-file-import" accept=".txt,.log,.csv,.json">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="log-log_content">Log Content</label>
|
||||
<textarea id="log-log_content" name="log_content" rows="5" required></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="log-log_date">Date (optional)</label>
|
||||
<input type="text" id="log-log_date" name="log_date" placeholder="YYYY-MM-DD">
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<div class="form-group">
|
||||
<label for="log-observation_name">Title (optional)</label>
|
||||
<input type="text" id="log-observation_name" name="observation_name" maxlength="256">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="log-log_description">Description (optional)</label>
|
||||
<textarea id="log-log_description" name="log_description" rows="2"></textarea>
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit">Send</button>
|
||||
</form>
|
||||
|
||||
<!-- Photo Form -->
|
||||
<form id="form-photo" class="obs-form">
|
||||
<div class="form-group">
|
||||
<label for="photo-photo_url">Reference URL (optional)</label>
|
||||
<input type="url" id="photo-photo_url" name="photo_url">
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<div class="form-group">
|
||||
<label>Select file</label>
|
||||
<input type="file" id="photo-file" name="file" accept="image/*">
|
||||
</div>
|
||||
<div id="paste-target" class="paste-target" tabindex="0">
|
||||
<span>Click to enable paste</span>
|
||||
</div>
|
||||
<div id="image-preview" class="image-preview hidden">
|
||||
<img id="image-preview-img" src="" alt="Preview">
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<div class="form-group">
|
||||
<label for="photo-observation_name">Title (optional)</label>
|
||||
<input type="text" id="photo-observation_name" name="observation_name" maxlength="256">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="photo-photo_description">Description (optional)</label>
|
||||
<textarea id="photo-photo_description" name="photo_description" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="photo-photo_date">Date (optional)</label>
|
||||
<input type="text" id="photo-photo_date" name="photo_date" placeholder="YYYY-MM-DD">
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit">Send</button>
|
||||
</form>
|
||||
|
||||
<!-- Email Form -->
|
||||
<form id="form-email" class="obs-form hidden">
|
||||
<div class="form-group">
|
||||
<label>Email File</label>
|
||||
<input type="file" id="email-file" name="msg_file" accept=".msg,.eml">
|
||||
</div>
|
||||
<div id="email-drop-target" class="drop-target" tabindex="0">
|
||||
<span>Drop .msg or .eml file to submit</span>
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<div class="form-group">
|
||||
<label for="email-observation_name">Title (optional)</label>
|
||||
<input type="text" id="email-observation_name" name="observation_name" maxlength="256">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email-email_description">Description (optional)</label>
|
||||
<textarea id="email-email_description" name="email_description" rows="3"></textarea>
|
||||
</div>
|
||||
<hr class="form-divider">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit">Send</button>
|
||||
</form>
|
||||
|
||||
<div id="status" class="status hidden"></div>
|
||||
</div>
|
||||
|
||||
<script src="sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,827 @@
|
|||
// CrowdProof Observe - Sidebar Script
|
||||
|
||||
let serverUrl = null;
|
||||
let pendingImageFile = null; // Stores image file from context menu
|
||||
|
||||
// Apply browser theme
|
||||
async function applyTheme() {
|
||||
const theme = await browser.theme.getCurrent();
|
||||
const colors = theme.colors || {};
|
||||
|
||||
document.documentElement.style.setProperty('--theme-bg', colors.popup || colors.frame || '');
|
||||
document.documentElement.style.setProperty('--theme-text', colors.popup_text || colors.tab_text || '');
|
||||
document.documentElement.style.setProperty('--theme-border', colors.popup_border || colors.toolbar_top_separator || '');
|
||||
document.documentElement.style.setProperty('--theme-button-bg', colors.button_background_active || colors.toolbar || '');
|
||||
document.documentElement.style.setProperty('--theme-highlight', colors.tab_line || colors.ntp_text || colors.popup_highlight || '');
|
||||
document.documentElement.style.setProperty('--theme-highlight-text', colors.popup_highlight_text || colors.toolbar_text || colors.popup_text || '');
|
||||
}
|
||||
|
||||
browser.theme.onUpdated.addListener(applyTheme);
|
||||
applyTheme();
|
||||
|
||||
// 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 browser.storage.local.get('pendingUrl');
|
||||
if (result.pendingUrl) {
|
||||
// Clear it
|
||||
await browser.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 browser.storage.local.get('pendingImage');
|
||||
if (result.pendingImage) {
|
||||
const { dataUrl, sourceUrl } = result.pendingImage;
|
||||
// Clear it
|
||||
await browser.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;
|
||||
}
|
||||
// Convert dataUrl to File and store it
|
||||
if (dataUrl) {
|
||||
const response = await fetch(dataUrl);
|
||||
const blob = await response.blob();
|
||||
pendingImageFile = new File([blob], `image-${Date.now()}.png`, { type: 'image/png' });
|
||||
// Show preview
|
||||
document.getElementById('image-preview-img').src = dataUrl;
|
||||
document.getElementById('image-preview').classList.remove('hidden');
|
||||
showStatus('Image captured and ready to submit!');
|
||||
} else {
|
||||
showStatus('Could not capture image data', 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 sidebar is already open
|
||||
browser.storage.onChanged.addListener((changes, area) => {
|
||||
if (area === '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 browser.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 browser.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 browser.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 browser.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 browser.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();
|
||||
});
|
||||
Loading…
Reference in New Issue