Merge pull request #8254 from 01zulfi/webclipper/start-clip-button

webclipper: improve clipping ux
This commit is contained in:
Abdullah Atta
2025-10-27 13:48:44 +05:00
committed by GitHub

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