Initial commit: Everclone self-hosted Evernote alternative

This commit is contained in:
chrisf committed 2026-10-22 17:13:30 -06:00
commit ee2ee84de6
51 files changed
+17728

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
{
"manifest_version": 3,
"name": "Everclone Web Clipper",
"version": "1.0.0",
"description": "Clip articles (with all graphics), selections, and bookmarks from Brave/Chrome directly into Everclone.",
"permissions": [
"activeTab",
"scripting",
"storage"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup.html",
"default_title": "Clip to Everclone"
}
}
+73
View File
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
body { width: 350px; margin: 0; padding: 14px; background: #1a1a1a; color: #f3f4f6; }
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid #333; }
.brand { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 15px; color: #fff; }
.logo { width: 24px; height: 24px; border-radius: 6px; background: #00a82d; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 13px; }
.gear-btn { background: none; border: none; color: #9ca3af; cursor: pointer; font-size: 12px; text-decoration: underline; }
.gear-btn:hover { color: #fff; }
label { display: block; font-size: 11px; font-weight: 600; color: #9ca3af; margin-bottom: 4px; margin-top: 10px; }
input, select, textarea { width: 100%; padding: 7px 9px; border-radius: 6px; border: 1px solid #374151; background: #262626; color: #fff; font-size: 12px; outline: none; }
input:focus, select:focus, textarea:focus { border-color: #00a82d; }
.modes { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-top: 4px; }
.mode-btn { padding: 6px 4px; font-size: 11px; font-weight: 600; border-radius: 6px; border: 1px solid #374151; background: #262626; color: #d1d5db; cursor: pointer; text-align: center; }
.mode-btn.active { background: #00a82d; border-color: #00a82d; color: #fff; }
.save-btn { width: 100%; margin-top: 14px; padding: 9px; border-radius: 8px; border: none; background: #00a82d; color: #fff; font-weight: 700; font-size: 13px; cursor: pointer; }
.save-btn:hover { background: #008f26; }
.save-btn:disabled { opacity: 0.5; cursor: wait; }
.status { margin-top: 10px; font-size: 12px; padding: 8px; border-radius: 6px; display: none; }
.status.ok { display: block; background: rgba(0, 168, 45, 0.2); border: 1px solid #00a82d; color: #86efac; }
.status.err { display: block; background: rgba(220, 38, 38, 0.2); border: 1px solid #dc2626; color: #fca5a5; }
#settingsPanel { display: none; margin-bottom: 12px; padding: 10px; background: #242424; border: 1px solid #374151; border-radius: 8px; }
</style>
</head>
<body>
<div class="header">
<div class="brand">
<div class="logo">E</div>
<span>Everclone Web Clipper</span>
</div>
<button type="button" id="toggleSettings" class="gear-btn">Settings</button>
</div>
<div id="settingsPanel">
<label style="margin-top:0">Everclone Server URL</label>
<input type="url" id="serverUrl" placeholder="https://everclone.vloser.us" />
<label>API Key</label>
<input type="password" id="apiKey" placeholder="evc_..." />
<button type="button" id="saveSettingsBtn" class="save-btn" style="margin-top:8px; padding:6px; font-size:11px;">Save Connection</button>
</div>
<label style="margin-top:0">Title</label>
<input type="text" id="noteTitle" placeholder="Page title" />
<label>Clip Format</label>
<div class="modes">
<button type="button" class="mode-btn active" data-mode="article">Article</button>
<button type="button" class="mode-btn" data-mode="selection">Selection</button>
<button type="button" class="mode-btn" data-mode="full">Full Page</button>
<button type="button" class="mode-btn" data-mode="bookmark">Bookmark</button>
</div>
<label>Notebook</label>
<select id="notebookSelect">
<option value="Web Clips">Web Clips</option>
<option value="Inbox">Inbox</option>
</select>
<label>Tags (comma-separated)</label>
<input type="text" id="noteTags" value="web-clip" />
<label>Add Remark / Note (optional)</label>
<textarea id="noteComment" rows="2" placeholder="Add your thoughts..."></textarea>
<button type="button" id="clipBtn" class="save-btn">Save Clip to Everclone</button>
<div id="statusBox" class="status"></div>
<script src="popup.js"></script>
</body>
</html>
+183
View File
@@ -0,0 +1,183 @@
const DEFAULT_SERVER = "https://everclone.vloser.us";
const DEFAULT_KEY = "evc_master_vloser_key_2026";
let currentMode = 'article';
let activeTab = null;
document.addEventListener('DOMContentLoaded', async () => {
const stored = await chrome.storage.local.get(['serverUrl', 'apiKey', 'lastNotebook']);
const serverUrl = (stored.serverUrl || DEFAULT_SERVER).replace(/\/$/, '');
const apiKey = stored.apiKey || DEFAULT_KEY;
document.getElementById('serverUrl').value = serverUrl;
document.getElementById('apiKey').value = apiKey;
if (!apiKey || apiKey === '<YOUR_API_KEY>') {
document.getElementById('settingsPanel').style.display = 'block';
}
document.getElementById('toggleSettings').addEventListener('click', () => {
const p = document.getElementById('settingsPanel');
p.style.display = p.style.display === 'block' ? 'none' : 'block';
});
document.getElementById('saveSettingsBtn').addEventListener('click', async () => {
const s = document.getElementById('serverUrl').value.trim().replace(/\/$/, '');
const k = document.getElementById('apiKey').value.trim();
await chrome.storage.local.set({ serverUrl: s, apiKey: k });
document.getElementById('settingsPanel').style.display = 'none';
loadNotebooks(s, k);
});
document.querySelectorAll('.mode-btn').forEach((btn) => {
btn.addEventListener('click', () => {
document.querySelectorAll('.mode-btn').forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
currentMode = btn.getAttribute('data-mode');
});
});
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
activeTab = tab;
if (tab && tab.title) {
document.getElementById('noteTitle').value = tab.title;
}
await loadNotebooks(serverUrl, apiKey, stored.lastNotebook);
document.getElementById('clipBtn').addEventListener('click', handleClip);
});
async function loadNotebooks(serverUrl, apiKey, preferredNb) {
if (!serverUrl || !apiKey) return;
try {
const res = await fetch(`${serverUrl}/api/hooks/note?action=notebooks`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) return;
const data = await res.json();
if (Array.isArray(data.notebooks) && data.notebooks.length > 0) {
const sel = document.getElementById('notebookSelect');
sel.innerHTML = '';
data.notebooks.forEach((nb) => {
const opt = document.createElement('option');
opt.value = nb.name;
opt.textContent = nb.name;
if (preferredNb ? nb.name === preferredNb : nb.name === 'Web Clips' || nb.isDefault) {
opt.selected = true;
}
sel.appendChild(opt);
});
}
} catch {
// keep default options
}
}
async function handleClip() {
const btn = document.getElementById('clipBtn');
const statusBox = document.getElementById('statusBox');
statusBox.className = 'status';
btn.disabled = true;
btn.textContent = 'Clipping & saving graphics...';
try {
const serverUrl = document.getElementById('serverUrl').value.trim().replace(/\/$/, '');
const apiKey = document.getElementById('apiKey').value.trim();
const title = document.getElementById('noteTitle').value.trim();
const notebook = document.getElementById('notebookSelect').value;
const tags = document.getElementById('noteTags').value.split(',').map((t) => t.trim()).filter(Boolean);
const comment = document.getElementById('noteComment').value.trim();
await chrome.storage.local.set({ serverUrl, apiKey, lastNotebook: notebook });
let captured = { pageHtml: '', selectionHtml: '', url: activeTab?.url || '' };
if (activeTab?.id && currentMode !== 'bookmark') {
const results = await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
func: () => {
// Clone document and resolve lazy-loaded images & relative URLs inside the live tab
const clone = document.documentElement.cloneNode(true);
clone.querySelectorAll('script, style, noscript, iframe').forEach((el) => el.remove());
clone.querySelectorAll('img').forEach((img) => {
const best =
img.getAttribute('data-img-url') ||
img.getAttribute('data-src') ||
img.getAttribute('data-lazy-src') ||
img.getAttribute('data-original') ||
img.currentSrc ||
img.src;
if (best) {
try {
img.setAttribute('src', new URL(best, location.href).href);
} catch {}
}
});
let selHtml = '';
const sel = window.getSelection();
if (sel && sel.rangeCount > 0 && !sel.isCollapsed) {
const container = document.createElement('div');
for (let i = 0; i < sel.rangeCount; i++) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
container.querySelectorAll('img').forEach((img) => {
if (img.src) img.setAttribute('src', img.src);
});
selHtml = container.innerHTML;
}
return {
pageHtml: '<!DOCTYPE html><html>' + clone.innerHTML + '</html>',
bodyHtml: clone.querySelector('body')?.innerHTML || '',
selectionHtml: selHtml,
url: location.href,
};
},
});
if (results && results[0] && results[0].result) {
captured = results[0].result;
}
}
const payload = {
title,
url: captured.url || activeTab?.url || '',
notebook,
tags,
comment,
clipMode: currentMode,
};
if (currentMode === 'article') {
payload.pageHtml = captured.pageHtml;
} else if (currentMode === 'selection') {
payload.html = captured.selectionHtml || '<p>No text selected on page.</p>';
} else if (currentMode === 'full') {
payload.html = captured.bodyHtml || captured.pageHtml;
} else if (currentMode === 'bookmark') {
payload.html = `<p>Bookmarked: <a href="${payload.url}" target="_blank">${payload.url}</a></p>`;
}
const res = await fetch(`${serverUrl}/api/hooks/note`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || `HTTP ${res.status}`);
}
statusBox.className = 'status ok';
statusBox.innerHTML = `✓ Saved to <strong>${notebook}</strong>! <a href="${serverUrl}/?noteId=${data.noteId}" target="_blank" style="color:#fff;text-decoration:underline;margin-left:6px;">Open Note</a>`;
btn.textContent = 'Clipped!';
} catch (err) {
statusBox.className = 'status err';
statusBox.textContent = 'Error: ' + (err.message || String(err));
btn.disabled = false;
btn.textContent = 'Save Clip to Everclone';
}
}