This commit is contained in:
Jackson Harper
2023-09-30 12:46:23 +08:00
parent db4604e21f
commit 44e85ea295
15 changed files with 536 additions and 272 deletions

View File

@ -160,3 +160,9 @@ dependencies {
apollo {
packageName.set 'app.omnivore.omnivore.graphql.generated'
}
task printVersion {
doLast {
println "omnivoreVersion: ${android.defaultConfig.versionName}"
}
}

View File

@ -131,26 +131,26 @@ const postgresMigration = postgrator
process.exit(1)
})
// elastic migration
log('Creating elastic index...')
const elasticMigration = esClient.indices
.exists({ index: INDEX_ALIAS })
.then(({ body: exists }) => {
if (!exists) {
return createIndex().then(() => log('Elastic index created.'))
} else {
log('Elastic index already exists.')
}
})
.then(() => {
log('Updating elastic index mappings...')
return updateMappings().then(() => {
log('Elastic index mappings updated.')
})
})
.catch((error) => {
log(`${chalk.red('Elastic migration failed: ')}${error.message}`, chalk.red)
process.exit(1)
})
// // elastic migration
// log('Creating elastic index...')
// const elasticMigration = esClient.indices
// .exists({ index: INDEX_ALIAS })
// .then(({ body: exists }) => {
// if (!exists) {
// return createIndex().then(() => log('Elastic index created.'))
// } else {
// log('Elastic index already exists.')
// }
// })
// .then(() => {
// log('Updating elastic index mappings...')
// return updateMappings().then(() => {
// log('Elastic index mappings updated.')
// })
// })
// .catch((error) => {
// log(`${chalk.red('Elastic migration failed: ')}${error.message}`, chalk.red)
// process.exit(1)
// })
Promise.all([postgresMigration, elasticMigration]).then(() => log('Exiting...'))
Promise.all([postgresMigration]).then(() => log('Exiting...'))

View File

