mirror of
https://github.com/linkwarden/browser-extension.git
synced 2026-06-22 20:00:19 +00:00
working adding links when addded to bookmark
This commit is contained in:
Generated
+31
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 }
|
||||
@@ -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'
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -5,6 +5,7 @@ const DEFAULTS: optionsFormValues = {
|
||||
baseUrl: '',
|
||||
username: '',
|
||||
password: '',
|
||||
syncBookmarks: false,
|
||||
};
|
||||
const CONFIG_KEY = 'lw_config_key';
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user