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",
"dependencies": {
"@hookform/resolvers": "^3.2.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-icons": "^1.3.0",
"@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": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
+1
View File
@@ -14,6 +14,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.2.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
+7 -7
View File
@@ -132,9 +132,9 @@ const BookmarkForm = () => {
const { handleSubmit, control } = form;
useEffect(() => {
getCurrentTabInfo().then((tabInfo) => {
form.setValue('url', tabInfo.url);
form.setValue('description', tabInfo.title);
getCurrentTabInfo().then(({ url, title }) => {
form.setValue('url', url);
form.setValue('description', title);
});
const getConfig = async () => {
configured = await isConfigured();
@@ -264,15 +264,15 @@ const BookmarkForm = () => {
aria-expanded={openCollections}
className={cn(
'w-full justify-between',
!field.value.name && 'text-muted-foreground'
!field.value?.name && 'text-muted-foreground'
)}
>
{loadingCollections
? 'Loading'
: field.value.name
: field.value?.name
? collections.response?.find(
(collection: { name: any }) =>
collection.name === field.value.name
(collection: { name: string }) =>
collection.name === field.value?.name
)?.name || 'Unorganized'
: 'Select a collection...'}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+22 -1
View File
@@ -33,6 +33,7 @@ import {
getSession,
performLoginOrLogout,
} from '../lib/auth/auth.ts';
import { Checkbox } from './ui/CheckBox.tsx';
let HAD_PREVIOUS_SESSION = false;
const OptionsForm = () => {
@@ -42,6 +43,7 @@ const OptionsForm = () => {
baseUrl: '',
username: '',
password: '',
syncBookmarks: false,
},
});
@@ -230,6 +232,25 @@ const OptionsForm = () => {
</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>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
@@ -237,7 +258,7 @@ const OptionsForm = () => {
<Button
type="button"
className="mb-2"
onClick={onReset as any}
onClick={onReset as never}
disabled={resetLoading}
>
Reset
+5 -3
View File
@@ -1,12 +1,14 @@
import { FC } from 'react';
import { cn } from '../lib/utils.ts';
interface WholeContainerProps {
interface WholeContainerProps extends React.HTMLAttributes<HTMLDivElement>{
children: React.ReactNode;
className?: string;
}
const WholeContainer: FC<WholeContainerProps> = ({ children }) => {
const WholeContainer: FC<WholeContainerProps> = ({ children, className }) => {
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}
</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);
}
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) {
const formBody = Object.entries(data)
.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) {
const session = await axios.get(`${url}/api/v1/auth/session`);
return session.data.user;
+1
View File
@@ -5,6 +5,7 @@ const DEFAULTS: optionsFormValues = {
baseUrl: '',
username: '',
password: '',
syncBookmarks: false,
};
const CONFIG_KEY = 'lw_config_key';
+1 -1
View File
@@ -6,7 +6,7 @@ export const bookmarkFormSchema = z.object({
id: z.number().optional(),
ownerId: z.number().optional(),
name: z.string(),
}),
}).optional(),
tags: z
.array(
z.object({
+3 -2
View File
@@ -2,8 +2,9 @@ import { z } from 'zod';
export const optionsFormSchema = z.object({
baseUrl: z.string().url('This has to be a URL'),
username: z.string().nonempty('This cannot be empty'),
password: z.string().nonempty('This cannot be empty'),
username: z.string().min(1, '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>;
+5 -1
View File
@@ -24,7 +24,11 @@
"48": "./48.png",
"128": "./128.png"
},
"permissions": ["storage", "activeTab", "tabs"],
"permissions": ["storage", "activeTab", "tabs", "bookmarks"],
"background": {
"service_worker": "background.js",
"type": "module"
},
"host_permissions": ["*://*/*"],
"commands": {
"_execute_action": {
+73 -2
View File
@@ -1,2 +1,73 @@
// Keeping this, maybe it will be needed somewhere in the future...
console.log('Background script running...');
import { getBrowser } from '../../@/lib/utils.ts';
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 = () => {
return (
<WholeContainer>
<WholeContainer className="max-h-[625px]">
<Container>
<div className="justify-center items-center p-2 flex">
<h1 className="text-lg">Options configuration</h1>
+1
View File
@@ -14,6 +14,7 @@ export default defineConfig({
input: {
main: path.resolve(__dirname, 'index.html'),
options: path.resolve(__dirname, 'src/pages/Options/options.html'),
background: path.resolve(__dirname, 'src/pages/Background/index.ts'),
},
output: {
entryFileNames: '[name].js',