Merge pull request #1926 from omnivore-app/feat/extension-ui

Improve the UI for the browser extension
This commit is contained in:
Jackson Harper
2023-03-21 20:40:16 +08:00
committed by GitHub
10 changed files with 1867 additions and 843 deletions

View File

@ -2,8 +2,8 @@
"manifest_version": 2,
"name": "process.env.EXTENSION_NAME",
"short_name": "process.env.EXTENSION_NAME",
"version": "0.1.26",
"description": "Save articles to your Omnivore library",
"version": "2.0.2",
"description": "Save PDFs and Articles to your Omnivore library",
"author": "Omnivore Media, Inc",
"default_locale": "en",
"developer": {
@ -21,14 +21,17 @@
"128": "/images/extension/icon-128.png",
"256": "/images/extension/icon-256.png"
},
"permissions": ["activeTab", "storage", "contextMenus", "https://*/**", "http://*/**"],
"permissions": [
"activeTab",
"storage",
"contextMenus",
"https://*/**",
"http://*/**"
],
"background": {
"page": "/views/background.html",
"persistent": true
},
"minimum_chrome_version": "21",
"minimum_opera_version": "15",
"applications": {
@ -41,25 +44,31 @@
"id": "save-extension@omnivore.app"
}
},
"content_scripts": [
{
"matches": ["https://*/**", "http://*/**"],
"matches": [
"https://*/**",
"http://*/**"
],
"js": [
"/scripts/constants.js",
"/scripts/common.js",
"/scripts/content/toast.js",
"/scripts/content/page-info.js",
"/scripts/content/prepare-content.js",
"/scripts/content/toast.js",
"/scripts/content/content-listener-script.js"
]
},
{
"matches": ["https://*/**", "http://*/**"],
"js": ["/scripts/content/grab-iframe-content.js"],
"matches": [
"https://*/**",
"http://*/**"
],
"js": [
"/scripts/content/grab-iframe-content.js"
],
"all_frames": true
}
],
"browser_action": {
"default_icon": {
"16": "/images/toolbar/icon-16.png",
@ -71,15 +80,15 @@
},
"default_title": "Omnivore Save Article"
},
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Alt + O"
},
"description": "Save the current tab to Omnivore"
"suggested_key": {
"default": "Alt + O"
},
"description": "Save the current tab to Omnivore"
}
},
"web_accessible_resources": ["views/cta-popup.html"]
}
"web_accessible_resources": [
"views/toast.html"
]
}

View File

@ -0,0 +1,128 @@
function gqlRequest(apiUrl, query) {
return fetch(apiUrl, {
method: 'POST',
redirect: 'follow',
credentials: 'include',
mode: 'cors',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: query,
})
.then((response) => response.json())
.then((json) => {
if (!json['data']) {
throw new Error('No response data')
}
return json['data']
})
}
async function updateLabelsCache(apiUrl, tab) {
const query = JSON.stringify({
query: `query GetLabels {
labels {
... on LabelsSuccess {
labels {
...LabelFields
}
}
... on LabelsError {
errorCodes
}
}
}
fragment LabelFields on Label {
id
name
color
description
createdAt
}
`,
})
const data = await gqlRequest(apiUrl, query)
if (!data.labels || data.labels['errorCodes'] || !data.labels['labels']) {
console.log('GQL Error updating label cache response:', data, data)
console.log(!data.labels, data.labels['errorCodes'], !data.labels['labels'])
return []
}
await setStorage({
labels: data.labels.labels,
labelsLastUpdated: new Date().toISOString(),
})
return data.labels.labels
}
async function updatePageTitle(apiUrl, pageId, title) {
const mutation = JSON.stringify({
query: `mutation UpdatePage($input: UpdatePageInput!) {
updatePage(input: $input) {
... on UpdatePageSuccess {
updatedPage {
id
}
}
... on UpdatePageError {
errorCodes
}
}
}
`,
variables: {
input: {
pageId,
title,
},
},
})
const data = await gqlRequest(apiUrl, mutation)
if (
!data.updatePage ||
data.updatePage['errorCodes'] ||
!data.updatePage['updatedPage']
) {
console.log('GQL Error updating page:', data)
throw new Error('Error updating title.')
}
return data.updatePage.updatePage
}
async function setLabels(apiUrl, pageId, labelIds) {
const mutation = JSON.stringify({
query: `mutation SetLabels($input: SetLabelsInput!) {
setLabels(input: $input) {
... on SetLabelsSuccess {
labels {
id
}
}
... on SetLabelsError {
errorCodes
}
}
}
`,
variables: {
input: {
pageId,
labelIds,
},
},
})
const data = await gqlRequest(apiUrl, mutation)
if (
!data.setLabels ||
data.setLabels['errorCodes'] ||
!data.setLabels['labels']
) {
console.log('GQL Error setting labels:', data)
throw new Error('Error setting labels.')
}
return data.setLabels.labels
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,120 @@
'use strict'
window.browserApi =
(typeof chrome === 'object' && chrome && chrome.runtime && chrome) ||
(typeof browser === 'object' && browser) ||
{} // eslint-disable-line no-undef
window.browserActionApi =
browserApi.action || browserApi.browserAction || browserApi.pageAction
window.browserScriptingApi = browserApi.scripting || browserApi.tabs
window.ENV_EXTENSION_ORIGIN = browserApi.runtime
.getURL('PATH/')
.replace('/PATH/', '')
window.ENV_IS_FIREFOX = ENV_EXTENSION_ORIGIN.startsWith('moz-extension://')
window.ENV_IS_EDGE = navigator.userAgent.toLowerCase().indexOf('edg') > -1
window.ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS =
/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
window.SELECTORS = {
CANONICAL_URL: ["head > link[rel='canonical']"],
TITLE: ["head > meta[property='og:title']"],
}
window.ACTIONS = {
Ping: 'PING',
ShowMessage: 'SHOW_MESSAGE',
GetContent: 'GET_CONTENT',
AddIframeContent: 'ADD_IFRAME_CONTENT',
RefreshDarkMode: 'REFRESH_DARK_MODE',
GetAuthToken: 'GET_AUTH_TOKEN',
LabelCacheUpdated: 'LABEL_CACHE_UPDATED',
ShowToolbar: 'SHOW_TOOLBAR',
UpdateStatus: 'UPDATE_STATUS',
EditTitle: 'EDIT_TITLE',
SetLabels: 'SET_LABELS',
}
window.SAVE_URL_QUERY = `mutation SaveUrl ($input: SaveUrlInput!) {
saveUrl(input:$input){
... on SaveSuccess {
url
clientRequestId
}
... on SaveError {
errorCodes
}
}
}`
window.SAVE_FILE_QUERY = `mutation SaveFile ($input: SaveFileInput!) {
saveFile(input:$input){
... on SaveSuccess {
url
clientRequestId
}
... on SaveError {
errorCodes
}
}
}`
window.SAVE_PAGE_QUERY = `mutation SavePage ($input: SavePageInput!) {
savePage(input:$input){
... on SaveSuccess {
url
clientRequestId
}
... on SaveError {
errorCodes
}
}
}`
function handleBackendUrl(url) {
try {
const FORCE_CONTENT_FETCH_URLS = [
// twitter status url regex
/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)(?:\/.*)?/,
/^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/,
]
return FORCE_CONTENT_FETCH_URLS.some((regex) => regex.test(url))
} catch (error) {
console.log('error checking url', url)
}
return false
}
/* storage helper functions */
function getStorage(keyOrKeys) {
return new Promise((resolve) => {
browserApi.storage.local.get(keyOrKeys || null, (result) => {
resolve(result || {})
})
})
}
function getStorageItem(singleKey) {
return new Promise((resolve) => {
browserApi.storage.local.get(singleKey, (result) => {
const finalResult = (result && result[singleKey]) || null
resolve(finalResult)
})
})
}
function setStorage(itemsToSet) {
return new Promise((resolve) => {
browserApi.storage.local.set(itemsToSet, resolve)
})
}
function removeStorage(itemsToRemove) {
return new Promise((resolve) => {
browserApi.storage.local.remove(itemsToRemove, resolve)
})
}

