This commit is contained in:
daniel31x13
2026-01-04 16:44:30 -05:00
parent 5063478efb
commit 62c3f35b8f
12 changed files with 182 additions and 199 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.1",
"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"
}
}
}
+15 -18
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.1",
"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

+110 -48
View File
@@ -26,7 +26,7 @@ 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 {
@@ -48,6 +48,18 @@ const BookmarkForm = () => {
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);
@@ -70,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;
@@ -118,28 +128,32 @@ const BookmarkForm = () => {
},
});
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 : '');
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: config.defaultCollection,
name: c.defaultCollection,
});
});
});
const getConfigUse = async () => {
const config = await getConfig();
const configured = await getIsConfigured();
const duplicate = await checkLinkExists(config.baseUrl, config.apiKey);
const duplicate = await checkLinkExists(c.baseUrl, c.apiKey);
setIsDuplicate(duplicate);
setIsConfigured(configured);
};
getConfigUse();
}, [form]);
setTabInformation();
}, []);
const { handleSubmit, control } = form;
// useEffect(() => {
// const syncBookmarks = async () => {
@@ -169,9 +183,10 @@ 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);
@@ -187,9 +202,10 @@ 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);
@@ -201,7 +217,10 @@ const BookmarkForm = () => {
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.
@@ -229,7 +248,7 @@ const BookmarkForm = () => {
}
>
{loadingCollections
? 'Loading'
? 'Unorganized'
: field.value?.name
? collections?.find(
(collection: { name: string }) =>
@@ -260,13 +279,20 @@ const BookmarkForm = () => {
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="Getting collections..."
key="Getting collections..."
value="Loading collections..."
key="Loading collections..."
onSelect={() => {
form.setValue('collection', {
name: 'Unorganized',
@@ -307,6 +333,8 @@ const BookmarkForm = () => {
)}
</CommandGroup>
)}
</>
)}
</Command>
</div>
) : openOptions && openCollections ? (
@@ -323,8 +351,8 @@ const BookmarkForm = () => {
<CommandGroup className="w-full">
{isLoading ? (
<CommandItem
value="Getting collections..."
key="Getting collections..."
value="Loading collections..."
key="Loading collections..."
onSelect={() => {
form.setValue('collection', {
name: 'Unorganized',
@@ -374,8 +402,19 @@ const BookmarkForm = () => {
</FormItem>
)}
/>
{!openOptions && (
<Label className="flex items-center gap-2 w-fit cursor-pointer">
<Checkbox
checked={uploadImage}
onCheckedChange={handleCheckedChange}
/>
Upload image from browser
</Label>
)}
{openOptions && (
<div className="details list-none space-y-5 pt-2">
<>
{tagsError ? <p>There was an error...</p> : null}
<FormField
control={control}
@@ -386,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
@@ -426,12 +465,18 @@ const BookmarkForm = () => {
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea placeholder="Description..." {...field} />
<Textarea
placeholder="Description..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{openOptions && (
<Label className="flex items-center gap-2 w-fit cursor-pointer">
<Checkbox
checked={uploadImage}
@@ -439,27 +484,44 @@ const BookmarkForm = () => {
/>
Upload image from browser
</Label>
</div>
)}
{isDuplicate && (
<p className="text-muted text-zinc-600 dark:text-zinc-400 mt-2">
You already saved this link.
</p>
</>
)}
<div className="flex justify-between items-center mt-4">
<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"
<div className="flex justify-between items-center">
<Button
variant="ghost"
type="button"
onClick={() => setOpenOptions((prevState) => !prevState)}
>
{openOptions ? 'Hide' : 'More'} Options
</div>
</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();
}}
>
Note: You've already saved this link{' '}
<ExternalLink size={16} className="inline-block mb-1" />
</a>
</div>
)}
</form>
</Form>
<Toaster />
-1
View File
@@ -60,7 +60,6 @@ export function openOptions() {
export async function updateBadge(tabId: number | undefined) {
if (!tabId) return;
// TODO: add url check endpoint for precise matching (instead of fuzzy search)
const browser = getBrowser();
const cachedConfig = await getConfig();
const linkExists = await checkLinkExists(
+8 -4
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>();
@@ -45,12 +47,14 @@ function App() {
</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"
<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}
>
Config
</p>
<Settings className="h-[1.2rem] w-[1.2rem] transition-colors" />
</Button>
</div>
</div>
<BookmarkForm />