working adding links when addded to bookmark

This commit is contained in:
Jordan Higuera Higuera
2024-01-02 22:54:03 -07:00
parent b8082618dd
commit b3945272e1
15 changed files with 211 additions and 18 deletions
+31
View File
@@ -10,6 +10,7 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.2.0", "@hookform/resolvers": "^3.2.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
@@ -1059,6 +1060,36 @@
} }
} }
}, },
"node_modules/@radix-ui/react-checkbox": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz",
"integrity": "sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-previous": "1.0.1",
"@radix-ui/react-use-size": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": { "node_modules/@radix-ui/react-collection": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
+1
View File
@@ -14,6 +14,7 @@
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.2.0", "@hookform/resolvers": "^3.2.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
+7 -7
View File
@@ -132,9 +132,9 @@ const BookmarkForm = () => {
const { handleSubmit, control } = form; const { handleSubmit, control } = form;
useEffect(() => { useEffect(() => {
getCurrentTabInfo().then((tabInfo) => { getCurrentTabInfo().then(({ url, title }) => {
form.setValue('url', tabInfo.url); form.setValue('url', url);
form.setValue('description', tabInfo.title); form.setValue('description', title);
}); });
const getConfig = async () => { const getConfig = async () => {
configured = await isConfigured(); configured = await isConfigured();
@@ -264,15 +264,15 @@ const BookmarkForm = () => {
aria-expanded={openCollections} aria-expanded={openCollections}
className={cn( className={cn(
'w-full justify-between', 'w-full justify-between',
!field.value.name && 'text-muted-foreground' !field.value?.name && 'text-muted-foreground'
)} )}
> >
{loadingCollections {loadingCollections
? 'Loading' ? 'Loading'
: field.value.name : field.value?.name
? collections.response?.find( ? collections.response?.find(
(collection: { name: any }) => (collection: { name: string }) =>
collection.name === field.value.name collection.name === field.value?.name
)?.name || 'Unorganized' )?.name || 'Unorganized'
: 'Select a collection...'} : 'Select a collection...'}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+22 -1
View File
@@ -33,6 +33,7 @@ import {
getSession, getSession,
performLoginOrLogout, performLoginOrLogout,
} from '../lib/auth/auth.ts'; } from '../lib/auth/auth.ts';
import { Checkbox } from './ui/CheckBox.tsx';
let HAD_PREVIOUS_SESSION = false; let HAD_PREVIOUS_SESSION = false;
const OptionsForm = () => { const OptionsForm = () => {
@@ -42,6 +43,7 @@ const OptionsForm = () => {
baseUrl: '', baseUrl: '',
username: '', username: '',
password: '', password: '',
syncBookmarks: false,
}, },
}); });
@@ -230,6 +232,25 @@ const OptionsForm = () => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={control}
name="syncBookmarks"
render={({field}) => (
<FormItem>
<FormLabel>Sync Bookmarks</FormLabel>
<FormDescription>
Sync your bookmarks with Linkwarden.
</FormDescription>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-between"> <div className="flex justify-between">
<div> <div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
@@ -237,7 +258,7 @@ const OptionsForm = () => {
<Button <Button
type="button" type="button"
className="mb-2" className="mb-2"
onClick={onReset as any} onClick={onReset as never}
disabled={resetLoading} disabled={resetLoading}
> >
Reset Reset
+5 -3
View File
@@ -1,12 +1,14 @@
import { FC } from 'react'; import { FC } from 'react';
import { cn } from '../lib/utils.ts';
interface WholeContainerProps { interface WholeContainerProps extends React.HTMLAttributes<HTMLDivElement>{
children: React.ReactNode; children: React.ReactNode;
className?: string;
} }
const WholeContainer: FC<WholeContainerProps> = ({ children }) => { const WholeContainer: FC<WholeContainerProps> = ({ children, className }) => {
return ( return (
<div className="inset-0 w-full flex justify-center max-h-[600px] overflow-y-hidden relative"> <div className={cn('inset-0 w-full flex justify-center max-h-[600px] overflow-y-hidden relative', className)}>
{children} {children}
</div> </div>
); );
+28
View File
@@ -0,0 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "@radix-ui/react-icons"
import { cn } from '../../lib/utils.ts';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
+12
View File
@@ -6,3 +6,15 @@ export async function postLink(baseUrl: string, data: bookmarkFormValues) {
return await axios.post(url, data); return await axios.post(url, data);
} }
export async function postLinkFetch(baseUrl: string, data: bookmarkFormValues) {
const url = `${baseUrl}/api/v1/links`;
return await fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
});
}
+20
View File
@@ -22,6 +22,12 @@ export async function getCsrfToken(url: string): Promise<string> {
} }
export async function getCsrfTokenFetch(url: string): Promise<string> {
const token = await fetch(`${url}/api/v1/auth/csrf`);
const { csrfToken } = await token.json();
return csrfToken;
}
export async function performLoginOrLogout(url: string, data: DataLogin | DataLogout) { export async function performLoginOrLogout(url: string, data: DataLogin | DataLogout) {
const formBody = Object.entries(data) const formBody = Object.entries(data)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
@@ -34,6 +40,20 @@ export async function performLoginOrLogout(url: string, data: DataLogin | DataLo
}); });
} }
export async function performLoginOrLogoutFetch(url: string, data: DataLogin | DataLogout) {
const formBody = Object.entries(data)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
return await fetch(url, {
method: 'POST',
body: formBody,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
}
export async function getSession(url: string) { export async function getSession(url: string) {
const session = await axios.get(`${url}/api/v1/auth/session`); const session = await axios.get(`${url}/api/v1/auth/session`);
return session.data.user; return session.data.user;
+1
View File
@@ -5,6 +5,7 @@ const DEFAULTS: optionsFormValues = {
baseUrl: '', baseUrl: '',
username: '', username: '',
password: '', password: '',
syncBookmarks: false,
}; };
const CONFIG_KEY = 'lw_config_key'; const CONFIG_KEY = 'lw_config_key';
+1 -1
View File
@@ -6,7 +6,7 @@ export const bookmarkFormSchema = z.object({
id: z.number().optional(), id: z.number().optional(),
ownerId: z.number().optional(), ownerId: z.number().optional(),
name: z.string(), name: z.string(),
}), }).optional(),
tags: z tags: z
.array( .array(
z.object({ z.object({
+3 -2
View File
@@ -2,8 +2,9 @@ import { z } from 'zod';
export const optionsFormSchema = z.object({ export const optionsFormSchema = z.object({
baseUrl: z.string().url('This has to be a URL'), baseUrl: z.string().url('This has to be a URL'),
username: z.string().nonempty('This cannot be empty'), username: z.string().min(1, 'This cannot be empty'),
password: z.string().nonempty('This cannot be empty'), password: z.string().min(1, 'This cannot be empty'),
syncBookmarks: z.boolean().default(false).optional(),
}); });
export type optionsFormValues = z.infer<typeof optionsFormSchema>; export type optionsFormValues = z.infer<typeof optionsFormSchema>;
+5 -1
View File
@@ -24,7 +24,11 @@
"48": "./48.png", "48": "./48.png",
"128": "./128.png" "128": "./128.png"
}, },
"permissions": ["storage", "activeTab", "tabs"], "permissions": ["storage", "activeTab", "tabs", "bookmarks"],
"background": {
"service_worker": "background.js",
"type": "module"
},
"host_permissions": ["*://*/*"], "host_permissions": ["*://*/*"],
"commands": { "commands": {
"_execute_action": { "_execute_action": {
+73 -2
View File
@@ -1,2 +1,73 @@
// Keeping this, maybe it will be needed somewhere in the future... import { getBrowser } from '../../@/lib/utils.ts';
console.log('Background script running...'); import BookmarkTreeNode = chrome.bookmarks.BookmarkTreeNode;
import { getConfig } from '../../@/lib/config.ts';
import { postLinkFetch } from '../../@/lib/actions/links.ts';
import { getCsrfTokenFetch, performLoginOrLogoutFetch } from '../../@/lib/auth/auth.ts';
const browser = getBrowser();
const getCurrentBookmarks = async () => {
return await browser.bookmarks.getTree();
}
// Testing will remove later
const logBookmarks = (bookmarks: BookmarkTreeNode[]) => {
for (const bookmark of bookmarks) {
if (bookmark.url) {
const { url, title, parentId } = bookmark;
console.log(url, title, parentId);
}
else if (bookmark.children) {
logBookmarks(bookmark.children);
}
}
}
// Testing will remove later
(async () => {
try {
const { syncBookmarks } = await getConfig();
if (!syncBookmarks) {
return;
}
const [root] = await getCurrentBookmarks();
logBookmarks(root.children);
} catch (error) {
console.error(error);
}
})();
// This is the main function that will be called when a bookmark is created
// idk why wont work with axios...
browser.bookmarks.onCreated.addListener(async (_id: string, bookmark: BookmarkTreeNode) => {
try {
const { syncBookmarks, baseUrl, username, password } = await getConfig();
if (!syncBookmarks || !bookmark.url) {
return;
}
const csrfToken = await getCsrfTokenFetch(baseUrl);
await performLoginOrLogoutFetch(
`${baseUrl}/api/v1/auth/callback/credentials`,
{
username: username,
password: password,
redirect: false,
csrfToken,
callbackUrl: `${baseUrl}/login`,
json: true,
}
);
await postLinkFetch(baseUrl, {
url: bookmark.url,
collection: {
name: "Unorganized",
},
tags: [],
name: bookmark.title,
description: bookmark.title,
});
console.log('Created', bookmark);
} catch (error) {
console.error(error);
}
});
+1 -1
View File
@@ -5,7 +5,7 @@ import OptionsForm from '../../@/components/OptionsForm.tsx';
const App = () => { const App = () => {
return ( return (
<WholeContainer> <WholeContainer className="max-h-[625px]">
<Container> <Container>
<div className="justify-center items-center p-2 flex"> <div className="justify-center items-center p-2 flex">
<h1 className="text-lg">Options configuration</h1> <h1 className="text-lg">Options configuration</h1>
+1
View File
@@ -14,6 +14,7 @@ export default defineConfig({
input: { input: {
main: path.resolve(__dirname, 'index.html'), main: path.resolve(__dirname, 'index.html'),
options: path.resolve(__dirname, 'src/pages/Options/options.html'), options: path.resolve(__dirname, 'src/pages/Options/options.html'),
background: path.resolve(__dirname, 'src/pages/Background/index.ts'),
}, },
output: { output: {
entryFileNames: '[name].js', entryFileNames: '[name].js',