Merge pull request #2814 from omnivore-app/feat/extension-create-labels
Allow creating labels from the extension
This commit is contained in:
@ -160,3 +160,9 @@ dependencies {
|
||||
apollo {
|
||||
packageName.set 'app.omnivore.omnivore.graphql.generated'
|
||||
}
|
||||
|
||||
task printVersion {
|
||||
doLast {
|
||||
println "omnivoreVersion: ${android.defaultConfig.versionName}"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -146,7 +146,6 @@ const textVariants = {
|
||||
},
|
||||
navLink: {
|
||||
m: 0,
|
||||
fontSize: '$1',
|
||||
fontWeight: 400,
|
||||
color: '$graySolid',
|
||||
cursor: 'pointer',
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
231
packages/web/components/templates/SettingsMenu.tsx
Normal file
231
packages/web/components/templates/SettingsMenu.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
|
||||
import { LIBRARY_LEFT_MENU_WIDTH } from './homeFeed/LibraryFilterMenu'
|
||||
import { LogoBox } from '../elements/LogoBox'
|
||||
import Link from 'next/link'
|
||||
import { styled, theme } from '../tokens/stitches.config'
|
||||
import { Button } from '../elements/Button'
|
||||
import { ArrowSquareUpRight } from 'phosphor-react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const HorizontalDivider = styled(SpanBox, {
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
my: '25px',
|
||||
background: `${theme.colors.grayLine.toString()}`,
|
||||
})
|
||||
|
||||
const StyledLink = styled(SpanBox, {
|
||||
pl: '25px',
|
||||
ml: '10px',
|
||||
mb: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2px',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
|
||||
width: 'calc(100% - 10px)',
|
||||
maxWidth: '100%',
|
||||
height: '32px',
|
||||
|
||||
fontSize: '14px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color: '$thLibraryMenuUnselected',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
})
|
||||
|
||||
export function SettingsMenu(): JSX.Element {
|
||||
const section1 = [
|
||||
{ name: 'Account', destination: '/settings/account' },
|
||||
{ name: 'API Keys', destination: '/settings/api' },
|
||||
{ name: 'Emails', destination: '/settings/emails' },
|
||||
{ name: 'Feeds', destination: '/settings/feeds' },
|
||||
{ name: 'Subscriptions', destination: '/settings/subscriptions' },
|
||||
{ name: 'Labels', destination: '/settings/labels' },
|
||||
]
|
||||
|
||||
const section2 = [
|
||||
{ name: 'Integrations', destination: '/settings/integrations' },
|
||||
{ name: 'Install', destination: '/settings/installation' },
|
||||
]
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
css={{
|
||||
left: '0px',
|
||||
top: '0px',
|
||||
position: 'fixed',
|
||||
bg: '$thLeftMenuBackground',
|
||||
height: '100%',
|
||||
width: LIBRARY_LEFT_MENU_WIDTH,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
'@mdDown': {
|
||||
visibility: 'hidden',
|
||||
width: '100%',
|
||||
transition: 'visibility 0s, top 150ms',
|
||||
},
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
px: '25px',
|
||||
pb: '50px',
|
||||
pt: '4.5px',
|
||||
lineHeight: '1',
|
||||
}}
|
||||
>
|
||||
<LogoBox />
|
||||
</Box>
|
||||
|
||||
<VStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
>
|
||||
{section1.map((item) => {
|
||||
return <SettingsButton key={item.name} {...item} />
|
||||
})}
|
||||
<HorizontalDivider />
|
||||
{section2.map((item) => {
|
||||
return <SettingsButton key={item.name} {...item} />
|
||||
})}
|
||||
<HorizontalDivider />
|
||||
<StyledLink>
|
||||
<Button
|
||||
style="link"
|
||||
onClick={(event) => {
|
||||
if (window.Intercom) {
|
||||
window.Intercom('show')
|
||||
}
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
Feedback
|
||||
</Button>
|
||||
</StyledLink>
|
||||
<StyledLink
|
||||
css={{
|
||||
'> a': {
|
||||
backgroundColor: 'transparent',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href="https://docs.omnivore.app"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
gap: '5px',
|
||||
color: '$thLibraryMenuUnselected',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Documentation
|
||||
<ArrowSquareUpRight size={12} />
|
||||
</HStack>
|
||||
</a>
|
||||
</StyledLink>
|
||||
</VStack>
|
||||
</Box>
|
||||
{/* This spacer pushes library content to the right of
|
||||
the fixed left side menu. */}
|
||||
<Box
|
||||
css={{
|
||||
minWidth: LIBRARY_LEFT_MENU_WIDTH,
|
||||
height: '100%',
|
||||
bg: '$thBackground',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
></Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type SettingsButtonProps = {
|
||||
name: string
|
||||
destination: string
|
||||
}
|
||||
|
||||
function SettingsButton(props: SettingsButtonProps): JSX.Element {
|
||||
const router = useRouter()
|
||||
const selected = useMemo(() => {
|
||||
if (router && router.isReady) {
|
||||
return router.asPath.endsWith(props.destination)
|
||||
}
|
||||
return false
|
||||
}, [props, router])
|
||||
|
||||
return (
|
||||
<Link href={props.destination} passHref title={props.name}>
|
||||
<SpanBox
|
||||
css={{
|
||||
mx: '10px',
|
||||
pl: '25px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2px',
|
||||
|
||||
width: 'calc(100% - 20px)',
|
||||
maxWidth: '100%',
|
||||
height: '32px',
|
||||
|
||||
fontSize: '14px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
|
||||
backgroundColor: selected ? '$thLibrarySelectionColor' : 'unset',
|
||||
|
||||
color: selected
|
||||
? '$thLibraryMenuSecondary'
|
||||
: '$thLibraryMenuUnselected',
|
||||
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
backgroundColor: selected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: selected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.name}
|
||||
</SpanBox>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
37
packages/web/lib/networking/mutations/updateUserMutation.ts
Normal file
37
packages/web/lib/networking/mutations/updateUserMutation.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { State } from '../fragments/articleFragment'
|
||||
|
||||
export type UpdateUserInput = {
|
||||
name: string
|
||||
bio: string
|
||||
}
|
||||
|
||||
export async function updateUserMutation(
|
||||
input: UpdateUserInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation UpdateUser($input: UpdateUserInput!) {
|
||||
updateUser(input: $input) {
|
||||
... on UpdateUserSuccess {
|
||||
user {
|
||||
name
|
||||
}
|
||||
}
|
||||
... on UpdateUserError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, {
|
||||
input,
|
||||
})
|
||||
const output = data as any
|
||||
return output.updateUser.user.name
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { State } from '../fragments/articleFragment'
|
||||
|
||||
export type UpdateUserProfileInput = {
|
||||
userId: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export async function updateUserProfileMutation(
|
||||
input: UpdateUserProfileInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation UpdateUserProfile($input: UpdateUserProfileInput!) {
|
||||
updateUserProfile(input: $input) {
|
||||
... on UpdateUserProfileSuccess {
|
||||
user {
|
||||
profile {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UpdateUserProfileError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, {
|
||||
input,
|
||||
})
|
||||
const output = data as any
|
||||
console.log('output: ', output)
|
||||
return output.updateUserProfile.user.profile.username
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
257
packages/web/pages/settings/account.tsx
Normal file
257
packages/web/pages/settings/account.tsx
Normal file
@ -0,0 +1,257 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { applyStoredTheme } from '../../lib/themeUpdater'
|
||||
|
||||
import { FormInput } from '../../components/elements/FormElements'
|
||||
import { StyledText } from '../../components/elements/StyledText'
|
||||
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { SettingsLayout } from '../../components/templates/SettingsLayout'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import {
|
||||
Box,
|
||||
SpanBox,
|
||||
VStack,
|
||||
} from '../../components/elements/LayoutPrimitives'
|
||||
import { Button } from '../../components/elements/Button'
|
||||
import { useValidateUsernameQuery } from '../../lib/networking/queries/useValidateUsernameQuery'
|
||||
import { updateUserMutation } from '../../lib/networking/mutations/updateUserMutation'
|
||||
import { updateUserProfileMutation } from '../../lib/networking/mutations/updateUserProfileMutation'
|
||||
import { styled } from '../../components/tokens/stitches.config'
|
||||
|
||||
const StyledLabel = styled('label', {
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
})
|
||||
|
||||
export default function Account(): JSX.Element {
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const [name, setName] = useState('')
|
||||
const [username, setUsername] = useState('')
|
||||
const [nameUpdating, setNameUpdating] = useState(false)
|
||||
const [usernameUpdating, setUsernameUpdating] = useState(false)
|
||||
|
||||
const [debouncedUsername, setDebouncedUsername] = useState('')
|
||||
const { usernameErrorMessage, isLoading: isUsernameValidationLoading } =
|
||||
useValidateUsernameQuery({
|
||||
username: debouncedUsername,
|
||||
})
|
||||
|
||||
const usernameEdited = useMemo(() => {
|
||||
return username !== viewerData?.me?.profile.username
|
||||
}, [username, viewerData])
|
||||
|
||||
const usernameError = useMemo(() => {
|
||||
return (
|
||||
usernameEdited &&
|
||||
username.length > 0 &&
|
||||
usernameErrorMessage &&
|
||||
!isUsernameValidationLoading
|
||||
)
|
||||
}, [
|
||||
usernameEdited,
|
||||
username,
|
||||
usernameErrorMessage,
|
||||
isUsernameValidationLoading,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewerData?.me?.profile.username) {
|
||||
setUsername(viewerData?.me?.profile.username)
|
||||
}
|
||||
}, [viewerData?.me?.profile.username])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewerData?.me?.name) {
|
||||
setName(viewerData?.me?.name)
|
||||
}
|
||||
}, [viewerData?.me?.name])
|
||||
|
||||
const handleUsernameChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setUsername(event.target.value)
|
||||
setTimeout(() => {
|
||||
if (event.target.value) {
|
||||
setDebouncedUsername(event.target.value)
|
||||
}
|
||||
}, 2000)
|
||||
event.preventDefault()
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleUpdateName = useCallback(() => {
|
||||
setNameUpdating(true)
|
||||
;(async () => {
|
||||
const updatedName = await updateUserMutation({ name, bio: '' })
|
||||
if (updatedName) {
|
||||
setName(updatedName)
|
||||
showSuccessToast('Name updated')
|
||||
} else {
|
||||
showErrorToast('Error updating name')
|
||||
}
|
||||
setNameUpdating(false)
|
||||
})()
|
||||
}, [name, nameUpdating, setName, setNameUpdating])
|
||||
|
||||
const handleUpdateUsername = useCallback(() => {
|
||||
setUsernameUpdating(true)
|
||||
|
||||
const userId = viewerData?.me?.id
|
||||
if (!userId) {
|
||||
showErrorToast('Error updating user info')
|
||||
return
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
const updatedUsername = await updateUserProfileMutation({
|
||||
userId,
|
||||
username,
|
||||
})
|
||||
if (updatedUsername) {
|
||||
setUsername(updatedUsername)
|
||||
setDebouncedUsername(updatedUsername)
|
||||
showSuccessToast('Username updated')
|
||||
} else {
|
||||
showErrorToast('Error updating username')
|
||||
}
|
||||
setUsernameUpdating(false)
|
||||
})()
|
||||
}, [
|
||||
username,
|
||||
usernameUpdating,
|
||||
setUsername,
|
||||
setUsernameUpdating,
|
||||
viewerData?.me,
|
||||
])
|
||||
|
||||
applyStoredTheme(false)
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<Toaster
|
||||
containerStyle={{
|
||||
top: '5rem',
|
||||
}}
|
||||
/>
|
||||
|
||||
<VStack
|
||||
css={{ width: '100%', height: '100%' }}
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
padding: '24px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
gap: '25px',
|
||||
minWidth: '300px',
|
||||
maxWidth: '865px',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<StyledText style="fixedHeadline" css={{ my: '6px' }}>
|
||||
Account Details
|
||||
</StyledText>
|
||||
</Box>
|
||||
<VStack
|
||||
css={{
|
||||
padding: '24px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bg: '$grayBg',
|
||||
gap: '5px',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
handleUpdateName()
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<StyledLabel>Name</StyledLabel>
|
||||
<FormInput
|
||||
type={'text'}
|
||||
value={name}
|
||||
tabIndex={1}
|
||||
placeholder={'Name'}
|
||||
disabled={nameUpdating}
|
||||
onChange={(event) => {
|
||||
setName(event.target.value)
|
||||
event.preventDefault()
|
||||
}}
|
||||
/>
|
||||
<StyledText style="footnote" css={{ mt: '10px', mb: '20px' }}>
|
||||
Your name is displayed on your profile and is used when
|
||||
communicating with you.
|
||||
</StyledText>
|
||||
<Button type="submit" style="ctaDarkYellow">
|
||||
Update Name
|
||||
</Button>
|
||||
</form>
|
||||
</VStack>
|
||||
|
||||
<VStack
|
||||
css={{
|
||||
padding: '24px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bg: '$grayBg',
|
||||
gap: '5px',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
<StyledLabel>Username</StyledLabel>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
handleUpdateUsername()
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
type={'text'}
|
||||
placeholder={'Username'}
|
||||
value={username}
|
||||
disabled={usernameUpdating}
|
||||
onChange={(event) => {
|
||||
handleUsernameChange(event)
|
||||
event.preventDefault()
|
||||
}}
|
||||
/>
|
||||
<SpanBox>
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
m: 0,
|
||||
minHeight: '20px',
|
||||
color: usernameError ? '$error' : 'unset',
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{usernameError && !isUsernameValidationLoading && (
|
||||
<>{usernameErrorMessage}</>
|
||||
)}
|
||||
{usernameEdited &&
|
||||
!usernameError &&
|
||||
!isUsernameValidationLoading && <>Username is available.</>}
|
||||
</StyledText>
|
||||
</SpanBox>
|
||||
<StyledText style="footnote" css={{ mt: '10px', mb: '20px' }}>
|
||||
Your username must be unique among all users. It can only
|
||||
contain letters, numbers, and the underscore character.
|
||||
</StyledText>
|
||||
<StyledText style="footnote" css={{ mt: '10px', mb: '20px' }}>
|
||||
* Changing your username may break some links from external
|
||||
apps.
|
||||
</StyledText>
|
||||
<Button style="ctaDarkYellow">Update Username</Button>
|
||||
</form>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -54,6 +54,7 @@ async function updateLabelsCache(apiUrl, tab) {
|
||||
console.log(!data.labels, data.labels['errorCodes'], !data.labels['labels'])
|
||||
return []
|
||||
}
|
||||
|
||||
await setStorage({
|
||||
labels: data.labels.labels,
|
||||
labelsLastUpdated: new Date().toISOString(),
|
||||
@ -97,13 +98,15 @@ async function updatePageTitle(apiUrl, pageId, title) {
|
||||
return data.updatePage.updatePage
|
||||
}
|
||||
|
||||
async function setLabels(apiUrl, pageId, labelIds) {
|
||||
async function setLabels(apiUrl, pageId, labels) {
|
||||
const mutation = JSON.stringify({
|
||||
query: `mutation SetLabels($input: SetLabelsInput!) {
|
||||
setLabels(input: $input) {
|
||||
... on SetLabelsSuccess {
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
}
|
||||
}
|
||||
... on SetLabelsError {
|
||||
@ -115,7 +118,7 @@ async function setLabels(apiUrl, pageId, labelIds) {
|
||||
variables: {
|
||||
input: {
|
||||
pageId,
|
||||
labelIds,
|
||||
labels,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -129,9 +132,34 @@ async function setLabels(apiUrl, pageId, labelIds) {
|
||||
console.log('GQL Error setting labels:', data)
|
||||
throw new Error('Error setting labels.')
|
||||
}
|
||||
|
||||
await appendLabelsToCache(data.setLabels.labels)
|
||||
|
||||
return data.setLabels.labels
|
||||
}
|
||||
|
||||
async function appendLabelsToCache(labels) {
|
||||
const cachedLabels = await getStorageItem('labels')
|
||||
if (cachedLabels) {
|
||||
labels.forEach((l) => {
|
||||
const existing = cachedLabels.find((cached) => cached.name === l.name)
|
||||
if (!existing) {
|
||||
cachedLabels.unshift(l)
|
||||
}
|
||||
})
|
||||
|
||||
await setStorage({
|
||||
labels: cachedLabels,
|
||||
labelsLastUpdated: new Date().toISOString(),
|
||||
})
|
||||
} else {
|
||||
await setStorage({
|
||||
labels: labels,
|
||||
labelsLastUpdated: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function addNote(apiUrl, pageId, noteId, shortId, note) {
|
||||
const query = JSON.stringify({
|
||||
query: `query GetArticle(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 || '',
|
||||
|
||||
@ -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
|
||||
})()
|
||||
|
||||
@ -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,7 +289,12 @@
|
||||
}
|
||||
|
||||
function toggleRow(rowId) {
|
||||
console.log('currentToastEl: ', currentToastEl)
|
||||
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(
|
||||
@ -361,7 +376,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
function createLabelRow(label, idx) {
|
||||
function createLabelRow(label) {
|
||||
const element = document.createElement('button')
|
||||
const dot = document.createElement('span')
|
||||
dot.style = 'width:10px;height:10px;border-radius:1000px;'
|
||||
@ -384,9 +399,8 @@
|
||||
element.appendChild(check)
|
||||
|
||||
element.onclick = labelClick
|
||||
element.onkeydown = labelKeyDown
|
||||
element.onkeydown = labelEditorKeyDownHandler
|
||||
element.setAttribute('data-label-id', label.id)
|
||||
element.setAttribute('data-label-idx', idx)
|
||||
element.setAttribute(
|
||||
'data-label-selected',
|
||||
label['selected'] ? 'on' : 'off'
|
||||
@ -421,68 +435,117 @@
|
||||
if (label) {
|
||||
label.selected = toggledValue
|
||||
}
|
||||
|
||||
const labelList = event.target.form.querySelector('#label-list')
|
||||
const labelInput = event.target.form.querySelector(
|
||||
'#omnivore-edit-label-input'
|
||||
)
|
||||
if (toggledValue) {
|
||||
addLabel(labelList, labelInput, label.name)
|
||||
} else {
|
||||
removeLabel(labelList, label.id)
|
||||
}
|
||||
}
|
||||
|
||||
function labelKeyDown(event) {
|
||||
function backspaceOnLastItem(labelsList, labelsInput) {
|
||||
// Get the last <li> item before the <li><input item
|
||||
const lastItem =
|
||||
labelsInput.closest('#label-entry-item').previousElementSibling
|
||||
if (lastItem) {
|
||||
const backspaced = lastItem.getAttribute('data-label-backspaced')
|
||||
if (backspaced) {
|
||||
removeLabel(
|
||||
labelsInput.closest('#label-list'),
|
||||
lastItem.getAttribute('data-label-id')
|
||||
)
|
||||
} else {
|
||||
lastItem.setAttribute('data-label-backspaced', 'on')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function labelEditorClickHandler(event) {
|
||||
const input = event.target.querySelector('#omnivore-edit-label-input')
|
||||
if (input && event.target != input) {
|
||||
input.focus()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function clearBackspacedLabels(form) {
|
||||
const selected = form.querySelectorAll('.label[data-label-backspaced="on"]')
|
||||
selected.forEach((node) => {
|
||||
node.removeAttribute('data-label-backspaced')
|
||||
})
|
||||
}
|
||||
|
||||
function labelEditorKeyDownHandler(event) {
|
||||
event.cancelBubble = true
|
||||
if (event.stopPropogation) {
|
||||
event.stopPropogation()
|
||||
}
|
||||
|
||||
// If any labels have been backspaced into (so they have the selected outline), clear their state
|
||||
if (event.target.form && event.key.toLowerCase() !== 'backspace') {
|
||||
clearBackspacedLabels(event.target.form)
|
||||
}
|
||||
|
||||
switch (event.key.toLowerCase()) {
|
||||
case 'arrowup': {
|
||||
if (
|
||||
event.target ==
|
||||
event.target.form.querySelector('#omnivore-edit-label-text')
|
||||
) {
|
||||
if (event.target.id == 'omnivore-edit-label-input') {
|
||||
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
|
||||
}
|
||||
if (!event.target.getAttribute('data-label-id')) {
|
||||
return
|
||||
}
|
||||
|
||||
const prev = event.target.form.querySelector(
|
||||
`button[data-label-idx='${prevIdx}']`
|
||||
)
|
||||
if (prev) {
|
||||
let prev = event.target.previousElementSibling
|
||||
if (prev && prev.getAttribute('data-label-id')) {
|
||||
prev.focus()
|
||||
} else {
|
||||
// Focus the text area
|
||||
event.target.form.querySelector('#omnivore-edit-label-text')?.focus()
|
||||
event.target.form.querySelector('#omnivore-edit-label-input')?.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()
|
||||
let next = undefined
|
||||
if (event.target.id == 'omnivore-edit-label-input') {
|
||||
idx = event.target.getAttribute('data-label-id')
|
||||
next = event.target
|
||||
.closest('#omnivore-edit-labels-form')
|
||||
.querySelector('#omnivore-edit-labels-list')
|
||||
.querySelector('[data-label-id]')
|
||||
} else {
|
||||
// Focus the save button
|
||||
event.target.form.querySelector('.omnivore-save-button')?.focus()
|
||||
next = event.target.nextElementSibling
|
||||
}
|
||||
|
||||
if (next && next.getAttribute('data-label-id')) {
|
||||
next.focus()
|
||||
}
|
||||
event.preventDefault()
|
||||
break
|
||||
}
|
||||
case 'backspace': {
|
||||
if (
|
||||
event.target.id == 'omnivore-edit-label-input' &&
|
||||
event.target.value.length == 0
|
||||
) {
|
||||
const labelList = event.target.form.querySelector('#label-list')
|
||||
backspaceOnLastItem(labelList, event.target)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'enter': {
|
||||
if (event.target.id == 'omnivore-edit-label-input') {
|
||||
if (event.target.value) {
|
||||
const labelList = event.target.form.querySelector('#label-list')
|
||||
addLabel(labelList, event.target, event.target.value)
|
||||
}
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
const labelId = event.target.getAttribute('data-label-id')
|
||||
toggleLabel(event, labelId)
|
||||
event.preventDefault()
|
||||
@ -491,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')
|
||||
|
||||
@ -500,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
|
||||
@ -529,7 +617,6 @@
|
||||
currentToastEl.shadowRoot.querySelector(
|
||||
'#omnivore-add-note-form'
|
||||
).onsubmit = (event) => {
|
||||
console.log('submitting form: ', event)
|
||||
updateStatusBox('#omnivore-add-note-status', 'loading', 'Adding note...')
|
||||
|
||||
browserApi.runtime.sendMessage({
|
||||
@ -541,7 +628,9 @@
|
||||
})
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropogation()
|
||||
if (event.stopPropogation) {
|
||||
event.stopPropogation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -585,6 +674,148 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getRandomColor() {
|
||||
const colors = [
|
||||
'#FF5D99',
|
||||
'#7CFF7B',
|
||||
'#FFD234',
|
||||
'#7BE4FF',
|
||||
'#CE88EF',
|
||||
'#EF8C43',
|
||||
]
|
||||
const randomIndex = Math.floor(Math.random() * colors.length)
|
||||
return colors[randomIndex]
|
||||
}
|
||||
|
||||
function getTempUUID() {
|
||||
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
||||
(
|
||||
c ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
||||
).toString(16)
|
||||
)
|
||||
}
|
||||
|
||||
function addLabel(labelList, labelInput, labelValue) {
|
||||
// first check if the label is already entered:
|
||||
const existingLabel = labels.find((l) => l.name === labelValue)
|
||||
const labelEntryItem = labelList.querySelector('#label-entry-item')
|
||||
const inputItem = labelEntryItem.querySelector('#omnivore-edit-label-input')
|
||||
|
||||
// Handle case where label is already selected
|
||||
if (
|
||||
existingLabel &&
|
||||
labelList.querySelector(`[data-label-id='${existingLabel.id}']`)
|
||||
) {
|
||||
const labelItem = labelList.querySelector(
|
||||
`[data-label-id='${existingLabel.id}']`
|
||||
)
|
||||
labelItem.setAttribute('data-item-highlighted', 'on')
|
||||
setTimeout(() => {
|
||||
labelItem.style.borderColor = 'rgb(222, 222, 222)'
|
||||
}, 500)
|
||||
|
||||
if (inputItem) {
|
||||
inputItem.value = ''
|
||||
inputItem.focus()
|
||||
updateLabels(undefined)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const labelColor = existingLabel ? existingLabel.color : getRandomColor()
|
||||
const labelElem = document.createElement('li')
|
||||
labelElem.classList.add('label')
|
||||
labelElem.innerHTML = `
|
||||
<span style="width: 10px; height: 10px; border-radius: 1000px; background-color: ${labelColor};"></span>
|
||||
${labelValue}
|
||||
<button class="label-remove-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="#6A6968" viewBox="0 0 256 256">
|
||||
<rect width="256" height="256" fill="none"></rect><line x1="200" y1="56" x2="56" y2="200" stroke="#6A6968" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line>
|
||||
<line x1="200" y1="200" x2="56" y2="56" stroke="#6A6968" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line>
|
||||
</svg>
|
||||
</button>
|
||||
`
|
||||
|
||||
labelList.insertBefore(labelElem, labelEntryItem)
|
||||
labelInput.value = ''
|
||||
|
||||
const form = labelList.closest('#omnivore-edit-labels-form')
|
||||
if (existingLabel) {
|
||||
const element = form.querySelector(
|
||||
`[data-label-id='${existingLabel.id}']`
|
||||
)
|
||||
existingLabel.selected = true
|
||||
element.setAttribute('data-label-selected', 'on')
|
||||
labelElem.setAttribute('data-label-id', existingLabel.id)
|
||||
} else {
|
||||
// insert a toggle row at the top
|
||||
const rowList = form.querySelector('#omnivore-edit-labels-list')
|
||||
const newLabel = {
|
||||
id: getTempUUID(),
|
||||
color: labelColor,
|
||||
name: labelValue,
|
||||
temporary: true,
|
||||
selected: true,
|
||||
}
|
||||
labels.push(newLabel)
|
||||
labelElem.setAttribute('data-label-id', newLabel.id)
|
||||
|
||||
// Now prepend a label in the rows at the bottom
|
||||
const rowHtml = createLabelRow(newLabel)
|
||||
const firstRow = rowList.querySelector('button[data-label-id]')
|
||||
rowHtml.setAttribute('data-label-selected', 'on')
|
||||
rowList.insertBefore(rowHtml, firstRow)
|
||||
}
|
||||
|
||||
if (inputItem) {
|
||||
inputItem.focus()
|
||||
updateLabels(undefined)
|
||||
}
|
||||
|
||||
syncLabelChanges()
|
||||
}
|
||||
|
||||
function removeLabel(labelList, labelID) {
|
||||
const form = labelList.closest('#omnivore-edit-labels-form')
|
||||
const element = labelList.querySelector(`[data-label-id='${labelID}']`)
|
||||
if (element) {
|
||||
element.remove()
|
||||
}
|
||||
|
||||
const rowElement = form.querySelector(`[data-label-id='${labelID}']`)
|
||||
if (rowElement) {
|
||||
rowElement.setAttribute('data-label-selected', 'off')
|
||||
}
|
||||
|
||||
syncLabelChanges()
|
||||
}
|
||||
|
||||
function syncLabelChanges() {
|
||||
updateStatusBox(
|
||||
'#omnivore-edit-labels-status',
|
||||
'loading',
|
||||
'Updating Labels...',
|
||||
undefined
|
||||
)
|
||||
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,
|
||||
labels: setLabels,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function editLabels() {
|
||||
cancelAutoDismiss()
|
||||
|
||||
@ -594,46 +825,36 @@
|
||||
|
||||
toggleRow('#omnivore-edit-labels-row')
|
||||
currentToastEl.shadowRoot
|
||||
.querySelector('#omnivore-edit-label-text')
|
||||
.querySelector('#omnivore-edit-label-input')
|
||||
?.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
|
||||
'#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.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,
|
||||
},
|
||||
})
|
||||
labels
|
||||
.sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
|
||||
)
|
||||
.forEach(function (label, idx) {
|
||||
const rowHtml = createLabelRow(label)
|
||||
list.appendChild(rowHtml)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -648,15 +869,22 @@
|
||||
.filter(
|
||||
(l) => l.name.toLowerCase().indexOf(filterValue.toLowerCase()) > -1
|
||||
)
|
||||
.forEach(function (label, idx) {
|
||||
const rowHtml = createLabelRow(label, idx)
|
||||
.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, idx) {
|
||||
const rowHtml = createLabelRow(label, idx)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,6 +68,11 @@
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
#omnivore-toast-container #omnivore-edit-labels-status {
|
||||
height: 22px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#omnivore-toast-button-row {
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
@ -228,7 +233,7 @@
|
||||
max-height: 200px;
|
||||
gap: 5px;
|
||||
color: #3B3A38;
|
||||
margin-top: 15px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@ -331,6 +336,86 @@
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.label-editor {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
|
||||
display: inline-block;
|
||||
background-color: var(--colors-thBackground2);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
line-height: 2;
|
||||
cursor: text;
|
||||
font-size: 12px;
|
||||
width: calc(100% - 10px);
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.label-list {
|
||||
flex-wrap: wrap;
|
||||
justify-content: start;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding: 0px;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
flex: 0 0 auto;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#omnivore-toast-container .omnivore-toast-func-row .label-remove-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: transparent;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#omnivore-edit-labels-row .label-input {
|
||||
box-sizing: content-box;
|
||||
font-size: 16px;
|
||||
min-width: 2px;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-table;
|
||||
padding: 1px;
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border: 1px solid rgb(222, 222, 222);
|
||||
background-color: rgb(249, 249, 249);
|
||||
}
|
||||
|
||||
#omnivore-toast-container .omnivore-toast-func-row .label button {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
.label[data-label-backspaced="on"] {
|
||||
border: 1px solid rgb(255, 234, 159);
|
||||
}
|
||||
|
||||
.label[data-item-highlighted="on"] {
|
||||
border: 1px solid black;
|
||||
transition: border-color 0.25s linear;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div id="omnivore-toast-container">
|
||||
<div id="omnivore-toast-button-row">
|
||||
@ -421,14 +506,16 @@
|
||||
</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 id="omnivore-edit-label-editor" class="label-editor">
|
||||
<ul id="label-list" class="label-list">
|
||||
<li id="label-entry-item">
|
||||
<input type="text" id="omnivore-edit-label-input" placeholder="Add a label..." maxlength="48" class="label-input">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="omnivore-save-button">Save</button>
|
||||
<span id="omnivore-edit-labels-status" class="omnivore-toast-func-status"></span>
|
||||
<div id="omnivore-edit-labels-list"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user