Merge pull request #132 from SoCuul/ux-changes

General UX improvements
This commit is contained in:
Daniel
2025-12-28 18:45:40 +03:30
committed by GitHub
9 changed files with 1558 additions and 840 deletions
+1370 -703
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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"
}
}
+54 -28
View File
@@ -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 />
-27
View File
@@ -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;
+25
View File
@@ -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
View File
@@ -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: '' });
}
}
}
+20 -33
View File
@@ -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
View File
@@ -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
View File
@@ -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/*"
]
},
}