This commit is contained in:
daniel31x13
2025-12-29 16:11:29 -05:00
parent f20c9b3958
commit afb1d76cd6
6 changed files with 269 additions and 301 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Linkwarden",
"description": "The browser extension for Linkwarden.",
"homepage_url": "https://linkwarden.app/",
"version": "1.4.0",
"version": "1.4.1",
"action": {
"default_popup": "./index.html",
"default_icon": {
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Linkwarden",
"description": "The browser extension for Linkwarden.",
"homepage_url": "https://linkwarden.app/",
"version": "1.4.0",
"version": "1.4.1",
"browser_action": {
"default_popup": "./index.html",
"default_icon": {
+182 -225
View File
@@ -17,7 +17,7 @@ import { Button } from './ui/Button.tsx';
import { TagInput } from './TagInput.tsx';
import { Textarea } from './ui/Textarea.tsx';
import {
getCurrentLinkItem,
checkDuplicatedItem,
getCurrentTabInfo,
updateBadge,
} from '../lib/utils.ts';
@@ -40,16 +40,13 @@ import {
CommandInput,
CommandItem,
} from './ui/Command.tsx';
import { bookmarkMetadata, saveLinksInCache } from '../lib/cache.ts';
import { saveLinksInCache } from '../lib/cache.ts';
import { Checkbox } from './ui/CheckBox.tsx';
import { Label } from './ui/Label.tsx';
let configured = false;
let baseUrl = '';
let duplicated = false;
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);
@@ -110,9 +107,6 @@ const BookmarkForm = () => {
return;
},
onSuccess: () => {
getCurrentLinkItem().then((item) => {
setSavedLwItem(item);
});
// Update badge to show link is saved
getCurrentTabInfo().then(({ id }) => {
updateBadge(id);
@@ -143,7 +137,7 @@ const BookmarkForm = () => {
});
const getConfigUse = async () => {
configured = await isConfigured();
setSavedLwItem(await getCurrentLinkItem());
duplicated = await checkDuplicatedItem();
};
getConfigUse();
}, [form]);
@@ -151,12 +145,7 @@ const BookmarkForm = () => {
useEffect(() => {
const syncBookmarks = async () => {
try {
const {
syncBookmarks,
baseUrl: configBaseUrl,
defaultCollection,
} = await getConfig();
baseUrl = configBaseUrl;
const { syncBookmarks, baseUrl, defaultCollection } = await getConfig();
form.setValue('collection', {
name: defaultCollection,
});
@@ -222,178 +211,171 @@ 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
? '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>
{!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..."
/>
<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>
)
)}
</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>
</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 && (
{openOptions && (
<div className="details list-none space-y-5 pt-2">
{tagsError ? <p>There was an error...</p> : null}
<FormField
@@ -460,50 +442,25 @@ const BookmarkForm = () => {
</Label>
</div>
)}
{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)}
>
{openOptions ? 'Hide' : 'More'} Options
</div>
<Button disabled={isLoading} type="submit">
Save
</Button>
</div>
) : (
<></>
{duplicated && (
<p className="text-muted text-zinc-600 dark:text-zinc-400 mt-2">
You already have this link saved.
</p>
)}
{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 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"
onClick={() => setOpenOptions((prevState) => !prevState)}
>
{openOptions ? 'Hide' : 'More'} Options
</div>
) : (
<></>
)}
<Button disabled={isLoading} type="submit">
Save
</Button>
</div>
</form>
</Form>
<Toaster />
+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>
);
};
+32 -29
View File
@@ -1,6 +1,9 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { getLinksFetch, checkLinkExists } from './actions/links.ts';
import {
getLinksFetch,
// checkLinkExists
} from './actions/links.ts';
import { getConfig } from './config.ts';
export function cn(...inputs: ClassValue[]) {
@@ -44,12 +47,12 @@ export async function getStorageItem(key: string) {
}
}
export const getCurrentLinkItem = async () => {
export const checkDuplicatedItem = 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;
const formatLinks = response.map((link) => link.url);
return formatLinks.includes(currentTab.url ?? '');
};
export async function setStorageItem(key: string, value: string) {
@@ -68,29 +71,29 @@ export function openOptions() {
export async function updateBadge(tabId: number | undefined) {
if (!tabId) return;
const browser = getBrowser();
const cachedConfig = await getConfig();
const linkExists = await checkLinkExists(
cachedConfig.baseUrl,
cachedConfig.apiKey
);
if (linkExists) {
if (browser.action) {
browser.action.setBadgeText({ tabId, text: '✓' });
browser.action.setBadgeBackgroundColor({ tabId, color: '#98c0ff' });
} else {
browser.browserAction.setBadgeText({ tabId, text: '✓' });
browser.browserAction.setBadgeBackgroundColor({
tabId,
color: '#98c0ff',
});
}
} else {
if (browser.action) {
browser.action.setBadgeText({ tabId, text: '' });
} else {
browser.browserAction.setBadgeText({ tabId, text: '' });
}
}
// TODO: add url check endpoint for precise matching (instead of fuzzy search)
// const browser = getBrowser();
// const cachedConfig = await getConfig();
// const linkExists = await checkLinkExists(
// cachedConfig.baseUrl,
// cachedConfig.apiKey
// );
// if (linkExists) {
// if (browser.action) {
// browser.action.setBadgeText({ tabId, text: '✓' });
// browser.action.setBadgeBackgroundColor({ tabId, color: '#98c0ff' });
// } else {
// browser.browserAction.setBadgeText({ tabId, text: '✓' });
// browser.browserAction.setBadgeBackgroundColor({
// tabId,
// color: '#98c0ff',
// });
// }
// } else {
// if (browser.action) {
// browser.action.setBadgeText({ tabId, text: '' });
// } else {
// browser.browserAction.setBadgeText({ tabId, text: '' });
// }
// }
}
+34 -38
View File
@@ -15,6 +15,7 @@ function App() {
(async () => {
const cachedOptions = await isConfigured();
const cachedConfig = await getConfig();
setBaseUrl(cachedConfig.baseUrl);
setIsAllConfigured(cachedOptions);
})();
@@ -22,44 +23,39 @@ 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 />
<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 />
<NotConfigured open={!isAllConfigured} />
</Container>
</WholeContainer>
);
}