clipper(web): add settings to configure cors proxy

This commit is contained in:
Abdullah Atta
2022-12-06 12:06:39 +05:00
parent c28ef94cf1
commit e71447c19d
9 changed files with 171 additions and 32 deletions

View File

@@ -16,20 +16,37 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect } from "react";
import { ThemeProvider } from "./components/theme-provider";
import { useAppStore } from "./stores/app-store";
import { Login } from "./views/login";
import { Main } from "./views/main";
import { Settings } from "./views/settings";
export function App() {
const isLoggedIn = useAppStore((s) => s.isLoggedIn);
const user = useAppStore((s) => s.user);
const route = useAppStore((s) => s.route);
const navigate = useAppStore((s) => s.navigate);
useEffect(() => {
if (!isLoggedIn) {
navigate("/login");
} else navigate("/");
}, [isLoggedIn]);
return (
<ThemeProvider accent={user?.accent} theme={user?.theme}>
{(() => {
if (!isLoggedIn) return <Login />;
return <Main />;
switch (route) {
case "/login":
return <Login />;
default:
case "/":
return <Main />;
case "/settings":
return <Settings />;
}
})()}
</ThemeProvider>
);

View File

@@ -37,7 +37,8 @@ import {
mdiNewspaper,
mdiMagnify,
mdiViewDayOutline,
mdiViewDashboardOutline
mdiViewDashboardOutline,
mdiArrowLeft
} from "@mdi/js";
export const Icons = {
@@ -66,7 +67,8 @@ export const Icons = {
chevronUp: mdiChevronUp,
chevronRight: mdiChevronRight,
none: ""
none: "",
back: mdiArrowLeft
};
export type IconNames = keyof typeof Icons;

View File

@@ -25,11 +25,13 @@ import {
enterNodeSelectionMode
} from "@notesnook/clipper";
import { ClipArea, ClipMode } from "../common/bridge";
import type { Config } from "@notesnook/clipper/dist/types";
type ClipMessage = {
type: "clip";
mode?: ClipMode;
area?: ClipArea;
settings?: Config;
};
type ViewportMessage = {
@@ -62,21 +64,22 @@ browser.runtime.onMessage.addListener(async (request) => {
function clip(message: ClipMessage) {
try {
const config = message.settings;
const isScreenshot = message.mode === "screenshot";
const withStyles = message.mode === "complete" || isScreenshot;
if (isScreenshot && message.area === "full-page") {
return clipScreenshot(document.body, "jpeg");
return clipScreenshot(document.body, "jpeg", config);
} else if (message.area === "full-page") {
return clipPage(document, withStyles, false);
return clipPage(document, withStyles, false, config);
} else if (message.area === "selection") {
enterNodeSelectionMode(document).then((result) =>
enterNodeSelectionMode(document, config).then((result) =>
browser.runtime.sendMessage({ type: "manual", data: result })
);
} else if (message.area === "article") {
return clipArticle(document, withStyles);
return clipArticle(document, withStyles, config);
} else if (message.area === "visible") {
return clipPage(document, withStyles, true);
return clipPage(document, withStyles, true, config);
}
} catch (e) {
console.error(e);

View File

@@ -27,8 +27,10 @@ interface AppStore {
notes: ItemReference[];
notebooks: NotebookReference[];
tags: ItemReference[];
route: string;
login(openNew?: boolean): Promise<void>;
navigate(route: string): void;
}
export const useAppStore = create<AppStore>((set) => ({
@@ -37,6 +39,11 @@ export const useAppStore = create<AppStore>((set) => ({
notebooks: [],
notes: [],
tags: [],
route: "/login",
navigate(route) {
set({ route });
},
async login(openNew = false) {
set({ isLoggingIn: true });

View File

@@ -29,13 +29,15 @@ import {
SelectedNotebook,
ClipArea,
ClipMode,
ClipData,
ClipData
} from "../common/bridge";
import { usePersistentState } from "../hooks/use-persistent-state";
import { deleteClip, getClip } from "../utils/storage";
import { useAppStore } from "../stores/app-store";
import { connectApi } from "../api";
import { FlexScrollContainer } from "../components/scroll-container";
import { DEFAULT_SETTINGS, SETTINGS_KEY } from "./settings";
import type { Config } from "@notesnook/clipper/dist/types";
const clipAreas: { name: string; id: ClipArea; icon: string }[] = [
{
@@ -86,6 +88,9 @@ export function Main() {
// const [colorMode, setColorMode] = useColorMode();
const isPremium = useAppStore((s) => s.user?.pro);
const navigate = useAppStore((s) => s.navigate);
const [settings] = usePersistentState<Config>(SETTINGS_KEY, DEFAULT_SETTINGS);
const [title, setTitle] = useState<string>();
const [url, setUrl] = useState<string>();
const [clipNonce, setClipNonce] = useState(0);
@@ -375,7 +380,13 @@ export function Main() {
justifyContent: "flex-end"
}}
>
<Button variant="icon" sx={{ p: 1 }}>
<Button
variant="icon"
sx={{ p: 1 }}
onClick={() => {
navigate("/settings");
}}
>
<Icon path={Icons.settings} color="text" size={16} />
</Button>
</Flex>
@@ -386,7 +397,8 @@ export function Main() {
export async function clip(
area: ClipArea,
mode: ClipMode
mode: ClipMode,
settings: Config = DEFAULT_SETTINGS
): Promise<ClipData | undefined> {
const clipData = await getClip();
if (area === "selection" && typeof clipData === "string") {
@@ -410,5 +422,10 @@ export async function clip(
};
}
return await browser.tabs.sendMessage(tab.id, { type: "clip", mode, area });
return await browser.tabs.sendMessage(tab.id, {
type: "clip",
mode,
area,
settings
});
}

View File

@@ -16,4 +16,80 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export const s = "";
import { Button, Flex, Input, Label, Text } from "@theme-ui/components";
import { Icons } from "../components/icons";
import { Icon } from "../components/icons/icon";
import { usePersistentState } from "../hooks/use-persistent-state";
import { useAppStore } from "../stores/app-store";
import type { Config } from "@notesnook/clipper/dist/types";
export const SETTINGS_KEY = "settings";
export const DEFAULT_SETTINGS: Config = {
corsProxy: "https://cors.notesnook.com"
};
export function Settings() {
const navigate = useAppStore((s) => s.navigate);
const [settings, saveSettings] = usePersistentState<Config>(
SETTINGS_KEY,
DEFAULT_SETTINGS
);
return (
<Flex
sx={{
flexDirection: "column",
p: 2,
width: 320,
backgroundColor: "background"
}}
as="form"
onSubmit={(e) => {
e.preventDefault();
const form = new FormData(e.target as HTMLFormElement);
let corsProxy = form.get("corsProxy")?.toString();
if (corsProxy) {
const url = new URL(corsProxy);
corsProxy = `${url.protocol}//${url.hostname}`;
}
saveSettings({
corsProxy
});
}}
>
<Flex sx={{ alignItems: "center" }}>
<Icon
path={Icons.back}
onClick={() => {
navigate("/");
}}
sx={{ mr: 2 }}
/>
<Text variant="title">Settings</Text>
</Flex>
<Label variant="text.body" sx={{ flexDirection: "column", mt: 2 }}>
Custom CORS Proxy:
<Text variant="subBody">
For clipping to work correctly, we have to bypass CORS. You can use
this setting to <a href="navigate">configure your own proxy</a> &amp;
protect your privacy.
</Text>
<Input
id="corsProxy"
name="corsProxy"
type="url"
defaultValue={settings?.corsProxy}
placeholder="https://cors.notesnook.com"
sx={{ p: 1, py: "7px", mt: 1 }}
/>
</Label>
<Button type="submit" sx={{ mt: 2 }}>
Save
</Button>
</Flex>
);
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export type FetchOptions = {
bypassCors?: boolean;
corsHost?: string;
corsHost: string;
noCache?: boolean;
crossOrigin?: "anonymous" | "use-credentials" | null;
};

View File

@@ -21,7 +21,7 @@ import { Readability } from "@mozilla/readability";
import { injectCss } from "./utils";
import { app, h, text } from "hyperapp";
import { getInlinedNode, toBlob, toJpeg, toPng } from "./domtoimage";
import { InlineOptions } from "./types";
import { Config, InlineOptions } from "./types";
import { FetchOptions } from "./fetch";
type ReadabilityEnhanced = Readability<string> & {
@@ -36,13 +36,6 @@ const CLASSES = {
const BLACKLIST = [CLASSES.nodeSelected, CLASSES.nodeSelectionContainer];
const fetchOptions: FetchOptions = {
bypassCors: true,
corsHost: "https://cors.notesnook.com",
crossOrigin: "anonymous",
noCache: true
};
const inlineOptions: InlineOptions = {
fonts: false,
images: true,
@@ -52,9 +45,15 @@ const inlineOptions: InlineOptions = {
async function clipPage(
document: Document,
withStyles: boolean,
onlyVisible: boolean
onlyVisible: boolean,
config?: Config
): Promise<string | null> {
const { body, head } = await getPage(document, withStyles, onlyVisible);
const { body, head } = await getPage(
document,
withStyles,
config,
onlyVisible
);
if (!body || !head) return null;
const result = toDocument(head, body).documentElement.outerHTML;
return `<!doctype html>\n${result}`;
@@ -62,9 +61,10 @@ async function clipPage(
async function clipArticle(
doc: Document,
withStyles: boolean
withStyles: boolean,
config?: Config
): Promise<string | null> {
const { body, head } = await getPage(doc, withStyles);
const { body, head } = await getPage(doc, withStyles, config);
if (!body || !head) return null;
const newDoc = toDocument(head, body);
@@ -99,7 +99,8 @@ async function clipScreenshot<
: Blob | undefined
>(
target?: HTMLElement,
output: TOutputFormat = "jpeg" as TOutputFormat
output: TOutputFormat = "jpeg" as TOutputFormat,
config?: Config
): Promise<TOutput> {
const screenshotTarget = target || document.body;
@@ -109,7 +110,7 @@ async function clipScreenshot<
backgroundColor: "white",
width: document.body.scrollWidth,
height: document.body.scrollHeight,
fetchOptions,
fetchOptions: resolveFetchOptions(config),
inlineOptions: {
fonts: true,
images: true,
@@ -181,7 +182,7 @@ function removeClickHandlers(doc: Document) {
doc.body.removeEventListener("click", onMouseClick);
}
function enterNodeSelectionMode(doc: Document) {
function enterNodeSelectionMode(doc: Document, config?: Config) {
setTimeout(() => {
registerClickListeners(doc);
registerHoverListeners(doc);
@@ -203,7 +204,7 @@ function enterNodeSelectionMode(doc: Document) {
node.classList.remove(CLASSES.nodeSelected);
const inlined = await getInlinedNode(node as HTMLElement, {
raster: false,
fetchOptions,
fetchOptions: resolveFetchOptions(config),
inlineOptions
});
if (!inlined) continue;
@@ -454,11 +455,12 @@ function cleanup() {
async function getPage(
document: Document,
styles: boolean,
config?: Config,
onlyVisible = false
) {
const body = await getInlinedNode(document.body, {
raster: true,
fetchOptions,
fetchOptions: resolveFetchOptions(config),
inlineOptions: {
fonts: false,
images: true,
@@ -482,3 +484,14 @@ async function getPage(
head
};
}
function resolveFetchOptions(config?: Config): FetchOptions | undefined {
return config?.corsProxy
? {
bypassCors: true,
corsHost: config.corsProxy,
crossOrigin: "anonymous",
noCache: true
}
: undefined;
}

View File

@@ -49,3 +49,7 @@ export type Options = {
inlineOptions?: InlineOptions;
styles?: boolean;
};
export type Config = {
corsProxy?: string;
};