Firefox WebExtension (Desktop and Mobile) that lets you share the current tab to Margit.at, frontpage.fyi, etc. with minimal effort.
at main 223 lines 7.2 kB view raw
1const api = globalThis.browser ?? globalThis.chrome; 2const MAX_TITLE = 120; 3 4const form = document.getElementById("submit-form"); 5const titleInput = document.getElementById("title"); 6const urlInput = document.getElementById("url"); 7const statusEl = document.getElementById("status"); 8const titleCountEl = document.getElementById("title-count"); 9const submitBtn = document.getElementById("submit-btn"); 10const openOptionsBtn = document.getElementById("open-options"); 11const openFrontpageLink = document.getElementById("open-frontpage"); 12 13// Tabs 14const tabFrontpage = document.getElementById("tab-frontpage"); 15const tabMargin = document.getElementById("tab-margin"); 16const panelFrontpage = document.getElementById("panel-frontpage"); 17const panelMargin = document.getElementById("panel-margin"); 18 19// Margin panel 20const marginForm = document.getElementById("margin-form"); 21const marginSelectionEl = document.getElementById("margin-selection"); 22const marginCommentEl = document.getElementById("margin-comment"); 23const marginSubmitBtn = document.getElementById("margin-submit-btn"); 24const marginStatusEl = document.getElementById("margin-status"); 25const openMarginLink = document.getElementById("open-margin"); 26 27let activeTab = null; // the browser tab object 28 29function showStatus(message, isError = false) { 30 statusEl.textContent = message; 31 statusEl.className = isError ? "status error" : "status success"; 32} 33 34function updateTitleCounter() { 35 const value = titleInput.value ?? ""; 36 titleCountEl.textContent = `${value.length}/${MAX_TITLE}`; 37 if (value.length > MAX_TITLE) { 38 titleCountEl.classList.add("over-limit"); 39 } else { 40 titleCountEl.classList.remove("over-limit"); 41 } 42} 43 44async function populateFromTab() { 45 try { 46 const tabs = await api.tabs.query({ active: true, currentWindow: true }); 47 const tab = tabs?.[0]; 48 if (!tab) return; 49 activeTab = tab; 50 if (tab.title) { 51 titleInput.value = tab.title.trim().slice(0, MAX_TITLE); 52 } 53 if (tab.url && /^https?:/i.test(tab.url)) { 54 urlInput.value = tab.url; 55 } 56 updateTitleCounter(); 57 } catch (error) { 58 console.error("Unable to read active tab", error); 59 } 60} 61 62function showMarginStatus(message, isError = false) { 63 marginStatusEl.textContent = message; 64 marginStatusEl.className = isError ? "status error" : "status success"; 65} 66 67async function loadMarginSelection() { 68 marginSelectionEl.value = ""; 69 showMarginStatus(""); 70 if (!activeTab?.id) return; 71 if (!/^https?:/i.test(activeTab.url ?? "")) { 72 showMarginStatus("Margin annotations require an http/https page.", true); 73 return; 74 } 75 try { 76 const response = await api.tabs.sendMessage(activeTab.id, { type: "margin-get-selection" }); 77 if (response?.selection?.exact) { 78 marginSelectionEl.value = response.selection.exact; 79 } else { 80 showMarginStatus("No text selected. Select text on the page and reopen.", true); 81 } 82 } catch { 83 showMarginStatus("Could not read selection. Try reloading the page.", true); 84 } 85} 86 87function switchTab(tab) { 88 if (tab === "margin") { 89 tabFrontpage.classList.remove("active"); 90 tabFrontpage.setAttribute("aria-selected", "false"); 91 tabMargin.classList.add("active"); 92 tabMargin.setAttribute("aria-selected", "true"); 93 panelFrontpage.hidden = true; 94 panelMargin.hidden = false; 95 loadMarginSelection(); 96 } else { 97 tabMargin.classList.remove("active"); 98 tabMargin.setAttribute("aria-selected", "false"); 99 tabFrontpage.classList.add("active"); 100 tabFrontpage.setAttribute("aria-selected", "true"); 101 panelMargin.hidden = true; 102 panelFrontpage.hidden = false; 103 } 104} 105 106let hasAuth = false; 107 108async function checkAuth() { 109 try { 110 const response = await api.runtime.sendMessage({ type: "frontpage-get-auth" }); 111 hasAuth = Boolean(response?.auth); 112 if (!hasAuth) { 113 showStatus("Configure your Frontpage credentials in the options page.", true); 114 } else { 115 showStatus(""); 116 } 117 } catch (error) { 118 console.error("Failed to query auth state", error); 119 showStatus("Unable to read authentication state.", true); 120 } 121} 122 123form.addEventListener("submit", async (event) => { 124 event.preventDefault(); 125 if (!hasAuth) { 126 showStatus("Configure your Frontpage credentials in the options page.", true); 127 return; 128 } 129 showStatus(""); 130 submitBtn.disabled = true; 131 try { 132 const payload = { 133 title: titleInput.value, 134 url: urlInput.value 135 }; 136 const response = await api.runtime.sendMessage({ 137 type: "frontpage-submit", 138 payload 139 }); 140 if (!response?.ok) { 141 throw new Error(response?.error ?? "Unknown error"); 142 } 143 const uri = response?.result?.uri; 144 showStatus(uri ? `Posted! ${uri}` : "Posted to Frontpage!", false); 145 } catch (error) { 146 console.error("Submission failed", error); 147 showStatus(error.message, true); 148 } finally { 149 submitBtn.disabled = false; 150 } 151}); 152 153titleInput.addEventListener("input", updateTitleCounter); 154 155openOptionsBtn.addEventListener("click", () => { 156 api.runtime.openOptionsPage(); 157}); 158 159openFrontpageLink.addEventListener("click", (event) => { 160 event.preventDefault(); 161 api.tabs.create({ url: "https://frontpage.fyi" }); 162}); 163 164tabFrontpage.addEventListener("click", () => switchTab("frontpage")); 165tabMargin.addEventListener("click", () => switchTab("margin")); 166 167marginCommentEl.addEventListener("input", () => { 168 marginSubmitBtn.textContent = marginCommentEl.value.trim() ? "Annotate on Margin" : "Highlight on Margin"; 169}); 170 171marginForm.addEventListener("submit", async (event) => { 172 event.preventDefault(); 173 if (!hasAuth) { 174 showMarginStatus("Configure your credentials in the options page.", true); 175 return; 176 } 177 const exact = marginSelectionEl.value.trim(); 178 if (!exact) { 179 showMarginStatus("No text selected. Select text on the page and reopen.", true); 180 return; 181 } 182 showMarginStatus(""); 183 marginSubmitBtn.disabled = true; 184 try { 185 const tabs = await api.tabs.query({ active: true, currentWindow: true }); 186 const tab = tabs?.[0]; 187 let selectionData = { exact, prefix: "", suffix: "" }; 188 try { 189 const res = await api.tabs.sendMessage(tab.id, { type: "margin-get-selection" }); 190 if (res?.selection) selectionData = res.selection; 191 } catch { 192 // use what we have 193 } 194 const payload = { 195 url: tab?.url ?? "", 196 title: tab?.title ?? "", 197 exact: selectionData.exact, 198 prefix: selectionData.prefix, 199 suffix: selectionData.suffix, 200 comment: marginCommentEl.value 201 }; 202 const response = await api.runtime.sendMessage({ type: "margin-submit", payload }); 203 if (!response?.ok) { 204 throw new Error(response?.error ?? "Unknown error"); 205 } 206 const type = payload.comment.trim() ? "Annotation" : "Highlight"; 207 showMarginStatus(`${type} published to Margin!`); 208 marginCommentEl.value = ""; 209 } catch (error) { 210 console.error("Margin submission failed", error); 211 showMarginStatus(error.message, true); 212 } finally { 213 marginSubmitBtn.disabled = false; 214 } 215}); 216 217openMarginLink.addEventListener("click", (event) => { 218 event.preventDefault(); 219 api.tabs.create({ url: "https://margin.at" }); 220}); 221 222populateFromTab().then(() => switchTab("margin")); 223checkAuth();