webclipper: improve clipping ux

* start clip when button is clicked, not on load or selection click
* add discard button

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2025-06-23 11:12:21 +05:00
committed by Abdullah Atta
parent 74ed529f16
commit 71fe6bbebc

View File

@@ -87,6 +87,20 @@ const clipModes: { name: string; id: ClipMode; icon: string; pro?: boolean }[] =
}
];
enum ClipperState {
Idle = "idle",
Clipping = "clipping",
Clipped = "clipped",
Error = "error"
}
const clipperButtonLabelMap: Record<ClipperState, string> = {
[ClipperState.Clipping]: "Clipping...",
[ClipperState.Clipped]: "Save clip",
[ClipperState.Error]: "Retry Clip",
[ClipperState.Idle]: "Start clip"
};
export function Main() {
const [error, setError] = useState<string>();
// const [colorMode, setColorMode] = useColorMode();
@@ -98,7 +112,6 @@ export function Main() {
const [title, setTitle] = useState<string>();
const [hasPermission, setHasPermission] = useState<boolean>(false);
const [url, setUrl] = useState<string>();
const [clipNonce, setClipNonce] = useState(0);
const [clipMode, setClipMode] = usePersistentState<ClipMode>(
"clipMode",
"simplified"
@@ -107,10 +120,12 @@ export function Main() {
"clipArea",
"article"
);
const [isClipping, setIsClipping] = useState(false);
const [note, setNote] = usePersistentState<ItemReference>("note");
const [refs, setRefs] = usePersistentState<SelectedReference[]>("refs", []);
const [clipData, setClipData] = useState<ClipData>();
const [clipperState, setClipperState] = useState<ClipperState>(
ClipperState.Idle
);
const pageTitle = useRef<string>();
useEffect(() => {
@@ -129,7 +144,6 @@ export function Main() {
useEffect(() => {
(async () => {
if (!clipArea || !clipMode) return;
if (
!isPremium &&
(clipMode === "complete" || clipMode === "screenshot")
@@ -137,25 +151,8 @@ export function Main() {
setClipMode("simplified");
return;
}
try {
setIsClipping(true);
setClipData(
await clip(clipArea, clipMode, {
...DEFAULT_SETTINGS,
...settings,
images: isPremium,
inlineImages: isPremium
})
);
} catch (e) {
console.error(e);
if (e instanceof Error) setError(e.message);
} finally {
setIsClipping(false);
}
})();
}, [isPremium, clipArea, clipMode, clipNonce]);
}, [isPremium, clipArea, clipMode]);
useEffect(() => {
(async () => {
@@ -168,6 +165,32 @@ export function Main() {
})();
}, [settings]);
async function startClip() {
if (!clipArea || !clipMode) return;
try {
setError(undefined);
setClipperState(ClipperState.Clipping);
setClipData(
await clip(clipArea, clipMode, {
...DEFAULT_SETTINGS,
...settings,
images: isPremium,
inlineImages: isPremium
})
);
setClipperState(ClipperState.Clipped);
} catch (e) {
console.error(e);
if (e instanceof Error) {
setError(e.message);
}
setClipperState(ClipperState.Error);
}
}
const isClipping = clipperState === ClipperState.Clipping;
if (!hasPermission && !!settings?.corsProxy) {
return (
<FlexScrollContainer style={{ maxHeight: 560 }}>
@@ -241,10 +264,11 @@ export function Main() {
key={item.id}
variant="icon"
onClick={() => {
setError(undefined);
setClipperState(ClipperState.Idle);
setClipArea(item.id);
setClipNonce((s) => ++s);
}}
disabled={isClipping}
disabled={isClipping || clipperState === ClipperState.Clipped}
sx={{
display: "flex",
borderRadius: "default",
@@ -283,10 +307,15 @@ export function Main() {
key={item.id}
variant="icon"
onClick={() => {
setError(undefined);
setClipperState(ClipperState.Idle);
setClipMode(item.id);
setClipNonce((s) => ++s);
}}
disabled={isClipping || (item.pro && !isPremium)}
disabled={
isClipping ||
clipperState === ClipperState.Clipped ||
(item.pro && !isPremium)
}
sx={{
display: "flex",
borderRadius: "default",
@@ -314,31 +343,53 @@ export function Main() {
))}
{clipData && clipData.data && !isClipping && (
<Text
variant="body"
sx={{
mt: 1,
bg: "shade",
color: "accent",
p: 1,
border: "1px solid var(--accent)",
borderRadius: "default",
cursor: "pointer",
":hover": {
filter: "brightness(80%)"
}
}}
onClick={async () => {
const winUrl = URL.createObjectURL(
new Blob(["\ufeff", clipData.data], { type: "text/html" })
);
await browser.windows.create({
url: winUrl
});
}}
>
Clip done. Click here to preview.
</Text>
<Flex sx={{ gap: 1, justifyContent: "space-between" }}>
<Text
variant="body"
sx={{
flex: 1,
mt: 1,
bg: "shade",
color: "accent",
p: 1,
border: "1px solid var(--accent)",
borderRadius: "default",
cursor: "pointer",
":hover": {
filter: "brightness(80%)"
}
}}
onClick={async () => {
const winUrl = URL.createObjectURL(
new Blob(["\ufeff", clipData.data], { type: "text/html" })
);
await browser.windows.create({
url: winUrl
});
}}
>
Clip done. Click here to preview.
</Text>
<Text
variant="body"
sx={{
mt: 1,
bg: "background-secondary",
p: 1,
borderRadius: "default",
cursor: "pointer",
":hover": {
filter: "brightness(80%)"
}
}}
onClick={async () => {
setClipData(undefined);
setClipperState(ClipperState.Idle);
}}
>
Discard
</Text>
</Flex>
)}
{error && (
@@ -357,7 +408,7 @@ export function Main() {
}
}}
onClick={async () => {
setClipNonce((s) => ++s);
await startClip();
}}
>
{ERROR_MAP[error] || error}
@@ -400,8 +451,16 @@ export function Main() {
<Button
variant="accent"
sx={{ mt: 1 }}
disabled={!clipData}
disabled={isClipping}
onClick={async () => {
if (
clipperState === ClipperState.Idle ||
clipperState === ClipperState.Error
) {
await startClip();
return;
}
if (!clipData || !title || !clipArea || !clipMode || !url) return;
const notesnook = await connectApi(false);
@@ -433,7 +492,7 @@ export function Main() {
window.close();
}}
>
Save clip
{clipperButtonLabelMap[clipperState]}
</Button>
<Flex