mirror of
https://github.com/linkwarden/browser-extension.git
synced 2026-06-23 04:10:26 +00:00
Generated
+1370
-703
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -55,7 +55,7 @@
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5",
|
||||
"vite": "^7.2.0",
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Input } from './ui/Input.tsx';
|
||||
import { Button } from './ui/Button.tsx';
|
||||
import { TagInput } from './TagInput.tsx';
|
||||
import { Textarea } from './ui/Textarea.tsx';
|
||||
import { checkDuplicatedItem, getCurrentTabInfo } from '../lib/utils.ts';
|
||||
import { getCurrentLinkItem, 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';
|
||||
@@ -36,13 +36,14 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from './ui/Command.tsx';
|
||||
import { saveLinksInCache } from '../lib/cache.ts';
|
||||
import { bookmarkMetadata, saveLinksInCache } from '../lib/cache.ts';
|
||||
import { Checkbox } from './ui/CheckBox.tsx';
|
||||
import { Label } from './ui/Label.tsx';
|
||||
|
||||
let configured = false;
|
||||
let duplicated = 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);
|
||||
@@ -103,6 +104,13 @@ const BookmarkForm = () => {
|
||||
return;
|
||||
},
|
||||
onSuccess: () => {
|
||||
getCurrentLinkItem().then((item) => {
|
||||
setSavedLwItem(item);
|
||||
});
|
||||
// Update badge to show link is saved
|
||||
getCurrentTabInfo().then(({ id }) => {
|
||||
updateBadge(id);
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
// I want to show some confirmation before it's closed...
|
||||
@@ -117,7 +125,8 @@ const BookmarkForm = () => {
|
||||
const { handleSubmit, control } = form;
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentTabInfo().then(({ url, title }) => {
|
||||
getCurrentTabInfo().then(({ id, url, title }) => {
|
||||
updateBadge(id);
|
||||
getConfig().then((config) => {
|
||||
form.setValue('url', url ? url : '');
|
||||
form.setValue('name', title ? title : '');
|
||||
@@ -128,7 +137,7 @@ const BookmarkForm = () => {
|
||||
});
|
||||
const getConfigUse = async () => {
|
||||
configured = await isConfigured();
|
||||
duplicated = await checkDuplicatedItem();
|
||||
setSavedLwItem(await getCurrentLinkItem());
|
||||
};
|
||||
getConfigUse();
|
||||
}, [form]);
|
||||
@@ -136,7 +145,8 @@ const BookmarkForm = () => {
|
||||
useEffect(() => {
|
||||
const syncBookmarks = async () => {
|
||||
try {
|
||||
const { syncBookmarks, baseUrl, defaultCollection } = await getConfig();
|
||||
const { syncBookmarks, baseUrl: configBaseUrl, defaultCollection } = await getConfig();
|
||||
baseUrl = configBaseUrl;
|
||||
form.setValue('collection', {
|
||||
name: defaultCollection,
|
||||
});
|
||||
@@ -202,7 +212,7 @@ const BookmarkForm = () => {
|
||||
<FormField
|
||||
control={control}
|
||||
name="collection"
|
||||
render={({ field }) => (
|
||||
render={({ field }) => savedLwItem === false ? (
|
||||
<FormItem className={`my-2`}>
|
||||
<FormLabel>Collection</FormLabel>
|
||||
<div className="min-w-full inset-x-0">
|
||||
@@ -332,10 +342,12 @@ const BookmarkForm = () => {
|
||||
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,
|
||||
@@ -345,7 +357,10 @@ const BookmarkForm = () => {
|
||||
setOpenCollections(false);
|
||||
}}
|
||||
>
|
||||
{collection.name}
|
||||
<p>{collection.name}</p>
|
||||
<p className="text-xs text-neutral-500">
|
||||
{collection.pathname}
|
||||
</p>
|
||||
</CommandItem>
|
||||
)
|
||||
)
|
||||
@@ -359,9 +374,9 @@ const BookmarkForm = () => {
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
) : <></>}
|
||||
/>
|
||||
{openOptions && (
|
||||
{openOptions && savedLwItem === false && (
|
||||
<div className="details list-none space-y-5 pt-2">
|
||||
{tagsError ? <p>There was an error...</p> : null}
|
||||
<FormField
|
||||
@@ -428,25 +443,36 @@ const BookmarkForm = () => {
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
{duplicated && (
|
||||
<p className="text-muted text-zinc-600 dark:text-zinc-400 mt-2">
|
||||
You already have this link saved.
|
||||
</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"
|
||||
onClick={() => setOpenOptions((prevState) => !prevState)}
|
||||
>
|
||||
{openOptions ? 'Hide' : 'More'} Options
|
||||
{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>
|
||||
<Button disabled={isLoading} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</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">
|
||||
<a href={baseUrl + '/collections/' + savedLwItem?.collectionId}>
|
||||
<Button type="button" onClick={() => setTimeout(() => window.close(), 1)}>
|
||||
Show in Collection
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : <></> }
|
||||
</form>
|
||||
</Form>
|
||||
<Toaster />
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { FC } from 'react';
|
||||
import { openOptions } from '../lib/utils.ts';
|
||||
import { Button } from './ui/Button.tsx';
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const Modal: FC<ModalProps> = ({ open }) => {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<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">
|
||||
<h2 className="text-lg text-zinc-700 ">Initial Config</h2>
|
||||
|
||||
<div className="flex justify-center items-center">
|
||||
<Button onClick={() => openOptions()} className="w-40">
|
||||
Configure
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { FC } from 'react';
|
||||
import { openOptions } from '../lib/utils.ts';
|
||||
import { Button } from './ui/Button.tsx';
|
||||
|
||||
const NotConfigured: FC = () => {
|
||||
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">
|
||||
<img
|
||||
src="./128.png"
|
||||
height="40px"
|
||||
width="40px"
|
||||
className="rounded"
|
||||
alt="Linkwarden Logo"
|
||||
/>
|
||||
<h1 className="font-medium" style={{ fontSize: "1.65rem" }}>Initial Setup</h1>
|
||||
</div>
|
||||
<Button onClick={() => openOptions()} className="w-37.5">
|
||||
Configure
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default NotConfigured;
|
||||
+43
-8
@@ -1,6 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { getLinksFetch } from './actions/links.ts';
|
||||
import { getLinksFetch, checkLinkExists } from './actions/links.ts';
|
||||
import { getConfig } from './config.ts';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
@@ -12,10 +12,17 @@ export interface TabInfo {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export async function getCurrentTabInfo(): Promise<{ title: string | undefined; url: string | undefined }> {
|
||||
const tabs = await getBrowser().tabs.query({ active: true, currentWindow: true });
|
||||
const { url, title } = tabs[0];
|
||||
return { url, title };
|
||||
export async function getCurrentTabInfo(): Promise<{
|
||||
id: number | undefined;
|
||||
title: string | undefined;
|
||||
url: string | undefined;
|
||||
}> {
|
||||
const tabs = await getBrowser().tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
const { id, url, title } = tabs[0];
|
||||
return { id, url, title };
|
||||
}
|
||||
|
||||
export function getBrowser() {
|
||||
@@ -37,12 +44,12 @@ export async function getStorageItem(key: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const checkDuplicatedItem = async () => {
|
||||
export const getCurrentLinkItem = async () => {
|
||||
const config = await getConfig();
|
||||
const currentTab = await getCurrentTabInfo();
|
||||
const { response } = await getLinksFetch(config.baseUrl, config.apiKey);
|
||||
const formatLinks = response.map((link) => link.url);
|
||||
return formatLinks.includes(currentTab.url ?? '');
|
||||
const itemInfo = response.find((link) => link.url === currentTab.url);
|
||||
return itemInfo || false;
|
||||
};
|
||||
|
||||
export async function setStorageItem(key: string, value: string) {
|
||||
@@ -57,3 +64,31 @@ export async function setStorageItem(key: string, value: string) {
|
||||
export function openOptions() {
|
||||
getBrowser().runtime.openOptionsPage();
|
||||
}
|
||||
|
||||
export async function updateBadge(tabId: number | undefined) {
|
||||
if (!tabId) return;
|
||||
|
||||
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: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { getBrowser, getCurrentTabInfo } from '../../@/lib/utils.ts';
|
||||
import {
|
||||
getBrowser,
|
||||
getCurrentTabInfo,
|
||||
updateBadge,
|
||||
} from '../../@/lib/utils.ts';
|
||||
// import BookmarkTreeNode = chrome.bookmarks.BookmarkTreeNode;
|
||||
import { getConfig, isConfigured } from '../../@/lib/config.ts';
|
||||
import {
|
||||
checkLinkExists,
|
||||
// deleteLinkFetch,
|
||||
// updateLinkFetch,
|
||||
postLinkFetch,
|
||||
@@ -272,7 +275,7 @@ async function genericOnClick(
|
||||
}
|
||||
}
|
||||
}
|
||||
browser.runtime.onInstalled.addListener(function () {
|
||||
browser.runtime.onInstalled.addListener(async function () {
|
||||
// Create one test item for each context type.
|
||||
const contexts: ContextType[] = [
|
||||
'page',
|
||||
@@ -296,38 +299,22 @@ browser.runtime.onInstalled.addListener(function () {
|
||||
title: 'Save all tabs to Linkwarden',
|
||||
contexts: ['page'],
|
||||
});
|
||||
|
||||
const { id: tabId } = await getCurrentTabInfo();
|
||||
await updateBadge(tabId);
|
||||
});
|
||||
|
||||
async function checkAndUpdateTab(tabId: number) {
|
||||
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: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for tab switches (to update icon for already-loaded tabs)
|
||||
browser.tabs.onActivated.addListener(async ({ tabId }) => {
|
||||
try {
|
||||
await checkAndUpdateTab(tabId);
|
||||
await updateBadge(tabId);
|
||||
} catch (error) {
|
||||
console.error(`Error checking tab ${tabId} on activation:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
browser.tabs.onUpdated.addListener(async (tabId) => {
|
||||
try {
|
||||
await updateBadge(tabId);
|
||||
} catch (error) {
|
||||
console.error(`Error checking tab ${tabId} on activation:`, error);
|
||||
}
|
||||
@@ -337,7 +324,7 @@ browser.tabs.onActivated.addListener(async ({ tabId }) => {
|
||||
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
||||
try {
|
||||
if (changeInfo.status === 'complete' && tab?.active) {
|
||||
await checkAndUpdateTab(tabId);
|
||||
await updateBadge(tabId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error checking tab ${tabId} on update:`, error);
|
||||
@@ -352,7 +339,7 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
||||
currentWindow: true,
|
||||
});
|
||||
if (tab?.id) {
|
||||
await checkAndUpdateTab(tab.id);
|
||||
await updateBadge(tab.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error checking tab on startup:`, error);
|
||||
|
||||
+39
-34
@@ -4,7 +4,7 @@ import BookmarkForm from '../../@/components/BookmarkForm.tsx';
|
||||
import { openOptions } from '../../@/lib/utils.ts';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getConfig, isConfigured } from '../../@/lib/config.ts';
|
||||
import Modal from '../../@/components/Modal.tsx';
|
||||
import NotConfigured from '../../@/components/NotConfigured.tsx';
|
||||
import { ModeToggle } from '../../@/components/ModeToggle.tsx';
|
||||
|
||||
function App() {
|
||||
@@ -22,39 +22,44 @@ function App() {
|
||||
|
||||
return (
|
||||
<WholeContainer>
|
||||
<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 />
|
||||
<Modal open={!isAllConfigured} />
|
||||
</Container>
|
||||
{
|
||||
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 />
|
||||
)
|
||||
}
|
||||
</WholeContainer>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-6
@@ -9,6 +9,12 @@
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
@@ -30,10 +36,4 @@
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user