@ -194,12 +194,13 @@ export const Button = styled('button', {
},
},
link: {
color: '$grayText',
border: 'none',
bg: 'transparent',
'&:hover': {
opacity: 0.8,
},
fontSize: '14px',
fontWeight: 'regular',
fontFamily: '$display',
color: '$thLibraryMenuUnselected',
cursor: 'pointer',
},
circularIcon: {
mx: '$1',

View File

@ -25,16 +25,21 @@ export interface FormInputProps {
}
export const FormInput = styled('input', {
border: 'none',
border: '1px solid $textNonessential',
width: '100%',
bg: 'transparent',
fontSize: '16px',
fontFamily: 'inter',
fontWeight: 'normal',
lineHeight: '1.35',
borderRadius: '5px',
textIndent: '8px',
marginBottom: '2px',
height: '38px',
color: '$grayTextContrast',
'&:focus': {
outline: 'none',
border: '1px solid transparent',
outline: '2px solid $omnivoreCtaYellow',
},
})
@ -63,6 +68,10 @@ export const BorderedFormInput = styled(FormInput, {
borderColor: '#d9d9d9',
borderRadius: '6px',
transition: 'all .2s',
'&:focus': {
border: '1px solid transparent',
outline: '2px solid $omnivoreCtaYellow',
},
})
export function GeneralFormInput(props: FormInputProps): JSX.Element {
@ -170,7 +179,7 @@ export function GeneralFormInput(props: FormInputProps): JSX.Element {
required={input.required}
css={{
border: '1px solid $textNonessential',
borderRadius: '8px',
borderRadius: '5px',
width: '100%',
bg: 'transparent',
fontSize: '16px',
@ -179,8 +188,8 @@ export function GeneralFormInput(props: FormInputProps): JSX.Element {
height: '38px',
color: '$grayTextContrast',
'&:focus': {
outline: 'none',
boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)',
border: '1px solid transparent',
outline: '2px solid $omnivoreCtaYellow',
},
}}
name={input.name}

View File

@ -146,7 +146,6 @@ const textVariants = {
},
navLink: {
m: 0,
fontSize: '$1',
fontWeight: 400,
color: '$graySolid',
cursor: 'pointer',

View File

@ -183,6 +183,12 @@ const LibraryGridCardContent = (props: LinkedItemCardProps): JSX.Element => {
const { isChecked, setIsChecked, item } = props
const [menuOpen, setMenuOpen] = useState(false)
const originText = siteName(props.item.originalArticleUrl, props.item.url)
console.log(
'const originText = siteName(props.item.originalArticleUrl, props.item.url)',
siteName(props.item.originalArticleUrl, props.item.url),
props.item.originalArticleUrl,
props.item.url
)
const handleCheckChanged = useCallback(() => {
const newValue = !isChecked

View File

@ -106,6 +106,10 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
cursor: 'pointer',
mouseEvents: 'all',
}}
onClick={(event) => {
router.push('/settings/account')
event.preventDefault()
}}
>
<Avatar
imageURL={viewerData.me.profile.pictureUrl}

View File

@ -1,4 +1,4 @@
import { Box, VStack } from '../elements/LayoutPrimitives'
import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
import { SettingsHeader } from '../patterns/SettingsHeader'
import { navigationCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
@ -13,6 +13,7 @@ import { PageMetaData } from '../patterns/PageMetaData'
import { HEADER_HEIGHT } from './homeFeed/HeaderSpacer'
import { deinitAnalytics } from '../../lib/analytics'
import { logout } from '../../lib/logout'
import { SettingsMenu } from './SettingsMenu'
type SettingsLayoutProps = {
title?: string
@ -51,7 +52,10 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
height: HEADER_HEIGHT,
}}
></Box>
{props.children}
<HStack css={{ width: '100%', height: '100%' }}>
<SettingsMenu />
{props.children}
</HStack>
<Box css={{ height: '120px', width: '100%' }} />
</VStack>
{showLogoutConfirmation ? (

View File

@ -7,6 +7,7 @@ type ValidateUsernameInput = {
}
type ValidateUsernameResponse = {
isLoading: boolean
isUsernameValid: boolean
usernameErrorMessage?: string
}
@ -20,12 +21,19 @@ export function useValidateUsernameQuery({
}
`
const { data } = useSWR([query, username], makePublicGqlFetcher({ username }))
// Don't fetch if username is empty
const { data, error, isValidating } = useSWR(
username ? [query, username] : null,
makePublicGqlFetcher({ username })
)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isUsernameValid = (data as any)?.validateUsername ?? false
if (isUsernameValid) {
return { isUsernameValid }
return {
isUsernameValid,
isLoading: !data && !error,
}
}
// Try to figure out why the username is invalid
@ -33,12 +41,14 @@ export function useValidateUsernameQuery({
if (usernameErrorMessage) {
return {
isUsernameValid: false,
isLoading: !data && !error,
usernameErrorMessage,
}
}
return {
isUsernameValid: false,
isLoading: !data && !error,
usernameErrorMessage: 'This username is not available',
}
}
@ -48,8 +58,8 @@ function validationErrorMessage(username: string): string | undefined {
return undefined
}
if (username.length < 3) {
return 'Username should contain at least three characters'
if (username.length < 4) {
return 'Username should contain at least four characters'
}
if (username.length > 15) {

View File

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "process.env.EXTENSION_NAME",
"short_name": "process.env.EXTENSION_NAME",
"version": "2.4.4",
"version": "2.6.1",
"description": "Save PDFs and Articles to your Omnivore library",
"author": "Omnivore Media, Inc",
"default_locale": "en",
@ -11,7 +11,7 @@
"url": "https://omnivore.app/"
},
"homepage_url": "https://omnivore.app/",
"content_security_policy": "default-src 'none'; child-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; worker-src 'none'; connect-src https://storage.googleapis.com/ process.env.OMNIVORE_GRAPHQL_URL blob:; frame-src 'none'; font-src 'none'; img-src data:; script-src 'self'; script-src-elem 'self'; script-src-attr 'none'; style-src 'self'; style-src-elem 'self'; style-src-attr 'none'; base-uri 'none'; form-action 'none'; block-all-mixed-content; upgrade-insecure-requests; report-uri https://api.jeurissen.co/reports/csp/webext/omnivore/",
"content_security_policy": "default-src 'none'; child-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; worker-src 'none'; connect-src https://storage.googleapis.com/ process.env.OMNIVORE_GRAPHQL_URL blob:; frame-src 'none'; font-src 'none'; img-src data:; script-src 'self'; script-src-elem 'self'; script-src-attr 'none'; style-src 'self'; style-src-elem 'self'; style-src-attr 'none'; base-uri 'none'; form-action 'none'; block-all-mixed-content; upgrade-insecure-requests; report-uri https://api.jeurissen.co/reports/csp/webext/omnivore/",
"icons": {
"16": "/images/extension/icon-16.png",
"24": "/images/extension/icon-24.png",

View File

@ -98,12 +98,7 @@ async function updatePageTitle(apiUrl, pageId, title) {
return data.updatePage.updatePage
}
async function setLabels(apiUrl, pageId, labelIds, createdLabels) {
console.log(
'setLabels(apiUrl, pageId, labelIds, createdLabels)',
labelIds,
createdLabels
)
async function setLabels(apiUrl, pageId, labels) {
const mutation = JSON.stringify({
query: `mutation SetLabels($input: SetLabelsInput!) {
setLabels(input: $input) {
@ -123,8 +118,7 @@ async function setLabels(apiUrl, pageId, labelIds, createdLabels) {
variables: {
input: {
pageId,
labelIds,
labels: createdLabels,
labels,
},
},
})

View File

@ -14,11 +14,51 @@
import { v4 as uuidv4 } from 'uuid'
import { nanoid } from 'nanoid'
class TaskQueue {
constructor() {
this.queue = []
this.isRunning = false
this.isReady = false
}
enqueue(task) {
this.queue.push(task)
// Only run the next task if the queue is ready
if (this.isReady) {
this.runNext()
}
}
async runNext() {
if (this.isRunning || this.queue.length === 0 || !this.isReady) return
this.isRunning = true
const task = this.queue.shift()
try {
await task()
} catch (err) {
console.error('Task failed:', err)
} finally {
this.isRunning = false
if (this.isReady) {
this.runNext()
}
}
}
setReady() {
this.isReady = true
this.runNext()
}
}
let authToken = undefined
const queue = new TaskQueue()
const omnivoreURL = process.env.OMNIVORE_URL
const omnivoreGraphqlURL = process.env.OMNIVORE_GRAPHQL_URL
let pendingRequests = []
let completedRequests = {}
function getCurrentTab() {
@ -135,7 +175,6 @@ async function savePdfFile(
contentType,
contentObjUrl
)
console.log(' uploadFileResult: ', uploadFileResult)
URL.revokeObjectURL(contentObjUrl)
if (uploadFileResult && uploadRequestResult.createdPageId) {
@ -255,7 +294,7 @@ async function saveApiRequest(currentTab, query, field, input) {
console.log('error saving: ', err)
}
processPendingRequests(currentTab.id)
queue.setReady()
}
function updateClientStatus(tabId, target, status, message) {
@ -312,12 +351,18 @@ async function setLabelsRequest(tabId, request, completedResponse) {
return setLabels(
omnivoreGraphqlURL + 'graphql',
completedResponse.responseId,
request.labelIds
request.labels
)
.then(() => {
updateClientStatus(tabId, 'labels', 'success', 'Labels updated.')
return true
})
.then(() => {
browserApi.tabs.sendMessage(tabId, {
action: ACTIONS.LabelCacheUpdated,
payload: {},
})
})
.catch(() => {
updateClientStatus(tabId, 'labels', 'failure', 'Error updating labels.')
return true
@ -351,48 +396,49 @@ async function deleteRequest(tabId, request, completedResponse) {
})
}
async function processPendingRequests(tabId) {
const tabRequests = pendingRequests.filter((pr) => pr.tabId === tabId)
tabRequests.forEach(async (pr) => {
let handled = false
const completed = completedRequests[pr.clientRequestId]
if (completed) {
switch (pr.type) {
case 'EDIT_TITLE':
handled = await editTitleRequest(tabId, pr, completed)
break
case 'ADD_NOTE':
handled = await addNoteRequest(tabId, pr, completed)
break
case 'SET_LABELS':
handled = await setLabelsRequest(tabId, pr, completed)
break
case 'ARCHIVE':
handled = await archiveRequest(tabId, pr, completed)
break
case 'DELETE':
handled = await deleteRequest(tabId, pr, completed)
break
}
}
if (handled) {
const idx = pendingRequests.findIndex((opr) => pr.id === opr.id)
if (idx > -1) {
pendingRequests.splice(idx, 1)
}
}
})
// TODO: need to handle clearing completedRequests also
async function processEditTitleRequest(tabId, pr) {
const completed = completedRequests[pr.clientRequestId]
handled = await editTitleRequest(tabId, pr, completed)
console.log('processEditTitleRequest: ', handled)
return handled
}
async function saveArticle(tab) {
async function processAddNoteRequest(tabId, pr) {
const completed = completedRequests[pr.clientRequestId]
const handled = await addNoteRequest(tabId, pr, completed)
console.log('processAddNoteRequest: ', handled)
return handled
}
async function processSetLabelsRequest(tabId, pr) {
const completed = completedRequests[pr.clientRequestId]
const handled = await setLabelsRequest(tabId, pr, completed)
console.log('processSetLabelsRequest: ', handled)
return handled
}
async function processArchiveRequest(tabId, pr) {
const completed = completedRequests[pr.clientRequestId]
const handled = await archiveRequest(tabId, pr, completed)
console.log('processArchiveRequest: ', handled)
return handled
}
async function processDeleteRequest(tabId, pr) {
const completed = completedRequests[pr.clientRequestId]
const handled = await deleteRequest(tabId, pr, completed)
console.log('processDeleteRequest: ', handled)
return handled
}
async function saveArticle(tab, createHighlight) {
browserApi.tabs.sendMessage(
tab.id,
{
action: ACTIONS.GetContent,
payload: {
createHighlight: createHighlight,
},
},
async (response) => {
if (!response || typeof response !== 'object') {
@ -521,7 +567,8 @@ async function clearPreviousIntervalTimer(tabId) {
clearTimeout(intervalTimeoutId)
}
function onExtensionClick(tabId) {
function extensionSaveCurrentPage(tabId, createHighlight) {
createHighlight = createHighlight ? true : false
/* clear any previous timers on each click */
clearPreviousIntervalTimer(tabId)
@ -544,7 +591,7 @@ function onExtensionClick(tabId) {
if (onSuccess && typeof onSuccess === 'function') {
onSuccess()
}
await saveArticle(tab)
await saveArticle(tab, createHighlight)
try {
await updateLabelsCache(omnivoreGraphqlURL + 'graphql', tab)
browserApi.tabs.sendMessage(tab.id, {
@ -577,7 +624,7 @@ function onExtensionClick(tabId) {
* post timeout, we proceed to save as some sites (people.com) take a
* long time to reach complete state and remain in interactive state.
*/
await saveArticle(tab)
await saveArticle(tab, createHighlight)
})
},
(intervalId, timeoutId) => {
@ -597,13 +644,12 @@ function checkAuthOnFirstClickPostInstall(tabId) {
function handleActionClick() {
executeAction(function (currentTab) {
onExtensionClick(currentTab.id)
extensionSaveCurrentPage(currentTab.id)
})
}
function executeAction(action) {
getCurrentTab().then((currentTab) => {
console.log('currentTab: ', currentTab)
browserApi.tabs.sendMessage(
currentTab.id,
{
@ -685,65 +731,65 @@ function init() {
}
if (request.action === ACTIONS.EditTitle) {
pendingRequests.push({
id: uuidv4(),
type: 'EDIT_TITLE',
tabId: sender.tab.id,
title: request.payload.title,
clientRequestId: request.payload.ctx.requestId,
})
processPendingRequests(sender.tab.id)
queue.enqueue(() =>
processEditTitleRequest(sender.tab.id, {
id: uuidv4(),
type: 'EDIT_TITLE',
tabId: sender.tab.id,
title: request.payload.title,
clientRequestId: request.payload.ctx.requestId,
})
)
}
if (request.action === ACTIONS.Archive) {
pendingRequests.push({
id: uuidv4(),
type: 'ARCHIVE',
tabId: sender.tab.id,
clientRequestId: request.payload.ctx.requestId,
})
processPendingRequests(sender.tab.id)
queue.enqueue(() =>
processArchiveRequest(sender.tab.id, {
id: uuidv4(),
type: 'ARCHIVE',
tabId: sender.tab.id,
clientRequestId: request.payload.ctx.requestId,
})
)
}
if (request.action === ACTIONS.Delete) {
pendingRequests.push({
type: 'DELETE',
tabId: sender.tab.id,
clientRequestId: request.payload.ctx.requestId,
})
processPendingRequests(sender.tab.id)
queue.enqueue(() =>
processDeleteRequest(sender.tab.id, {
type: 'DELETE',
tabId: sender.tab.id,
clientRequestId: request.payload.ctx.requestId,
})
)
}
if (request.action === ACTIONS.AddNote) {
pendingRequests.push({
id: uuidv4(),
type: 'ADD_NOTE',
tabId: sender.tab.id,
note: request.payload.note,
clientRequestId: request.payload.ctx.requestId,
})
processPendingRequests(sender.tab.id)
queue.enqueue(() =>
processAddNoteRequest(sender.tab.id, {
id: uuidv4(),
type: 'ADD_NOTE',
tabId: sender.tab.id,
note: request.payload.note,
clientRequestId: request.payload.ctx.requestId,
})
)
}
if (request.action === ACTIONS.SetLabels) {
pendingRequests.push({
id: uuidv4(),
type: 'SET_LABELS',
tabId: sender.tab.id,
labelIds: request.payload.labelIds,
clientRequestId: request.payload.ctx.requestId,
})
processPendingRequests(sender.tab.id)
queue.enqueue(() =>
processSetLabelsRequest(sender.tab.id, {
id: uuidv4(),
type: 'SET_LABELS',
tabId: sender.tab.id,
labels: request.payload.labels,
clientRequestId: request.payload.ctx.requestId,
})
)
}
})
browserApi.contextMenus.create({
id: 'save-selection',
id: 'save-link-selection',
title: 'Save this link to Omnivore',
contexts: ['link'],
onclick: async function (obj) {
@ -752,6 +798,28 @@ function init() {
})
},
})
browserApi.contextMenus.create({
id: 'save-page-selection',
title: 'Save this page to Omnivore',
contexts: ['page'],
onclick: async function (obj) {
executeAction(function (currentTab) {
extensionSaveCurrentPage(currentTab.id)
})
},
})
browserApi.contextMenus.create({
id: 'save-text-selection',
title: 'Create Highlight and Save to Omnivore',
contexts: ['selection'],
onclick: async function (obj) {
executeAction(function (currentTab) {
extensionSaveCurrentPage(currentTab.id, true)
})
},
})
}
init()

View File

@ -23,7 +23,8 @@
browserApi.runtime.onMessage.addListener(
({ action, payload }, sender, sendResponse) => {
if (action === ACTIONS.GetContent) {
prepareContent().then((pageContent) => {
const createHighlight = payload && payload.createHighlight
prepareContent(createHighlight).then((pageContent) => {
sendResponse({
type: pageContent.type,
doc: pageContent.content || '',

View File

@ -5,37 +5,38 @@
ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS
*/
'use strict';
'use strict'
;(function () {
const iframes = {}
(function () {
const iframes = {};
browserApi.runtime.onMessage.addListener(
({ action, payload }, sender, sendResponse) => {
if (action !== ACTIONS.AddIframeContent) return
const { url, content } = payload
iframes[url] = content
sendResponse({})
}
)
browserApi.runtime.onMessage.addListener(({ action, payload }, sender, sendResponse) => {
if (action !== ACTIONS.AddIframeContent) return;
const { url, content } = payload;
iframes[url] = content;
sendResponse({});
});
async function grabPdfContent () {
const fileExtension = window.location.pathname.slice(-4).toLowerCase();
const hasPdfExtension = fileExtension === '.pdf';
async function grabPdfContent() {
const fileExtension = window.location.pathname.slice(-4).toLowerCase()
const hasPdfExtension = fileExtension === '.pdf'
const pdfContentTypes = [
'application/acrobat',
'application/pdf',
'application/x-pdf',
'applications/vnd.pdf',
'text/pdf',
'text/x-pdf'
];
const isPdfContent = pdfContentTypes.indexOf(document.contentType) !== -1;
'text/x-pdf',
]
const isPdfContent = pdfContentTypes.indexOf(document.contentType) !== -1
if (!hasPdfExtension && !isPdfContent) {
return Promise.resolve(null);
return Promise.resolve(null)
}
const embedEl = document.querySelector('embed');
const embedEl = document.querySelector('embed')
if (embedEl && embedEl.type !== 'application/pdf') {
return Promise.resolve(null);
return Promise.resolve(null)
}
if (ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS && embedEl.src) {
@ -43,115 +44,120 @@
}
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest()
// load `document` from `cache`
xhr.open('GET', '', true);
xhr.responseType = 'blob';
xhr.open('GET', '', true)
xhr.responseType = 'blob'
xhr.onload = function (e) {
if (this.status === 200) {
resolve({ type: 'pdf', uploadContentObjUrl: URL.createObjectURL(this.response) })
resolve({
type: 'pdf',
uploadContentObjUrl: URL.createObjectURL(this.response),
})
} else {
reject(e);
reject(e)
}
};
xhr.send();
});
}
xhr.send()
})
}
function prepareContentPostItem (itemEl) {
const lowerTagName = itemEl.tagName.toLowerCase();
function prepareContentPostItem(itemEl) {
const lowerTagName = itemEl.tagName.toLowerCase()
if (lowerTagName === 'iframe') {
const frameHtml = iframes[itemEl.src];
if (!frameHtml) return;
const frameHtml = iframes[itemEl.src]
if (!frameHtml) return
const containerEl = document.createElement('div');
containerEl.className = 'omnivore-instagram-embed';
containerEl.innerHTML = frameHtml;
const containerEl = document.createElement('div')
containerEl.className = 'omnivore-instagram-embed'
containerEl.innerHTML = frameHtml
const parentEl = itemEl.parentNode;
if (!parentEl) return;
const parentEl = itemEl.parentNode
if (!parentEl) return
parentEl.replaceChild(containerEl, itemEl);
parentEl.replaceChild(containerEl, itemEl)
return;
return
}
if (lowerTagName === 'img' || lowerTagName === 'image') {
// Removing blurred images since they are mostly the copies of lazy loaded ones
const style = window.getComputedStyle(itemEl);
const filter = style.getPropertyValue('filter');
if (filter.indexOf('blur(') === -1) return;
itemEl.remove();
return;
const style = window.getComputedStyle(itemEl)
const filter = style.getPropertyValue('filter')
if (filter.indexOf('blur(') === -1) return
itemEl.remove()
return
}
const style = window.getComputedStyle(itemEl);
const backgroundImage = style.getPropertyValue('background-image');
const style = window.getComputedStyle(itemEl)
const backgroundImage = style.getPropertyValue('background-image')
// convert all nodes with background image to img nodes
const noBackgroundImage = !backgroundImage || backgroundImage === 'none';
if (!noBackgroundImage) return;
const noBackgroundImage = !backgroundImage || backgroundImage === 'none'
if (!noBackgroundImage) return
const filter = style.getPropertyValue('filter');
const filter = style.getPropertyValue('filter')
// avoiding image nodes with a blur effect creation
if (filter && filter.indexOf('blur(') !== -1) {
itemEl.remove();
return;
itemEl.remove()
return
}
// Replacing element only of there are no content inside, b/c might remove important div with content.
// Article example: http://www.josiahzayner.com/2017/01/genetic-designer-part-i.html
// DIV with class "content-inner" has `url("https://resources.blogblog.com/blogblog/data/1kt/travel/bg_container.png")` background image.
if (itemEl.src) return;
if (itemEl.innerHTML.length > 24) return;
if (itemEl.src) return
if (itemEl.innerHTML.length > 24) return
const BI_SRC_REGEXP = /url\("(.+?)"\)/gi;
const matchedSRC = BI_SRC_REGEXP.exec(backgroundImage);
const BI_SRC_REGEXP = /url\("(.+?)"\)/gi
const matchedSRC = BI_SRC_REGEXP.exec(backgroundImage)
// Using "g" flag with a regex we have to manually break down lastIndex to zero after every usage
// More details here: https://stackoverflow.com/questions/1520800/why-does-a-regexp-with-global-flag-give-wrong-results
BI_SRC_REGEXP.lastIndex = 0;
BI_SRC_REGEXP.lastIndex = 0
const targetSrc = matchedSRC && matchedSRC[1];
if (!targetSrc) return;
const targetSrc = matchedSRC && matchedSRC[1]
if (!targetSrc) return
const imgEl = document.createElement('img');
imgEl.src = targetSrc;
const parentEl = itemEl.parentNode;
if (!parentEl) return;
const imgEl = document.createElement('img')
imgEl.src = targetSrc
const parentEl = itemEl.parentNode
if (!parentEl) return
parentEl.replaceChild(imgEl, itemEl);
parentEl.replaceChild(imgEl, itemEl)
}
function prepareContentPostScroll () {
const contentCopyEl = document.createElement('div');
contentCopyEl.style.position = 'absolute';
contentCopyEl.style.left = '-2000px';
contentCopyEl.style.zIndex = '-2000';
contentCopyEl.innerHTML = document.body.innerHTML;
function prepareContentPostScroll() {
const contentCopyEl = document.createElement('div')
contentCopyEl.style.position = 'absolute'
contentCopyEl.style.left = '-2000px'
contentCopyEl.style.zIndex = '-2000'
contentCopyEl.innerHTML = document.body.innerHTML
// Appending copy of the content to the DOM to enable computed styles capturing ability
// Without adding that copy to the DOM the `window.getComputedStyle` method will always return undefined.
document.documentElement.appendChild(contentCopyEl);
document.documentElement.appendChild(contentCopyEl)
Array.from(contentCopyEl.getElementsByTagName('*')).forEach(prepareContentPostItem);
Array.from(contentCopyEl.getElementsByTagName('*')).forEach(
prepareContentPostItem
)
/*
* Grab head and body separately as using clone on entire document into a div
* removes the head and body tags while grabbing html in them. Instead we
* capture them separately and concatenate them here with head and body tags
* preserved.
*/
const contentCopyHtml = `<html><head>${document.head.innerHTML}</head><body>${contentCopyEl.innerHTML}</body></html>`;
* Grab head and body separately as using clone on entire document into a div
* removes the head and body tags while grabbing html in them. Instead we
* capture them separately and concatenate them here with head and body tags
* preserved.
*/
const contentCopyHtml = `<html><head>${document.head.innerHTML}</head><body>${contentCopyEl.innerHTML}</body></html>`
// Cleaning up the copy element
contentCopyEl.remove();
return contentCopyHtml;
contentCopyEl.remove()
return contentCopyHtml
}
function createBackdrop () {
const backdropEl = document.createElement('div');
backdropEl.className = 'webext-omnivore-backdrop';
function createBackdrop() {
const backdropEl = document.createElement('div')
backdropEl.className = 'webext-omnivore-backdrop'
backdropEl.style.cssText = `all: initial !important;
position: fixed !important;
top: 0 !important;
@ -164,74 +170,171 @@
transition: opacity 0.3s !important;
-webkit-backdrop-filter: blur(4px) !important;
backdrop-filter: blur(4px) !important;
`;
return backdropEl;
`
return backdropEl
}
function clearExistingBackdrops () {
const backdropCol = document.querySelectorAll('.webext-omnivore-backdrop');
const getQuoteText = (containerNode) => {
const nonParagraphTagsRegEx =
/^(a|b|basefont|bdo|big|em|font|i|s|small|span|strike|strong|su[bp]|tt|u|code|mark)$/i
let textResult = ''
let newParagraph = false
const getTextNodes = (node) => {
let isPre = false
const nodeElement =
node instanceof HTMLElement ? node : node.parentElement
if (nodeElement) {
isPre = window
.getComputedStyle(nodeElement)
.whiteSpace.startsWith('pre')
}
if (node.nodeType == 3) {
const text = isPre ? node.nodeValue : node.nodeValue.replace(/\n/g, '')
textResult += text
} else if (node != containerNode) {
if (!nonParagraphTagsRegEx.test(node.tagName)) {
textResult += '\n\n'
}
}
const children = node.childNodes
children.forEach(function (child) {
getTextNodes(child)
})
}
getTextNodes(containerNode)
return textResult.trim()
}
const markHighlightSelection = () => {
// First remove any previous markers, this would only normally happen during debugging
try {
const markers = window.document.querySelectorAll(
`span[data-omnivore-highlight-start="true"],
span[data-omnivore-highlight-end="true"]`
)
for (let i = 0; i < markers.length; i++) {
markers[i].remove()
}
} catch (error) {
console.log('remove marker error: ', error)
// This should be OK
}
try {
const sel = window.getSelection()
if (sel.rangeCount) {
const range = sel.getRangeAt(0)
const endMarker = document.createElement('span')
const startMarker = document.createElement('span')
endMarker.setAttribute('data-omnivore-highlight-end', 'true')
startMarker.setAttribute('data-omnivore-highlight-start', 'true')
var container = document.createElement('div')
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents())
}
const endRange = range.cloneRange()
endRange.collapse(false)
endRange.insertNode(endMarker)
range.insertNode(startMarker)
return {
highlightHTML: container.innerHTML,
highlightText: getQuoteText(container),
}
}
} catch (error) {
console.log('get text error', error)
}
return null
}
function clearExistingBackdrops() {
const backdropCol = document.querySelectorAll('.webext-omnivore-backdrop')
for (let i = 0; i < backdropCol.length; i++) {
const backdropEl = backdropCol[i];
backdropEl.style.setProperty('opacity', '0', 'important');
const backdropEl = backdropCol[i]
backdropEl.style.setProperty('opacity', '0', 'important')
}
setTimeout(() => {
for (let i = 0; i < backdropCol.length; i++) {
backdropCol[i].remove();
backdropCol[i].remove()
}
}, 0.5e3);
}, 0.5e3)
}
async function prepareContent () {
const pdfContent = await grabPdfContent();
async function prepareContent(createHighlight) {
const pdfContent = await grabPdfContent()
if (pdfContent) {
return pdfContent
}
const url = window.location.href;
const url = window.location.href
try {
if (handleBackendUrl(url)) {
if (!createHighlight && handleBackendUrl(url)) {
return { type: 'url' }
}
} catch {
console.log('error checking url')
}
async function scrollPage (url) {
const scrollingEl = (document.scrollingElement || document.body);
const lastScrollPos = scrollingEl.scrollTop;
const currentScrollHeight = scrollingEl.scrollHeight;
console.log('get content: ', createHighlight)
if (createHighlight) {
console.log('creating highlight while saving')
const highlightSelection = markHighlightSelection()
console.log('highlightSelection', highlightSelection)
}
async function scrollPage(url) {
const scrollingEl = document.scrollingElement || document.body
const lastScrollPos = scrollingEl.scrollTop
const currentScrollHeight = scrollingEl.scrollHeight
/* add blurred overlay while scrolling */
clearExistingBackdrops();
clearExistingBackdrops()
const backdropEl = createBackdrop();
document.body.appendChild(backdropEl);
const backdropEl = createBackdrop()
document.body.appendChild(backdropEl)
/*
* check below compares scrollTop against initial page height to handle
* pages with infinite scroll else we shall be infinitely scrolling here.
* stop scrolling if the url has changed in the meantime.
*/
while (scrollingEl.scrollTop <= (currentScrollHeight - 500) && window.location.href === url) {
const prevScrollTop = scrollingEl.scrollTop;
scrollingEl.scrollTop += 500;
while (
scrollingEl.scrollTop <= currentScrollHeight - 500 &&
window.location.href === url
) {
const prevScrollTop = scrollingEl.scrollTop
scrollingEl.scrollTop += 500
/* sleep upon scrolling position change for event loop to handle events from scroll */
await (new Promise((resolve) => { setTimeout(resolve, 10); }));
await new Promise((resolve) => {
setTimeout(resolve, 10)
})
if (scrollingEl.scrollTop === prevScrollTop) {
/* break out scroll loop if we are not able to scroll for any reason */
// console.log('breaking out scroll loop', scrollingEl.scrollTop, currentScrollHeight);
break;
break
}
}
scrollingEl.scrollTop = lastScrollPos;
scrollingEl.scrollTop = lastScrollPos
/* sleep upon scrolling position change for event loop to handle events from scroll */
await (new Promise((resolve) => { setTimeout(resolve, 10); }));
await new Promise((resolve) => {
setTimeout(resolve, 10)
})
}
await scrollPage(url);
await scrollPage(url)
clearExistingBackdrops();
return { type: 'html', content: prepareContentPostScroll() };
clearExistingBackdrops()
return { type: 'html', content: prepareContentPostScroll() }
}
window.prepareContent = prepareContent;
})();
window.prepareContent = prepareContent
})()

View File

@ -192,8 +192,18 @@
function updateLabelsFromCache(payload) {
;(async () => {
await getStorageItem('labels').then((cachedLabels) => {
if (labels) {
const selectedLabels = labels.filter((l) => l.selected)
selectedLabels.forEach((l) => {
const cached = cachedLabels.find((cached) => cached.name == l.name)
if (cached) {
cached.selected = true
} else {
cachedLabels.push(l)
}
})
}
labels = cachedLabels
console.log(' == updated labels', cachedLabels)
})
})()
}
@ -279,6 +289,12 @@
}
function toggleRow(rowId) {
if (!currentToastEl) {
// 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(
@ -538,7 +554,15 @@
}
}
function addNote() {
function noteCacheKey() {
return document.location
? `cached-note-${document.location.href}`
: undefined
}
async function addNote() {
const cachedNoteKey = noteCacheKey()
cancelAutoDismiss()
toggleRow('#omnivore-add-note-row')
@ -547,7 +571,24 @@
)
if (noteArea) {
noteArea.focus()
if (cachedNoteKey) {
const existingNote = await getStorageItem(cachedNoteKey)
noteArea.value = existingNote
}
if (noteArea.value) {
noteArea.select()
} else {
noteArea.focus()
}
noteArea.addEventListener('input', (event) => {
;(async () => {
const note = {}
note[cachedNoteKey] = event.target.value
await setStorage(note)
})()
})
noteArea.onkeydown = (e) => {
e.cancelBubble = true
@ -587,7 +628,9 @@
})
event.preventDefault()
event.stopPropogation()
if (event.stopPropogation) {
event.stopPropogation()
}
}
}
@ -745,27 +788,32 @@
rowElement.setAttribute('data-label-selected', 'off')
}
updateLabels()
syncLabelChanges()
}
function syncLabelChanges() {
console.log('syncLabels')
updateStatusBox(
'#omnivore-edit-labels-status',
'loading',
'Updating Labels...',
undefined
)
const labelIds = labels.filter((l) => l['selected']).map((l) => l.id)
const setLabels = labels
.filter((l) => l['selected'])
.map((l) => {
return {
name: l.name,
color: l.color,
}
})
// browserApi.runtime.sendMessage({
// - action: ACTIONS.SetLabels,
// - payload: {
// - ctx: ctx,
// - labelIds: labelIds,
// - },
// - })
browserApi.runtime.sendMessage({
action: ACTIONS.SetLabels,
payload: {
ctx: ctx,
labels: setLabels,
},
})
}
async function editLabels() {
@ -799,10 +847,14 @@
if (list) {
list.innerHTML = ''
labels.forEach(function (label, idx) {
const rowHtml = createLabelRow(label)
list.appendChild(rowHtml)
})
labels
.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
)
.forEach(function (label, idx) {
const rowHtml = createLabelRow(label)
list.appendChild(rowHtml)
})
}
}
@ -817,15 +869,22 @@
.filter(
(l) => l.name.toLowerCase().indexOf(filterValue.toLowerCase()) > -1
)
.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
)
.forEach(function (label) {
const rowHtml = createLabelRow(label)
list.appendChild(rowHtml)
})
} else {
labels.forEach(function (label) {
const rowHtml = createLabelRow(label)
list.appendChild(rowHtml)
})
labels
.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
)
.forEach(function (label) {
const rowHtml = createLabelRow(label)
list.appendChild(rowHtml)
})
}
}
}