View File

@ -1,114 +0,0 @@
'use strict';
window.browserApi = (typeof chrome === 'object' && chrome && chrome.runtime && chrome) || (typeof browser === 'object' && browser) || {}; // eslint-disable-line no-undef
window.browserActionApi = browserApi.action || browserApi.browserAction || browserApi.pageAction;
window.browserScriptingApi = browserApi.scripting || browserApi.tabs;
window.ENV_EXTENSION_ORIGIN = browserApi.runtime.getURL('PATH/').replace('/PATH/', '');
window.ENV_IS_FIREFOX = ENV_EXTENSION_ORIGIN.startsWith('moz-extension://');
window.ENV_IS_EDGE = navigator.userAgent.toLowerCase().indexOf('edg') > -1;
window.ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
window.SELECTORS = {
CANONICAL_URL: ["head > link[rel='canonical']"],
TITLE: ["head > meta[property='og:title']"]
};
window.ACTIONS = {
ShowMessage: 'SHOW_MESSAGE',
GetContent: 'GET_CONTENT',
GetPageInfo: 'GET_PAGE_INFO',
Ping: 'PING',
AddIframeContent: 'ADD_IFRAME_CONTENT',
RefreshDarkMode: 'REFRESH_DARK_MODE',
GetAuthToken: 'GET_AUTH_TOKEN',
};
window.DONT_REMOVE_ELEMENTS = [
'meta',
'script',
'title'
];
window.SAVE_URL_QUERY = `mutation SaveUrl ($input: SaveUrlInput!) {
saveUrl(input:$input){
... on SaveSuccess {
url
}
... on SaveError {
errorCodes
}
}
}`
window.SAVE_FILE_QUERY = `mutation SaveFile ($input: SaveFileInput!) {
saveFile(input:$input){
... on SaveSuccess {
url
}
... on SaveError {
errorCodes
}
}
}`
window.SAVE_PAGE_QUERY = `mutation SavePage ($input: SavePageInput!) {
savePage(input:$input){
... on SaveSuccess {
url
}
... on SaveError {
errorCodes
}
}
}`
window.CREATE_ARTICLE_QUERY = `mutation CreateArticle ($input: CreateArticleInput!){
createArticle(input:$input){
... on CreateArticleSuccess{
createdArticle{
id
title
slug
hasContent
}
user {
id
profile {
id
username
}
}
}
... on CreateArticleError{
errorCodes
}
}
}`;
window.CREATE_ARTICLE_SAVING_REQUEST_QUERY = `mutation CreateArticleSavingRequest ($input: CreateArticleSavingRequestInput!){
createArticleSavingRequest(input:$input){
... on CreateArticleSavingRequestSuccess{
articleSavingRequest{
id
}
}
... on CreateArticleSavingRequestError{
errorCodes
}
}
}`;
function handleBackendUrl(url) {
try {
const FORCE_CONTENT_FETCH_URLS = [
// twitter status url regex
/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)(?:\/.*)?/,
/^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/,
]
return FORCE_CONTENT_FETCH_URLS.some((regex) => regex.test(url))
} catch (error) {
console.log('error checking url', url)
}
return false
}

View File

@ -1,53 +1,54 @@
/* global
browserApi
showMessage
showToolbar
prepareContent
getPageInfo
ACTIONS
*/
'use strict';
(function () {
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
'use strict'
;(function () {
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
if (darkModeQuery) {
darkModeQuery.onchange = function (ev) {
browserApi.runtime.sendMessage({
action: ACTIONS.RefreshDarkMode,
payload: {
value: ev.matches
}
});
};
value: ev.matches,
},
})
}
}
browserApi.runtime.onMessage.addListener(({ action, payload }, sender, sendResponse) => {
if (action === ACTIONS.GetContent) {
prepareContent().then((pageContent) => {
sendResponse({
type: pageContent.type,
doc: pageContent.content || '',
uploadContentObjUrl: pageContent.uploadContentObjUrl,
pageInfo: getPageInfo()
});
});
browserApi.runtime.onMessage.addListener(
({ action, payload }, sender, sendResponse) => {
if (action === ACTIONS.GetContent) {
prepareContent().then((pageContent) => {
sendResponse({
type: pageContent.type,
doc: pageContent.content || '',
uploadContentObjUrl: pageContent.uploadContentObjUrl,
pageInfo: getPageInfo(),
})
})
/* return true to signify handlers above can asynchronously invoke the response callback */
return true;
}
return true
}
/* other actions */
if (action === ACTIONS.Ping) {
sendResponse({ pong: true });
} else if (action === ACTIONS.ShowMessage) {
showMessage(payload);
} else if (action === ACTIONS.GetPageInfo) {
const pageInfo = getPageInfo();
sendResponse(pageInfo);
} else if (action === ACTIONS.AddIframeContent) {
// do nothing, handled by prepare-content.js
} else {
console.warn('Unknown message has been taken');
console.log('handling ', action, payload)
if (action === ACTIONS.Ping) {
sendResponse({ pong: true })
} else if (action === ACTIONS.ShowToolbar) {
showToolbar(payload)
} else if (action === ACTIONS.UpdateStatus) {
updateStatus(payload)
} else if (action === ACTIONS.LabelCacheUpdated) {
updateLabelsFromCache(payload)
} else if (action === ACTIONS.AddIframeContent) {
// do nothing, handled by prepare-content.js
} else {
console.warn('Unknown message has been taken')
}
}
});
})();
)
})()

View File

