diff --git a/docs/guides/images/1-extension-page.png b/docs/guides/images/1-extension-page.png
new file mode 100644
index 000000000..c908fe27a
Binary files /dev/null and b/docs/guides/images/1-extension-page.png differ
diff --git a/docs/guides/images/10-omnivore-api-keys.png b/docs/guides/images/10-omnivore-api-keys.png
new file mode 100644
index 000000000..3d8670c3e
Binary files /dev/null and b/docs/guides/images/10-omnivore-api-keys.png differ
diff --git a/docs/guides/images/11-generate-key.png b/docs/guides/images/11-generate-key.png
new file mode 100644
index 000000000..420dbc996
Binary files /dev/null and b/docs/guides/images/11-generate-key.png differ
diff --git a/docs/guides/images/12-copy-key.png b/docs/guides/images/12-copy-key.png
new file mode 100644
index 000000000..8b148a341
Binary files /dev/null and b/docs/guides/images/12-copy-key.png differ
diff --git a/docs/guides/images/13-update-settings.png b/docs/guides/images/13-update-settings.png
new file mode 100644
index 000000000..25b20edbb
Binary files /dev/null and b/docs/guides/images/13-update-settings.png differ
diff --git a/docs/guides/images/2-developer-mode.png b/docs/guides/images/2-developer-mode.png
new file mode 100644
index 000000000..a06c247bd
Binary files /dev/null and b/docs/guides/images/2-developer-mode.png differ
diff --git a/docs/guides/images/3-load-unpacked.png b/docs/guides/images/3-load-unpacked.png
new file mode 100644
index 000000000..323330a2f
Binary files /dev/null and b/docs/guides/images/3-load-unpacked.png differ
diff --git a/docs/guides/images/4-folder.png b/docs/guides/images/4-folder.png
new file mode 100644
index 000000000..ecbc5fba5
Binary files /dev/null and b/docs/guides/images/4-folder.png differ
diff --git a/docs/guides/images/5-folders.png b/docs/guides/images/5-folders.png
new file mode 100644
index 000000000..5c3295dc6
Binary files /dev/null and b/docs/guides/images/5-folders.png differ
diff --git a/docs/guides/images/6-installed.png b/docs/guides/images/6-installed.png
new file mode 100644
index 000000000..92e0c6b6e
Binary files /dev/null and b/docs/guides/images/6-installed.png differ
diff --git a/docs/guides/images/7-options.png b/docs/guides/images/7-options.png
new file mode 100644
index 000000000..41dc4eefc
Binary files /dev/null and b/docs/guides/images/7-options.png differ
diff --git a/docs/guides/images/8-options-page.png b/docs/guides/images/8-options-page.png
new file mode 100644
index 000000000..a8231d6f5
Binary files /dev/null and b/docs/guides/images/8-options-page.png differ
diff --git a/docs/guides/images/9-omnivore-settings.png b/docs/guides/images/9-omnivore-settings.png
new file mode 100644
index 000000000..b3e15fd20
Binary files /dev/null and b/docs/guides/images/9-omnivore-settings.png differ
diff --git a/pkg/extension-v3/Makefile b/pkg/extension-v3/Makefile
new file mode 100644
index 000000000..807bb6d64
--- /dev/null
+++ b/pkg/extension-v3/Makefile
@@ -0,0 +1,26 @@
+all: firefox chrome edge
+
+build: *
+ rm -rf extension
+ yarn build
+
+firefox:
+ echo "building firefox package"
+ rm -rf extension
+ yarn build
+ FIREFOX_PKG_NAME="firefox-$(shell cat extension/manifest.json| jq -j .version).zip" ; \
+ FIREFOX_SRC_NAME="firefox-$(shell cat extension/manifest.json| jq -j .version)-src.zip" ; \
+ cd extension; zip -r ../$$FIREFOX_PKG_NAME *; cd ..;\
+ zip -r $$FIREFOX_SRC_NAME src/* Makefile yarn.lock package.json; \
+ echo "done"
+
+chrome: build
+ echo "building chrome package"
+ zip -r chrome-$(shell cat dist/manifest.json| jq -j .version).zip ./dist/*
+
+edge: build
+ echo "building edge package"
+ EDGE_PKG_NAME="omnivore-extension-edge-$(shell cat dist/manifest.json| jq -j .version).zip" ; \
+ pushd dist; zip -r $${EDGE_PKG_NAME} ./*; popd;
+
+
diff --git a/pkg/extension-v3/extension/background.js b/pkg/extension-v3/extension/background.js
new file mode 100644
index 000000000..57944e188
--- /dev/null
+++ b/pkg/extension-v3/extension/background.js
@@ -0,0 +1 @@
+(()=>{"use strict";var e,t=new Uint8Array(16);function n(){if(!e&&!(e="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return e(t)}const i=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;for(var r=[],o=0;o<256;++o)r.push((o+256).toString(16).substr(1));const s=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=(r[e[t+0]]+r[e[t+1]]+r[e[t+2]]+r[e[t+3]]+"-"+r[e[t+4]]+r[e[t+5]]+"-"+r[e[t+6]]+r[e[t+7]]+"-"+r[e[t+8]]+r[e[t+9]]+"-"+r[e[t+10]]+r[e[t+11]]+r[e[t+12]]+r[e[t+13]]+r[e[t+14]]+r[e[t+15]]).toLowerCase();if(!function(e){return"string"==typeof e&&i.test(e)}(n))throw TypeError("Stringified UUID is invalid");return n},a=function(e,t,i){var r=(e=e||{}).random||(e.rng||n)();if(r[6]=15&r[6]|64,r[8]=63&r[8]|128,t){i=i||0;for(var o=0;o<16;++o)t[i+o]=r[o];return t}return s(r)};const l=e=>{return t=void 0,n=void 0,r=function*(){return new Promise((t=>{chrome.storage.local.get(e,(n=>{const i=n&&n[e]||null;t(i)}))}))},new((i=void 0)||(i=Promise))((function(e,o){function s(e){try{l(r.next(e))}catch(e){o(e)}}function a(e){try{l(r.throw(e))}catch(e){o(e)}}function l(t){var n;t.done?e(t.value):(n=t.value,n instanceof i?n:new i((function(e){e(n)}))).then(s,a)}l((r=r.apply(t,n||[])).next())}));var t,n,i,r};var u=function(e,t,n,i){return new(n||(n=Promise))((function(r,o){function s(e){try{l(i.next(e))}catch(e){o(e)}}function a(e){try{l(i.throw(e))}catch(e){o(e)}}function l(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(s,a)}l((i=i.apply(e,t||[])).next())}))};const c=e=>u(void 0,void 0,void 0,(function*(){var t,n;const i=yield l("omnivoreApiKey");let r={Accept:"application/json","Content-Type":"application/json"};i&&(r.Authorization=i);try{const i=null!==(n=null!==(t=yield l("omnivoreApiUrl"))&&void 0!==t?t:process.env.OMNIVORE_GRAPHQL_URL)&&void 0!==n?n:"";console.log(i);const o=yield fetch(`${i}/api/graphql`,{method:"POST",redirect:"follow",credentials:"include",mode:"cors",headers:r,body:e}),s=yield o.json();if(!("data"in s)||!s.data)throw new Error("No response data");return s.data}catch(t){console.log("[omnivore] error making api request: ",e)}}));var d=function(e,t,n,i){return new(n||(n=Promise))((function(r,o){function s(e){try{l(i.next(e))}catch(e){o(e)}}function a(e){try{l(i.throw(e))}catch(e){o(e)}}function l(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(s,a)}l((i=i.apply(e,t||[])).next())}))};class g{constructor(){this.queue=[],this.isRunning=!1,this.isReady=!1,this.executeTask=e=>d(this,void 0,void 0,(function*(){var t,n;if(console.log("executing task: ",e),!this.libraryItemId)throw Error("Attempting to execute queue that is not ready.");try{let i=!1;switch(e.task){case"archive":yield(n=this.libraryItemId,u(void 0,void 0,void 0,(function*(){var e,t;const i=JSON.stringify({query:"mutation SetLinkArchived($input: ArchiveLinkInput!) {\n setLinkArchived(input: $input) {\n ... on ArchiveLinkSuccess {\n linkId\n message\n }\n ... on ArchiveLinkError {\n message\n errorCodes\n }\n }\n }\n ",variables:{input:{linkId:n,archived:!0}}}),r=yield c(i);return(null===(t=null===(e=r.setLinkArchived)||void 0===e?void 0:e.errorCodes)||void 0===t?void 0:t.length)?(console.log("[omnivore] api: error getting article:",r),r.setLinkArchived.errorCodes.indexOf("UNAUTHORIZED")>-1?(console.log("[omnivore] api is not authorized"),"unauthorized"):"failure"):"success"}))),i=!0;break;case"delete":yield function(e){return u(this,void 0,void 0,(function*(){const t=JSON.stringify({query:"mutation SetBookmarkArticle($input: SetBookmarkArticleInput!) {\n setBookmarkArticle(input: $input) {\n ... on SetBookmarkArticleSuccess {\n bookmarkedArticle {\n id\n }\n }\n ... on SetBookmarkArticleError {\n errorCodes\n }\n }\n }\n ",variables:{input:{articleID:e,bookmark:!1}}}),n=yield c(t);if(!n.setBookmarkArticle||n.setBookmarkArticle.errorCodes||!n.setBookmarkArticle.bookmarkedArticle)throw console.log("GQL Error deleting:",n),new Error("Error deleting.");return"success"}))}(this.libraryItemId),i=!0;break;case"addNote":yield function(e){var t,n,i,r;return u(this,void 0,void 0,(function*(){const o=JSON.stringify({query:"query GetArticle(\n $username: String!\n $slug: String!\n $includeFriendsHighlights: Boolean\n ) {\n article(username: $username, slug: $slug) {\n ... on ArticleSuccess {\n article {\n highlights(input: { includeFriends: $includeFriendsHighlights }) {\n ...HighlightFields\n }\n }\n }\n ... on ArticleError {\n errorCodes\n }\n }\n }\n fragment HighlightFields on Highlight {\n id\n type\n annotation\n }\n ",variables:{username:"me",slug:e.libraryItemId,includeFriendsHighlights:!1}}),s=yield c(o);if(null===(n=null===(t=s.article)||void 0===t?void 0:t.errorCodes)||void 0===n?void 0:n.length)return console.log("[omnivore] api: error getting article:",s),s.article.errorCodes.indexOf("UNAUTHORIZED")>-1?(console.log("[omnivore] api is not authorized"),"unauthorized"):"failure";console.log("DATA.ARTICLE: ",s.article);const l=null===(r=null===(i=s.article)||void 0===i?void 0:i.highlights)||void 0===r?void 0:r.find((e=>"NOTE"==e.type));if(l){const t=JSON.stringify({query:"\n mutation UpdateHighlight($input: UpdateHighlightInput!) {\n updateHighlight(input: $input) {\n ... on UpdateHighlightSuccess {\n highlight {\n id\n }\n }\n ... on UpdateHighlightError {\n errorCodes\n }\n }\n }\n ",variables:{input:{highlightId:l.id,annotation:l.annotation?l.annotation+"\n\n"+e.note:e.note}}}),n=yield c(t);return n.updateHighlight&&!n.updateHighlight.errorCodes&&n.updateHighlight.highlight?n.updateHighlight.highlight.id:void console.log("GQL Error updating note:",n)}{const t=a(),n=((e=21)=>crypto.getRandomValues(new Uint8Array(e)).reduce(((e,t)=>e+((t&=63)<36?t.toString(36):t<62?(t-26).toString(36).toUpperCase():t>62?"-":"_")),""))(8),i=JSON.stringify({query:"\n mutation CreateHighlight($input: CreateHighlightInput!) {\n createHighlight(input: $input) {\n ... on CreateHighlightSuccess {\n highlight {\n id\n }\n }\n ... on CreateHighlightError {\n errorCodes\n }\n }\n }\n ",variables:{input:{id:t,shortId:n,type:"NOTE",articleId:e.libraryItemId,annotation:e.note}}}),r=yield c(i);return r.createHighlight&&!r.createHighlight.errorCodes&&r.createHighlight.highlight?"success":(console.log("GQL Error setting note:",r),"failure")}}))}({note:e.note||"",libraryItemId:this.libraryItemId}),i=!0;break;case"setLabels":yield function(e,t){return u(this,void 0,void 0,(function*(){const n=JSON.stringify({query:"mutation SetLabels($input: SetLabelsInput!) {\n setLabels(input: $input) {\n ... on SetLabelsSuccess {\n labels {\n id\n name\n color\n }\n }\n ... on SetLabelsError {\n errorCodes\n }\n }\n }\n ",variables:{input:{pageId:e,labelIds:t}}}),i=yield c(n);if(!i.setLabels||i.setLabels.errorCodes||!i.setLabels.labels)throw console.log("GQL Error setting labels:",i),new Error("Error setting labels.");return i.setLabels.labels}))}(this.libraryItemId,null!==(t=e.labels)&&void 0!==t?t:[]),i=!0;break;case"editTitle":if(!e.title||!this.libraryItemId)throw new Error("Title not set, or library item not yet saved.");yield function(e,t){return u(this,void 0,void 0,(function*(){const n=JSON.stringify({query:"mutation UpdatePage($input: UpdatePageInput!) {\n updatePage(input: $input) {\n ... on UpdatePageSuccess {\n updatedPage {\n id\n }\n }\n ... on UpdatePageError {\n errorCodes\n }\n }\n }\n ",variables:{input:{pageId:e,title:t}}}),i=yield c(n);if(console.log(i),!i.updatePage||i.updatePage.errorCodes||!i.updatePage.updatedPage)throw console.log("GQL Error updating page:",i),new Error("Error updating title.");return"success"}))}(this.libraryItemId,e.title),i=!0;break;case"updateLabelCache":yield function(){return u(this,void 0,void 0,(function*(){const e=JSON.stringify({query:"query GetLabels {\n labels {\n ... on LabelsSuccess {\n labels {\n ...LabelFields\n }\n }\n ... on LabelsError {\n errorCodes\n }\n }\n }\n fragment LabelFields on Label {\n id\n name\n color\n description\n createdAt\n }\n "}),t=yield c(e);return t.labels&&!t.labels.errorCodes&&t.labels.labels?(yield(n={labels:t.labels.labels,labelsLastUpdated:(new Date).toISOString()},chrome.storage.local.set(n)),t.labels.labels):(console.log("GQL Error updating label cache response:",t,t),console.log(!t.labels,t.labels.errorCodes,!t.labels.labels),[]);var n}))}(),this.tabId&&chrome.tabs.sendMessage(this.tabId,{message:"updateLabelCache",status:"success",task:e.task})}i&&this.tabId&&chrome.tabs.sendMessage(this.tabId,{message:"updateToolbar",status:"success",task:e.task})}catch(t){console.log("[omnivore] task queue error: ",t),this.tabId&&(console.log("sending error message"),chrome.tabs.sendMessage(this.tabId,{message:"updateToolbar",status:"failure",task:e.task}))}}))}enqueue(e){this.queue.push(e),this.isReady&&this.runNext()}runNext(){return d(this,void 0,void 0,(function*(){if(this.isRunning||0===this.queue.length||!this.isReady)return;this.isRunning=!0;const e=this.queue.shift();if(e)try{yield this.executeTask(e)}catch(e){console.error("Task failed:",e)}finally{this.isRunning=!1,this.isReady&&this.runNext()}}))}setReady(e,t){console.log("setting ready"),this.tabId=e,this.libraryItemId=t,this.isReady=!0,this.runNext()}}var h=function(e,t,n,i){return new(n||(n=Promise))((function(r,o){function s(e){try{l(i.next(e))}catch(e){o(e)}}function a(e){try{l(i.throw(e))}catch(e){o(e)}}function l(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(s,a)}l((i=i.apply(e,t||[])).next())}))};const p="undefined"!=typeof browser?browser:chrome,v={};chrome.runtime.onMessage.addListener(((e,t,n)=>h(void 0,void 0,void 0,(function*(){var n,i;console.log("message: ",e,"sender",null===(n=t.tab)||void 0===n?void 0:n.id);const r=null===(i=t.tab)||void 0===i?void 0:i.id;if("savePage"===e.action&&"object"==typeof(o=e)&&"string"==typeof o.url&&"string"==typeof o.title&&"string"==typeof o.clientRequestId&&"string"==typeof o.originalContent){const{result:t,libraryItemId:n}=yield function(e){var t,n,i;return u(this,void 0,void 0,(function*(){const r=JSON.stringify({query:"mutation SavePage ($input: SavePageInput!) {\n savePage(input:$input){\n ... on SaveSuccess {\n url\n clientRequestId\n }\n ... on SaveError {\n errorCodes\n }\n }\n }",variables:{input:Object.assign({source:"extension"},e)}}),o=yield c(r);return(null===(n=null===(t=o.savePage)||void 0===t?void 0:t.errorCodes)||void 0===n?void 0:n.length)?(console.log("[omnivore] api: error saving page:",o),o.savePage.errorCodes.indexOf("UNAUTHORIZED")>-1?(console.log("[omnivore] api is not authorized"),{result:"unauthorized"}):{result:"failure"}):{result:"success",libraryItemId:null===(i=o.savePage)||void 0===i?void 0:i.clientRequestId}}))}({url:e.url,title:e.title,clientRequestId:e.clientRequestId,originalContent:e.originalContent});if(console.log("result: ",t,"libraryItemId",n),r)switch(t){case"success":chrome.tabs.sendMessage(r,{message:"startToolbarDismiss",status:"success"}),n&&v[e.clientRequestId].setReady(r,n);break;case"failure":chrome.tabs.sendMessage(r,{message:"startToolbarDismiss",status:"failure"});break;case"unauthorized":chrome.tabs.sendMessage(r,{message:"showLoggedOutToolbar"})}}else if("enqueueTask"==e.action&&function(e){return"object"==typeof e&&"string"==typeof e.task&&"string"==typeof e.clientRequestId}(e)){const t=v[e.clientRequestId];console.log("enqueing task message: ",e),t.enqueue(Object.assign(Object.assign({},e),{libraryItemId:e.clientRequestId}))}var o;return!0})))),p.action.onClicked.addListener((e=>h(void 0,void 0,void 0,(function*(){const t=e.id;if(t){try{const e=yield(e=>h(void 0,void 0,void 0,(function*(){try{const t=yield chrome.tabs.sendMessage(e,{message:"ping"});return console.log("pingCheck: ",t),!0}catch(e){return!1}})))(t);e||(yield chrome.scripting.executeScript({target:{tabId:t},files:["content.js"]}))}catch(e){console.log("[omnivore] error injecting content script: ",e)}const e=a();v[e]=new g,chrome.tabs.sendMessage(t,{message:"savePage",clientRequestId:e})}}))))})();
\ No newline at end of file
diff --git a/pkg/extension-v3/extension/content.js b/pkg/extension-v3/extension/content.js
new file mode 100644
index 000000000..69945df3e
--- /dev/null
+++ b/pkg/extension-v3/extension/content.js
@@ -0,0 +1 @@
+(()=>{"use strict";var e={322:(e,o,t)=>{t.d(o,{W:()=>s});var n=t(454),i=t(782),r=function(e,o,t,n){return new(t||(t=Promise))((function(i,r){function s(e){try{l(n.next(e))}catch(e){r(e)}}function d(e){try{l(n.throw(e))}catch(e){r(e)}}function l(e){var o;e.done?i(e.value):(o=e.value,o instanceof t?o:new t((function(e){e(o)}))).then(s,d)}l((n=n.apply(e,o||[])).next())}))};function s(){var e;return r(this,void 0,void 0,(function*(){(0,i.ig)();const o=document.querySelector("#omnivore-extension-root");if(!o||!o.shadowRoot)return void console.log("no statusBox to update");let t=yield(0,n.$)("labels").then((e=>{if(e&&e.length>0)return e}));(0,i.jC)("#omnivore-edit-labels-row"),null===(e=o.shadowRoot.querySelector("#omnivore-edit-label-input"))||void 0===e||e.focus();const r=o.shadowRoot.querySelector("#omnivore-edit-labels-list");if(!t)return console.error("No labels found, trying to update the cache."),void chrome.runtime.sendMessage({action:"enqueueTask",task:"updateLabelCache",clientRequestId:(0,i.mm)()});r&&(r.innerHTML="",t.sort(((e,o)=>e.name.localeCompare(o.name,void 0,{sensitivity:"base"}))).forEach((function(e,o){const t=function(e){const o=document.createElement("button"),t=document.createElement("span");t.style="width:10px;height:10px;border-radius:1000px;",t.style.setProperty("background-color",e.color);const n=document.createElement("span");n.style="margin-left: 10px;pointer-events: none;",n.innerText=e.name;const i=document.createElement("span");return i.style="margin-left: auto;pointer-events: none;",i.className="checkbox",i.innerHTML='\n \n ',o.appendChild(t),o.appendChild(n),o.appendChild(i),o.onclick=d,o.setAttribute("data-label-id",e.id),o.setAttribute("data-label-selected",e.selected?"on":"off"),o.setAttribute("tabIndex","-1"),o}(e);r.appendChild(t)})))}))}function d(e){var o;e.preventDefault();const t=null===(o=e.target)||void 0===o?void 0:o.getAttribute("data-label-id");t&&function(e,o){var t,s;r(this,void 0,void 0,(function*(){const r=null===(t=e.target)||void 0===t?void 0:t.getAttribute("data-label-selected");if(!o||!r)return;const d="on"!=r;null===(s=e.target)||void 0===s||s.setAttribute("data-label-selected",d?"on":"off");let l=yield(0,n.$)("labels").then((e=>{if(e&&e.length>0)return e}));if(!l)throw Error("No labels selected");l.find((e=>e.id===o))&&function(){var e;(0,i.PC)("#omnivore-edit-labels-status","waiting","Updating Labels...",void 0);const o=document.querySelector("#omnivore-extension-root"),t=null===(e=null==o?void 0:o.shadowRoot)||void 0===e?void 0:e.querySelector("#omnivore-edit-labels-list");if(t){const e=[...t.children].filter((e=>"on"===e.getAttribute("data-label-selected"))).map((e=>e.getAttribute("data-label-id")));chrome.runtime.sendMessage({action:"enqueueTask",task:"setLabels",clientRequestId:(0,i.mm)(),labels:e})}}()}))}(e,t)}},782:(e,o,t)=>{t.d(o,{Dz:()=>l,Jw:()=>m,PC:()=>f,Wj:()=>c,ig:()=>a,jC:()=>h,ll:()=>d,mm:()=>p});var n=t(454),i=t(322),r=function(e,o,t,n){return new(t||(t=Promise))((function(i,r){function s(e){try{l(n.next(e))}catch(e){r(e)}}function d(e){try{l(n.throw(e))}catch(e){r(e)}}function l(e){var o;e.done?i(e.value):(o=e.value,o instanceof t?o:new t((function(e){e(o)}))).then(s,d)}l((n=n.apply(e,o||[])).next())}))};const s={waiting:'
',success:'\n ',failure:'\n \n '},d=e=>r(void 0,void 0,void 0,(function*(){var o;let t=null!==(o=document.querySelector("#omnivore-extension-root"))&&void 0!==o?o:void 0;document.body&&(console.log("existing currentToastEl: ",t),t||(t=yield r(void 0,void 0,void 0,(function*(){console.log("===== CREATING TOAST CONTAINER ===== ");const e=yield fetch(chrome.runtime.getURL("views/toast.html")),o=yield e.text(),t=document.createElement("div");if(t.tabIndex=0,t.id="omnivore-extension-root",t.attachShadow({mode:"open"}),t.style.opacity="1.0",!t.shadowRoot)return void alert("Error opening Omnivore user interface.");t.shadowRoot.innerHTML="";const n=document.createElement("div");return n.id="#omnivore-toast",n.innerHTML=o,n.tabIndex=0,t.shadowRoot.appendChild(n),document.body.appendChild(t),u(t),l("waiting"),t}))),(yield(0,n.$)("disableAutoDismiss"))&&(null==t||t.setAttribute("data-disable-auto-dismiss","true")),null==t||t.setAttribute("data-omnivore-client-request-id",e),null==t||t.focus({preventScroll:!0}),l("waiting"))})),l=(e,o=void 0)=>r(void 0,void 0,void 0,(function*(){var t;const n=document.querySelector("#omnivore-extension-root"),i=null===(t=null==n?void 0:n.shadowRoot)||void 0===t?void 0:t.querySelector(".omnivore-toast-statusBox");if(console.log("updating",e,i),i)switch(e){case"success":i.innerHTML=s.success;break;case"failure":i.innerHTML=s.failure;break;case"waiting":i.innerHTML=s.waiting}o&&("addNote"==o&&"failure"==e&&f("#omnivore-add-note-status","failure","Error adding note...",void 0),"addNote"==o&&"success"==e&&(f("#omnivore-add-note-status","success","Note updated.",2500),setTimeout((()=>{h("#omnivore-add-note-status")}),3e3)),"setLabels"==o&&"success"==e&&f("#omnivore-edit-labels-status","success","Labels Updated",2500),"setLabels"==o&&"failure"==e&&f("#omnivore-edit-labels-status","failure","Error Updating Labels...",2500),"editTitle"==o&&"failure"==e&&f("#omnivore-add-note-status","failure","Error updating title...",void 0),"editTitle"==o&&"success"==e&&(f("omnivore-edit-title-status","success","Title updated.",2500),setTimeout((()=>{h("#omnivore-edit-title-status")}),3e3)),"archive"==o&&(f("#omnivore-extra-status",e,"success"==e?"Success":"Error","success"==e?2500:void 0),"success"==e&&q()))})),a=()=>{const e=document.querySelector("#omnivore-extension-root");e&&e.setAttribute("data-disable-auto-dismiss","true")},c=e=>r(void 0,void 0,void 0,(function*(){e.status&&l(e.status);const o=yield r(void 0,void 0,void 0,(function*(){var e;const o=null!==(e=yield(0,n.$)("autoDismissTime"))&&void 0!==e?e:"2500";return Number.isNaN(Number(o))?2500:Number(o)}));setTimeout((()=>{const e=document.querySelector("#omnivore-extension-root");e&&!e.getAttribute("data-disable-auto-dismiss")&&(e.style.transition="opacity 3.5s ease;",e.style.opacity="0",setTimeout((()=>{const e=document.querySelector("#omnivore-extension-root");e&&!e.getAttribute("data-disable-auto-dismiss")&&e.remove()}),500))}),o)})),u=e=>{var o,t,n;const r=[{id:"#omnivore-toast-add-note-btn",func:w},{id:"#omnivore-toast-edit-title-btn",func:v},{id:"#omnivore-toast-edit-labels-btn",func:i.W},{id:"#omnivore-toast-read-now-btn",func:L},{id:"#omnivore-open-menu-btn",func:y},{id:"#omnivore-toast-close-btn",func:R},{id:"#omnivore-toast-login-btn",func:b},{id:"#omnivore-toast-archive-btn",func:x},{id:"#omnivore-toast-delete-btn",func:C}];for(const t of r){const n=null===(o=e.shadowRoot)||void 0===o?void 0:o.querySelector(t.id);n&&(console.log(t.id),n.addEventListener("click",t.func))}if(window.matchMedia("(max-width: 500px)").matches){const o=null===(t=e.shadowRoot)||void 0===t?void 0:t.querySelectorAll(".omnivore-top-button-label");null==o||o.forEach((e=>{e.style.display="none"}));const i=null===(n=e.shadowRoot)||void 0===n?void 0:n.querySelector("#omnivore-toast-container");i&&(i.style.width="280px",i.style.top="unset",i.style.bottom="20px")}};function v(){var e,o,t;a(),h("#omnivore-edit-title-row");let n=null!==(e=document.querySelector("#omnivore-extension-root"))&&void 0!==e?e:void 0;if(!n)return void console.log("no statusBox to update");const i=null===(o=null==n?void 0:n.shadowRoot)||void 0===o?void 0:o.querySelector("#omnivore-edit-title-textarea");i&&(i.focus(),i.onkeydown=e=>{e.cancelBubble=!0});const r=null===(t=null==n?void 0:n.shadowRoot)||void 0===t?void 0:t.querySelector("#omnivore-edit-title-form");r?r.onsubmit=e=>{var o;f("#omnivore-edit-title-status","waiting","Updating title...",void 0);const t=null!==(o=null==i?void 0:i.value)&&void 0!==o?o:"";chrome.runtime.sendMessage({action:"enqueueTask",task:"editTitle",clientRequestId:p(),title:t}),e.preventDefault()}:console.log("no form to update")}const m=()=>{a(),l("failure"),h("#omnivore-logged-out-row"),g(),f("#omnivore-logged-out-status",void 0,"You are not logged in.",void 0)},f=(e,o,t,n)=>{var i;const r=document.querySelector("#omnivore-extension-root"),d=null===(i=null==r?void 0:r.shadowRoot)||void 0===i?void 0:i.querySelector(e),l=(()=>{switch(o){case"waiting":return s.animatedLoader;case"success":return s.success;case"failure":return s.failure;default:return}})();if(l&&d){const e="failure"==o?"red":"unset";d.innerHTML=`${l}${t}`}else d&&(d.innerHTML=t);n&&d&&setTimeout((()=>{d.innerHTML=""}),n)},g=()=>{let e=document.querySelector("#omnivore-extension-root");["#omnivore-toast-edit-title-btn","#omnivore-toast-edit-labels-btn","#omnivore-toast-read-now-btn","#omnivore-toast-add-note-btn","#omnivore-open-menu-btn"].forEach((o=>{var t;const n=null===(t=null==e?void 0:e.shadowRoot)||void 0===t?void 0:t.querySelector(o);n&&(n.disabled=!0)}))},h=e=>{var o,t;let n=document.querySelector("#omnivore-extension-root");if(!n)return void console.log("toggleRow: no row to toggle");const i=null===(o=null==n?void 0:n.shadowRoot)||void 0===o?void 0:o.querySelector(e),r=null==i?void 0:i.getAttribute("data-state"),s=null===(t=null==n?void 0:n.shadowRoot)||void 0===t?void 0:t.querySelectorAll(".omnivore-toast-func-row");if(null==s||s.forEach((e=>{e.setAttribute("data-state","closed")})),i&&r){const e="open"===r?"closed":"open";i.setAttribute("data-state",e)}},p=()=>{const e=document.querySelector("#omnivore-extension-root");return null==e?void 0:e.getAttribute("data-omnivore-client-request-id")},b=()=>{window.open(new URL("/login",process.env.OMNIVORE_URL),"_blank"),q()},y=()=>{a(),h("#omnivore-extra-buttons-row")},w=()=>r(void 0,void 0,void 0,(function*(){var e,o,t;console.log("[omnivore] adding note"),a();const i=document.querySelector("#omnivore-extension-root"),s=null==i?void 0:i.getAttribute("data-omnivore-client-request-id");if(console.log("client request id: ",s),!s)return void f("#omnivore-add-note-status","failure","Error adding note...",void 0);const d=`cached-note-${document.location.href}`;a(),h("#omnivore-add-note-row");const l=null===(e=null==i?void 0:i.shadowRoot)||void 0===e?void 0:e.querySelector("#omnivore-add-note-textarea");if(l){if(d){const e=null!==(o=yield(0,n.$)(d))&&void 0!==o?o:"";l.value=e}l.value?l.select():l.focus(),l.addEventListener("input",(e=>r(void 0,void 0,void 0,(function*(){const o={};o[d]=e.target.value,yield(0,n.c)(o)})))),l.onkeydown=e=>r(void 0,void 0,void 0,(function*(){e.stopPropagation(),console.log("handling the enter key: ",e.keyCode),13==e.keyCode&&(e.metaKey||e.ctrlKey)&&(f("#omnivore-add-note-status","waiting","Adding note...",void 0),yield T(s,l.value))}))}const c=null===(t=null==i?void 0:i.shadowRoot)||void 0===t?void 0:t.querySelector("#omnivore-add-note-form");c&&(c.onsubmit=e=>r(void 0,void 0,void 0,(function*(){console.log("handling form submit"),f("#omnivore-add-note-status","waiting","Adding note...",void 0),l&&(yield T(s,l.value)),e.preventDefault(),e.stopPropagation()})))})),x=e=>r(void 0,void 0,void 0,(function*(){const o=p();try{yield chrome.runtime.sendMessage({action:"enqueueTask",task:"archive",clientRequestId:o})}catch(e){console.log("error archiving item")}e.preventDefault()})),C=e=>r(void 0,void 0,void 0,(function*(){const o=p();try{yield chrome.runtime.sendMessage({action:"enqueueTask",task:"delete",clientRequestId:o})}catch(e){console.log("error archiving item")}e.preventDefault()})),L=()=>r(void 0,void 0,void 0,(function*(){var e;a();let o=document.querySelector("#omnivore-extension-root");const t=null===(e=null==o?void 0:o.shadowRoot)||void 0===e?void 0:e.querySelector("#omnivore-toast-container");null==t||t.setAttribute("data-state","open"),window.open(new URL(`/article?url=${encodeURI(document.location.href)}`,yield(0,n.$)("omnivoreUrl")),"_blank"),q()})),q=()=>{setTimeout((()=>{R()}),1e3)},R=()=>{const e=document.querySelector("#omnivore-extension-root");e&&e.remove()},T=(e,o)=>r(void 0,void 0,void 0,(function*(){try{yield chrome.runtime.sendMessage({action:"enqueueTask",task:"addNote",note:o,clientRequestId:e})}catch(e){console.log("error adding note: ",e)}}))},454:(e,o,t)=>{t.d(o,{$:()=>n,c:()=>i});const n=e=>{return o=void 0,t=void 0,i=function*(){return new Promise((o=>{chrome.storage.local.get(e,(t=>{const n=t&&t[e]||null;o(n)}))}))},new((n=void 0)||(n=Promise))((function(e,r){function s(e){try{l(i.next(e))}catch(e){r(e)}}function d(e){try{l(i.throw(e))}catch(e){r(e)}}function l(o){var t;o.done?e(o.value):(t=o.value,t instanceof n?t:new n((function(e){e(t)}))).then(s,d)}l((i=i.apply(o,t||[])).next())}));var o,t,n,i},i=e=>chrome.storage.local.set(e)}},o={};function t(n){var i=o[n];if(void 0!==i)return i.exports;var r=o[n]={exports:{}};return e[n](r,r.exports,t),r.exports}t.d=(e,o)=>{for(var n in o)t.o(o,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:o[n]})},t.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o);var n=t(782),i=t(322),r=function(e,o,t,n){return new(t||(t=Promise))((function(i,r){function s(e){try{l(n.next(e))}catch(e){r(e)}}function d(e){try{l(n.throw(e))}catch(e){r(e)}}function l(e){var o;e.done?i(e.value):(o=e.value,o instanceof t?o:new t((function(e){e(o)}))).then(s,d)}l((n=n.apply(e,o||[])).next())}))};const s=(e,o,t)=>r(void 0,void 0,void 0,(function*(){switch(console.log("[omnivore] content script message:",e),e.message){case"showLoggedOutToolbar":(0,n.Jw)(),t({success:!0});break;case"updateToolbar":(0,n.Dz)(e.status,e.task),t({success:!0});break;case"startToolbarDismiss":return(0,n.Wj)(e),void t({success:!0});case"updateLabelCache":(0,i.W)();break;default:return void t({success:!1})}}));chrome.runtime.onMessage.addListener(((e,o,t)=>r(void 0,void 0,void 0,(function*(){if("savePage"===e.message)return yield(o=e.clientRequestId,r(void 0,void 0,void 0,(function*(){console.log("[omnivore] v3 extension triggered: ",o),yield(e=>r(void 0,void 0,void 0,(function*(){chrome.runtime.onMessage.hasListener(s)||chrome.runtime.onMessage.addListener(s),yield(0,n.ll)(e)})))(o);const e=yield r(void 0,void 0,void 0,(function*(){const e=document.documentElement.outerHTML;return console.log("[omnivore] captured mainContent"),e}));console.log("[omnivore] collected page content: ",e);try{const t={clientRequestId:o,title:document.title,url:document.location.href,originalContent:e};yield chrome.runtime.sendMessage(Object.assign({action:"savePage"},t))}catch(e){console.log("error sending content: ",e)}}))),void t({success:!0});var o}))))})();
\ No newline at end of file
diff --git a/pkg/extension-v3/extension/icons/icon-128.png b/pkg/extension-v3/extension/icons/icon-128.png
new file mode 100644
index 000000000..d6df58ae7
Binary files /dev/null and b/pkg/extension-v3/extension/icons/icon-128.png differ
diff --git a/pkg/extension-v3/extension/icons/icon-16.png b/pkg/extension-v3/extension/icons/icon-16.png
new file mode 100644
index 000000000..5c0b8ad30
Binary files /dev/null and b/pkg/extension-v3/extension/icons/icon-16.png differ
diff --git a/pkg/extension-v3/extension/icons/icon-19.png b/pkg/extension-v3/extension/icons/icon-19.png
new file mode 100644
index 000000000..9c5f2a8ee
Binary files /dev/null and b/pkg/extension-v3/extension/icons/icon-19.png differ
diff --git a/pkg/extension-v3/extension/icons/icon-24.png b/pkg/extension-v3/extension/icons/icon-24.png
new file mode 100644
index 000000000..a0654130b
Binary files /dev/null and b/pkg/extension-v3/extension/icons/icon-24.png differ
diff --git a/pkg/extension-v3/extension/icons/icon-256.png b/pkg/extension-v3/extension/icons/icon-256.png
new file mode 100644
index 000000000..a4413ba25
Binary files /dev/null and b/pkg/extension-v3/extension/icons/icon-256.png differ
diff --git a/pkg/extension-v3/extension/icons/icon-32.png b/pkg/extension-v3/extension/icons/icon-32.png
new file mode 100644
index 000000000..2270c0762
Binary files /dev/null and b/pkg/extension-v3/extension/icons/icon-32.png differ
diff --git a/pkg/extension-v3/extension/icons/icon-38.png b/pkg/extension-v3/extension/icons/icon-38.png
new file mode 100644
index 000000000..1041a3f2a
Binary files /dev/null and b/pkg/extension-v3/extension/icons/icon-38.png differ
diff --git a/pkg/extension-v3/extension/icons/icon-48.png b/pkg/extension-v3/extension/icons/icon-48.png
new file mode 100644
index 000000000..dda13bd4e
Binary files /dev/null and b/pkg/extension-v3/extension/icons/icon-48.png differ
diff --git a/pkg/extension-v3/extension/icons/icon-70.png b/pkg/extension-v3/extension/icons/icon-70.png
new file mode 100644
index 000000000..41337352f
Binary files /dev/null and b/pkg/extension-v3/extension/icons/icon-70.png differ
diff --git a/pkg/extension-v3/extension/icons/icon-96.png b/pkg/extension-v3/extension/icons/icon-96.png
new file mode 100644
index 000000000..3af64b0be
Binary files /dev/null and b/pkg/extension-v3/extension/icons/icon-96.png differ
diff --git a/pkg/extension-v3/extension/images/highlight.svg b/pkg/extension-v3/extension/images/highlight.svg
new file mode 100644
index 000000000..e81aef802
--- /dev/null
+++ b/pkg/extension-v3/extension/images/highlight.svg
@@ -0,0 +1,14 @@
+
diff --git a/pkg/extension-v3/extension/images/label.svg b/pkg/extension-v3/extension/images/label.svg
new file mode 100644
index 000000000..69fca4c19
--- /dev/null
+++ b/pkg/extension-v3/extension/images/label.svg
@@ -0,0 +1,12 @@
+
diff --git a/pkg/extension-v3/extension/images/listen-hover.svg b/pkg/extension-v3/extension/images/listen-hover.svg
new file mode 100644
index 000000000..0a8fe4a55
--- /dev/null
+++ b/pkg/extension-v3/extension/images/listen-hover.svg
@@ -0,0 +1,11 @@
+
diff --git a/pkg/extension-v3/extension/images/listen.svg b/pkg/extension-v3/extension/images/listen.svg
new file mode 100644
index 000000000..f77b5bb33
--- /dev/null
+++ b/pkg/extension-v3/extension/images/listen.svg
@@ -0,0 +1,11 @@
+
diff --git a/pkg/extension-v3/extension/images/pause-w-circle.svg b/pkg/extension-v3/extension/images/pause-w-circle.svg
new file mode 100644
index 000000000..c8f4c1082
--- /dev/null
+++ b/pkg/extension-v3/extension/images/pause-w-circle.svg
@@ -0,0 +1,3 @@
+
diff --git a/pkg/extension-v3/extension/images/playback-speed.svg b/pkg/extension-v3/extension/images/playback-speed.svg
new file mode 100644
index 000000000..41dab8b19
--- /dev/null
+++ b/pkg/extension-v3/extension/images/playback-speed.svg
@@ -0,0 +1,5 @@
+
diff --git a/pkg/extension-v3/extension/images/playback-voice.svg b/pkg/extension-v3/extension/images/playback-voice.svg
new file mode 100644
index 000000000..c7b1670bd
--- /dev/null
+++ b/pkg/extension-v3/extension/images/playback-voice.svg
@@ -0,0 +1,15 @@
+
diff --git a/pkg/extension-v3/extension/images/save.svg b/pkg/extension-v3/extension/images/save.svg
new file mode 100644
index 000000000..7deeb4d1f
--- /dev/null
+++ b/pkg/extension-v3/extension/images/save.svg
@@ -0,0 +1,11 @@
+
diff --git a/pkg/extension-v3/extension/manifest.json b/pkg/extension-v3/extension/manifest.json
new file mode 100644
index 000000000..fe2a13dca
--- /dev/null
+++ b/pkg/extension-v3/extension/manifest.json
@@ -0,0 +1,42 @@
+{
+ "manifest_version": 3,
+ "name": "Omnivore - Self Hosted",
+ "version": "3.0",
+ "description": "Save content to your Omnivore library.",
+ "permissions": [
+ "activeTab",
+ "scripting",
+ "storage"
+ ],
+ "host_permissions": [
+ ""
+ ],
+ "background": {
+ "service_worker": "background.js",
+ "scripts": ["background.js"]
+ },
+ "web_accessible_resources": [
+ {
+ "resources": [
+ "images/*.svg",
+ "views/toast.html"
+ ],
+ "matches": [
+ ""
+ ]
+ }
+ ],
+ "options_page": "views/options.html",
+ "action": {
+ "default_icon": {
+ "16": "icons/icon-16.png",
+ "48": "icons/icon-48.png",
+ "128": "icons/icon-128.png"
+ }
+ },
+ "icons": {
+ "16": "icons/icon-16.png",
+ "48": "icons/icon-48.png",
+ "128": "icons/icon-128.png"
+ }
+}
diff --git a/pkg/extension-v3/extension/toolbar.js b/pkg/extension-v3/extension/toolbar.js
new file mode 100644
index 000000000..3fc659d3a
--- /dev/null
+++ b/pkg/extension-v3/extension/toolbar.js
@@ -0,0 +1 @@
+(()=>{"use strict";var r={322:(r,e,t)=>{t(454),t(782)},782:(r,e,t)=>{t(454),t(322)},454:(r,e,t)=>{}},e={};function t(o){var n=e[o];if(void 0!==n)return n.exports;var p=e[o]={exports:{}};return r[o](p,p.exports,t),p.exports}t.d=(r,e)=>{for(var o in e)t.o(e,o)&&!t.o(r,o)&&Object.defineProperty(r,o,{enumerable:!0,get:e[o]})},t.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),t(782)})();
\ No newline at end of file
diff --git a/pkg/extension-v3/extension/views/options.html b/pkg/extension-v3/extension/views/options.html
new file mode 100644
index 000000000..dd4838e6b
--- /dev/null
+++ b/pkg/extension-v3/extension/views/options.html
@@ -0,0 +1,36 @@
+
+
+
+ Omnivore Extension Settings
+
+
+
+
+
API Key
+
+
+
+
+
+
+
+
+
+
+
API URL
+
+
+
+
+
+
+
OMNIVORE URL
+
+
+
+
+
+
+
+
+
diff --git a/pkg/extension-v3/extension/views/options.js b/pkg/extension-v3/extension/views/options.js
new file mode 100644
index 000000000..357e8dab3
--- /dev/null
+++ b/pkg/extension-v3/extension/views/options.js
@@ -0,0 +1 @@
+function addStorage(e){return chrome.storage.local.set(e)}document.addEventListener("DOMContentLoaded",(()=>{const e=document.getElementById("save-api-key-btn"),t=document.getElementById("api-key");chrome.storage.local.get("omnivoreApiKey").then((e=>{t.value=e.omnivoreApiKey??""})),e.addEventListener("click",(e=>{addStorage({omnivoreApiKey:t.value})}));const o=document.getElementById("save-api-url-btn"),n=document.getElementById("api-url");chrome.storage.local.get("omnivoreApiUrl").then((e=>{n.value=e.omnivoreApiUrl??""})),o.addEventListener("click",(e=>{addStorage({omnivoreApiUrl:n.value})}));const r=document.getElementById("save-omnivore-url-btn"),a=document.getElementById("omnivore-url");chrome.storage.local.get("omnivoreUrl").then((e=>{a.value=e.omnivoreUrl??""})),r.addEventListener("click",(e=>{addStorage({omnivoreUrl:a.value})}))}));
\ No newline at end of file
diff --git a/pkg/extension-v3/extension/views/toast.html b/pkg/extension-v3/extension/views/toast.html
new file mode 100644
index 000000000..adc760304
--- /dev/null
+++ b/pkg/extension-v3/extension/views/toast.html
@@ -0,0 +1,555 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/extension-v3/firefox-3.0-src.zip b/pkg/extension-v3/firefox-3.0-src.zip
new file mode 100644
index 000000000..019f0541b
Binary files /dev/null and b/pkg/extension-v3/firefox-3.0-src.zip differ
diff --git a/pkg/extension-v3/firefox-3.0.zip b/pkg/extension-v3/firefox-3.0.zip
new file mode 100644
index 000000000..51860ce13
Binary files /dev/null and b/pkg/extension-v3/firefox-3.0.zip differ
diff --git a/pkg/extension-v3/package.json b/pkg/extension-v3/package.json
new file mode 100644
index 000000000..e0e510b58
--- /dev/null
+++ b/pkg/extension-v3/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "omnivore-extension",
+ "version": "1.0.0",
+ "description": "browser extension for omnivore",
+ "scripts": {
+ "build": "env webpack --mode production",
+ "watch": "webpack --mode development --watch"
+ },
+ "devDependencies": {
+ "@types/chrome": "^0.0.268",
+ "@types/dompurify": "^3.0.5",
+ "@types/firefox-webext-browser": "^120.0.4",
+ "clean-webpack-plugin": "^4.0.0",
+ "copy-webpack-plugin": "^12.0.2",
+ "dotenv-webpack": "^8.1.0",
+ "ts-loader": "^8.0.0",
+ "typescript": "^4.0.0",
+ "webpack": "^5.0.0",
+ "webpack-cli": "^4.0.0"
+ },
+ "dependencies": {
+ "nanoid": "^4.0.2",
+ "uuid": "^8.3.2"
+ }
+}
diff --git a/pkg/extension-v3/src/background.ts b/pkg/extension-v3/src/background.ts
new file mode 100644
index 000000000..0024400f2
--- /dev/null
+++ b/pkg/extension-v3/src/background.ts
@@ -0,0 +1,90 @@
+import { v4 as uuidv4 } from 'uuid'
+import { addNoteToLibraryItem, savePageRequest } from './scripts/omnivore-api'
+import {
+ isAddNoteInput,
+ isEnqueueTaskMessage,
+ isSavePageInput,
+} from './scripts/types'
+import { TaskQueue } from './task-queue'
+
+const browserAPI = typeof browser !== 'undefined' ? browser : chrome
+
+const taskQueues: Record
= {}
+
+chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
+ console.log('message: ', message, 'sender', sender.tab?.id)
+
+ const tabId = sender.tab?.id
+ if (message.action === 'savePage' && isSavePageInput(message)) {
+ const { result, libraryItemId } = await savePageRequest({
+ url: message.url,
+ title: message.title,
+ clientRequestId: message.clientRequestId,
+ originalContent: message.originalContent,
+ })
+ console.log('result: ', result, 'libraryItemId', libraryItemId)
+ if (tabId) {
+ switch (result) {
+ case 'success':
+ chrome.tabs.sendMessage(tabId, {
+ message: 'startToolbarDismiss',
+ status: 'success',
+ })
+ if (libraryItemId) {
+ const taskQueue = taskQueues[message.clientRequestId]
+ taskQueue.setReady(tabId, libraryItemId)
+ }
+ break
+ case 'failure':
+ chrome.tabs.sendMessage(tabId, {
+ message: 'startToolbarDismiss',
+ status: 'failure',
+ })
+ break
+ case 'unauthorized':
+ chrome.tabs.sendMessage(tabId, {
+ message: 'showLoggedOutToolbar',
+ })
+ break
+ }
+ }
+ } else if (message.action == 'enqueueTask' && isEnqueueTaskMessage(message)) {
+ const taskQueue = taskQueues[message.clientRequestId]
+ console.log('enqueing task message: ', message)
+ taskQueue.enqueue({ ...message, libraryItemId: message.clientRequestId })
+ }
+ return true
+})
+
+const scriptsAlreadyLoaded = async (tabId: number) => {
+ try {
+ const pingCheck = await chrome.tabs.sendMessage(tabId, {
+ message: 'ping',
+ })
+ console.log('pingCheck: ', pingCheck)
+ return true
+ } catch {
+ return false
+ }
+}
+
+browserAPI.action.onClicked.addListener(async (tab) => {
+ const tabId = tab.id
+ if (tabId) {
+ try {
+ const scriptsLoaded = await scriptsAlreadyLoaded(tabId)
+ if (!scriptsLoaded) {
+ await chrome.scripting.executeScript({
+ target: { tabId },
+ files: ['content.js'],
+ })
+ }
+ } catch (err) {
+ console.log('[omnivore] error injecting content script: ', err)
+ }
+
+ const clientRequestId = uuidv4()
+ taskQueues[clientRequestId] = new TaskQueue()
+ chrome.tabs.sendMessage(tabId, { message: 'savePage', clientRequestId })
+ }
+})
diff --git a/pkg/extension-v3/src/icons/icon-128.png b/pkg/extension-v3/src/icons/icon-128.png
new file mode 100644
index 000000000..d6df58ae7
Binary files /dev/null and b/pkg/extension-v3/src/icons/icon-128.png differ
diff --git a/pkg/extension-v3/src/icons/icon-16.png b/pkg/extension-v3/src/icons/icon-16.png
new file mode 100644
index 000000000..5c0b8ad30
Binary files /dev/null and b/pkg/extension-v3/src/icons/icon-16.png differ
diff --git a/pkg/extension-v3/src/icons/icon-19.png b/pkg/extension-v3/src/icons/icon-19.png
new file mode 100644
index 000000000..9c5f2a8ee
Binary files /dev/null and b/pkg/extension-v3/src/icons/icon-19.png differ
diff --git a/pkg/extension-v3/src/icons/icon-24.png b/pkg/extension-v3/src/icons/icon-24.png
new file mode 100644
index 000000000..a0654130b
Binary files /dev/null and b/pkg/extension-v3/src/icons/icon-24.png differ
diff --git a/pkg/extension-v3/src/icons/icon-256.png b/pkg/extension-v3/src/icons/icon-256.png
new file mode 100644
index 000000000..a4413ba25
Binary files /dev/null and b/pkg/extension-v3/src/icons/icon-256.png differ
diff --git a/pkg/extension-v3/src/icons/icon-32.png b/pkg/extension-v3/src/icons/icon-32.png
new file mode 100644
index 000000000..2270c0762
Binary files /dev/null and b/pkg/extension-v3/src/icons/icon-32.png differ
diff --git a/pkg/extension-v3/src/icons/icon-38.png b/pkg/extension-v3/src/icons/icon-38.png
new file mode 100644
index 000000000..1041a3f2a
Binary files /dev/null and b/pkg/extension-v3/src/icons/icon-38.png differ
diff --git a/pkg/extension-v3/src/icons/icon-48.png b/pkg/extension-v3/src/icons/icon-48.png
new file mode 100644
index 000000000..dda13bd4e
Binary files /dev/null and b/pkg/extension-v3/src/icons/icon-48.png differ
diff --git a/pkg/extension-v3/src/icons/icon-70.png b/pkg/extension-v3/src/icons/icon-70.png
new file mode 100644
index 000000000..41337352f
Binary files /dev/null and b/pkg/extension-v3/src/icons/icon-70.png differ
diff --git a/pkg/extension-v3/src/icons/icon-96.png b/pkg/extension-v3/src/icons/icon-96.png
new file mode 100644
index 000000000..3af64b0be
Binary files /dev/null and b/pkg/extension-v3/src/icons/icon-96.png differ
diff --git a/pkg/extension-v3/src/images/highlight.svg b/pkg/extension-v3/src/images/highlight.svg
new file mode 100644
index 000000000..e81aef802
--- /dev/null
+++ b/pkg/extension-v3/src/images/highlight.svg
@@ -0,0 +1,14 @@
+
diff --git a/pkg/extension-v3/src/images/label.svg b/pkg/extension-v3/src/images/label.svg
new file mode 100644
index 000000000..69fca4c19
--- /dev/null
+++ b/pkg/extension-v3/src/images/label.svg
@@ -0,0 +1,12 @@
+
diff --git a/pkg/extension-v3/src/images/listen-hover.svg b/pkg/extension-v3/src/images/listen-hover.svg
new file mode 100644
index 000000000..0a8fe4a55
--- /dev/null
+++ b/pkg/extension-v3/src/images/listen-hover.svg
@@ -0,0 +1,11 @@
+
diff --git a/pkg/extension-v3/src/images/listen.svg b/pkg/extension-v3/src/images/listen.svg
new file mode 100644
index 000000000..f77b5bb33
--- /dev/null
+++ b/pkg/extension-v3/src/images/listen.svg
@@ -0,0 +1,11 @@
+
diff --git a/pkg/extension-v3/src/images/pause-w-circle.svg b/pkg/extension-v3/src/images/pause-w-circle.svg
new file mode 100644
index 000000000..c8f4c1082
--- /dev/null
+++ b/pkg/extension-v3/src/images/pause-w-circle.svg
@@ -0,0 +1,3 @@
+
diff --git a/pkg/extension-v3/src/images/playback-speed.svg b/pkg/extension-v3/src/images/playback-speed.svg
new file mode 100644
index 000000000..41dab8b19
--- /dev/null
+++ b/pkg/extension-v3/src/images/playback-speed.svg
@@ -0,0 +1,5 @@
+
diff --git a/pkg/extension-v3/src/images/playback-voice.svg b/pkg/extension-v3/src/images/playback-voice.svg
new file mode 100644
index 000000000..c7b1670bd
--- /dev/null
+++ b/pkg/extension-v3/src/images/playback-voice.svg
@@ -0,0 +1,15 @@
+
diff --git a/pkg/extension-v3/src/images/save.svg b/pkg/extension-v3/src/images/save.svg
new file mode 100644
index 000000000..7deeb4d1f
--- /dev/null
+++ b/pkg/extension-v3/src/images/save.svg
@@ -0,0 +1,11 @@
+
diff --git a/pkg/extension-v3/src/manifest.json b/pkg/extension-v3/src/manifest.json
new file mode 100644
index 000000000..fe2a13dca
--- /dev/null
+++ b/pkg/extension-v3/src/manifest.json
@@ -0,0 +1,42 @@
+{
+ "manifest_version": 3,
+ "name": "Omnivore - Self Hosted",
+ "version": "3.0",
+ "description": "Save content to your Omnivore library.",
+ "permissions": [
+ "activeTab",
+ "scripting",
+ "storage"
+ ],
+ "host_permissions": [
+ ""
+ ],
+ "background": {
+ "service_worker": "background.js",
+ "scripts": ["background.js"]
+ },
+ "web_accessible_resources": [
+ {
+ "resources": [
+ "images/*.svg",
+ "views/toast.html"
+ ],
+ "matches": [
+ ""
+ ]
+ }
+ ],
+ "options_page": "views/options.html",
+ "action": {
+ "default_icon": {
+ "16": "icons/icon-16.png",
+ "48": "icons/icon-48.png",
+ "128": "icons/icon-128.png"
+ }
+ },
+ "icons": {
+ "16": "icons/icon-16.png",
+ "48": "icons/icon-48.png",
+ "128": "icons/icon-128.png"
+ }
+}
diff --git a/pkg/extension-v3/src/scripts/content/content.ts b/pkg/extension-v3/src/scripts/content/content.ts
new file mode 100644
index 000000000..27a87c517
--- /dev/null
+++ b/pkg/extension-v3/src/scripts/content/content.ts
@@ -0,0 +1,87 @@
+import { SavePageInput, ToolbarMessage } from '../types'
+import {
+ showLoggedOutToolbar,
+ showToolbar,
+ startToolbarDismiss,
+ updateToolbarStatus
+} from './toolbar'
+import { editLabels } from './labels'
+
+const collectPageContent = async (): Promise => {
+ const mainContent = document.documentElement.outerHTML
+ console.log('[omnivore] captured mainContent')
+ return mainContent
+}
+
+const handleToolbarMessage = async (
+ request: any,
+ sender: chrome.runtime.MessageSender,
+ sendResponse: (response?: any) => void
+) => {
+ console.log('[omnivore] content script message:', request)
+
+ switch (request.message) {
+ case 'showLoggedOutToolbar':
+ showLoggedOutToolbar()
+ sendResponse({ success: true })
+ break
+ case 'updateToolbar':
+ updateToolbarStatus(request.status, request.task)
+
+ sendResponse({ success: true })
+ break
+ case 'startToolbarDismiss':
+ startToolbarDismiss(request as ToolbarMessage)
+ sendResponse({ success: true })
+ return
+ case 'updateLabelCache':
+ editLabels()
+ break
+ default:
+ sendResponse({ success: false })
+ return
+ }
+}
+
+const setupToolbar = async (clientRequestId: string) => {
+ // toolbar message listener
+ if (!chrome.runtime.onMessage.hasListener(handleToolbarMessage)) {
+ chrome.runtime.onMessage.addListener(handleToolbarMessage)
+ }
+
+ await showToolbar(clientRequestId)
+}
+
+const savePage = async (clientRequestId: string) => {
+ console.log('[omnivore] v3 extension triggered: ', clientRequestId)
+
+ await setupToolbar(clientRequestId)
+
+ const content = await collectPageContent()
+ console.log('[omnivore] collected page content: ', content)
+
+ try {
+ const page: SavePageInput = {
+ clientRequestId,
+ title: document.title,
+ url: document.location.href,
+ originalContent: content,
+ }
+ await chrome.runtime.sendMessage({
+ action: 'savePage',
+ ...page,
+ })
+ } catch (err) {
+ console.log('error sending content: ', err)
+ }
+}
+
+// toolbar message listener
+chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
+ switch (request.message) {
+ case 'savePage':
+ await savePage(request.clientRequestId)
+ sendResponse({ success: true })
+ return
+ }
+})
diff --git a/pkg/extension-v3/src/scripts/content/labels.ts b/pkg/extension-v3/src/scripts/content/labels.ts
new file mode 100644
index 000000000..9dc9239f1
--- /dev/null
+++ b/pkg/extension-v3/src/scripts/content/labels.ts
@@ -0,0 +1,177 @@
+import { getStorageItem } from '../utils'
+import { Label } from '../types'
+import { cancelAutoDismiss, getClientRequestId, toggleRow, updateStatusBox, updateToolbarStatus } from './toolbar'
+
+export async function editLabels() {
+ cancelAutoDismiss()
+
+ const currentToastEl = document.querySelector('#omnivore-extension-root')
+
+ if (!currentToastEl || !currentToastEl.shadowRoot) {
+ console.log('no statusBox to update')
+ return
+ }
+
+ let labels = await getStorageItem('labels').then((value: any) => {
+ if (value && value.length > 0) {
+ return value as Label[]
+ }
+
+ return undefined
+ })
+
+ toggleRow('#omnivore-edit-labels-row')
+ currentToastEl.shadowRoot
+ .querySelector('#omnivore-edit-label-input')
+ ?.focus()
+
+ const list = currentToastEl.shadowRoot.querySelector(
+ '#omnivore-edit-labels-list'
+ )
+
+ // Add a box for waiting for the labels.
+ if (!labels) {
+ console.error('No labels found, trying to update the cache.')
+ chrome.runtime.sendMessage({
+ action: 'enqueueTask',
+ task: 'updateLabelCache',
+ clientRequestId: getClientRequestId(),
+ })
+
+ return;
+ }
+
+
+ // currentToastEl.shadowRoot.querySelector(
+ // '#omnivore-edit-label-input'
+ // ).onkeydown = labelEditorKeyDownHandler
+ //
+ // currentToastEl.shadowRoot.querySelector(
+ // '#omnivore-edit-label-editor'
+ // ).onclick = labelEditorClickHandler
+ //
+ // currentToastEl.shadowRoot
+ // .querySelector('#omnivore-edit-label-input')
+ // .addEventListener('input', (event) => {
+ // updateLabels(event.target.value)
+ // })
+
+ if (list) {
+ list.innerHTML = ''
+ labels
+ .sort((a, b) =>
+ a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
+ )
+ .forEach(function (label, idx) {
+ const rowHtml = createLabelRow(label)
+ list.appendChild(rowHtml)
+ })
+ }
+}
+
+function createLabelRow(label: Label) {
+ const element = document.createElement('button')
+ const dot = document.createElement('span')
+ // @ts-ignore
+ dot.style = 'width:10px;height:10px;border-radius:1000px;'
+ dot.style.setProperty('background-color', label.color)
+ const title = document.createElement('span')
+ // @ts-ignore
+ title.style = 'margin-left: 10px;pointer-events: none;'
+ title.innerText = label.name
+
+ const check = document.createElement('span')
+ // @ts-ignore
+ check.style = 'margin-left: auto;pointer-events: none;'
+ check.className = 'checkbox'
+ check.innerHTML = `
+
+ `
+
+ element.appendChild(dot)
+ element.appendChild(title)
+ element.appendChild(check)
+
+ element.onclick = labelClick
+ // element.onkeydown = labelEditorKeyDownHandler
+ element.setAttribute('data-label-id', label.id)
+ element.setAttribute(
+ 'data-label-selected',
+ label['selected'] ? 'on' : 'off'
+ )
+ element.setAttribute('tabIndex', '-1')
+
+ return element
+}
+
+
+function labelClick(event: any) {
+ event.preventDefault()
+ const labelId = event.target?.getAttribute('data-label-id')
+
+ if (labelId) {
+ toggleLabel(event, labelId)
+ }
+}
+
+async function toggleLabel(event: any, labelId: string) {
+ const labelSelected = event.target?.getAttribute('data-label-selected')
+
+ if (!labelId || !labelSelected) {
+ return
+ }
+
+ const toggledValue = labelSelected == 'on' ? false : true
+ event.target?.setAttribute(
+ 'data-label-selected',
+ toggledValue ? 'on' : 'off'
+ )
+
+
+ let labels = await getStorageItem('labels').then((value: any) => {
+ if (value && value.length > 0) {
+ return value as Label[]
+ }
+
+ return undefined
+ })
+
+ if (!labels) {
+ throw Error("No labels selected")
+ }
+
+ const label = labels.find((l) => l.id === labelId)
+ if (label) {
+ syncLabelChanges()
+ }
+
+}
+
+function syncLabelChanges() {
+ updateStatusBox(
+ '#omnivore-edit-labels-status',
+ 'waiting',
+ 'Updating Labels...',
+ undefined
+ )
+
+ const currentToastEl = document.querySelector('#omnivore-extension-root')
+ const labels = currentToastEl?.shadowRoot?.querySelector("#omnivore-edit-labels-list")
+
+ if (labels) {
+ const setLabels = [...labels.children]
+ .filter((l) => l.getAttribute('data-label-selected') === 'on')
+ .map((l) => l.getAttribute('data-label-id')!)
+
+ chrome.runtime.sendMessage({
+ action: 'enqueueTask',
+ task: 'setLabels',
+ clientRequestId: getClientRequestId(),
+ labels: setLabels,
+ })
+ }
+
+
+}
diff --git a/pkg/extension-v3/src/scripts/content/toolbar.ts b/pkg/extension-v3/src/scripts/content/toolbar.ts
new file mode 100644
index 000000000..2c160877f
--- /dev/null
+++ b/pkg/extension-v3/src/scripts/content/toolbar.ts
@@ -0,0 +1,593 @@
+import { ToolbarMessage, ToolbarStatus } from '../types'
+import { getStorageItem, setStorage } from '../utils'
+import { editLabels } from './labels'
+
+const systemIcons: { [key: string]: string } = {
+ waiting: '',
+ success: `
+ `,
+ failure: `
+
+ `,
+}
+
+const createToastContainer = async (clientRequestId: string) => {
+ console.log('===== CREATING TOAST CONTAINER ===== ')
+ const file = await fetch(chrome.runtime.getURL('views/toast.html'))
+ const html = await file.text()
+
+ const root = document.createElement('div')
+ root.tabIndex = 0
+ root.id = 'omnivore-extension-root'
+ root.attachShadow({ mode: 'open' })
+ root.style.opacity = '1.0'
+
+ if (root.shadowRoot) {
+ root.shadowRoot.innerHTML = ``
+ } else {
+ alert('Error opening Omnivore user interface.')
+ return
+ }
+
+ const toastEl = document.createElement('div')
+ toastEl.id = '#omnivore-toast'
+ toastEl.innerHTML = html
+ toastEl.tabIndex = 0
+ root.shadowRoot.appendChild(toastEl)
+
+ document.body.appendChild(root)
+ connectButtons(root)
+ // connectKeyboard(root)
+
+ updateToolbarStatus('waiting')
+
+ return root
+}
+
+export const showToolbar = async (clientRequestId: string) => {
+ let currentToastEl =
+ document.querySelector('#omnivore-extension-root') ?? undefined
+
+ const bodyEl = document.body
+ if (!bodyEl) return
+
+ console.log('existing currentToastEl: ', currentToastEl)
+ if (!currentToastEl) {
+ currentToastEl = await createToastContainer(clientRequestId)
+ }
+
+ const disableAutoDismiss = await getStorageItem('disableAutoDismiss')
+ if (disableAutoDismiss) {
+ currentToastEl?.setAttribute('data-disable-auto-dismiss', 'true')
+ }
+ currentToastEl?.setAttribute(
+ 'data-omnivore-client-request-id',
+ clientRequestId
+ )
+ ;(currentToastEl as HTMLDivElement)?.focus({
+ preventScroll: true,
+ })
+
+ updateToolbarStatus('waiting')
+}
+
+const autoDismissTime = async () => {
+ const strVal = (await getStorageItem('autoDismissTime')) ?? '2500'
+ return !Number.isNaN(Number(strVal)) ? Number(strVal) : 2500
+}
+
+export const updateToolbarStatus = async (
+ status: ToolbarStatus,
+ task: string | undefined = undefined
+) => {
+ const currentToastEl = document.querySelector('#omnivore-extension-root')
+ const statusBox = currentToastEl?.shadowRoot?.querySelector(
+ '.omnivore-toast-statusBox'
+ )
+ console.log('updating', status, statusBox)
+
+ if (statusBox) {
+ switch (status) {
+ case 'success':
+ statusBox.innerHTML = systemIcons.success
+ break
+ case 'failure':
+ statusBox.innerHTML = systemIcons.failure
+ break
+ case 'waiting':
+ statusBox.innerHTML = systemIcons.waiting
+ break
+ }
+ }
+
+ // Set a task specific message
+ if (task) {
+ if (task == 'addNote' && status == 'failure') {
+ updateStatusBox(
+ '#omnivore-add-note-status',
+ 'failure',
+ 'Error adding note...',
+ undefined
+ )
+ }
+ if (task == 'addNote' && status == 'success') {
+ updateStatusBox(
+ '#omnivore-add-note-status',
+ 'success',
+ 'Note updated.',
+ 2500
+ )
+ setTimeout(() => {
+ toggleRow('#omnivore-add-note-status')
+ }, 3000)
+ }
+ if (task == 'setLabels' && status == 'success') {
+ updateStatusBox(
+ '#omnivore-edit-labels-status',
+ 'success',
+ 'Labels Updated',
+ 2500
+ )
+ }
+ if (task == 'setLabels' && status == 'failure') {
+ updateStatusBox(
+ '#omnivore-edit-labels-status',
+ 'failure',
+ 'Error Updating Labels...',
+ 2500
+ )
+ }
+ if (task == 'editTitle' && status == 'failure') {
+ updateStatusBox(
+ '#omnivore-add-note-status',
+ 'failure',
+ 'Error updating title...',
+ undefined
+ )
+ }
+ if (task == 'editTitle' && status == 'success') {
+ updateStatusBox(
+ 'omnivore-edit-title-status',
+ 'success',
+ 'Title updated.',
+ 2500
+ )
+ setTimeout(() => {
+ toggleRow('#omnivore-edit-title-status')
+ }, 3000)
+ }
+ if (task == 'archive') {
+ updateStatusBox(
+ '#omnivore-extra-status',
+ status,
+ status == 'success' ? 'Success' : 'Error',
+ status == 'success' ? 2500 : undefined
+ )
+ if (status == 'success') {
+ closeToolbarLater()
+ }
+ }
+ }
+}
+
+export const cancelAutoDismiss = () => {
+ const currentToastEl = document.querySelector('#omnivore-extension-root')
+ if (currentToastEl) {
+ currentToastEl.setAttribute('data-disable-auto-dismiss', 'true')
+ }
+}
+
+// If the user has not disabled auto dismiss on the toolbar this
+// will remove it. If the user interacts with the toolbar, this
+// dismiss will also be ignored.
+export const startToolbarDismiss = async (message: ToolbarMessage) => {
+ if (message.status) {
+ updateToolbarStatus(message.status)
+ }
+
+ const dimissTime = await autoDismissTime()
+
+ setTimeout(() => {
+ const currentToastEl = document.querySelector('#omnivore-extension-root')
+ if (
+ currentToastEl &&
+ !currentToastEl.getAttribute('data-disable-auto-dismiss')
+ ) {
+ ;(currentToastEl as HTMLElement).style.transition = 'opacity 3.5s ease;'
+ ;(currentToastEl as HTMLElement).style.opacity = '0'
+ setTimeout(() => {
+ const currentToastEl = document.querySelector(
+ '#omnivore-extension-root'
+ )
+ if (
+ currentToastEl &&
+ !currentToastEl.getAttribute('data-disable-auto-dismiss')
+ ) {
+ currentToastEl.remove()
+ }
+ }, 500)
+ }
+ }, dimissTime)
+}
+
+const connectButtons = (root: HTMLElement) => {
+ const btns = [
+ { id: '#omnivore-toast-add-note-btn', func: addNote },
+ { id: '#omnivore-toast-edit-title-btn', func: editTitle },
+ { id: '#omnivore-toast-edit-labels-btn', func: editLabels },
+ { id: '#omnivore-toast-read-now-btn', func: readNow },
+ { id: '#omnivore-open-menu-btn', func: openMenu },
+ { id: '#omnivore-toast-close-btn', func: closeToolbar },
+ { id: '#omnivore-toast-login-btn', func: login },
+ { id: '#omnivore-toast-archive-btn', func: archive },
+ { id: '#omnivore-toast-delete-btn', func: deleteItem },
+ ]
+
+ for (const btnInfo of btns) {
+ const btn = root.shadowRoot?.querySelector(btnInfo.id)
+ if (btn) {
+ console.log(btnInfo.id)
+ btn.addEventListener('click', btnInfo.func)
+ }
+ }
+
+ var x = window.matchMedia('(max-width: 500px)')
+ if (x.matches) {
+ const labels = root.shadowRoot?.querySelectorAll(
+ '.omnivore-top-button-label'
+ )
+ labels?.forEach((label) => {
+ label.style.display = 'none'
+ })
+ const container = root.shadowRoot?.querySelector(
+ '#omnivore-toast-container'
+ )
+ if (container) {
+ container.style.width = '280px'
+ container.style.top = 'unset'
+ container.style.bottom = '20px'
+ }
+ }
+}
+
+function editTitle() {
+ cancelAutoDismiss()
+ toggleRow('#omnivore-edit-title-row')
+ let currentToastEl =
+ document.querySelector('#omnivore-extension-root') ?? undefined
+
+ if (!currentToastEl) {
+ console.log('no statusBox to update')
+ return
+ }
+
+ const titleArea = currentToastEl?.shadowRoot?.querySelector(
+ '#omnivore-edit-title-textarea'
+ )
+
+ if (titleArea) {
+ titleArea.focus()
+
+ titleArea.onkeydown = (e) => {
+ e.cancelBubble = true
+ }
+ }
+
+ const formElement = currentToastEl?.shadowRoot?.querySelector(
+ '#omnivore-edit-title-form'
+ );
+
+ if (!formElement) {
+ console.log('no form to update')
+ return
+ }
+
+ formElement.onsubmit = (event) => {
+ updateStatusBox(
+ '#omnivore-edit-title-status',
+ 'waiting',
+ 'Updating title...',
+ undefined
+ )
+
+ const title = titleArea?.value ?? ''
+
+ chrome.runtime.sendMessage({
+ action: 'enqueueTask',
+ task: 'editTitle',
+ clientRequestId: getClientRequestId(),
+ title
+ })
+ event.preventDefault()
+ }
+}
+
+export const showLoggedOutToolbar = () => {
+ cancelAutoDismiss()
+ updateToolbarStatus('failure')
+ toggleRow('#omnivore-logged-out-row')
+ disableAllButtons()
+ updateStatusBox(
+ '#omnivore-logged-out-status',
+ undefined,
+ `You are not logged in.`,
+ undefined
+ )
+}
+
+export const updateStatusBox = (
+ boxId: string,
+ state: ToolbarStatus | undefined,
+ message: string,
+ dismissAfter: number | undefined
+) => {
+ const currentToastEl = document.querySelector('#omnivore-extension-root')
+ const statusBox = currentToastEl?.shadowRoot?.querySelector(boxId)
+ const image = (() => {
+ switch (state) {
+ case 'waiting':
+ return systemIcons.animatedLoader
+ case 'success':
+ return systemIcons.success
+ case 'failure':
+ return systemIcons.failure
+ default:
+ return undefined
+ }
+ })()
+ if (image && statusBox) {
+ const color = state == 'failure' ? 'red' : 'unset'
+ statusBox.innerHTML = `${image}${message}`
+ } else if (statusBox) {
+ statusBox.innerHTML = message
+ }
+ if (dismissAfter && statusBox) {
+ setTimeout(() => {
+ statusBox.innerHTML = ''
+ }, dismissAfter)
+ }
+}
+
+const disableAllButtons = () => {
+ const actionButtons = [
+ '#omnivore-toast-edit-title-btn',
+ '#omnivore-toast-edit-labels-btn',
+ '#omnivore-toast-read-now-btn',
+ '#omnivore-toast-add-note-btn',
+ '#omnivore-open-menu-btn',
+ ]
+ let currentToastEl = document.querySelector('#omnivore-extension-root')
+ actionButtons.forEach((btnId) => {
+ const btn =
+ currentToastEl?.shadowRoot?.querySelector(btnId)
+ if (btn) {
+ btn.disabled = true
+ }
+ })
+}
+
+export const toggleRow = (rowId: string) => {
+ let currentToastEl = document.querySelector('#omnivore-extension-root')
+
+ if (!currentToastEl) {
+ console.log('toggleRow: no row to toggle')
+ // its possible this was called after closing the extension
+ // so just return
+ return
+ }
+
+ const container = currentToastEl?.shadowRoot?.querySelector(rowId)
+ const initialState = container?.getAttribute('data-state')
+ const rows = currentToastEl?.shadowRoot?.querySelectorAll(
+ '.omnivore-toast-func-row'
+ )
+
+ rows?.forEach((r) => {
+ r.setAttribute('data-state', 'closed')
+ })
+
+ if (container && initialState) {
+ const newState = initialState === 'open' ? 'closed' : 'open'
+ container.setAttribute('data-state', newState)
+ }
+}
+
+const noteCacheKey = () => {
+ return `cached-note-${document.location.href}`
+}
+
+export const getClientRequestId = () => {
+ const currentToastEl = document.querySelector('#omnivore-extension-root')
+ const clientRequestId = currentToastEl?.getAttribute(
+ 'data-omnivore-client-request-id'
+ )
+ return clientRequestId
+}
+
+//
+// Button functions
+//
+
+const login = () => {
+ window.open(new URL(`/login`, process.env.OMNIVORE_URL), '_blank')
+ closeToolbarLater()
+}
+
+const openMenu = () => {
+ cancelAutoDismiss()
+ toggleRow('#omnivore-extra-buttons-row')
+}
+
+const addNote = async () => {
+ console.log('[omnivore] adding note')
+ cancelAutoDismiss()
+
+ const currentToastEl = document.querySelector('#omnivore-extension-root')
+ const clientRequestId = currentToastEl?.getAttribute(
+ 'data-omnivore-client-request-id'
+ )
+ console.log('client request id: ', clientRequestId)
+ if (!clientRequestId) {
+ // TODO: move into an error state
+ updateStatusBox(
+ '#omnivore-add-note-status',
+ 'failure',
+ 'Error adding note...',
+ undefined
+ )
+ return
+ }
+
+ const cachedNoteKey = noteCacheKey()
+
+ cancelAutoDismiss()
+ toggleRow('#omnivore-add-note-row')
+
+ const noteArea =
+ currentToastEl?.shadowRoot?.querySelector(
+ '#omnivore-add-note-textarea'
+ )
+
+ if (noteArea) {
+ if (cachedNoteKey) {
+ const existingNote =
+ ((await getStorageItem(cachedNoteKey)) as string) ?? ''
+ noteArea.value = existingNote
+ }
+
+ if (noteArea.value) {
+ noteArea.select()
+ } else {
+ noteArea.focus()
+ }
+
+ noteArea.addEventListener('input', async (event) => {
+ const note: Record = {}
+ note[cachedNoteKey] = (event.target as HTMLTextAreaElement).value
+ await setStorage(note)
+ })
+
+ noteArea.onkeydown = async (e: KeyboardEvent) => {
+ // e.preventDefault()
+ e.stopPropagation()
+ // Handle the enter key
+ console.log('handling the enter key: ', e.keyCode)
+ if (e.keyCode == 13 && (e.metaKey || e.ctrlKey)) {
+ updateStatusBox(
+ '#omnivore-add-note-status',
+ 'waiting',
+ 'Adding note...',
+ undefined
+ )
+
+ await saveNote(clientRequestId, noteArea.value)
+ }
+ }
+ }
+
+ const form = currentToastEl?.shadowRoot?.querySelector(
+ '#omnivore-add-note-form'
+ )
+
+ if (form) {
+ form.onsubmit = async (event) => {
+ console.log('handling form submit')
+ updateStatusBox(
+ '#omnivore-add-note-status',
+ 'waiting',
+ 'Adding note...',
+ undefined
+ )
+
+ if (noteArea) {
+ await saveNote(clientRequestId, noteArea.value)
+ }
+
+ event.preventDefault()
+ event.stopPropagation()
+ }
+ }
+}
+
+const archive = async (event: Event) => {
+ const clientRequestId = getClientRequestId()
+ try {
+ await chrome.runtime.sendMessage({
+ action: 'enqueueTask',
+ task: 'archive',
+ clientRequestId,
+ })
+ } catch (err) {
+ console.log('error archiving item')
+ }
+ event.preventDefault()
+}
+
+const deleteItem = async (event: Event) => {
+ const clientRequestId = getClientRequestId()
+ try {
+ await chrome.runtime.sendMessage({
+ action: 'enqueueTask',
+ task: 'delete',
+ clientRequestId,
+ })
+ } catch (err) {
+ console.log('error archiving item')
+ }
+ event.preventDefault()
+}
+
+const readNow = async () => {
+ cancelAutoDismiss()
+
+ let currentToastEl = document.querySelector('#omnivore-extension-root')
+ const container = currentToastEl?.shadowRoot?.querySelector(
+ '#omnivore-toast-container'
+ )
+ container?.setAttribute('data-state', 'open')
+
+ window.open(
+ new URL(
+ `/article?url=${encodeURI(document.location.href)}`,
+ (await getStorageItem("omnivoreUrl")) as string,
+ ),
+ '_blank'
+ )
+
+ closeToolbarLater()
+}
+
+const closeToolbarLater = () => {
+ setTimeout(() => {
+ closeToolbar()
+ }, 1000)
+}
+const closeToolbar = () => {
+ const currentToastEl = document.querySelector('#omnivore-extension-root')
+ if (currentToastEl) {
+ currentToastEl.remove()
+ }
+}
+
+//
+// API interactions
+//
+
+const saveNote = async (clientRequestId: string, note: string) => {
+ try {
+ await chrome.runtime.sendMessage({
+ action: 'enqueueTask',
+ task: 'addNote',
+ note,
+ clientRequestId,
+ })
+ } catch (err) {
+ console.log('error adding note: ', err)
+ }
+}
diff --git a/pkg/extension-v3/src/scripts/omnivore-api.ts b/pkg/extension-v3/src/scripts/omnivore-api.ts
new file mode 100644
index 000000000..1bf6d9892
--- /dev/null
+++ b/pkg/extension-v3/src/scripts/omnivore-api.ts
@@ -0,0 +1,528 @@
+import { ArticleData, Label, SavePageData, SetLinkArchivedData } from './types'
+import { getStorageItem, setStorage } from './utils'
+import { v4 as uuidv4 } from 'uuid'
+import { nanoid } from 'nanoid'
+
+export type ApiResult = 'success' | 'failure' | 'unauthorized'
+
+const gqlRequest = async (query: string) => {
+ const apiKey = (await getStorageItem('omnivoreApiKey')) as string | undefined
+ let headers = {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ } as Record
+ if (apiKey) {
+ headers['Authorization'] = apiKey
+ }
+
+ try {
+ const apiUrl= (await getStorageItem('omnivoreApiUrl')) as string | undefined
+ ?? process.env.OMNIVORE_GRAPHQL_URL
+ ?? ''
+
+ console.log(apiUrl)
+ const response = await fetch(`${apiUrl}/api/graphql`, {
+ method: 'POST',
+ redirect: 'follow',
+ credentials: 'include',
+ mode: 'cors',
+ headers,
+ body: query,
+ })
+ const json = await response.json()
+ if (!('data' in json) || !json.data) {
+ throw new Error('No response data')
+ }
+ return json.data
+ } catch (err) {
+ console.log('[omnivore] error making api request: ', query)
+ }
+}
+
+export async function savePageRequest(input: {
+ url: string
+ title: string
+ clientRequestId: string
+ originalContent: string
+}) {
+ const mutation = JSON.stringify({
+ query: `mutation SavePage ($input: SavePageInput!) {
+ savePage(input:$input){
+ ... on SaveSuccess {
+ url
+ clientRequestId
+ }
+ ... on SaveError {
+ errorCodes
+ }
+ }
+ }`,
+ variables: {
+ input: {
+ source: 'extension',
+ ...input,
+ },
+ },
+ })
+
+ const data = (await gqlRequest(mutation)) as SavePageData
+ if (data.savePage?.errorCodes?.length) {
+ console.log('[omnivore] api: error saving page:', data)
+ if (data.savePage.errorCodes.indexOf('UNAUTHORIZED') > -1) {
+ console.log('[omnivore] api is not authorized')
+ return { result: 'unauthorized' }
+ }
+ return { result: 'failure' }
+ }
+ return { result: 'success', libraryItemId: data.savePage?.clientRequestId }
+}
+
+export async function addNoteToLibraryItem(input: {
+ libraryItemId: string
+ note: string
+}) {
+ const query = JSON.stringify({
+ query: `query GetArticle(
+ $username: String!
+ $slug: String!
+ $includeFriendsHighlights: Boolean
+ ) {
+ article(username: $username, slug: $slug) {
+ ... on ArticleSuccess {
+ article {
+ highlights(input: { includeFriends: $includeFriendsHighlights }) {
+ ...HighlightFields
+ }
+ }
+ }
+ ... on ArticleError {
+ errorCodes
+ }
+ }
+ }
+ fragment HighlightFields on Highlight {
+ id
+ type
+ annotation
+ }
+ `,
+ variables: {
+ username: 'me',
+ slug: input.libraryItemId,
+ includeFriendsHighlights: false,
+ },
+ })
+
+ const data = (await gqlRequest(query)) as ArticleData
+ if (data.article?.errorCodes?.length) {
+ console.log('[omnivore] api: error getting article:', data)
+ if (data.article.errorCodes.indexOf('UNAUTHORIZED') > -1) {
+ console.log('[omnivore] api is not authorized')
+ return 'unauthorized'
+ }
+ return 'failure'
+ }
+
+ console.log('DATA.ARTICLE: ', data.article)
+ const existingNote = data.article?.highlights?.find((h) => h.type == 'NOTE')
+
+ if (existingNote) {
+ const mutation = JSON.stringify({
+ query: `
+ mutation UpdateHighlight($input: UpdateHighlightInput!) {
+ updateHighlight(input: $input) {
+ ... on UpdateHighlightSuccess {
+ highlight {
+ id
+ }
+ }
+ ... on UpdateHighlightError {
+ errorCodes
+ }
+ }
+ }
+ `,
+ variables: {
+ input: {
+ highlightId: existingNote.id,
+ annotation: existingNote.annotation
+ ? existingNote.annotation + '\n\n' + input.note
+ : input.note,
+ },
+ },
+ })
+ const result = await gqlRequest(mutation)
+ if (
+ !result.updateHighlight ||
+ result.updateHighlight['errorCodes'] ||
+ !result.updateHighlight.highlight
+ ) {
+ console.log('GQL Error updating note:', result)
+ return
+ }
+ return result.updateHighlight.highlight.id
+ } else {
+ const noteId = uuidv4()
+ const shortId = nanoid(8)
+ const mutation = JSON.stringify({
+ query: `
+ mutation CreateHighlight($input: CreateHighlightInput!) {
+ createHighlight(input: $input) {
+ ... on CreateHighlightSuccess {
+ highlight {
+ id
+ }
+ }
+ ... on CreateHighlightError {
+ errorCodes
+ }
+ }
+ }
+ `,
+ variables: {
+ input: {
+ id: noteId,
+ shortId: shortId,
+ type: 'NOTE',
+ articleId: input.libraryItemId,
+ annotation: input.note,
+ },
+ },
+ })
+ const result = await gqlRequest(mutation)
+ if (
+ !result.createHighlight ||
+ result.createHighlight['errorCodes'] ||
+ !result.createHighlight.highlight
+ ) {
+ console.log('GQL Error setting note:', result)
+ return 'failure'
+ }
+ return 'success'
+ }
+}
+
+export async function updateLabelsCache(): Promise