Initial commit

Add Chrome extension for Read It Later functionality with background
script, manifest, and icons.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 23:23:15 -05:00
commit 3dbf791621
7 changed files with 147 additions and 0 deletions

123
background.js Normal file
View File

@@ -0,0 +1,123 @@
const API_URL = "https://www.jaredlog.com/readitlater/api/v1/capture";
async function getToken() {
return new Promise((resolve) => {
chrome.storage.sync.get(["READITLATER_TOKEN"], (res) => resolve(res.READITLATER_TOKEN || ""));
});
}
async function injectBanner(tabId, text, ok = true) {
try {
await chrome.scripting.executeScript({
target: { tabId },
world: "MAIN", // we want to touch the page DOM
func: (text, ok) => {
// Create a host and shadow root so site CSS can't break us
const host = document.createElement("div");
host.setAttribute("id", "readitlater-toast-host");
host.style.all = "initial"; // reduce leakage in some edge cases
const shadow = host.attachShadow({ mode: "closed" });
// Styles
const style = document.createElement("style");
style.textContent = `
@keyframes slideDown {
from { transform: translateY(-110%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes fadeOut {
to { opacity: 0; transform: translateY(-110%); }
}
.toast {
position: fixed;
top: 0; left: 50%;
transform: translateX(-50%);
z-index: 2147483647;
margin: 12px auto 0;
padding: 10px 14px;
border-radius: 8px;
font: 14px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #0b1f0b;
background: ${ok ? "#c7f9cc" : "#ffd6d6"};
border: 1px solid ${ok ? "#94d7a2" : "#ffabab"};
box-shadow: 0 10px 20px rgba(0,0,0,.12), 0 2px 6px rgba(0,0,0,.08);
animation: slideDown 180ms ease-out;
pointer-events: none; /* don't block page */
max-width: 90vw;
text-align: center;
}
.toast .text {
white-space: pre-wrap;
}
@media (prefers-reduced-motion: reduce) {
.toast { animation: none; }
}
`;
// Container
const toast = document.createElement("div");
toast.className = "toast";
toast.setAttribute("role", "status");
toast.setAttribute("aria-live", "polite");
toast.innerHTML = `<span class="text">${text}</span>`;
shadow.append(style, toast);
document.documentElement.appendChild(host);
// Auto-remove after 2 seconds (fade for 200ms)
const remove = () => host.remove();
setTimeout(() => {
toast.style.animation = "fadeOut 200ms ease-in forwards";
setTimeout(remove, 220);
}, 2000);
},
args: [text, ok],
});
} catch (e) {
// Non-fatal if injection fails (e.g., restricted pages)
console.warn("readitlater: banner inject failed", e);
}
}
chrome.action.onClicked.addListener(async (tab) => {
if (!tab?.id) return;
const [result] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => ({
url: window.location.href,
title: document.title,
html: "<!doctype html>" + document.documentElement.outerHTML
}),
});
const payload = result?.result || null;
if (!payload) return;
const token = await getToken();
try {
const resp = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
},
body: JSON.stringify(payload),
});
if (resp.ok) {
// Success banner
await injectBanner(tab.id, "Link saved.", true);
} else {
console.error("Capture failed:", resp.status, await resp.text());
await injectBanner(tab.id, "Save failed.", false);
}
} catch (e) {
console.error("readitlater: network error", e);
await injectBanner(tab.id, "Network error.", false);
}
});
// One-time: set your token in DevTools console on any page:
// chrome.storage.sync.set({ READITLATER_TOKEN: "YOUR_HEX_TOKEN" });