@ -1,216 +1,572 @@
/* global ENV_EXTENSION_ORIGIN */
'use strict';
(function () {
let currentToastEl;
let currentIconEl;
let currentTextEl;
let hideToastTimeout;
;('use strict')
;(function () {
let currentToastEl
let labels = []
let ctx = undefined
let doNotHide = false
let hideToastTimeout = undefined
const systemIcons = {
spinner: '<path d="M8.25.004a8 8 0 0 1 0 15.992L8 16a.5.5 0 0 1-.09-.992L8 15a7 7 0 0 0 .24-13.996L8 1a.5.5 0 0 1-.09-.992L8 0l.25.004z"><animateTransform attributeName="transform" attributeType="XML" dur="800ms" from="0 8 8" repeatCount="indefinite" to="360 8 8" type="rotate"/></path>',
success: '<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zm0 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm3.043 4.502.085.015.063.02.076.04.055.04.032.03.037.041.042.062.03.06.02.062.015.082.002.067-.008.068-.03.102-.05.093-.047.057-4.011 4.013a.5.5 0 0 1-.638.057l-.07-.057-2-2-.037-.042-.042-.062-.03-.06-.02-.062-.012-.06-.004-.053v-.057l.016-.086.02-.063.04-.076.04-.055.03-.032.041-.037.062-.042.06-.03.062-.02.059-.012.054-.004h.058l.085.016.063.02.093.052.057.046L7 9.293l3.646-3.647.042-.037.062-.042.06-.03.062-.02.059-.012.054-.004h.058z"/>',
failed: '<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zm0 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm0 10.5a.5.5 0 1 1 0 1 .5.5 0 0 1 0-1zm0-8a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0V4a.5.5 0 0 1 .5-.5z"/>',
close: '<path d="M3.646 3.646a.5.5 0 0 1 .708 0L8 7.293l3.646-3.647a.5.5 0 0 1 .708.708L8.707 8l3.647 3.646a.5.5 0 0 1-.708.708L8 8.707l-3.646 3.647a.5.5 0 0 1-.708-.708L7.293 8 3.646 4.354a.5.5 0 0 1 0-.708z"/>'
};
function createToastContainer () {
const toastEl = document.createElement('div');
toastEl.className = 'webext-omnivore-toast';
toastEl.style.cssText = `all: initial !important;
position: fixed !important;
top: 20px !important;
right: 45px !important;
z-index: 9999999 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
overflow: hidden !important;
width: 240px !important;
height: 80px !important;
border-radius: 10px !important;
background: #fff !important;
color: #3d3d3d !important;
fill: currentColor !important;
font: 700 13px Inter, sans-serif !important;
box-shadow: 0 1px 89px rgba(57, 59, 67, 0.25) !important;
user-select: none !important;
transition: all 300ms ease !important;
`;
return toastEl;
spinner: `
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.5835 17.7729C14.5541 17.7729 18.5835 13.9674 18.5835 9.27295C18.5835 4.57853 14.5541 0.772949 9.5835 0.772949C4.61293 0.772949 0.583496 4.57853 0.583496 9.27295C0.583496 13.9674 4.61293 17.7729 9.5835 17.7729ZM9.5835 16.3563C13.7256 16.3563 17.0835 13.185 17.0835 9.27295C17.0835 5.36093 13.7256 2.18962 9.5835 2.18962C5.44136 2.18962 2.0835 5.36093 2.0835 9.27295C2.0835 13.185 5.44136 16.3563 9.5835 16.3563Z" fill="url(#paint0_angular_980_6213)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.6697 7.57353C18.0805 7.52347 18.4565 7.79742 18.5095 8.1854C18.5588 8.54608 18.5835 8.90937 18.5835 9.27303C18.5835 9.66424 18.2477 9.98137 17.8335 9.98137C17.4193 9.98137 17.0835 9.66424 17.0835 9.27303C17.0835 8.96998 17.0629 8.66724 17.0219 8.36667C16.9689 7.97869 17.2589 7.62359 17.6697 7.57353Z" fill="#FFD234"/>
<defs>
<radialGradient id="paint0_angular_980_6213" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.5835 9.27295) scale(9 8.5)">
<stop stop-color="#FFD234"/>
<stop offset="0.0001" stop-color="white"/>
<stop offset="1" stop-color="#FFD234"/>
</radialGradient>
</defs>
`,
success: `
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.91626 18.6047C14.8868 18.6047 18.9163 14.5752 18.9163 9.60468C18.9163 4.63411 14.8868 0.604675 9.91626 0.604675C4.9457 0.604675 0.91626 4.63411 0.91626 9.60468C0.91626 14.5752 4.9457 18.6047 9.91626 18.6047ZM9.91626 17.1046C14.0584 17.1046 17.4163 13.7468 17.4163 9.60463C17.4163 5.4625 14.0584 2.10463 9.91626 2.10463C5.77412 2.10463 2.41626 5.4625 2.41626 9.60463C2.41626 13.7468 5.77412 17.1046 9.91626 17.1046Z" fill="#32D74B"/>
<path d="M13.3538 7.28851L8.7704 11.9209L6.47876 9.60469" stroke="#32D74B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`,
failure: `
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.74048 18.5508C14.711 18.5508 18.7405 14.5213 18.7405 9.55078C18.7405 4.58022 14.711 0.550781 9.74048 0.550781C4.76992 0.550781 0.740479 4.58022 0.740479 9.55078C0.740479 14.5213 4.76992 18.5508 9.74048 18.5508ZM9.74048 17.0507C13.8826 17.0507 17.2405 13.6929 17.2405 9.55074C17.2405 5.4086 13.8826 2.05074 9.74048 2.05074C5.59834 2.05074 2.24048 5.4086 2.24048 9.55074C2.24048 13.6929 5.59834 17.0507 9.74048 17.0507Z" fill="#C7372F"/>
<path d="M12.794 11.897L12.794 11.897L10.4474 9.55078L12.794 7.2046L12.794 7.20459C12.8878 7.11079 12.9405 6.98358 12.9405 6.85093C12.9405 6.71828 12.8878 6.59107 12.794 6.49727C12.7002 6.40348 12.573 6.35078 12.4403 6.35078C12.3077 6.35078 12.1805 6.40348 12.0867 6.49727L12.0867 6.49728L9.74048 8.84382L7.3943 6.49728L7.39429 6.49727C7.30049 6.40348 7.17328 6.35078 7.04063 6.35078C6.90798 6.35078 6.78077 6.40348 6.68697 6.49727C6.59317 6.59107 6.54048 6.71828 6.54048 6.85093C6.54048 6.98358 6.59317 7.11079 6.68697 7.20459L6.68698 7.2046L9.03351 9.55078L6.68698 11.897L6.68697 11.897C6.59317 11.9908 6.54048 12.118 6.54048 12.2506C6.54048 12.3833 6.59317 12.5105 6.68697 12.6043C6.78077 12.6981 6.90798 12.7508 7.04063 12.7508C7.17328 12.7508 7.30049 12.6981 7.39429 12.6043L7.3943 12.6043L9.74048 10.2577L12.0867 12.6043L12.0867 12.6043C12.1331 12.6507 12.1882 12.6876 12.2489 12.7127C12.3096 12.7378 12.3746 12.7508 12.4403 12.7508C12.506 12.7508 12.571 12.7378 12.6317 12.7127C12.6924 12.6876 12.7475 12.6507 12.794 12.6043C12.8404 12.5578 12.8773 12.5027 12.9024 12.442C12.9275 12.3814 12.9405 12.3163 12.9405 12.2506C12.9405 12.1849 12.9275 12.1199 12.9024 12.0592C12.8773 11.9986 12.8404 11.9434 12.794 11.897Z" fill="#C7372F" stroke="#C7372F" stroke-width="0.4"/>
</svg>`,
close:
'<path d="M3.646 3.646a.5.5 0 0 1 .708 0L8 7.293l3.646-3.647a.5.5 0 0 1 .708.708L8.707 8l3.647 3.646a.5.5 0 0 1-.708.708L8 8.707l-3.646 3.647a.5.5 0 0 1-.708-.708L7.293 8 3.646 4.354a.5.5 0 0 1 0-.708z"/>',
animatedLoader: `
<style>
.loading-spinner {
/* control spinner size with setting font-size */
font-size: 3rem;
border: 2px solid #FFD234;
border-top-color: transparent;
border-radius: 50%;
width: 18px;
height: 18px;
animation: loading-spinner 1.5s linear infinite;
margin: 0 auto;
box-sizing: border-box;
}
@keyframes loading-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<div class="loading-spinner"></div>
`,
}
function createToastCloseButton () {
const buttonEl = document.createElement('button');
buttonEl.style.cssText = `all: initial !important;
position: absolute !important;
top: 8px !important;
right: 8px !important;
border: none !important;
background: none !important;
color: inherit !important;
fill: inherit !important;
outline: none !important;
cursor: pointer !important;
`;
async function createToastContainer() {
const file = await fetch(browserApi.runtime.getURL('views/toast.html'))
const html = await file.text()
const iconEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconEl.setAttribute('viewBox', '0 0 16 16');
iconEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
iconEl.style.cssText = `all: initial !important;
width: 16px !important;
height: 16px !important;
color: inherit !important;
fill: inherit !important;
`;
iconEl.addEventListener('click', function () {
currentToastEl.remove();
});
iconEl.innerHTML = systemIcons.close;
buttonEl.appendChild(iconEl);
return buttonEl;
}
function createCtaModal (url) {
const fragment = document.createDocumentFragment();
const closeButtonEl = createToastCloseButton();
fragment.appendChild(closeButtonEl);
const iframeEl = document.createElement('iframe');
const iframePath = '/views/cta-popup.html?url=' + encodeURIComponent(url);
const iframeUrl = ENV_EXTENSION_ORIGIN + iframePath;
iframeEl.src = iframeUrl;
iframeEl.style.cssText = `all: initial !important;
width: 310px !important;
height: 360px !important;
`;
fragment.appendChild(iframeEl);
return fragment;
}
function updateToastText (payload) {
if (!currentTextEl) return;
if (!payload) {
currentTextEl.textContent = '';
return;
const root = document.createElement('div')
root.attachShadow({ mode: 'open' })
if (root.shadowRoot) {
root.shadowRoot.innerHTML = `<style>:host {all initial;}</style>`
}
currentTextEl.textContent = payload.text || '';
const toastEl = document.createElement('div')
toastEl.id = '#omnivore-toast'
toastEl.innerHTML = html
root.shadowRoot.appendChild(toastEl)
const potentialLink = payload.link;
if (!potentialLink) return;
document.body.appendChild(root)
connectButtons(root)
const linkEl = document.createElement('a');
if (potentialLink.startsWith('http')) {
linkEl.href = potentialLink;
return root
}
async function createCtaModal(url) {
if (currentToastEl) {
currentToastEl.remove()
currentToastEl = undefined
}
linkEl.target = '_blank';
linkEl.rel = 'external nofollow noopener noreferrer';
linkEl.textContent = payload.linkText || 'link';
linkEl.style.cssText = `all: initial !important;
margin-left: 1rem !important;
color: #0645ad !important;
font: inherit !important;
cursor: pointer !important;
`;
currentTextEl.appendChild(linkEl);
const file = await fetch(browserApi.runtime.getURL('/views/cta-popup.html'))
const html = await file.text()
const root = document.createElement('div')
root.attachShadow({ mode: 'open' })
if (root.shadowRoot) {
root.shadowRoot.innerHTML = `<style>:host {all initial;}</style>`
}
const toastEl = document.createElement('div')
toastEl.id = '#omnivore-toast'
toastEl.innerHTML = html
root.shadowRoot.appendChild(toastEl)
document.body.appendChild(root)
connectButtons(root)
return root
}
function loadInitialToastContent () {
currentToastEl.textContent = '';
const iconEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconEl.setAttribute('viewBox', '0 0 16 16');
iconEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
iconEl.style.cssText = `all: initial !important;
height: 20px !important;
width: 20px !important;
margin-left: 24px !important;
fill: inherit !important;
color: inherit !important;
`;
currentIconEl = iconEl;
currentToastEl.appendChild(iconEl);
const textEl = document.createElement('div');
textEl.style.cssText = `all: initial !important;
flex: 1 !important;
padding: 0 24px !important;
color: inherit !important;
font: inherit !important;
`;
currentTextEl = textEl;
currentToastEl.appendChild(textEl);
function displayLoggedOutView() {
cancelAutoDismiss()
updatePageStatus('failure')
toggleRow('#omnivore-logged-out-row')
updateStatusBox(
'#omnivore-logged-out-status',
'empty',
`You are not logged in.`
)
disableAllButtons()
}
function hideToastAfter (timeInMs) {
if (hideToastTimeout) clearTimeout(hideToastTimeout);
hideToastTimeout = setTimeout(function () {
currentToastEl.remove();
}, timeInMs);
function disableAllButtons() {
const actionButtons = [
'#omnivore-toast-edit-title-btn',
'#omnivore-toast-edit-labels-btn',
'#omnivore-toast-read-now-btn',
]
actionButtons.forEach((btnId) => {
const btn = currentToastEl.shadowRoot.querySelector(btnId)
btn.disabled = true
})
}
function showMessageToast (payload) {
const bodyEl = document.body;
if (!bodyEl) return;
function cancelAutoDismiss() {
doNotHide = true
if (hideToastTimeout) clearTimeout(hideToastTimeout)
}
let duration = 5e3;
function updateStatus(payload) {
if (!currentToastEl) {
console.log('no statusBox to update')
return
}
if (payload.ctx) {
ctx = { ...ctx, ...payload.ctx }
}
switch (payload.target) {
case 'logged_out':
displayLoggedOutView()
break
case 'page':
updatePageStatus(payload.status)
break
case 'title':
updateStatusBox(
'#omnivore-edit-title-status',
payload.status,
payload.message,
payload.status == 'success' ? 2500 : undefined
)
break
case 'labels':
updateStatusBox(
'#omnivore-edit-labels-status',
payload.status,
payload.message,
payload.status == 'success' ? 2500 : undefined
)
break
}
}
function showToolbar(payload) {
ctx = payload.ctx
showToolbarAsync(payload).catch((err) =>
console.log('error showing toast', err)
)
}
function updateLabelsFromCache(payload) {
;(async () => {
await getStorageItem('labels').then((cachedLabels) => {
labels = cachedLabels
console.log(' == updated labels', cachedLabels)
})
})()
}
async function showToolbarAsync(payload) {
const bodyEl = document.body
if (!bodyEl) return
if (!currentToastEl) {
currentToastEl = createToastContainer();
currentToastEl = await createToastContainer()
}
if (!currentIconEl || !currentTextEl) {
loadInitialToastContent();
}
let styleAsError = false;
if (payload.type === 'loading') {
duration = 20e3;
currentIconEl.innerHTML = systemIcons.spinner;
updateToastText(payload);
} else if (payload.type !== 'error') {
currentIconEl.innerHTML = systemIcons.success;
updateToastText(payload);
} else if (payload.errorCode && payload.errorCode === 401) {
currentToastEl.textContent = '';
currentToastEl.style.setProperty('width', '310px', 'important');
currentToastEl.style.setProperty('height', 'auto', 'important');
currentIconEl = null;
currentTextEl = null;
const ctaModalEl = createCtaModal(payload.url);
currentToastEl.appendChild(ctaModalEl);
duration = 8e3;
} else {
styleAsError = true;
currentIconEl.innerHTML = systemIcons.failed;
updateToastText(payload);
updateStatus({
status: 'loading',
target: 'page',
})
}
const newBackgroundColor = styleAsError ? '#808080' : '#fff';
const newTextColor = styleAsError ? '#fff' : '#3d3d3d';
currentToastEl.style.setProperty('background', newBackgroundColor, 'important');
currentToastEl.style.setProperty('color', newTextColor, 'important');
if (currentToastEl.parentNode !== bodyEl) {
bodyEl.appendChild(currentToastEl);
}
hideToastAfter(duration);
// remove any existing toasts not created by current content script
const presentToastsCol = document.querySelectorAll('.webext-omnivore-toast');
for (let i = 0; i < presentToastsCol.length; i++) {
const toastEl = presentToastsCol[i];
document.querySelectorAll('#omnivore-toast').forEach((toastEl) => {
if (toastEl !== currentToastEl) {
toastEl.remove();
console.log('removing current toast el: ', currentToastEl)
toastEl.remove()
}
})
}
function updatePageStatus(status) {
const statusBox = currentToastEl.shadowRoot.querySelector(
'.omnivore-toast-statusBox'
)
switch (status) {
case 'loading':
statusBox.innerHTML = systemIcons.animatedLoader
break
case 'success':
// Auto hide if everything went well and the user
// has not initiated any interaction.
hideToastTimeout = setTimeout(function () {
console.log('hiding: ', currentToastEl, doNotHide)
if (!doNotHide) {
currentToastEl.remove()
currentToastEl = undefined
}
}, 2500)
statusBox.innerHTML = systemIcons.success
break
case 'failure':
statusBox.innerHTML = systemIcons.failure
}
}
function updateStatusBox(boxId, state, message, dismissAfter) {
const statusBox = currentToastEl.shadowRoot.querySelector(boxId)
const image = (() => {
switch (state) {
case 'loading':
return systemIcons.animatedLoader
case 'success':
return systemIcons.success
case 'failure':
return systemIcons.failure
case 'none':
return ''
default:
return undefined
}
})()
if (image) {
statusBox.innerHTML = `<span style='padding-right: 10px'>${image}</span><span style='line-height: 20px'>${message}</span>`
} else {
statusBox.innerHTML = message
}
if (dismissAfter) {
setTimeout(() => {
statusBox.innerHTML = null
}, dismissAfter)
}
}
function toggleRow(rowId) {
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)
}
}
function connectButtons(root) {
const btns = [
{ 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: closeToast },
{ id: '#omnivore-toast-login-btn', func: login },
]
for (const btnInfo of btns) {
const btn = root.shadowRoot.querySelector(btnInfo.id)
if (btn) {
btn.addEventListener('click', btnInfo.func)
}
}
}
window.showMessage = showMessageToast;
})();
function createLabelRow(label, idx) {
const element = document.createElement('button')
const dot = document.createElement('span')
dot.style = 'width:10px;height:10px;border-radius:1000px;'
dot.style.setProperty('background-color', label.color)
const title = document.createElement('span')
title.style = 'margin-left: 10px;pointer-events: none;'
title.innerText = label.name
const check = document.createElement('span')
check.style = 'margin-left: auto;pointer-events: none;'
check.className = 'checkbox'
check.innerHTML = `
<svg width="14" height="11" viewBox="0 0 14 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.7411 1.75864L4.79692 10.7028L0.69751 6.60341L1.74845 5.55246L4.79692 8.59348L12.6902 0.707703L13.7411 1.75864Z" fill="#888888"/>
</svg>
`
element.appendChild(dot)
element.appendChild(title)
element.appendChild(check)
element.onclick = labelClick
element.onkeydown = labelKeyDown
element.setAttribute('data-label-id', label.id)
element.setAttribute('data-label-idx', idx)
element.setAttribute(
'data-label-selected',
label['selected'] ? 'on' : 'off'
)
element.setAttribute('tabIndex', '-1')
return element
}
function labelClick(event) {
event.preventDefault()
const labelId = event.target.getAttribute('data-label-id')
toggleLabel(event, labelId)
}
function toggleLabel(event, labelId) {
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'
)
const label = labels.find((l) => l.id === labelId)
if (label) {
label.selected = toggledValue
}
}
function labelKeyDown(event) {
switch (event.key.toLowerCase()) {
case 'arrowup': {
if (
event.target ==
event.target.form.querySelector('#omnivore-edit-label-text')
) {
return
}
const idx = event.target.getAttribute('data-label-idx')
let prevIdx = idx && Number(idx) != NaN ? Number(idx) - 1 : 0
if (
event.target ==
event.target.form.querySelector('#omnivore-save-button')
) {
// Focus the last label index
const maxItemIdx = Math.max(
...Array.from(
event.target.form.querySelectorAll(`button[data-label-idx]`)
).map((b) => Number(b.getAttribute('data-label-idx')))
)
if (maxItemIdx != NaN) {
prevIdx = maxItemIdx
}
}
const prev = event.target.form.querySelector(
`button[data-label-idx='${prevIdx}']`
)
if (prev) {
prev.focus()
} else {
// Focus the text area
event.target.form.querySelector('#omnivore-edit-label-text')?.focus()
}
event.preventDefault()
break
}
case 'arrowdown': {
const idx = event.target.getAttribute('data-label-idx')
const nextIdx = idx && Number(idx) != NaN ? Number(idx) + 1 : 0
const next = event.target.form.querySelector(
`button[data-label-idx='${nextIdx}']`
)
if (next) {
next.focus()
} else {
// Focus the save button
event.target.form.querySelector('.omnivore-save-button')?.focus()
}
event.preventDefault()
break
}
case 'enter': {
const labelId = event.target.getAttribute('data-label-id')
toggleLabel(event, labelId)
event.preventDefault()
break
}
}
}
function editTitle() {
cancelAutoDismiss()
toggleRow('#omnivore-edit-title-row')
currentToastEl.shadowRoot
.querySelector('#omnivore-edit-title-textarea')
?.focus()
currentToastEl.shadowRoot.querySelector(
'#omnivore-edit-title-form'
).onsubmit = (event) => {
updateStatusBox(
'#omnivore-edit-title-status',
'loading',
'Updating title...'
)
browserApi.runtime.sendMessage({
action: ACTIONS.EditTitle,
payload: {
ctx: ctx,
title: event.target.elements.title.value,
},
})
event.preventDefault()
}
}
async function editLabels() {
cancelAutoDismiss()
await getStorageItem('labels').then((cachedLabels) => {
labels = cachedLabels
})
toggleRow('#omnivore-edit-labels-row')
currentToastEl.shadowRoot
.querySelector('#omnivore-edit-label-text')
?.focus()
const list = currentToastEl.shadowRoot.querySelector(
'#omnivore-edit-labels-list'
)
currentToastEl.shadowRoot
.querySelector('#omnivore-edit-label-text')
.addEventListener('input', function () {
updateLabels(this.value)
})
currentToastEl.shadowRoot.querySelector(
'#omnivore-edit-label-text'
).onkeydown = labelKeyDown
if (list) {
list.innerHTML = ''
labels.forEach(function (label, idx) {
const rowHtml = createLabelRow(label, idx)
list.appendChild(rowHtml)
})
}
currentToastEl.shadowRoot.querySelector(
'#omnivore-edit-labels-form'
).onsubmit = (event) => {
event.preventDefault()
const statusBox = currentToastEl.shadowRoot.querySelector(
'#omnivore-edit-labels-status'
)
statusBox.innerText = 'Updating labels...'
const labelIds = labels.filter((l) => l['selected']).map((l) => l.id)
browserApi.runtime.sendMessage({
action: ACTIONS.SetLabels,
payload: {
ctx: ctx,
labelIds: labelIds,
},
})
}
}
async function updateLabels(filterValue) {
const list = currentToastEl.shadowRoot.querySelector(
'#omnivore-edit-labels-list'
)
if (list) {
list.innerHTML = ''
if (filterValue) {
labels
.filter(
(l) => l.name.toLowerCase().indexOf(filterValue.toLowerCase()) > -1
)
.forEach(function (label, idx) {
const rowHtml = createLabelRow(label, idx)
list.appendChild(rowHtml)
})
} else {
labels.forEach(function (label, idx) {
const rowHtml = createLabelRow(label, idx)
list.appendChild(rowHtml)
})
}
}
}
function readNow() {
cancelAutoDismiss()
const container = currentToastEl.shadowRoot.querySelector(
'#omnivore-toast-container'
)
container.setAttribute('data-state', 'open')
if (ctx && ctx.readerURL) {
window.open(ctx.readerURL, '_blank')
} else if (ctx) {
window.open(
new URL(`/article?url=${encodeURI(ctx.originalURL)}`, ctx.omnivoreURL),
'_blank'
)
} else {
alert('Error no URL found.')
}
setTimeout(() => {
closeToast()
}, 1000)
}
function openMenu() {
cancelAutoDismiss()
toggleRow('omnivore-extra-buttons-row')
}
function closeToast() {
currentToastEl.remove()
currentToastEl = undefined
}
function login() {
window.open(new URL(`/login`, ctx.omnivoreURL), '_blank')
setTimeout(closeToast, 2000)
}
window.showToolbar = showToolbar
window.updateStatus = updateStatus
window.updateLabelsFromCache = updateLabelsFromCache
})()

View File

@ -6,7 +6,8 @@
<title>Omnivore background</title>
<script src="/scripts/constants.js"></script>
<script src="/scripts/api.js"></script>
<script src="/scripts/common.js"></script>
<script src="/scripts/background.js"></script>
</head>

View File

@ -7,7 +7,102 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/styles/cta-popup.css">
<style>
@font-face {
font-style: normal;
font-weight: 400;
font-family: Inter;
src:
local(""),
url("/fonts/inter-v3-latin-400.woff2") format("woff2");
}
@font-face {
font-style: normal;
font-weight: 600;
font-family: Inter;
src:
local(""),
url("/fonts/inter-v3-latin-600.woff2") format("woff2");
}
@font-face {
font-style: normal;
font-weight: 700;
font-family: Inter;
src:
local(""),
url("/fonts/inter-v3-latin-700.woff2") format("woff2");
}
html,
body {
width: 100%;
height: 100%;
}
body {
margin: 0;
}
.cta-container {
position: relative;
overflow: hidden;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 24px 12px;
background: linear-gradient(150deg, #fff 55%, #ffde8c 55%);
background-color: #fff;
color: #3d3d3d;
font-family: Inter, sans-serif;
text-align: center;
}
.cta-container__wrapper {
position: relative;
z-index: 1;
}
.cta-container__title {
padding: 0 24px 24px;
font-weight: 600;
font-size: 16px;
}
.cta-container__icon {
vertical-align: unset;
margin-bottom: -4px;
fill: none;
}
.cta-container__link {
display: flex;
align-items: center;
justify-content: center;
width: 184px;
height: 34px;
margin: 36px auto 14px;
border-radius: 40px;
background-color: #fff;
color: inherit;
font-weight: 700;
font-size: 12px;
text-decoration: none;
cursor: pointer;
}
.cta-container__text {
margin: 0;
font-size: 12px;
}
.cta-container__textlink {
color: inherit;
font-weight: 700;
}
</style>
</head>
<body>
<div class="cta-container">

View File

@ -0,0 +1,337 @@
<style>
#omnivore-toast-container {
position: fixed;
top: 20px;
right: 30px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-end;
overflow: hidden;
border-radius: 4px;
background: #fff;
font: 400 12px Inter, sans-serif;
line-height: 20px;
box-shadow: 0px 5px 20px rgba(32, 31, 29, 0.12);
transition: all 300ms ease;
z-index: 9999999;
width: 455px;
}
#omnivore-toast-container .omnivore-toast-func-row {
display: flex;
width: 100%;
padding-top: 0px;
padding-left: 15px;
padding-right: 15px;
padding-bottom: 10px;
}
#omnivore-toast-container #omnivore-logged-out-row {
flex-direction: column;
align-items: center;
justify-content: center;
}
#omnivore-toast-container .omnivore-toast-func-row[data-state="open"] {
display: flex;
}
#omnivore-toast-container .omnivore-toast-func-row[data-state="closed"] {
display: none;
}
#omnivore-toast-container .omnivore-toast-func-status {
display: flex;
align-items: flex-start;
justify-content: center;
width: 100%;
color:#898989;
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 20px;
text-align: center;
}
#omnivore-toast-button-row {
gap: 5px;
align-items: center;
padding-top: 7px;
padding-bottom: 7px;
display: flex;
width: 100%;
padding-left: 15px;
padding-right: 10px;
}
#omnivore-toast-container button {
display: flex;
align-items: center;
gap: 8px;
color: #6A6968;
border: none;
background-color: transparent;
cursor: pointer;
border-radius: 5px;
height: 30px;
margin-left: auto;
}
#omnivore-toast-container button:hover {
color: #3B3A38;
background-color: #EBEBEB;
}
#omnivore-toast-container .omnivore-save-button button {
background-color: rgb(255, 210, 52)
}
#omnivore-toast-container #omnivore-toast-close-btn:hover {
background-color: unset;
}
#omnivore-toast-container #omnivore-toast-close-btn svg {
fill: #3D3D3D;
}
#omnivore-toast-container #omnivore-toast-close-btn svg:hover {
fill: #D9D9D9;
stroke: white;
}
#omnivore-toast-container form {
display: block;
margin: 0px;
}
.omnivore-toast-divider {
height:20px;
border-right: 1px solid #D9D9D9;
}
.omnivore-toast-statusBox {
display: flex;
width: 20px;
margin-left: 0px;
margin-right: 7px;
}
#omnivore-edit-title-row {
flex-direction: column;
visibility: unset;
padding-top: 20px;
height: 100%;
gap: 10px;
}
#omnivore-edit-title-row textarea {
width: 100%;
height: 100px;
padding-top: 10px;
padding-bottom: 10px;
resize: none;
border: 1px solid #8E8E93;
border-radius: 4px;
box-sizing: border-box;
background-color: transparent;
}
#omnivore-toast-container .omnivore-toast-func-row button {
height: 30px;
padding: 15px;
margin-left: auto;
background-color: #FFEA9F;
}
#omnivore-edit-labels-row {
flex-direction: column;
visibility: unset;
padding-top: 20px;
height: 100%;
gap: 10px;
}
#omnivore-edit-labels-row input {
width: 100%;
height: 34px;
padding: 10px;
border: 1px solid #8E8E93;
border-radius: 4px;
box-sizing: border-box;
background-color: transparent;
}
#omnivore-extra-buttons-row {
flex-direction: column;
padding-top: 5px;
padding-bottom: 10px;
gap: 5px;
}
#omnivore-toast-container #omnivore-extra-buttons-row button {
width: 80%;
align-self: flex-start;
padding: 10px;
margin: 0px;
background-color: transparent;
border-radius: 5px;
}
#omnivore-toast-container #omnivore-extra-buttons-row button:hover {
background-color: #EBEBEB;
}
#omnivore-edit-labels-list {
overflow-y: scroll;
max-height: 200px;
gap: 5px;
color: #3B3A38;
margin-top: 15px;
margin-bottom: 10px;
}
#omnivore-toast-container #omnivore-edit-labels-list button {
height: 35px;
display: flex;
align-items: center;
padding-left: 10px;
padding-right: 10px;
border-bottom: 1px solid #EEEEEE;
border-radius: 0px;
width: 100%;
background: unset;
}
#omnivore-toast-container #omnivore-edit-labels-list button[data-label-selected="on"] .checkbox {
visibility: visible;
}
#omnivore-toast-container #omnivore-edit-labels-list button[data-label-selected="off"] .checkbox {
visibility: hidden;
}
#omnivore-toast-container #omnivore-edit-labels-list button:focus-visible {
outline: unset;
border-radius: 5px;
background-color: #EBEBEB;
}
#omnivore-edit-labels-list div:hover {
background-color: #EBEBEB;
}
#omnivore-edit-labels-row form {
margin-bottom: unset;
}
</style>
<div id="omnivore-toast-container">
<div id="omnivore-toast-button-row">
<span class="omnivore-toast-statusBox">
<style>
.loading-spinner {
/* control spinner size with setting font-size */
font-size: 3rem;
border: 2px solid #FFD234;
border-top-color: transparent;
border-radius: 50%;
width: 18px;
height: 18px;
animation: loading-spinner 1.5s linear infinite;
margin: 0 auto;
box-sizing: border-box;
}
@keyframes loading-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<div class="loading-spinner"></div>
</span>
<span class="omnivore-toast-divider"></span>
<button class="omnivore-top-button" id="omnivore-toast-edit-title-btn">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5625 4.50004L13.5 8.43754M6.71406 15.1516L2.84828 11.2858M6.517 15.1875H3.375C3.22582 15.1875 3.08274 15.1283 2.97725 15.0228C2.87176 14.9173 2.8125 14.7742 2.8125 14.625V11.483C2.8125 11.4092 2.82705 11.336 2.85532 11.2678C2.88359 11.1995 2.92502 11.1375 2.97725 11.0853L11.4148 2.64778C11.5202 2.5423 11.6633 2.48303 11.8125 2.48303C11.9617 2.48303 12.1048 2.5423 12.2102 2.64778L15.3523 5.78979C15.4577 5.89528 15.517 6.03835 15.517 6.18754C15.517 6.33672 15.4577 6.4798 15.3523 6.58528L6.91475 15.0228C6.86252 15.075 6.80051 15.1165 6.73226 15.1447C6.66402 15.173 6.59087 15.1875 6.517 15.1875Z" stroke="#6A6968" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Edit Title
</button>
<span class="omnivore-toast-divider"></span>
<button id="omnivore-toast-edit-labels-btn">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.99031 14.9786L15.3061 8.67029C15.3757 8.6002 15.4307 8.51707 15.468 8.42568C15.5053 8.33429 15.5242 8.23643 15.5237 8.13772L15.5237 1.38683C15.5237 1.18789 15.4446 0.997101 15.304 0.85643C15.1633 0.715759 14.9725 0.636731 14.7736 0.636731L8.02269 0.636731C7.92397 0.63616 7.82611 0.655082 7.73472 0.69241C7.64333 0.729738 7.5602 0.784739 7.49012 0.85426L1.18179 7.17009C0.76038 7.59202 0.523681 8.16397 0.523681 8.7603C0.523681 9.35663 0.76038 9.92857 1.18179 10.3505L5.77239 14.9786C6.19432 15.4 6.76627 15.6367 7.3626 15.6367C7.95893 15.6367 8.53087 15.4 8.95281 14.9786L8.99031 14.9786ZM6.87503 13.921L2.24693 9.28536C2.10722 9.14482 2.0288 8.95471 2.0288 8.75655C2.0288 8.55838 2.10722 8.36827 2.24693 8.22773L8.33022 2.13693L14.0235 2.13693L14.0235 7.83018L7.93267 13.921C7.86258 13.9905 7.77946 14.0455 7.68807 14.0828C7.59668 14.1202 7.49882 14.1391 7.4001 14.1385C7.20332 14.1377 7.01475 14.0595 6.87503 13.921Z" fill="#6A6968"/>
<circle cx="10.8818" cy="5.48069" r="1.24925" fill="#6A6968"/>
</svg>
Set Labels
</button>
<span class="omnivore-toast-divider"></span>
<button id="omnivore-toast-read-now-btn">
<svg width="18" height="15" viewBox="0 0 18 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.1272 0.939454H12.6584C11.6995 0.939454 10.762 1.21484 9.95532 1.73438L9.0022 2.3457L8.04907 1.73438C7.24323 1.21494 6.30469 0.938941 5.34595 0.939454H0.877197C0.531494 0.939454 0.252197 1.21875 0.252197 1.56445V12.6582C0.252197 13.0039 0.531494 13.2832 0.877197 13.2832H5.34595C6.30493 13.2832 7.24243 13.5586 8.04907 14.0781L8.91626 14.6367C8.94165 14.6523 8.97095 14.6621 9.00024 14.6621C9.02954 14.6621 9.05884 14.6543 9.08423 14.6367L9.95142 14.0781C10.76 13.5586 11.6995 13.2832 12.6584 13.2832H17.1272C17.4729 13.2832 17.7522 13.0039 17.7522 12.6582V1.56445C17.7522 1.21875 17.4729 0.939454 17.1272 0.939454ZM5.34595 11.877H1.65845V2.3457H5.34595C6.03735 2.3457 6.70923 2.54297 7.28931 2.91602L8.24243 3.52734L8.3772 3.61523V12.6387C7.44751 12.1387 6.40845 11.877 5.34595 11.877ZM16.3459 11.877H12.6584C11.5959 11.877 10.5569 12.1387 9.6272 12.6387V3.61523L9.76196 3.52734L10.7151 2.91602C11.2952 2.54297 11.967 2.3457 12.6584 2.3457H16.3459V11.877ZM6.75415 4.8457H3.12524C3.04907 4.8457 2.98657 4.91211 2.98657 4.99219V5.87109C2.98657 5.95117 3.04907 6.01758 3.12524 6.01758H6.7522C6.82837 6.01758 6.89087 5.95117 6.89087 5.87109V4.99219C6.89282 4.91211 6.83032 4.8457 6.75415 4.8457ZM11.1116 4.99219V5.87109C11.1116 5.95117 11.1741 6.01758 11.2502 6.01758H14.8772C14.9534 6.01758 15.0159 5.95117 15.0159 5.87109V4.99219C15.0159 4.91211 14.9534 4.8457 14.8772 4.8457H11.2502C11.1741 4.8457 11.1116 4.91211 11.1116 4.99219ZM6.75415 7.58008H3.12524C3.04907 7.58008 2.98657 7.64648 2.98657 7.72656V8.60547C2.98657 8.68555 3.04907 8.75195 3.12524 8.75195H6.7522C6.82837 8.75195 6.89087 8.68555 6.89087 8.60547V7.72656C6.89282 7.64648 6.83032 7.58008 6.75415 7.58008ZM14.8792 7.58008H11.2502C11.1741 7.58008 11.1116 7.64648 11.1116 7.72656V8.60547C11.1116 8.68555 11.1741 8.75195 11.2502 8.75195H14.8772C14.9534 8.75195 15.0159 8.68555 15.0159 8.60547V7.72656C15.0178 7.64648 14.9553 7.58008 14.8792 7.58008Z" fill="#6A6968"/>
</svg>
Read Now
</button>
<span class="omnivore-toast-divider"></span>
<!--
<button id="omnivore-open-menu-btn">
<svg width="15" height="4" viewBox="0 0 15 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="1.48679" cy="1.79492" rx="1.4846" ry="1.5" fill="#6A6968"/>
<ellipse cx="7.00217" cy="1.79492" rx="1.4846" ry="1.5" fill="#6A6968"/>
<ellipse cx="7.00217" cy="1.79492" rx="1.4846" ry="1.5" fill="#6A6968"/>
<ellipse cx="12.5176" cy="1.79492" rx="1.4846" ry="1.5" fill="#6A6968"/>
</svg>
</button>
<span class="omnivore-toast-divider"></span>
-->
<button id="omnivore-toast-close-btn">
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9.50049" cy="9.52783" r="9" />
<path d="M12.554 11.874L12.554 11.874L10.2075 9.52783L12.554 7.18165L12.554 7.18164C12.6478 7.08785 12.7005 6.96063 12.7005 6.82798C12.7005 6.69533 12.6478 6.56812 12.554 6.47432C12.4602 6.38053 12.333 6.32783 12.2003 6.32783C12.0677 6.32783 11.9405 6.38053 11.8467 6.47432L11.8467 6.47433L9.50049 8.82087L7.15431 6.47433L7.1543 6.47432C7.0605 6.38053 6.93329 6.32783 6.80064 6.32783C6.66799 6.32783 6.54078 6.38053 6.44698 6.47432C6.35318 6.56812 6.30049 6.69533 6.30049 6.82798C6.30049 6.96063 6.35318 7.08785 6.44698 7.18164L6.44699 7.18165L8.79352 9.52783L6.44699 11.874L6.44698 11.874C6.35318 11.9678 6.30049 12.095 6.30049 12.2277C6.30049 12.3603 6.35318 12.4875 6.44698 12.5813C6.54078 12.6751 6.66799 12.7278 6.80064 12.7278C6.93329 12.7278 7.0605 12.6751 7.1543 12.5813L7.15431 12.5813L9.50049 10.2348L11.8467 12.5813L11.8467 12.5813C11.8931 12.6278 11.9483 12.6646 12.0089 12.6898C12.0696 12.7149 12.1347 12.7278 12.2003 12.7278C12.266 12.7278 12.3311 12.7149 12.3917 12.6898C12.4524 12.6646 12.5076 12.6278 12.554 12.5813C12.6004 12.5349 12.6373 12.4798 12.6624 12.4191C12.6876 12.3584 12.7005 12.2934 12.7005 12.2277C12.7005 12.162 12.6876 12.097 12.6624 12.0363C12.6373 11.9756 12.6004 11.9205 12.554 11.874Z" fill="#EBEBEB" stroke="#EBEBEB" stroke-width="0.4"/>
</svg>
</button>
</div>
<div id="omnivore-edit-title-row" class="omnivore-toast-func-row" data-state="closed">
<span id="omnivore-edit-title-status" class="omnivore-toast-func-status"></span>
<form id="omnivore-edit-title-form">
<textarea id="omnivore-edit-title-textarea" name="title"></textarea>
<button class="omnivore-save-button">Save</button>
</form>
</div>
<div id="omnivore-edit-labels-row" class="omnivore-toast-func-row" data-state="closed">
<span id="omnivore-edit-labels-status" class="omnivore-toast-func-status"></span>
<form id="omnivore-edit-labels-form">
<input type="text" id="omnivore-edit-label-text" placeholder="Filter for labels" tabindex="0"> </input>
<div id="omnivore-edit-labels-list">
</div>
<button class="omnivore-save-button">Save</button>
</form>
</div>
<div id="omnivore-extra-buttons-row" class="omnivore-toast-func-row" data-state="closed">
<button>
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.0143 0.166748H1.93155C1.157 0.166748 0.523193 0.800512 0.523193 1.5751V3.92222C0.523193 4.6264 1.03956 5.18958 1.69675 5.30698V14.0148C1.69675 14.7893 2.33052 15.4231 3.10511 15.4231H14.8407C15.6152 15.4231 16.249 14.7894 16.249 14.0148V5.30698C16.9062 5.18959 17.4226 4.6264 17.4226 3.92222V1.5751C17.4226 0.800554 16.7888 0.166748 16.0143 0.166748ZM1.93155 1.5751H16.0143V3.92222H1.93155V1.5751ZM14.8407 14.0148H3.10511V5.33049H14.8407V14.0148Z" fill="#6A6968"/>
<path d="M7.82307 8.26431H10.1702C10.5692 8.26431 10.8744 7.95914 10.8744 7.56013C10.8744 7.16114 10.5692 6.85596 10.1702 6.85596H7.82307C7.42408 6.85596 7.1189 7.16113 7.1189 7.56013C7.1189 7.95912 7.44748 8.26431 7.82307 8.26431Z" fill="#6A6968"/>
</svg>
Archive
</button>
<span style="border-bottom: 1px solid #D9D9D9;width:80%"></span>
<button>
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6602 5.16992L3.51147 5.16993" stroke="#6A6968" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.29272 8.91992V13.9199" stroke="#6A6968" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.8787 8.91992V13.9199" stroke="#6A6968" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.09741 2.66992H13.0741" stroke="#6A6968" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.4648 5.16993V17.0449C15.4648 17.2107 15.4018 17.3697 15.2897 17.4869C15.1777 17.6041 15.0256 17.6699 14.8671 17.6699H5.30445C5.14594 17.6699 4.99392 17.6041 4.88184 17.4869C4.76976 17.3697 4.70679 17.2107 4.70679 17.0449V5.16992" stroke="#6A6968" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Delete
</button>
</div>
<div id="omnivore-logged-out-row" class="omnivore-toast-func-row" data-state="closed">
<span id="omnivore-logged-out-status" class="omnivore-toast-func-status"></span>
<a href="" id="omnivore-toast-login-btn">Login to Omnivore</a>
</div>
</div>