Merge pull request #138 from linkwarden/dev

Dev
This commit is contained in:
Daniel
2026-01-05 01:15:31 +03:30
committed by GitHub
15 changed files with 502 additions and 531 deletions
+2 -1
View File
@@ -50,7 +50,8 @@ cd browser-extension
And run:
```
chmod +x ./build.sh && ./build.sh
npm install
npm run build
```
After the above command, use the `/dist` folder as an unpacked extension in your browser.
-21
View File
@@ -1,21 +0,0 @@
#!/usr/bin/env bash
# Install deps
npm install
# Build
npm run build
# Check if --firefox argument was passed
if [ "$1" = "--firefox" ]; then
# Copy to firefox/manifest.json
echo "Built for Firefox..."
cp firefox/manifest.json dist/manifest.json
else
# Copy to dist/manifest.json
echo "Built for Chromium..."
cp chromium/manifest.json dist/manifest.json
fi
# Done (for now...)
echo "Done! ✅"
-59
View File
@@ -1,59 +0,0 @@
{
"manifest_version": 2,
"name": "Linkwarden",
"description": "The browser extension for Linkwarden.",
"homepage_url": "https://linkwarden.app/",
"version": "1.4.0",
"browser_action": {
"default_popup": "./index.html",
"default_icon": {
"16": "./16.png",
"32": "./32.png",
"48": "./48.png",
"128": "./128.png"
},
"default_title": "Linkwarden"
},
"options_ui": {
"page": "./src/pages/Options/options.html",
"browser_style": false
},
"icons": {
"16": "./16.png",
"32": "./32.png",
"48": "./48.png",
"128": "./128.png"
},
"permissions": [
"storage",
"activeTab",
"tabs",
"bookmarks",
"contextMenus",
"<all_urls>",
"http://*/*",
"https://*/*"
],
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Ctrl+Shift+F",
"mac": "Command+Shift+K"
}
}
},
"omnibox": {
"keyword": "lk"
},
"background": {
"scripts": ["background.js"],
"persistent": false,
"type": "module"
},
"browser_specific_settings": {
"gecko": {
"id": "jordanlinkwarden@gmail.com",
"strict_min_version": "109.0"
}
}
}
+16 -19
View File
@@ -1,11 +1,12 @@
{
"manifest_version": 3,
"minimum_chrome_version": "121",
"name": "Linkwarden",
"description": "The browser extension for Linkwarden.",
"homepage_url": "https://linkwarden.app/",
"version": "1.4.0",
"version": "1.5.0",
"action": {
"default_popup": "./index.html",
"default_popup": "index.html",
"default_icon": {
"16": "16.png",
"32": "32.png",
@@ -15,38 +16,34 @@
"default_title": "Linkwarden"
},
"options_ui": {
"page": "./src/pages/Options/options.html",
"page": "src/pages/Options/options.html",
"browser_style": false
},
"icons": {
"16": "16.png",
"32": "32.png",
"48": "48.png",
"128": "128.png"
},
"icons": { "16": "16.png", "32": "32.png", "48": "48.png", "128": "128.png" },
"permissions": [
"storage",
"scripting",
"activeTab",
"tabs",
"bookmarks",
"commands",
"contextMenus"
],
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "./background.js",
"service_worker": "background.js",
"scripts": ["background.js"],
"type": "module"
},
"omnibox": {
"keyword": "lk"
},
"host_permissions": ["*://*/*"],
"omnibox": { "keyword": "lk" },
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+F",
"mac": "Command+Shift+Y"
}
"suggested_key": { "default": "Ctrl+Shift+F", "mac": "Command+Shift+Y" }
}
},
"browser_specific_settings": {
"gecko": {
"id": "jordanlinkwarden@gmail.com",
"strict_min_version": "121.0"
}
}
}
+2 -2
View File
@@ -1,14 +1,14 @@
{
"name": "linkwarden-extension",
"private": true,
"version": "0.0.1",
"version": "0.0.0",
"author": "Jordan Higuera Higuera <jordan_higuera@hotmail.com>",
"type": "module",
"license": "MIT",
"description": "Linkwarden browser extension",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "tsc && vite build && cp ./manifest.json dist/manifest.json",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 774 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

+311 -293
View File
@@ -16,21 +16,17 @@ import { Input } from './ui/Input.tsx';
import { Button } from './ui/Button.tsx';
import { TagInput } from './TagInput.tsx';
import { Textarea } from './ui/Textarea.tsx';
import {
getCurrentLinkItem,
getCurrentTabInfo,
updateBadge,
} from '../lib/utils.ts';
import { getCurrentTabInfo, updateBadge } from '../lib/utils.ts';
import { useEffect, useState } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { getConfig, isConfigured } from '../lib/config.ts';
import { postLink } from '../lib/actions/links.ts';
import { getConfig, isConfigured as getIsConfigured } from '../lib/config.ts';
import { checkLinkExists, postLink } from '../lib/actions/links.ts';
import { AxiosError } from 'axios';
import { toast } from '../../hooks/use-toast.ts';
import { Toaster } from './ui/Toaster.tsx';
import { getCollections } from '../lib/actions/collections.ts';
import { getTags } from '../lib/actions/tags.ts';
import { X } from 'lucide-react';
import { ExternalLink, X } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from './ui/Popover.tsx';
import { CaretSortIcon } from '@radix-ui/react-icons';
import {
@@ -40,21 +36,30 @@ import {
CommandInput,
CommandItem,
} from './ui/Command.tsx';
import { bookmarkMetadata, saveLinksInCache } from '../lib/cache.ts';
import { Checkbox } from './ui/CheckBox.tsx';
import { Label } from './ui/Label.tsx';
let configured = false;
let baseUrl = '';
const BookmarkForm = () => {
const [savedLwItem, setSavedLwItem] = useState<
bookmarkMetadata | false | void
>(undefined);
const [openOptions, setOpenOptions] = useState<boolean>(false);
const [openCollections, setOpenCollections] = useState<boolean>(false);
const [uploadImage, setUploadImage] = useState<boolean>(false);
const [state, setState] = useState<'capturing' | 'uploading' | null>(null);
const [isConfigured, setIsConfigured] = useState(false);
const [isDuplicate, setIsDuplicate] = useState(false);
const [config, setConfig] = useState<{
baseUrl: string;
defaultCollection: string;
apiKey: string;
syncBookmarks: boolean;
}>();
const [tabInfo, setTabInfo] = useState<{
id: number | undefined;
title: string | undefined;
url: string | undefined;
}>();
const handleCheckedChange = (s: boolean | 'indeterminate') => {
if (s === 'indeterminate') return;
setUploadImage(s);
@@ -77,14 +82,12 @@ const BookmarkForm = () => {
const { mutate: onSubmit, isLoading } = useMutation({
mutationFn: async (values: bookmarkFormValues) => {
const config = await getConfig();
await postLink(
config.baseUrl,
config?.baseUrl as string,
uploadImage,
values,
setState,
config.apiKey
config?.apiKey as string
);
return;
@@ -110,9 +113,6 @@ const BookmarkForm = () => {
return;
},
onSuccess: () => {
getCurrentLinkItem().then((item) => {
setSavedLwItem(item);
});
// Update badge to show link is saved
getCurrentTabInfo().then(({ id }) => {
updateBadge(id);
@@ -128,51 +128,53 @@ const BookmarkForm = () => {
},
});
useEffect(() => {
const setTabInformation = async () => {
const t = await getCurrentTabInfo();
const c = await getConfig();
setTabInfo(t);
setConfig(c);
updateBadge(t.id);
form.setValue('url', t.url ? t.url : '');
form.setValue('name', t.title ? t.title : '');
form.setValue('collection', {
name: c.defaultCollection,
});
const configured = await getIsConfigured();
const duplicate = await checkLinkExists(c.baseUrl, c.apiKey);
setIsDuplicate(duplicate);
setIsConfigured(configured);
};
setTabInformation();
}, []);
const { handleSubmit, control } = form;
useEffect(() => {
getCurrentTabInfo().then(({ id, url, title }) => {
updateBadge(id);
getConfig().then((config) => {
form.setValue('url', url ? url : '');
form.setValue('name', title ? title : '');
form.setValue('collection', {
name: config.defaultCollection,
});
});
});
const getConfigUse = async () => {
configured = await isConfigured();
setSavedLwItem(await getCurrentLinkItem());
};
getConfigUse();
}, [form]);
useEffect(() => {
const syncBookmarks = async () => {
try {
const {
syncBookmarks,
baseUrl: configBaseUrl,
defaultCollection,
} = await getConfig();
baseUrl = configBaseUrl;
form.setValue('collection', {
name: defaultCollection,
});
if (!syncBookmarks) {
return;
}
if (await isConfigured()) {
await saveLinksInCache(baseUrl);
//await syncLocalBookmarks(baseUrl);
}
} catch (error) {
console.error(error);
}
};
syncBookmarks();
}, [form]);
// useEffect(() => {
// const syncBookmarks = async () => {
// try {
// const { syncBookmarks, baseUrl, defaultCollection } = await getConfig();
// form.setValue('collection', {
// name: defaultCollection,
// });
// if (!syncBookmarks) {
// return;
// }
// if (await isConfigured()) {
// await saveLinksInCache(baseUrl);
// await syncLocalBookmarks(baseUrl);
// }
// } catch (error) {
// console.error(error);
// }
// };
// syncBookmarks();
// }, [form]);
const {
isLoading: loadingCollections,
@@ -181,15 +183,16 @@ const BookmarkForm = () => {
} = useQuery({
queryKey: ['collections'],
queryFn: async () => {
const config = await getConfig();
const response = await getCollections(config.baseUrl, config.apiKey);
const response = await getCollections(
config?.baseUrl as string,
config?.apiKey as string
);
return response.data.response.sort((a, b) => {
return a.pathname.localeCompare(b.pathname);
});
},
enabled: configured,
enabled: isConfigured,
});
const {
@@ -199,21 +202,25 @@ const BookmarkForm = () => {
} = useQuery({
queryKey: ['tags'],
queryFn: async () => {
const config = await getConfig();
const response = await getTags(config.baseUrl, config.apiKey);
const response = await getTags(
config?.baseUrl as string,
config?.apiKey as string
);
return response.data.response.sort((a, b) => {
return a.name.localeCompare(b.name);
});
},
enabled: configured,
enabled: isConfigured,
});
return (
<div>
<Form {...form}>
<form onSubmit={handleSubmit((e) => onSubmit(e))} className="py-1">
<form
onSubmit={handleSubmit((e) => onSubmit(e))}
className="py-1 space-y-5"
>
{collectionError ? (
<p className="text-red-600">
There was an error, please make sure the website is available.
@@ -222,179 +229,192 @@ const BookmarkForm = () => {
<FormField
control={control}
name="collection"
render={({ field }) =>
savedLwItem === false ? (
<FormItem className={`my-2`}>
<FormLabel>Collection</FormLabel>
<div className="min-w-full inset-x-0">
<Popover
open={openCollections}
onOpenChange={setOpenCollections}
>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
aria-expanded={openCollections}
className={
'w-full justify-between bg-neutral-100 dark:bg-neutral-900'
}
>
{loadingCollections
? 'Loading'
: field.value?.name
? collections?.find(
(collection: { name: string }) =>
collection.name === field.value?.name
)?.name || form.getValues('collection')?.name
: 'Select a collection...'}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
render={({ field }) => (
<FormItem className={`my-2`}>
<FormLabel>Collection</FormLabel>
<div className="min-w-full inset-x-0">
<Popover
open={openCollections}
onOpenChange={setOpenCollections}
>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
aria-expanded={openCollections}
className={
'w-full justify-between bg-neutral-100 dark:bg-neutral-900'
}
>
{loadingCollections
? 'Unorganized'
: field.value?.name
? collections?.find(
(collection: { name: string }) =>
collection.name === field.value?.name
)?.name || form.getValues('collection')?.name
: 'Select a collection...'}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
{!openOptions && openCollections ? (
<div
className={`fade-up min-w-full p-0 overflow-y-auto ${
openCollections
? 'fixed inset-0 w-full h-full z-50 bg-white'
: ''
}`}
{!openOptions && openCollections ? (
<div
className={`fade-up min-w-full p-0 overflow-y-auto ${
openCollections
? 'fixed inset-0 w-full h-full z-50 bg-white'
: ''
}`}
>
<Button
className="absolute top-1 right-1 bg-transparent hover:bg-transparent hover:opacity-50 transition-colors ease-in-out duration-200"
onClick={() => setOpenCollections(false)}
>
<Button
className="absolute top-1 right-1 bg-transparent hover:bg-transparent hover:opacity-50 transition-colors ease-in-out duration-200"
onClick={() => setOpenCollections(false)}
>
<X
className={`h-4 w-4 text-black dark:text-white`}
/>
</Button>
<Command className="flex-grow min-w-full dropdown-content rounded-none">
<CommandInput
className="min-w-[280px]"
placeholder="Search Collection..."
autoFocus={true}
/>
<CommandEmpty>No Collection found.</CommandEmpty>
{Array.isArray(collections) && (
<CommandGroup className="w-full overflow-y-auto">
{isLoading ? (
<CommandItem
value="Getting collections..."
key="Getting collections..."
onSelect={() => {
form.setValue('collection', {
name: 'Unorganized',
});
setOpenCollections(false);
}}
>
Unorganized
</CommandItem>
) : (
collections?.map(
(collection: {
name: string;
id: number;
ownerId: number;
pathname: string;
}) => (
<CommandItem
value={collection.name}
key={collection.id}
className="cursor-pointer flex flex-col items-start justify-start"
onSelect={() => {
form.setValue('collection', {
ownerId: collection.ownerId,
id: collection.id,
name: collection.name,
});
setOpenCollections(false);
}}
>
<p>{collection.name}</p>
<p className="text-xs text-neutral-500">
{collection.pathname}
</p>
</CommandItem>
<X className={`h-4 w-4 text-black dark:text-white`} />
</Button>
<Command className="flex-grow min-w-full dropdown-content rounded-none">
<CommandInput
className="min-w-[280px]"
placeholder="Search Collection..."
/>
{loadingCollections ? (
<p className="w-full text-center my-auto">
Loading...
</p>
) : (
<>
<CommandEmpty>No Collection found.</CommandEmpty>
{Array.isArray(collections) && (
<CommandGroup className="w-full overflow-y-auto">
{isLoading ? (
<CommandItem
value="Loading collections..."
key="Loading collections..."
onSelect={() => {
form.setValue('collection', {
name: 'Unorganized',
});
setOpenCollections(false);
}}
>
Unorganized
</CommandItem>
) : (
collections?.map(
(collection: {
name: string;
id: number;
ownerId: number;
pathname: string;
}) => (
<CommandItem
value={collection.name}
key={collection.id}
className="cursor-pointer flex flex-col items-start justify-start"
onSelect={() => {
form.setValue('collection', {
ownerId: collection.ownerId,
id: collection.id,
name: collection.name,
});
setOpenCollections(false);
}}
>
<p>{collection.name}</p>
<p className="text-xs text-neutral-500">
{collection.pathname}
</p>
</CommandItem>
)
)
)}
</CommandGroup>
)}
</>
)}
</Command>
</div>
) : openOptions && openCollections ? (
<PopoverContent
className={`min-w-full p-0 overflow-y-auto max-h-[200px]`}
>
<Command className="flex-grow min-w-full dropdown-content">
<CommandInput
className="min-w-[280px]"
placeholder="Search collection..."
/>
<CommandEmpty>No Collection found.</CommandEmpty>
{Array.isArray(collections) && (
<CommandGroup className="w-full">
{isLoading ? (
<CommandItem
value="Loading collections..."
key="Loading collections..."
onSelect={() => {
form.setValue('collection', {
name: 'Unorganized',
});
setOpenCollections(false);
}}
>
Unorganized
</CommandItem>
) : (
collections?.map(
(collection: {
name: string;
id: number;
ownerId: number;
pathname: string;
}) => (
<CommandItem
value={collection.name}
key={collection.id}
className="cursor-pointer flex flex-col items-start justify-start"
onSelect={() => {
form.setValue('collection', {
ownerId: collection.ownerId,
id: collection.id,
name: collection.name,
});
setOpenCollections(false);
}}
>
<p>{collection.name}</p>
<p className="text-xs text-neutral-500">
{collection.pathname}
</p>
</CommandItem>
)
)}
</CommandGroup>
)}
</Command>
</div>
) : openOptions && openCollections ? (
<PopoverContent
className={`min-w-full p-0 overflow-y-auto max-h-[200px]`}
>
<Command className="flex-grow min-w-full dropdown-content">
<CommandInput
className="min-w-[280px]"
placeholder="Search collection..."
/>
<CommandEmpty>No Collection found.</CommandEmpty>
{Array.isArray(collections) && (
<CommandGroup className="w-full">
{isLoading ? (
<CommandItem
value="Getting collections..."
key="Getting collections..."
onSelect={() => {
form.setValue('collection', {
name: 'Unorganized',
});
setOpenCollections(false);
}}
>
Unorganized
</CommandItem>
) : (
collections?.map(
(collection: {
name: string;
id: number;
ownerId: number;
pathname: string;
}) => (
<CommandItem
value={collection.name}
key={collection.id}
className="cursor-pointer flex flex-col items-start justify-start"
onSelect={() => {
form.setValue('collection', {
ownerId: collection.ownerId,
id: collection.id,
name: collection.name,
});
setOpenCollections(false);
}}
>
<p>{collection.name}</p>
<p className="text-xs text-neutral-500">
{collection.pathname}
</p>
</CommandItem>
)
)
)}
</CommandGroup>
)}
</Command>
</PopoverContent>
) : undefined}
</Popover>
</div>
<FormMessage />
</FormItem>
) : (
<></>
)
}
)
)}
</CommandGroup>
)}
</Command>
</PopoverContent>
) : undefined}
</Popover>
</div>
<FormMessage />
</FormItem>
)}
/>
{openOptions && savedLwItem === false && (
<div className="details list-none space-y-5 pt-2">
{!openOptions && (
<Label className="flex items-center gap-2 w-fit cursor-pointer">
<Checkbox
checked={uploadImage}
onCheckedChange={handleCheckedChange}
/>
Upload image from browser
</Label>
)}
{openOptions && (
<>
{tagsError ? <p>There was an error...</p> : null}
<FormField
control={control}
@@ -405,8 +425,8 @@ const BookmarkForm = () => {
{loadingTags ? (
<TagInput
onChange={field.onChange}
value={[{ name: 'Getting tags...' }]}
tags={[{ id: 1, name: 'Getting tags...' }]}
value={[{ name: 'Loading tags...' }]}
tags={[{ id: 1, name: 'Loading tags...' }]}
/>
) : tagsError ? (
<TagInput
@@ -445,64 +465,62 @@ const BookmarkForm = () => {
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea placeholder="Description..." {...field} />
<Textarea
placeholder="Description..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Label className="flex items-center gap-2 w-fit cursor-pointer">
<Checkbox
checked={uploadImage}
onCheckedChange={handleCheckedChange}
/>
Upload image from browser
</Label>
</div>
{openOptions && (
<Label className="flex items-center gap-2 w-fit cursor-pointer">
<Checkbox
checked={uploadImage}
onCheckedChange={handleCheckedChange}
/>
Upload image from browser
</Label>
)}
</>
)}
{savedLwItem === false ? (
<div className="flex justify-between items-center mt-6">
<div
className="inline-flex select-none items-center justify-center rounded-md text-sm font-medium ring-offset-background
transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50
hover:bg-accent hover:text-accent-foreground hover:cursor-pointer p-2"
onClick={() => setOpenOptions((prevState) => !prevState)}
<div className="flex justify-between items-center">
<Button
variant="ghost"
type="button"
onClick={() => setOpenOptions((prevState) => !prevState)}
>
{openOptions ? 'Hide' : 'More'} Options
</Button>
<Button disabled={isLoading} type="submit">
Save
</Button>
</div>
{isDuplicate && (
<div className="w-fit ml-auto">
<a
className="text-muted text-xs font-bold text-zinc-600 dark:text-zinc-400 hover:underline cursor-pointer"
onClick={(e) => {
e.preventDefault();
window.open(
config?.baseUrl +
'/search?q=' +
encodeURIComponent(`url:${tabInfo?.url}`),
'_blank'
);
window.close();
}}
>
{openOptions ? 'Hide' : 'More'} Options
</div>
<Button disabled={isLoading} type="submit">
Save
</Button>
Note: You've already saved this link{' '}
<ExternalLink size={16} className="inline-block mb-1" />
</a>
</div>
) : (
<></>
)}
{savedLwItem ? (
<div>
<p className="text-muted text-zinc-600 dark:text-zinc-400 mt-4">
You already have this link saved.
</p>
<div className="flex justify-end mt-6">
<Button
type="button"
onClick={(e) => {
e.preventDefault();
window.open(
baseUrl +
'/search?q=' +
encodeURIComponent(savedLwItem.url),
'_blank'
);
window.close();
}}
>
Show in Linkwarden
</Button>
</div>
</div>
) : (
<></>
)}
</form>
</Form>
+19 -7
View File
@@ -2,10 +2,12 @@ import { FC } from 'react';
import { openOptions } from '../lib/utils.ts';
import { Button } from './ui/Button.tsx';
const NotConfigured: FC = () => {
const NotConfigured: FC<{ open: boolean }> = ({ open }) => {
if (!open) return null;
return (
<div className="flex flex-col w-[350px] h-full overflow-y-hidden items-center gap-10 py-10">
<div className="flex flex-row gap-4">
<div className="fixed top-0 bottom-0 left-0 right-0 inset-0 bg-white z-10">
<div className="container flex flex-col gap-3 justify-center items-center h-full max-w-lg mx-auto">
<img
src="./128.png"
height="40px"
@@ -13,13 +15,23 @@ const NotConfigured: FC = () => {
className="rounded"
alt="Linkwarden Logo"
/>
<h1 className="font-medium" style={{ fontSize: '1.65rem' }}>
<h1
className="font-medium text-lg text-zinc-700"
style={{ fontSize: '1.65rem' }}
>
Initial Setup
</h1>
<div className="flex justify-center items-center">
<Button
onClick={() => openOptions()}
className="w-40"
variant="outline"
>
Configure
</Button>
</div>
</div>
<Button onClick={() => openOptions()} className="w-37.5">
Configure
</Button>
</div>
);
};
+17 -17
View File
@@ -1,7 +1,7 @@
import captureScreenshot from '../screenshot.ts';
import { bookmarkFormValues } from '../validators/bookmarkForm.ts';
import axios from 'axios';
import { bookmarkMetadata } from '../cache.ts';
// import { bookmarkMetadata } from '../cache.ts';
import { getCurrentTabInfo } from '../utils.ts';
export async function postLink(
@@ -100,18 +100,18 @@ export async function deleteLinkFetch(
});
}
export async function getLinksFetch(
baseUrl: string,
apiKey: string
): Promise<{ response: bookmarkMetadata[] }> {
const url = `${baseUrl}/api/v1/links`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
return await response.json();
}
// export async function getLinksFetch(
// baseUrl: string,
// apiKey: string
// ): Promise<{ response: bookmarkMetadata[] }> {
// const url = `${baseUrl}/api/v1/links`;
// const response = await fetch(url, {
// headers: {
// Authorization: `Bearer ${apiKey}`,
// },
// });
// return await response.json();
// }
export async function checkLinkExists(
baseUrl: string,
@@ -124,8 +124,8 @@ export async function checkLinkExists(
}
const url =
`${baseUrl}/api/v1/links?cursor=0&sort=0&searchQueryString=` +
encodeURIComponent(`${tabInfo.url}`);
`${baseUrl}/api/v1/search?sort=0&searchQueryString=` +
encodeURIComponent(`url:${tabInfo.url}`);
const response = await fetch(url, {
headers: {
@@ -133,9 +133,9 @@ export async function checkLinkExists(
},
});
const data = await response.json();
const { data } = await response.json();
const exists = data.response.length > 0;
const exists = !!data && data.links?.length > 0;
return exists;
}
+96 -64
View File
@@ -1,6 +1,10 @@
import { getBrowser, getStorageItem, setStorageItem } from './utils.ts';
import { bookmarkFormValues } from './validators/bookmarkForm.ts';
import { deleteLinkFetch, getLinksFetch, postLinkFetch, updateLinkFetch } from './actions/links.ts';
import {
deleteLinkFetch,
postLinkFetch,
updateLinkFetch,
} from './actions/links.ts';
import BookmarkTreeNode = chrome.bookmarks.BookmarkTreeNode;
import { getConfig } from './config.ts';
@@ -26,32 +30,52 @@ export async function getBookmarksMetadata(): Promise<bookmarkMetadata[]> {
return bookmarksMetadata ? JSON.parse(bookmarksMetadata) : DEFAULTS;
}
export async function saveBookmarksMetadata(bookmarksMetadata: bookmarkMetadata[]) {
return await setStorageItem(BOOKMARKS_METADATA_KEY, JSON.stringify(bookmarksMetadata));
export async function saveBookmarksMetadata(
bookmarksMetadata: bookmarkMetadata[]
) {
return await setStorageItem(
BOOKMARKS_METADATA_KEY,
JSON.stringify(bookmarksMetadata)
);
}
export async function clearBookmarksMetadata() {
return await setStorageItem(BOOKMARKS_METADATA_KEY, JSON.stringify([]));
}
export async function getBookmarkMetadataById(id: number): Promise<bookmarkMetadata | undefined> {
export async function getBookmarkMetadataById(
id: number
): Promise<bookmarkMetadata | undefined> {
const bookmarksMetadata = await getBookmarksMetadata();
return bookmarksMetadata.find((bookmarkMetadata) => bookmarkMetadata.id === id);
return bookmarksMetadata.find(
(bookmarkMetadata) => bookmarkMetadata.id === id
);
}
export async function getBookmarkMetadataByBookmarkId(bookmarkId: string): Promise<bookmarkMetadata | undefined> {
export async function getBookmarkMetadataByBookmarkId(
bookmarkId: string
): Promise<bookmarkMetadata | undefined> {
const bookmarksMetadata = await getBookmarksMetadata();
return bookmarksMetadata.find((bookmarkMetadata) => bookmarkMetadata.bookmarkId === bookmarkId);
return bookmarksMetadata.find(
(bookmarkMetadata) => bookmarkMetadata.bookmarkId === bookmarkId
);
}
export async function getBookmarkMetadataByUrl(url: string): Promise<bookmarkMetadata | undefined> {
export async function getBookmarkMetadataByUrl(
url: string
): Promise<bookmarkMetadata | undefined> {
const bookmarksMetadata = await getBookmarksMetadata();
return bookmarksMetadata.find((bookmarkMetadata) => bookmarkMetadata.url === url);
return bookmarksMetadata.find(
(bookmarkMetadata) => bookmarkMetadata.url === url
);
}
export async function saveBookmarkMetadata(bookmarkMetadata: bookmarkMetadata) {
const bookmarksMetadata = await getBookmarksMetadata();
const index = bookmarksMetadata.findIndex((bookmarkMetadataObject) => bookmarkMetadataObject.id === bookmarkMetadata.id);
const index = bookmarksMetadata.findIndex(
(bookmarkMetadataObject) =>
bookmarkMetadataObject.id === bookmarkMetadata.id
);
if (index !== -1) {
bookmarksMetadata[index] = bookmarkMetadata;
} else {
@@ -62,7 +86,9 @@ export async function saveBookmarkMetadata(bookmarkMetadata: bookmarkMetadata) {
export async function deleteBookmarkMetadata(id: string | undefined) {
const bookmarksMetadata = await getBookmarksMetadata();
const index = bookmarksMetadata.findIndex((bookmarkMetadata) => bookmarkMetadata.bookmarkId === id);
const index = bookmarksMetadata.findIndex(
(bookmarkMetadata) => bookmarkMetadata.bookmarkId === id
);
if (index !== -1) {
bookmarksMetadata.splice(index, 1);
}
@@ -70,55 +96,57 @@ export async function deleteBookmarkMetadata(id: string | undefined) {
}
// It just works, don't MOVE
export async function saveLinksInCache(baseUrl: string) {
try {
const { apiKey } = await getConfig();
const links = await getLinksFetch(baseUrl, apiKey);
const linksResponse = links.response;
// export async function saveLinksInCache(baseUrl: string) {
// try {
// const { apiKey } = await getConfig();
// const links = await getLinksFetch(baseUrl, apiKey);
// const linksResponse = links.response;
// Create a map to track which bookmarks are still present on the server
const serverBookmarkMap = new Map<number, bookmarkMetadata>();
linksResponse.forEach(link => serverBookmarkMap.set(link.id, link));
// // Create a map to track which bookmarks are still present on the server
// const serverBookmarkMap = new Map<number, bookmarkMetadata>();
// linksResponse.forEach(link => serverBookmarkMap.set(link.id, link));
// Get the current bookmarks metadata from the cache
const bookmarksMetadata = await getBookmarksMetadata();
// // Get the current bookmarks metadata from the cache
// const bookmarksMetadata = await getBookmarksMetadata();
// Update or add bookmarks based on server response
for (let link of linksResponse) {
const existingLinkIndex = bookmarksMetadata.findIndex((bookmarkMetadata) => bookmarkMetadata.id === link.id);
if (existingLinkIndex !== -1) {
// Update existing bookmark if there are changes
link = { ...bookmarksMetadata[existingLinkIndex], ...link };
bookmarksMetadata[existingLinkIndex] = link;
} else {
// Add new bookmark from the server
const newLocalBookmark = await createBookmarkInBrowser(link);
link.bookmarkId = newLocalBookmark.id;
bookmarksMetadata.push(link);
}
}
// // Update or add bookmarks based on server response
// for (let link of linksResponse) {
// const existingLinkIndex = bookmarksMetadata.findIndex((bookmarkMetadata) => bookmarkMetadata.id === link.id);
// if (existingLinkIndex !== -1) {
// // Update existing bookmark if there are changes
// link = { ...bookmarksMetadata[existingLinkIndex], ...link };
// bookmarksMetadata[existingLinkIndex] = link;
// } else {
// // Add new bookmark from the server
// const newLocalBookmark = await createBookmarkInBrowser(link);
// link.bookmarkId = newLocalBookmark.id;
// bookmarksMetadata.push(link);
// }
// }
// Remove cached bookmarks that are no longer present on the server
const bookmarksToRemove = bookmarksMetadata.filter((bookmarkMetadata) => !serverBookmarkMap.has(bookmarkMetadata.id));
for (const bookmarkToRemove of bookmarksToRemove) {
const indexToRemove = bookmarksMetadata.indexOf(bookmarkToRemove);
if (indexToRemove !== -1) {
bookmarksMetadata.splice(indexToRemove, 1);
if (bookmarkToRemove.bookmarkId != null) {
await browser.bookmarks.remove(bookmarkToRemove.bookmarkId);
}
}
}
// // Remove cached bookmarks that are no longer present on the server
// const bookmarksToRemove = bookmarksMetadata.filter((bookmarkMetadata) => !serverBookmarkMap.has(bookmarkMetadata.id));
// for (const bookmarkToRemove of bookmarksToRemove) {
// const indexToRemove = bookmarksMetadata.indexOf(bookmarkToRemove);
// if (indexToRemove !== -1) {
// bookmarksMetadata.splice(indexToRemove, 1);
// if (bookmarkToRemove.bookmarkId != null) {
// await browser.bookmarks.remove(bookmarkToRemove.bookmarkId);
// }
// }
// }
// Save the updated bookmarks metadata back to the cache
await saveBookmarksMetadata(bookmarksMetadata);
// // Save the updated bookmarks metadata back to the cache
// await saveBookmarksMetadata(bookmarksMetadata);
} catch (error) {
console.error(error);
}
}
// } catch (error) {
// console.error(error);
// }
// }
export async function createBookmarkInBrowser(bookmark: bookmarkMetadata): Promise<BookmarkTreeNode> {
export async function createBookmarkInBrowser(
bookmark: bookmarkMetadata
): Promise<BookmarkTreeNode> {
const { url, name } = bookmark;
return await browser.bookmarks.create({ url, title: name });
}
@@ -141,7 +169,9 @@ export async function syncLocalBookmarks(baseUrl: string) {
const bookmarksMetadata = await getBookmarksMetadata();
// Create a map of cached bookmarks by URL for easy lookup
const cachedBookmarksMap = new Map(bookmarksMetadata.map(bm => [bm.url, bm]));
const cachedBookmarksMap = new Map(
bookmarksMetadata.map((bm) => [bm.url, bm])
);
// Prepare arrays to hold promises for new, updated, and deleted bookmarks
const createPromises = [];
@@ -156,7 +186,9 @@ export async function syncLocalBookmarks(baseUrl: string) {
createPromises.push(postLinkFetch(baseUrl, localBookmark, apiKey));
} else if (cachedBookmark.name !== localBookmark.name) {
// Updated bookmark
updatePromises.push(updateLinkFetch(baseUrl, cachedBookmark.id, localBookmark, apiKey));
updatePromises.push(
updateLinkFetch(baseUrl, cachedBookmark.id, localBookmark, apiKey)
);
}
// Remove from the map to track deleted bookmarks
cachedBookmarksMap.delete(localBookmark.url);
@@ -168,7 +200,11 @@ export async function syncLocalBookmarks(baseUrl: string) {
}
// Run all create, update, and delete operations in parallel
await Promise.all([...createPromises, ...updatePromises, ...deletePromises]);
await Promise.all([
...createPromises,
...updatePromises,
...deletePromises,
]);
// Update the cached bookmarks metadata
await saveBookmarksMetadata(localBookmarks);
@@ -178,7 +214,10 @@ export async function syncLocalBookmarks(baseUrl: string) {
}
// Helper function to collect all bookmarks recursively
function logBookmarks(bookmarks: BookmarkTreeNode[], accumulator: bookmarkMetadata[]) {
function logBookmarks(
bookmarks: BookmarkTreeNode[],
accumulator: bookmarkMetadata[]
) {
for (const bookmark of bookmarks) {
if (bookmark.url) {
accumulator.push({
@@ -196,10 +235,3 @@ function logBookmarks(bookmarks: BookmarkTreeNode[], accumulator: bookmarkMetada
}
}
}
+1 -10
View File
@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { getLinksFetch, checkLinkExists } from './actions/links.ts';
import { checkLinkExists } from './actions/links.ts';
import { getConfig } from './config.ts';
export function cn(...inputs: ClassValue[]) {
@@ -44,14 +44,6 @@ export async function getStorageItem(key: string) {
}
}
export const getCurrentLinkItem = async () => {
const config = await getConfig();
const currentTab = await getCurrentTabInfo();
const { response } = await getLinksFetch(config.baseUrl, config.apiKey);
const itemInfo = response.find((link) => link.url === currentTab.url);
return itemInfo || false;
};
export async function setStorageItem(key: string, value: string) {
if (getChromeStorage()) {
return await chrome.storage.local.set({ [key]: value });
@@ -69,7 +61,6 @@ export async function updateBadge(tabId: number | undefined) {
if (!tabId) return;
const browser = getBrowser();
const cachedConfig = await getConfig();
const linkExists = await checkLinkExists(
cachedConfig.baseUrl,
+38 -38
View File
@@ -6,6 +6,8 @@ import { useEffect, useState } from 'react';
import { getConfig, isConfigured } from '../../@/lib/config.ts';
import NotConfigured from '../../@/components/NotConfigured.tsx';
import { ModeToggle } from '../../@/components/ModeToggle.tsx';
import { Button } from '@/@/components/ui/Button.tsx';
import { Settings } from 'lucide-react';
function App() {
const [isAllConfigured, setIsAllConfigured] = useState<boolean>();
@@ -15,6 +17,7 @@ function App() {
(async () => {
const cachedOptions = await isConfigured();
const cachedConfig = await getConfig();
setBaseUrl(cachedConfig.baseUrl);
setIsAllConfigured(cachedOptions);
})();
@@ -22,44 +25,41 @@ function App() {
return (
<WholeContainer>
{
isAllConfigured ? (
<Container>
<div className="flex justify-between w-full items-center">
<div className="flex space-x-2 w-full items-center">
<a
href={baseUrl}
rel="noopener"
target="_blank"
referrerPolicy="no-referrer"
className="hover:opacity-80 duration-200 rounded ease-in-out"
>
<img
src="./128.png"
height="30px"
width="30px"
className="rounded"
alt="Linkwarden Logo"
/>
</a>
<h1 className="text-lg">Add Link</h1>
</div>
<div className="flex items-center justify-center space-x-2">
<ModeToggle />
<p
className="text-blue-500 text-xs cursor-pointer hover:opacity-80 duration-200 ease-in-out w-fit"
onClick={openOptions}
>
Config
</p>
</div>
</div>
<BookmarkForm />
</Container>
) : (
<NotConfigured />
)
}
<Container>
<div className="flex justify-between w-full items-center">
<div className="flex space-x-2 w-full items-center">
<a
href={baseUrl}
rel="noopener"
target="_blank"
referrerPolicy="no-referrer"
className="hover:opacity-80 duration-200 rounded ease-in-out"
>
<img
src="./128.png"
height="30px"
width="30px"
className="rounded"
alt="Linkwarden Logo"
/>
</a>
<h1 className="text-lg">Add Link</h1>
</div>
<div className="flex items-center justify-center space-x-2">
<ModeToggle />
<Button
variant="ghost"
size="icon"
className="ring-0 focus:ring-0 outline-none focus:outline-none ring-offset-0 focus:ring-offset-0 focus-visible:ring-offset-0 focus-visible:ring-0 focus-visible:outline-none"
onClick={openOptions}
>
<Settings className="h-[1.2rem] w-[1.2rem] transition-colors" />
</Button>
</div>
</div>
<BookmarkForm />
<NotConfigured open={!isAllConfigured} />
</Container>
</WholeContainer>
);
}