mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
12 Commits
3.4.5-andr
...
feat/make-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f35d36a2a | ||
|
|
873ba49e32 | ||
|
|
9f88eaae77 | ||
|
|
704ad578fa | ||
|
|
cf608977e1 | ||
|
|
bdd78df452 | ||
|
|
cec05b6dfc | ||
|
|
3e4779e6bf | ||
|
|
9ebcbb7bd8 | ||
|
|
f3f8020929 | ||
|
|
43186d28e7 | ||
|
|
d9d6a40276 |
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.5",
|
||||
"version": "3.3.6-beta.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.5",
|
||||
"version": "3.3.6-beta.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.5",
|
||||
"version": "3.3.6-beta.0",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
|
||||
1427
apps/web/package-lock.json
generated
1427
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.5",
|
||||
"version": "3.3.6-beta.0",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
@@ -30,7 +29,6 @@
|
||||
"@notesnook/theme": "file:../../packages/theme",
|
||||
"@notesnook/themes-server": "file:../../servers/themes",
|
||||
"@notesnook/ui": "file:../../packages/ui",
|
||||
"@notesnook/web-clipper": "file:../../extensions/web-clipper",
|
||||
"@paddle/paddle-js": "^1.4.2",
|
||||
"@react-pdf-viewer/core": "^3.12.0",
|
||||
"@react-pdf-viewer/toolbar": "^3.12.0",
|
||||
|
||||
@@ -30,13 +30,18 @@ export async function handleDrop(
|
||||
item:
|
||||
| ItemReference
|
||||
| Context
|
||||
| { type: "trash" | "notebooks" | "favorites" | undefined }
|
||||
| { type: "trash" | "notebooks" | "favorites" | "archive" | undefined }
|
||||
) {
|
||||
if (!item.type) return;
|
||||
|
||||
const noteIds = getDragData(dataTransfer, "note");
|
||||
const notebookIds = getDragData(dataTransfer, "notebook");
|
||||
const { setColor, favorite, delete: trashNotes } = useNoteStore.getState();
|
||||
const {
|
||||
setColor,
|
||||
favorite,
|
||||
delete: trashNotes,
|
||||
archive
|
||||
} = useNoteStore.getState();
|
||||
switch (item.type) {
|
||||
case "notebook":
|
||||
if (noteIds.length > 0) {
|
||||
@@ -83,5 +88,8 @@ export async function handleDrop(
|
||||
await useNoteStore.getState().refresh();
|
||||
}
|
||||
break;
|
||||
case "archive":
|
||||
archive(true, ...noteIds);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ function countCharacters(text: string) {
|
||||
function countParagraphs(fragment: Fragment) {
|
||||
let count = 0;
|
||||
fragment.nodesBetween(0, fragment.size, (node) => {
|
||||
if (node.type.name === "paragraph") {
|
||||
if (node.type.name === "paragraph" && node.content.size > 0) {
|
||||
count++;
|
||||
}
|
||||
return true;
|
||||
@@ -737,7 +737,7 @@ function toIEditor(editor: Editor): IEditor {
|
||||
function getSelectedParagraphs(editor: Editor, selection: Selection): number {
|
||||
let count = 0;
|
||||
editor.state.doc.nodesBetween(selection.from, selection.to, (node) => {
|
||||
if (node.type.name === "paragraph") {
|
||||
if (node.type.name === "paragraph" && node.content.size > 0) {
|
||||
count++;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -506,6 +506,8 @@ function RouteItem({
|
||||
? "trash"
|
||||
: item.path === "/favorites"
|
||||
? "favorites"
|
||||
: item.path == "/archive"
|
||||
? "archive"
|
||||
: undefined
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -91,18 +91,7 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
)
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "Notesnook Circle",
|
||||
subtitle:
|
||||
"Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom. As a member you get exclusive discounts and offers from our partners. Check it out in Settings > Notesnook Circle."
|
||||
},
|
||||
{
|
||||
title: "Collapsible headings",
|
||||
subtitle:
|
||||
"You can now collapse and expand headings in your notes. This makes it easier to manage large notes and focus on specific sections."
|
||||
}
|
||||
],
|
||||
: [],
|
||||
cta: {
|
||||
title: strings.gotIt(),
|
||||
icon: Checkmark,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true
|
||||
|
||||
@@ -19,18 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
const { version } = require("../package.json");
|
||||
|
||||
const ICONS = {
|
||||
16: "16x16.png",
|
||||
32: "32x32.png",
|
||||
48: "48x48.png",
|
||||
64: "64x64.png",
|
||||
128: "128x128.png",
|
||||
256: "256x256.png"
|
||||
16: "assets/16x16.png",
|
||||
32: "assets/32x32.png",
|
||||
48: "assets/48x48.png",
|
||||
64: "assets/64x64.png",
|
||||
128: "assets/128x128.png",
|
||||
256: "assets/256x256.png"
|
||||
};
|
||||
const BACKGROUND_SCRIPT = "background.bundle.js";
|
||||
const ACTION = {
|
||||
default_icon: ICONS,
|
||||
default_title: "Notesnook Web Clipper",
|
||||
default_popup: "popup.html"
|
||||
default_popup: "public/index.html"
|
||||
};
|
||||
|
||||
const nnHost =
|
||||
@@ -60,8 +59,20 @@ const v2 = {
|
||||
},
|
||||
manifest_version: 2,
|
||||
background: {
|
||||
scripts: [BACKGROUND_SCRIPT]
|
||||
scripts: ["src/background.ts"]
|
||||
},
|
||||
content_scripts: [
|
||||
{
|
||||
matches: ["<all_urls>"],
|
||||
js: ["src/content-scripts/all.ts"],
|
||||
run_at: "document_end"
|
||||
},
|
||||
{
|
||||
matches: [nnHost, v3nnHost],
|
||||
js: ["src/content-scripts/nn.ts"],
|
||||
run_at: "document_end"
|
||||
}
|
||||
],
|
||||
browser_action: ACTION
|
||||
};
|
||||
|
||||
@@ -72,12 +83,37 @@ const v3 = {
|
||||
optional_host_permissions: ["http://*/*", "https://*/*"],
|
||||
manifest_version: 3,
|
||||
background: {
|
||||
service_worker: BACKGROUND_SCRIPT
|
||||
service_worker: "src/background.ts",
|
||||
type: "module"
|
||||
},
|
||||
content_scripts: [
|
||||
{
|
||||
matches: ["<all_urls>"],
|
||||
js: ["src/content-scripts/all.ts"]
|
||||
},
|
||||
{
|
||||
matches: [nnHost],
|
||||
js: ["src/content-scripts/nn.ts"]
|
||||
}
|
||||
],
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ["*.wasm"],
|
||||
matches: ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
action: ACTION
|
||||
};
|
||||
|
||||
function getManifest(version) {
|
||||
return version === "2" ? v2 : v3;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
v2,
|
||||
v3
|
||||
v3,
|
||||
getManifest
|
||||
};
|
||||
107
extensions/web-clipper/build-utils/manifest.ts
Normal file
107
extensions/web-clipper/build-utils/manifest.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
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 packageJson from "../package.json";
|
||||
|
||||
const { version } = packageJson;
|
||||
|
||||
const ICONS = {
|
||||
"16": "assets/16x16.png",
|
||||
"32": "assets/32x32.png",
|
||||
"48": "assets/48x48.png",
|
||||
"64": "assets/64x64.png",
|
||||
"128": "assets/128x128.png",
|
||||
"256": "assets/256x256.png"
|
||||
};
|
||||
const ACTION = {
|
||||
default_icon: ICONS,
|
||||
default_title: "Notesnook Web Clipper",
|
||||
default_popup: "index.html"
|
||||
};
|
||||
|
||||
const nnHost =
|
||||
process.env.NODE_ENV === "production"
|
||||
? "*://app.notesnook.com/*"
|
||||
: "*://localhost/*";
|
||||
const corsHost = "https://cors.notesnook.com/*";
|
||||
const common = {
|
||||
name: "Notesnook Web Clipper",
|
||||
version,
|
||||
description:
|
||||
"Clip web pages & save interesting things you find on the web directly into Notesnook in a private & secure way.",
|
||||
permissions: ["activeTab", "tabs", "storage", "notifications"],
|
||||
icons: ICONS
|
||||
};
|
||||
|
||||
const v2 = {
|
||||
...common,
|
||||
permissions: [...common.permissions, corsHost, nnHost],
|
||||
optional_permissions: ["http://*/*", "https://*/*"],
|
||||
browser_specific_settings: {
|
||||
gecko: {
|
||||
id: "notesnook-web-clipper-unlisted@notesnook.com",
|
||||
strict_min_version: "105.0"
|
||||
}
|
||||
},
|
||||
manifest_version: 2,
|
||||
background: {
|
||||
scripts: ["src/background.ts"]
|
||||
},
|
||||
content_scripts: [
|
||||
{
|
||||
matches: ["<all_urls>"],
|
||||
js: ["src/content-scripts/all.ts"],
|
||||
run_at: "document_end"
|
||||
}
|
||||
],
|
||||
browser_action: ACTION
|
||||
};
|
||||
|
||||
const v3 = {
|
||||
...common,
|
||||
permissions: [...common.permissions, "scripting"],
|
||||
host_permissions: [corsHost, nnHost],
|
||||
optional_host_permissions: ["http://*/*", "https://*/*"],
|
||||
manifest_version: 3,
|
||||
background: {
|
||||
service_worker: "src/background.ts",
|
||||
type: "module"
|
||||
},
|
||||
content_scripts: [
|
||||
{
|
||||
matches: ["<all_urls>"],
|
||||
js: ["src/content-scripts/all.ts"]
|
||||
}
|
||||
],
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ["*.wasm"],
|
||||
matches: ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
action: ACTION
|
||||
};
|
||||
|
||||
export function getManifest(version: string) {
|
||||
return version === "2" ? v2 : v3;
|
||||
}
|
||||
|
||||
export { v2, v3 };
|
||||
3
extensions/web-clipper/global.d.ts
vendored
3
extensions/web-clipper/global.d.ts
vendored
@@ -16,6 +16,9 @@ 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 "vite/client";
|
||||
|
||||
declare module "*.svg" {
|
||||
import React = require("react");
|
||||
export const ReactComponent: React.SFC<React.SVGProps<SVGSVGElement>>;
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3847
extensions/web-clipper/package-lock.json
generated
3847
extensions/web-clipper/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,40 +2,27 @@
|
||||
"name": "@notesnook/web-clipper",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/cjs/index.js"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
}
|
||||
},
|
||||
"./*": {
|
||||
"require": {
|
||||
"types": "./dist/types/*",
|
||||
"default": "./dist/cjs/*"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/types/*",
|
||||
"default": "./dist/esm/*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/react": "1.6.1",
|
||||
"@lingui/core": "5.1.2",
|
||||
"@lingui/react": "5.1.2",
|
||||
"@notesnook/clipper": "file:../../packages/clipper",
|
||||
"@notesnook/common": "file:../../packages/common",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@notesnook/logger": "file:../../packages/logger",
|
||||
"@notesnook/streamable-fs": "file:../../packages/streamable-fs",
|
||||
"@notesnook/theme": "file:../../packages/theme",
|
||||
"@notesnook/web": "file:../../apps/web",
|
||||
"@notesnook/intl": "file:../../packages/intl",
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@theme-ui/components": "0.16.1",
|
||||
"@theme-ui/core": "0.16.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
"comlink": "^4.3.1",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"hyperapp": "^2.0.22",
|
||||
"mac-scrollbar": "0.13.6",
|
||||
"react": "18.3.1",
|
||||
@@ -45,14 +32,15 @@
|
||||
"react-scripts": "5.0.0",
|
||||
"svg-react-loader": "^0.4.6",
|
||||
"webextension-polyfill-ts": "^0.26.0",
|
||||
"zustand": "4.5.5"
|
||||
"zustand": "^4.5.7",
|
||||
"zustand-mutative": "^1.3.1",
|
||||
"dayjs": "1.11.13"
|
||||
},
|
||||
"scripts": {
|
||||
"build:firefox": "cross-env MANIFEST_VERSION=2 node build-utils/build.js",
|
||||
"build:chrome": "cross-env MANIFEST_VERSION=3 node build-utils/build.js",
|
||||
"dev:chrome": "cross-env MANIFEST_VERSION=3 node build-utils/dev.js",
|
||||
"dev:firefox": "cross-env MANIFEST_VERSION=2 node build-utils/dev.js",
|
||||
"build": "node ../../scripts/build.mjs"
|
||||
"build:firefox": "cross-env MANIFEST_VERSION=2 NODE_ENV=production vite build",
|
||||
"build:chrome": "cross-env MANIFEST_VERSION=3 NODE_ENV=production vite build",
|
||||
"dev:chrome": "cross-env MANIFEST_VERSION=3 vite",
|
||||
"dev:firefox": "cross-env MANIFEST_VERSION=2 vite"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -67,11 +55,10 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@crxjs/vite-plugin": "^2.0.0-beta.25",
|
||||
"@emotion/babel-plugin": "^11.11.0",
|
||||
"@types/chrome": "^0.0.180",
|
||||
"@types/event-source-polyfill": "1.0.5",
|
||||
"@types/firefox-webext-browser": "^94.0.1",
|
||||
"@types/inline-css": "^3.0.1",
|
||||
"@types/jest": "^26.0.14",
|
||||
@@ -81,12 +68,8 @@
|
||||
"@types/react-modal": "3.16.3",
|
||||
"@types/sanitize-html": "^2.6.2",
|
||||
"@types/webextension-polyfill": "^0.8.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-preset-react-app": "^10.0.1",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^7.0.0",
|
||||
"css-loader": "^6.6.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.8.0",
|
||||
"eslint-config-react-app": "^7.0.0",
|
||||
"eslint-plugin-flowtype": "^8.0.3",
|
||||
@@ -94,21 +77,10 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"html-loader": "^3.1.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"node-sass": "^9.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"sass-loader": "^13.2.0",
|
||||
"source-map-loader": "^3.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"terser-webpack-plugin": "^5.3.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"web-ext": "^7.12.0",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
"vite": "^5.4.11",
|
||||
"web-ext": "^7.12.0"
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": {
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
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 { browser, Runtime, Tabs } from "webextension-polyfill-ts";
|
||||
import { Remote, wrap } from "comlink";
|
||||
import { createEndpoint } from "./utils/comlink-extension";
|
||||
import { Server } from "./common/bridge";
|
||||
import { APP_URL, APP_URL_FILTER } from "./common/constants";
|
||||
|
||||
type WebExtensionChannelMessage = { success: boolean };
|
||||
|
||||
let api: Remote<Server> | undefined;
|
||||
export async function connectApi(openNew = false, onDisconnect?: () => void) {
|
||||
if (api) return api;
|
||||
|
||||
const tabs = await findNotesnookTabs(openNew);
|
||||
for (const tab of tabs) {
|
||||
try {
|
||||
const api = await Promise.race([
|
||||
connectToTab(tab, onDisconnect),
|
||||
timeout(5000)
|
||||
]);
|
||||
if (!api) continue;
|
||||
return api as Remote<Server>;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function timeout(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms, false));
|
||||
}
|
||||
|
||||
async function connectToTab(tab: Tabs.Tab, onDisconnect?: () => void) {
|
||||
if (!tab.id) return false;
|
||||
|
||||
if (browser.scripting) {
|
||||
await browser.scripting.executeScript({
|
||||
files: ["nnContentScript.bundle.js"],
|
||||
target: { tabId: tab.id }
|
||||
});
|
||||
} else {
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
file: "nnContentScript.bundle.js"
|
||||
});
|
||||
}
|
||||
|
||||
const port = browser.tabs.connect(tab.id);
|
||||
port.onDisconnect.addListener(() => {
|
||||
api = undefined;
|
||||
onDisconnect?.();
|
||||
});
|
||||
|
||||
return new Promise<Remote<Server> | false>(function connect(resolve) {
|
||||
async function onMessage(
|
||||
message: WebExtensionChannelMessage,
|
||||
port: Runtime.Port
|
||||
) {
|
||||
if (message.success) {
|
||||
port.onMessage.removeListener(onMessage);
|
||||
api = wrap<Server>(createEndpoint(port));
|
||||
resolve(api);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
port.onMessage.addListener(onMessage);
|
||||
});
|
||||
}
|
||||
|
||||
export async function findNotesnookTabs(openNew = false) {
|
||||
const tabs = await browser.tabs.query({
|
||||
url: APP_URL_FILTER,
|
||||
discarded: false,
|
||||
status: "complete"
|
||||
});
|
||||
|
||||
if (tabs.length) return tabs;
|
||||
|
||||
if (openNew) {
|
||||
const tab = await browser.tabs.create({ url: APP_URL, active: false });
|
||||
await new Promise<void>((resolve) =>
|
||||
browser.tabs.onUpdated.addListener(function onUpdated(id, info) {
|
||||
if (id === tab.id && info.status === "complete") {
|
||||
browser.tabs.onUpdated.removeListener(onUpdated);
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
);
|
||||
return [tab];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -27,27 +27,35 @@ import {
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import { Global, css } from "@emotion/react";
|
||||
import { useStore as useUserStore } from "@notesnook/web/src/stores/user-store";
|
||||
import AppEffects from "@notesnook/web/src/app-effects";
|
||||
|
||||
export function App() {
|
||||
const isLoggedIn = useAppStore((s) => s.isLoggedIn);
|
||||
const user = useAppStore((s) => s.user);
|
||||
// const isLoggedIn = useUserStore((store) => store.isLoggedIn);
|
||||
// const isLoggedIn = useAppStore((s) => s.isLoggedIn);
|
||||
// const user = useAppStore((s) => s.user);
|
||||
const route = useAppStore((s) => s.route);
|
||||
const navigate = useAppStore((s) => s.navigate);
|
||||
const theme = useThemeEngineStore((store) => store.theme);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
navigate("/login");
|
||||
} else navigate("/");
|
||||
}, [isLoggedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && user.theme) {
|
||||
document.body.style.backgroundColor =
|
||||
user.theme.scopes.base.primary.background;
|
||||
useThemeEngineStore.getState().setTheme(user.theme);
|
||||
async function main() {
|
||||
await useUserStore.getState().init();
|
||||
const { isLoggedIn } = useUserStore.getState();
|
||||
if (!isLoggedIn) {
|
||||
navigate("/login");
|
||||
} else navigate("/");
|
||||
}
|
||||
}, [user]);
|
||||
main();
|
||||
}, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (user && user.theme) {
|
||||
// document.body.style.backgroundColor =
|
||||
// user.theme.scopes.base.primary.background;
|
||||
// useThemeEngineStore.getState().setTheme(user.theme);
|
||||
// }
|
||||
// }, [user]);
|
||||
const cssTheme = useMemo(() => themeToCSS(theme), [theme]);
|
||||
|
||||
return (
|
||||
@@ -57,6 +65,7 @@ export function App() {
|
||||
${cssTheme}
|
||||
`}
|
||||
/>
|
||||
<AppEffects />
|
||||
<EmotionThemeProvider scope="base" injectCssVars>
|
||||
{(() => {
|
||||
switch (route) {
|
||||
|
||||
@@ -17,17 +17,10 @@ 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 { ThemeDefinition } from "@notesnook/theme";
|
||||
|
||||
export type ClipArea = "full-page" | "visible" | "selection" | "article";
|
||||
|
||||
export type ClipMode = "bookmark" | "simplified" | "screenshot" | "complete";
|
||||
|
||||
export type User = {
|
||||
email?: string;
|
||||
pro: boolean;
|
||||
theme: ThemeDefinition;
|
||||
};
|
||||
export type ItemReference = {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -35,15 +28,6 @@ export type ItemReference = {
|
||||
|
||||
export type NotebookReference = ItemReference;
|
||||
|
||||
export type ClientMetadata = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export interface Gateway {
|
||||
connect(): ClientMetadata;
|
||||
}
|
||||
|
||||
type SelectedNotebookReference = ItemReference & {
|
||||
type: "notebook";
|
||||
};
|
||||
@@ -55,27 +39,6 @@ export type SelectedReference =
|
||||
| SelectedTagReference
|
||||
| SelectedNotebookReference;
|
||||
|
||||
export type Clip = {
|
||||
url: string;
|
||||
title: string;
|
||||
data: string;
|
||||
area: ClipArea;
|
||||
mode: ClipMode;
|
||||
width?: number;
|
||||
height?: number;
|
||||
pageTitle?: string;
|
||||
note?: ItemReference;
|
||||
refs?: SelectedReference[];
|
||||
};
|
||||
|
||||
export interface Server {
|
||||
login(): Promise<User | null>;
|
||||
getNotes(): Promise<ItemReference[] | undefined>;
|
||||
getNotebooks(parentId?: string): Promise<NotebookReference[] | undefined>;
|
||||
getTags(): Promise<ItemReference[] | undefined>;
|
||||
saveClip(clip: Clip): Promise<void>;
|
||||
}
|
||||
|
||||
export const WEB_EXTENSION_CHANNEL_EVENTS = {
|
||||
ON_CREATED: "web-extension-channel-created",
|
||||
ON_READY: "web-extension-channel-ready"
|
||||
|
||||
135
extensions/web-clipper/src/common/db.ts
Normal file
135
extensions/web-clipper/src/common/db.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
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 { EventSourcePolyfill as EventSource } from "event-source-polyfill";
|
||||
import { database, getFeature, getFeatureLimit } from "@notesnook/common";
|
||||
import { createDialect } from "@notesnook/web/src/common/sqlite";
|
||||
import { isFeatureSupported } from "@notesnook/web/src/utils/feature-check";
|
||||
import {
|
||||
deriveKey,
|
||||
useKeyStore
|
||||
} from "@notesnook/web/src/interfaces/key-store";
|
||||
import {
|
||||
DatabasePersistence,
|
||||
NNStorage
|
||||
} from "@notesnook/web/src/interfaces/storage";
|
||||
import { generatePassword } from "@notesnook/web/src/utils/password-generator";
|
||||
|
||||
const db = database;
|
||||
async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
db.host({
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
MONOGRAPH_HOST: "https://monogr.ph",
|
||||
NOTESNOOK_HOST: "https://notesnook.com"
|
||||
});
|
||||
|
||||
await useKeyStore.getState().init();
|
||||
let databaseKey = await useKeyStore.getState().getValue("databaseKey");
|
||||
if (!databaseKey) {
|
||||
databaseKey = await deriveKey(generatePassword());
|
||||
await useKeyStore.getState().setValue("databaseKey", databaseKey);
|
||||
}
|
||||
|
||||
const storage = new NNStorage(
|
||||
"Notesnook",
|
||||
() => useKeyStore.getState(),
|
||||
persistence
|
||||
);
|
||||
await storage.migrate();
|
||||
|
||||
const multiTab = !!globalThis.SharedWorker && isFeatureSupported("opfs");
|
||||
database.setup({
|
||||
sqliteOptions: {
|
||||
dialect: (name, init) =>
|
||||
createDialect({
|
||||
name: persistence === "memory" ? ":memory:" : name,
|
||||
encrypted: true,
|
||||
async: !isFeatureSupported("opfs"),
|
||||
init,
|
||||
multiTab
|
||||
}),
|
||||
...(isFeatureSupported("opfs")
|
||||
? { journalMode: "WAL", lockingMode: "exclusive" }
|
||||
: {
|
||||
journalMode: "MEMORY",
|
||||
lockingMode: "normal"
|
||||
}),
|
||||
tempStore: "memory",
|
||||
synchronous: "normal",
|
||||
pageSize: 8192,
|
||||
cacheSize: -32000,
|
||||
password: Buffer.from(databaseKey).toString("hex"),
|
||||
skipInitialization: multiTab
|
||||
},
|
||||
storage: storage,
|
||||
eventsource: EventSource,
|
||||
// @ts-ignore
|
||||
fs: {},
|
||||
compressor: () =>
|
||||
import("@notesnook/web/src/utils/compressor").then(
|
||||
({ Compressor }) => new Compressor()
|
||||
),
|
||||
maxNoteVersions: async () => {
|
||||
const limit = await getFeatureLimit(getFeature("maxNoteVersions"));
|
||||
return typeof limit.caption === "number" ? limit.caption : undefined;
|
||||
},
|
||||
batchSize: 100
|
||||
});
|
||||
|
||||
// if (IS_TESTING) {
|
||||
|
||||
// } else {
|
||||
// db.host({
|
||||
// API_HOST: "http://localhost:5264",
|
||||
// AUTH_HOST: "http://localhost:8264",
|
||||
// SSE_HOST: "http://localhost:7264",
|
||||
// });
|
||||
// const base = `http://localhost`;
|
||||
// db.host({
|
||||
// API_HOST: `${base}:5264`,
|
||||
// AUTH_HOST: `${base}:8264`,
|
||||
// SSE_HOST: `${base}:7264`,
|
||||
// ISSUES_HOST: `${base}:2624`,
|
||||
// SUBSCRIPTIONS_HOST: `${base}:9264`
|
||||
// });
|
||||
// }
|
||||
|
||||
await db.init();
|
||||
|
||||
// window.addEventListener("beforeunload", async () => {
|
||||
// if (IS_DESKTOP_APP) {
|
||||
// await db.sql().destroy();
|
||||
// await logManager?.close();
|
||||
// }
|
||||
// });
|
||||
|
||||
// if (db.migrations?.required()) {
|
||||
// await import("../dialogs/migration-dialog").then(({ MigrationDialog }) =>
|
||||
// MigrationDialog.show({})
|
||||
// );
|
||||
// }
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
export { db, initializeDatabase };
|
||||
@@ -43,7 +43,8 @@ import {
|
||||
mdiCircleOutline,
|
||||
mdiBookOutline,
|
||||
mdiBookmarkOutline,
|
||||
mdiPound
|
||||
mdiPound,
|
||||
mdiSync
|
||||
} from "@mdi/js";
|
||||
|
||||
export const Icons = {
|
||||
@@ -78,6 +79,7 @@ export const Icons = {
|
||||
notebook: mdiBookOutline,
|
||||
topic: mdiBookmarkOutline,
|
||||
tag: mdiPound,
|
||||
sync: mdiSync,
|
||||
|
||||
none: "",
|
||||
back: mdiArrowLeft
|
||||
|
||||
@@ -16,15 +16,18 @@ 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 { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { FilteredList } from "../filtered-list";
|
||||
import { ItemReference } from "../../common/bridge";
|
||||
import { Icon } from "../icons/icon";
|
||||
import { Icons } from "../icons";
|
||||
import { useAppStore } from "../../stores/app-store";
|
||||
import { Picker } from "../picker";
|
||||
import { CheckListItem } from "../check-list-item";
|
||||
import { FilteredList } from "@notesnook/web/src/components/filtered-list";
|
||||
import { Note, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { db } from "../../common/db";
|
||||
import { ResolvedItem } from "@notesnook/common";
|
||||
|
||||
type NotePickerProps = {
|
||||
selectedNote?: ItemReference;
|
||||
@@ -34,8 +37,20 @@ export const NotePicker = (props: NotePickerProps) => {
|
||||
const { selectedNote, onSelected } = props;
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const notes = useAppStore((s) => s.notes);
|
||||
|
||||
const [notes, setNotes] = useState<VirtualizedGrouping<Note> | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
if (!notes) {
|
||||
setNotes(
|
||||
await db.notes.all.grouped(db.settings.getGroupOptions("notes"))
|
||||
);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
console.log(notes);
|
||||
const close = () => {
|
||||
setModalVisible(false);
|
||||
};
|
||||
@@ -97,25 +112,51 @@ export const NotePicker = (props: NotePickerProps) => {
|
||||
</Flex>
|
||||
|
||||
<Picker onClose={close} onDone={close} isOpen={modalVisible}>
|
||||
<FilteredList
|
||||
getAll={() => notes}
|
||||
filter={(items, query) =>
|
||||
items.filter((i) => i.title.toLowerCase().indexOf(query) > -1)
|
||||
}
|
||||
itemName="note"
|
||||
placeholder={"Search for a note"}
|
||||
refreshItems={() => notes}
|
||||
renderItem={(note) => (
|
||||
<CheckListItem
|
||||
title={note.title}
|
||||
onSelected={() => {
|
||||
onSelected(note);
|
||||
close();
|
||||
}}
|
||||
isSelected={selectedNote?.id === note.id}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{notes && (
|
||||
<FilteredList
|
||||
getItemKey={(index) => notes.key(index)}
|
||||
mode="fixed"
|
||||
estimatedSize={30}
|
||||
items={notes.placeholders}
|
||||
sx={{ mt: 2 }}
|
||||
itemGap={5}
|
||||
placeholders={{
|
||||
empty: strings.notesEmpty(),
|
||||
filter: strings.searchANote()
|
||||
}}
|
||||
filter={async (query) => {
|
||||
setNotes(
|
||||
query
|
||||
? await db.lookup.notes(query).sorted()
|
||||
: await db.notes.all.grouped(
|
||||
db.settings.getGroupOptions("notes")
|
||||
)
|
||||
);
|
||||
}}
|
||||
onCreateNewItem={async () => {}}
|
||||
renderItem={({ index }) => {
|
||||
console.log("Rendering note at index:", index);
|
||||
return (
|
||||
<ResolvedItem
|
||||
key={index}
|
||||
type="note"
|
||||
items={notes}
|
||||
index={index}
|
||||
>
|
||||
{({ item }) => (
|
||||
<CheckListItem
|
||||
title={item.title}
|
||||
onSelected={() => {
|
||||
onSelected({ id: item.id, title: item.title });
|
||||
}}
|
||||
isSelected={selectedNote?.id === item.id}
|
||||
/>
|
||||
)}
|
||||
</ResolvedItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,15 +16,25 @@ 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 { useState } from "react";
|
||||
import { Flex } from "@theme-ui/components";
|
||||
import { FilteredList } from "../filtered-list";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { NotebookReference, SelectedReference } from "../../common/bridge";
|
||||
import { Icons } from "../icons";
|
||||
import { useAppStore } from "../../stores/app-store";
|
||||
import { Picker } from "../picker";
|
||||
import { InlineTag } from "../inline-tag";
|
||||
import { CheckListItem } from "../check-list-item";
|
||||
import { FilteredList } from "@notesnook/web/src/components/filtered-list";
|
||||
import Field from "@notesnook/web/src/components/field";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { db } from "../../common/db";
|
||||
import {
|
||||
TreeNode,
|
||||
VirtualizedTree,
|
||||
VirtualizedTreeHandle
|
||||
} from "@notesnook/web/src/components/virtualized-tree";
|
||||
import { NotebookItem } from "@notesnook/web/src/dialogs/move-note-dialog";
|
||||
import { Notebook } from "@notesnook/core";
|
||||
|
||||
type NotebookPickerProps = {
|
||||
selectedItems: SelectedReference[];
|
||||
@@ -33,10 +43,18 @@ type NotebookPickerProps = {
|
||||
export const NotebookPicker = (props: NotebookPickerProps) => {
|
||||
const { onSelected } = props;
|
||||
|
||||
const treeRef = useRef<VirtualizedTreeHandle<Notebook>>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [selectedItems, setSelectedItems] = useState<SelectedReference[]>(
|
||||
props.selectedItems
|
||||
);
|
||||
const [notebooks, setNotebooks] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
db.notebooks.roots
|
||||
.ids(db.settings.getGroupOptions("notebooks"))
|
||||
.then((ids) => setNotebooks(ids));
|
||||
}, []);
|
||||
|
||||
const close = () => {
|
||||
setModalVisible(false);
|
||||
@@ -90,92 +108,121 @@ export const NotebookPicker = (props: NotebookPickerProps) => {
|
||||
}}
|
||||
isOpen={modalVisible}
|
||||
>
|
||||
<FilteredList
|
||||
getAll={() => useAppStore.getState().notebooks}
|
||||
filter={(items, query) =>
|
||||
items.filter((item) => item.title.toLowerCase().indexOf(query) > -1)
|
||||
}
|
||||
itemName="notebook"
|
||||
placeholder={"Search for a notebook"}
|
||||
refreshItems={() => useAppStore.getState().notebooks}
|
||||
renderItem={(item) => (
|
||||
<Notebook
|
||||
notebook={item}
|
||||
isSelected={
|
||||
!!selectedItems.find(
|
||||
(n) => n.id === item.id && n.type === "notebook"
|
||||
)
|
||||
}
|
||||
onSelected={(ref) => {
|
||||
setSelectedItems((items) => {
|
||||
const copy = items.slice();
|
||||
const index = copy.findIndex(
|
||||
(n) => n.id === ref.id && n.type === ref.type
|
||||
);
|
||||
if (index > -1) {
|
||||
copy.splice(index, 1);
|
||||
} else {
|
||||
copy.push(ref);
|
||||
<Flex
|
||||
id="subnotebooks"
|
||||
variant="columnFill"
|
||||
sx={{
|
||||
height: "80vh"
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
autoFocus
|
||||
sx={{ m: 0, mb: 2 }}
|
||||
styles={{
|
||||
input: { p: "7.5px" }
|
||||
}}
|
||||
placeholder={strings.searchNotebooks()}
|
||||
onChange={async (e) => {
|
||||
const query = e.target.value.trim();
|
||||
const ids = await (query
|
||||
? db.lookup.notebooks(query).ids()
|
||||
: db.notebooks.roots.ids(
|
||||
db.settings.getGroupOptions("notebooks")
|
||||
));
|
||||
setNotebooks(ids);
|
||||
}}
|
||||
/>
|
||||
{notebooks.length > 0 ? (
|
||||
<>
|
||||
<VirtualizedTree
|
||||
rootId={"root"}
|
||||
itemHeight={30}
|
||||
treeRef={treeRef}
|
||||
getChildNodes={async ({ id, depth }) => {
|
||||
const nodes: TreeNode<Notebook>[] = [];
|
||||
if (id === "root") {
|
||||
for (const id of notebooks) {
|
||||
const notebook = (await db.notebooks.notebook(id))!;
|
||||
const children = await db.relations
|
||||
.from(notebook, "notebook")
|
||||
.count();
|
||||
nodes.push({
|
||||
data: notebook,
|
||||
depth: depth + 1,
|
||||
hasChildren: children > 0,
|
||||
id,
|
||||
parentId: "root"
|
||||
});
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
|
||||
const subNotebooks = await db.relations
|
||||
.from({ type: "notebook", id }, "notebook")
|
||||
.resolve();
|
||||
|
||||
for (const notebook of subNotebooks) {
|
||||
const hasChildren =
|
||||
(await db.relations.from(notebook, "notebook").count()) >
|
||||
0;
|
||||
nodes.push({
|
||||
parentId: id,
|
||||
id: notebook.id,
|
||||
data: notebook,
|
||||
depth: depth + 1,
|
||||
hasChildren
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}}
|
||||
renderItem={({ item, expanded, index, collapse, expand }) => (
|
||||
<NotebookItem
|
||||
notebook={item.data}
|
||||
depth={item.depth}
|
||||
isExpandable={item.hasChildren}
|
||||
isExpanded={expanded}
|
||||
toggle={expanded ? collapse : expand}
|
||||
onCreateItem={() => {
|
||||
treeRef.current?.refreshItem(index, item.data, {
|
||||
expand: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Flex
|
||||
sx={{
|
||||
my: 2,
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Text variant="body">{strings.notebooksEmpty()}</Text>
|
||||
<Button
|
||||
data-test-id="add-new-notebook"
|
||||
variant="secondary"
|
||||
sx={{ mt: 2 }}
|
||||
onClick={
|
||||
() => {}
|
||||
// AddNotebookDialog.show({}).then((res) =>
|
||||
// res
|
||||
// ? db.notebooks.roots
|
||||
// .ids(db.settings.getGroupOptions("notebooks"))
|
||||
// .then((ids) => setNotebooks(ids))
|
||||
// : null
|
||||
// )
|
||||
}
|
||||
>
|
||||
{strings.addNotebook()}
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type NotebookProps = {
|
||||
notebook: NotebookReference;
|
||||
isSelected: boolean;
|
||||
onSelected: (notebook: SelectedReference) => void;
|
||||
};
|
||||
function Notebook(props: NotebookProps) {
|
||||
const { notebook, isSelected, onSelected } = props;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<CheckListItem
|
||||
title={notebook.title}
|
||||
isSelected={isSelected}
|
||||
onSelected={() => {
|
||||
onSelected({
|
||||
id: notebook.id,
|
||||
title: notebook.title,
|
||||
type: "notebook"
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <FilteredList
|
||||
getAll={() => notebook.topics}
|
||||
itemName="topic"
|
||||
placeholder={"Search for a topic"}
|
||||
refreshItems={() => notebook.topics}
|
||||
renderItem={(topic) => (
|
||||
<CheckListItem
|
||||
title={topic.title}
|
||||
isSelected={isTopicSelected(topic)}
|
||||
indentLevel={1}
|
||||
onSelected={() => {
|
||||
onSelected({
|
||||
id: topic.id,
|
||||
title: topic.title,
|
||||
type: "topic",
|
||||
parentId: notebook.id
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/> */}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,15 +16,23 @@ 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 { useState } from "react";
|
||||
import { Flex } from "@theme-ui/components";
|
||||
import { FilteredList } from "../filtered-list";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import { Icons } from "../icons";
|
||||
import { useAppStore } from "../../stores/app-store";
|
||||
import { Picker } from "../picker";
|
||||
import { InlineTag } from "../inline-tag";
|
||||
import { CheckListItem } from "../check-list-item";
|
||||
import { SelectedReference } from "../../common/bridge";
|
||||
import { FilteredList } from "@notesnook/web/src/components/filtered-list";
|
||||
import { Tag, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { db } from "../../common/db";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { checkFeature } from "@notesnook/web/src/common";
|
||||
import {
|
||||
SelectedCheck,
|
||||
selectMultiple,
|
||||
useSelectionStore
|
||||
} from "@notesnook/web/src/dialogs/move-note-dialog";
|
||||
import { ResolvedItem } from "@notesnook/common";
|
||||
|
||||
type TagPickerProps = {
|
||||
selectedTags: SelectedReference[];
|
||||
@@ -34,6 +42,24 @@ export const TagPicker = (props: TagPickerProps) => {
|
||||
const { selectedTags, onSelected } = props;
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [tags, setTags] = useState<VirtualizedGrouping<Tag> | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
if (!tags) {
|
||||
setTags(await db.tags.all.grouped(db.settings.getGroupOptions("tags")));
|
||||
return;
|
||||
}
|
||||
|
||||
useSelectionStore.getState().setSelected(
|
||||
selectedTags.map((t) => ({
|
||||
id: t.id,
|
||||
new: false,
|
||||
op: "add"
|
||||
}))
|
||||
);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const close = () => {
|
||||
setModalVisible(false);
|
||||
@@ -87,35 +113,78 @@ export const TagPicker = (props: TagPickerProps) => {
|
||||
}}
|
||||
isOpen={modalVisible}
|
||||
>
|
||||
<FilteredList
|
||||
getAll={() => useAppStore.getState().tags}
|
||||
filter={(items, query) =>
|
||||
items.filter((item) => item.title.toLowerCase().indexOf(query) > -1)
|
||||
}
|
||||
itemName="tag"
|
||||
placeholder={"Search for a tag"}
|
||||
refreshItems={() => useAppStore.getState().tags}
|
||||
renderItem={(tag) => (
|
||||
<CheckListItem
|
||||
title={`#${tag.title}`}
|
||||
onSelected={() => {
|
||||
const copy = selectedTags.slice();
|
||||
const index = copy.findIndex(
|
||||
(c) => c.type === "tag" && c.id === tag.id
|
||||
);
|
||||
if (index <= -1) copy.push({ ...tag, type: "tag" });
|
||||
else copy.splice(index, 1);
|
||||
onSelected(copy);
|
||||
}}
|
||||
isSelected={
|
||||
selectedTags.findIndex(
|
||||
(s) => s.type === "tag" && s.id === tag.id
|
||||
) > -1
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{tags && (
|
||||
<FilteredList
|
||||
getItemKey={(index) => tags.key(index)}
|
||||
mode="fixed"
|
||||
estimatedSize={30}
|
||||
items={tags.placeholders}
|
||||
sx={{ mt: 2 }}
|
||||
itemGap={5}
|
||||
placeholders={{
|
||||
empty: strings.addATag(),
|
||||
filter: strings.searchForTags()
|
||||
}}
|
||||
filter={async (query) => {
|
||||
setTags(
|
||||
query
|
||||
? await db.lookup.tags(query).sorted()
|
||||
: await db.tags.all.grouped(
|
||||
db.settings.getGroupOptions("tags")
|
||||
)
|
||||
);
|
||||
}}
|
||||
onCreateNewItem={async (title) => {
|
||||
if (!(await checkFeature("tags", { type: "toast" }))) return;
|
||||
|
||||
const tagId = await db.tags.add({ title });
|
||||
if (!tagId) return;
|
||||
setTags(
|
||||
await db.tags.all.grouped(db.settings.getGroupOptions("tags"))
|
||||
);
|
||||
const { selected, setSelected } = useSelectionStore.getState();
|
||||
setSelected([...selected, { id: tagId, new: true, op: "add" }]);
|
||||
}}
|
||||
renderItem={({ index }) => {
|
||||
return (
|
||||
<ResolvedItem key={index} type="tag" items={tags} index={index}>
|
||||
{({ item }) => <TagItem tag={item} />}
|
||||
</ResolvedItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function TagItem(props: { tag: Tag }) {
|
||||
const { tag } = props;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
as="li"
|
||||
data-test-id="tag"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
bg: "var(--background-secondary)",
|
||||
borderRadius: "default",
|
||||
p: 1
|
||||
}}
|
||||
onClick={() => {
|
||||
const { selected, setSelected } = useSelectionStore.getState();
|
||||
setSelected(selectMultiple(tag, selected));
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<SelectedCheck size={18} item={tag} />
|
||||
<Text className="title" data-test-id="tag-title" variant="body">
|
||||
#{tag.title}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
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 { browser } from "webextension-polyfill-ts";
|
||||
import { expose, Remote, wrap } from "comlink";
|
||||
import { createEndpoint } from "../utils/comlink-extension";
|
||||
import {
|
||||
Clip,
|
||||
Gateway,
|
||||
Server,
|
||||
WEB_EXTENSION_CHANNEL_EVENTS
|
||||
} from "../common/bridge";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var clipperBridgeConnected: boolean;
|
||||
}
|
||||
|
||||
function attachOnConnectListener() {
|
||||
if (globalThis.clipperBridgeConnected) return;
|
||||
globalThis.clipperBridgeConnected = true;
|
||||
|
||||
browser.runtime.onConnect.addListener((port) => {
|
||||
window.addEventListener("message", (ev) => {
|
||||
const { type } = ev.data;
|
||||
switch (type) {
|
||||
case WEB_EXTENSION_CHANNEL_EVENTS.ON_CREATED:
|
||||
if (ev.ports.length) {
|
||||
const mainPort = ev.ports.at(0);
|
||||
if (mainPort) {
|
||||
expose(new BackgroundGateway(), mainPort);
|
||||
const server: Remote<Server> = wrap<Server>(mainPort);
|
||||
expose(
|
||||
{
|
||||
login: () => server.login(),
|
||||
getNotes: () => server.getNotes(),
|
||||
getNotebooks: () => server.getNotebooks(),
|
||||
getTags: () => server.getTags(),
|
||||
saveClip: (clip: Clip) => server.saveClip(clip)
|
||||
},
|
||||
createEndpoint(port)
|
||||
);
|
||||
port.postMessage({ success: true });
|
||||
} else {
|
||||
port.postMessage({ success: false });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
window.postMessage({ type: WEB_EXTENSION_CHANNEL_EVENTS.ON_READY }, "*");
|
||||
});
|
||||
}
|
||||
|
||||
class BackgroundGateway implements Gateway {
|
||||
connect() {
|
||||
return {
|
||||
name: "Web clipper",
|
||||
id: "unknown-id"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
attachOnConnectListener();
|
||||
@@ -16,24 +16,35 @@ 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 "../assets/16x16.png";
|
||||
import "../assets/32x32.png";
|
||||
import "../assets/48x48.png";
|
||||
import "../assets/64x64.png";
|
||||
import "../assets/128x128.png";
|
||||
import "../assets/256x256.png";
|
||||
|
||||
import "./polyfills";
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./app";
|
||||
import "./index.css";
|
||||
import { initializeFeatureChecks } from "@notesnook/web/src/utils/feature-check";
|
||||
import { initializeDatabase } from "./common/db";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { setI18nGlobal, Messages } from "@notesnook/intl";
|
||||
|
||||
declare let module: NodeModule & {
|
||||
hot?: { accept: () => void };
|
||||
};
|
||||
const root = createRoot(document.getElementById("root")!);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
if (module.hot) module.hot.accept();
|
||||
const locale = !import.meta.env.DEV
|
||||
? import("@notesnook/intl/locales/$pseudo-LOCALE.json")
|
||||
: import("@notesnook/intl/locales/$en.json");
|
||||
locale.then(({ default: locale }) => {
|
||||
i18n.load({
|
||||
en: locale.messages as unknown as Messages
|
||||
});
|
||||
i18n.activate("en");
|
||||
|
||||
const root = createRoot(document.getElementById("root")!);
|
||||
initializeFeatureChecks().then(() =>
|
||||
initializeDatabase("db").then(() => {
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
setI18nGlobal(i18n);
|
||||
|
||||
21
extensions/web-clipper/src/polyfills.ts
Normal file
21
extensions/web-clipper/src/polyfills.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
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 { Buffer } from "buffer";
|
||||
window.Buffer = Buffer;
|
||||
@@ -17,67 +17,15 @@ 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 create from "zustand";
|
||||
import { ItemReference, NotebookReference, User } from "../common/bridge";
|
||||
import { connectApi } from "../api";
|
||||
|
||||
interface AppStore {
|
||||
isLoggedIn: boolean;
|
||||
isLoggingIn: boolean;
|
||||
user?: User;
|
||||
notes: ItemReference[];
|
||||
notebooks: NotebookReference[];
|
||||
tags: ItemReference[];
|
||||
route: string;
|
||||
|
||||
login(openNew?: boolean): Promise<void>;
|
||||
navigate(route: string): void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppStore>((set) => ({
|
||||
isLoggedIn: false,
|
||||
isLoggingIn: false,
|
||||
notebooks: [],
|
||||
notes: [],
|
||||
tags: [],
|
||||
route: "/login",
|
||||
|
||||
navigate(route) {
|
||||
set({ route });
|
||||
},
|
||||
|
||||
async login(openNew = false) {
|
||||
set({ isLoggingIn: true });
|
||||
|
||||
const notesnook = await connectApi(openNew, () => {
|
||||
set({
|
||||
user: undefined,
|
||||
isLoggedIn: false,
|
||||
isLoggingIn: false,
|
||||
notes: [],
|
||||
notebooks: [],
|
||||
tags: []
|
||||
});
|
||||
});
|
||||
|
||||
if (!notesnook) {
|
||||
set({ isLoggingIn: false });
|
||||
throw new Error(
|
||||
"Please refresh the Notesnook web app to connect with the Web Clipper."
|
||||
);
|
||||
}
|
||||
|
||||
const user = await notesnook.login();
|
||||
const notes = await notesnook.getNotes();
|
||||
const notebooks = await notesnook.getNotebooks();
|
||||
const tags = await notesnook.getTags();
|
||||
|
||||
set({
|
||||
user: user || undefined,
|
||||
isLoggedIn: true,
|
||||
isLoggingIn: false,
|
||||
notes: notes,
|
||||
notebooks: notebooks,
|
||||
tags: tags
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -19,21 +19,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Button, Flex, Image, Text } from "@theme-ui/components";
|
||||
import { useEffect, useState } from "react";
|
||||
import Logo from "../../assets/logo.svg";
|
||||
import { useStore as useUserStore } from "@notesnook/web/src/stores/user-store";
|
||||
import Field from "@notesnook/web/src/components/field";
|
||||
import { HeadlessAuth } from "@notesnook/web/src/views/auth";
|
||||
import { useAppStore } from "../stores/app-store";
|
||||
|
||||
export function Login() {
|
||||
const [error, setError] = useState<string>();
|
||||
const isLoggingIn = useAppStore((s) => s.isLoggingIn);
|
||||
const login = useAppStore((s) => s.login);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await login().catch((e) => {
|
||||
console.error(e);
|
||||
setError(e.message);
|
||||
});
|
||||
})();
|
||||
}, [login]);
|
||||
const isLoggingIn = useUserStore((store) => store.isLoggingIn);
|
||||
const navigate = useAppStore((s) => s.navigate);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -41,37 +35,33 @@ export function Login() {
|
||||
flexDirection: "column",
|
||||
m: 2,
|
||||
my: 50,
|
||||
width: 300,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
width: 300
|
||||
}}
|
||||
>
|
||||
<Image src={Logo} width={64} />
|
||||
<Text variant="heading" sx={{ textAlign: "center", mt: 2 }}>
|
||||
Notesnook Web Clipper
|
||||
</Text>
|
||||
{isLoggingIn ? (
|
||||
<Image src={Logo} width={50} sx={{ alignSelf: "center", mb: 4 }} />
|
||||
<HeadlessAuth
|
||||
route="login:email"
|
||||
isolated
|
||||
openURL={(url, context) => {
|
||||
if (context?.authenticated) {
|
||||
navigate("/");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* {isLoggingIn ? (
|
||||
<Text variant="body" sx={{ mt: 4 }}>
|
||||
Connecting with Notesnook...
|
||||
Logging you in...
|
||||
</Text>
|
||||
) : (
|
||||
<Button
|
||||
variant="accent"
|
||||
sx={{ px: 4, mt: 4, borderRadius: 100 }}
|
||||
onClick={async () =>
|
||||
await login(true).catch((e) => {
|
||||
setError(e.message);
|
||||
})
|
||||
}
|
||||
>
|
||||
Connect with Notesnook
|
||||
</Button>
|
||||
<>
|
||||
<Field />
|
||||
</>
|
||||
)}
|
||||
{error && (
|
||||
<Text variant="error" sx={{ mt: 2 }}>
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
)} */}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,10 +34,11 @@ import {
|
||||
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";
|
||||
import { useStore as useNNAppStore } from "@notesnook/web/src/stores/app-store";
|
||||
import { db } from "../common/db";
|
||||
|
||||
const ERROR_MAP: Record<string, string> = {
|
||||
"Could not establish connection. Receiving end does not exist.":
|
||||
@@ -110,9 +111,10 @@ export function Main() {
|
||||
const [error, setError] = useState<string>();
|
||||
// const [colorMode, setColorMode] = useColorMode();
|
||||
|
||||
const isPremium = useAppStore((s) => s.user?.pro);
|
||||
const navigate = useAppStore((s) => s.navigate);
|
||||
|
||||
const sync = useNNAppStore((store) => store.sync);
|
||||
const syncStatus = useNNAppStore((store) => store.syncStatus);
|
||||
const [settings] = usePersistentState<Config>(SETTINGS_KEY, DEFAULT_SETTINGS);
|
||||
const [title, setTitle] = useState<string>();
|
||||
const [hasPermission, setHasPermission] = useState<boolean>(false);
|
||||
@@ -125,7 +127,7 @@ export function Main() {
|
||||
"clipArea",
|
||||
"article"
|
||||
);
|
||||
const [note, setNote] = usePersistentState<ItemReference>("note");
|
||||
const [note, setNote] = usePersistentState<ItemReference | undefined>("note");
|
||||
const [refs, setRefs] = usePersistentState<SelectedReference[]>("refs", []);
|
||||
const [clipData, setClipData] = useState<ClipData>();
|
||||
const [clipperState, setClipperState] = useState<ClipperState>(
|
||||
@@ -149,15 +151,12 @@ export function Main() {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (
|
||||
!isPremium &&
|
||||
(clipMode === "complete" || clipMode === "screenshot")
|
||||
) {
|
||||
if (clipMode === "complete" || clipMode === "screenshot") {
|
||||
setClipMode("simplified");
|
||||
return;
|
||||
}
|
||||
})();
|
||||
}, [isPremium, clipArea, clipMode]);
|
||||
}, [clipArea, clipMode]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -180,8 +179,8 @@ export function Main() {
|
||||
await clip(clipArea, clipMode, {
|
||||
...DEFAULT_SETTINGS,
|
||||
...settings,
|
||||
images: isPremium,
|
||||
inlineImages: isPremium
|
||||
images: true,
|
||||
inlineImages: true
|
||||
})
|
||||
);
|
||||
setClipperState(ClipperState.Clipped);
|
||||
@@ -320,11 +319,7 @@ export function Main() {
|
||||
setClipperState(ClipperState.Idle);
|
||||
setClipMode(item.id);
|
||||
}}
|
||||
disabled={
|
||||
isClipping ||
|
||||
clipperState === ClipperState.Clipped ||
|
||||
(item.pro && !isPremium)
|
||||
}
|
||||
disabled={isClipping || clipperState === ClipperState.Clipped}
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderRadius: "default",
|
||||
@@ -451,7 +446,7 @@ export function Main() {
|
||||
)}
|
||||
<NotebookPicker
|
||||
selectedItems={refs?.filter((r) => r.type === "notebook") || []}
|
||||
onSelected={(items) => setRefs(items)}
|
||||
onSelected={(items) => setRefs(items || [])}
|
||||
/>
|
||||
<Box sx={{ mt: 1 }} />
|
||||
<TagPicker
|
||||
@@ -485,21 +480,21 @@ export function Main() {
|
||||
|
||||
if (!data) return;
|
||||
|
||||
const notesnook = await connectApi(false);
|
||||
if (!notesnook) {
|
||||
setError("You are not connected to Notesnook.");
|
||||
return;
|
||||
}
|
||||
await notesnook.saveClip({
|
||||
url,
|
||||
title,
|
||||
area: clipArea,
|
||||
mode: clipMode,
|
||||
note,
|
||||
refs,
|
||||
pageTitle: pageTitle.current,
|
||||
...data
|
||||
});
|
||||
// const notesnook = await connectApi(false);
|
||||
// if (!notesnook) {
|
||||
// setError("You are not connected to Notesnook.");
|
||||
// return;
|
||||
// }
|
||||
// await notesnook.saveClip({
|
||||
// url,
|
||||
// title,
|
||||
// area: clipArea,
|
||||
// mode: clipMode,
|
||||
// note,
|
||||
// refs,
|
||||
// pageTitle: pageTitle.current,
|
||||
// ...data
|
||||
// });
|
||||
|
||||
setClipData(undefined);
|
||||
|
||||
@@ -536,6 +531,21 @@ export function Main() {
|
||||
>
|
||||
<Icon path={Icons.settings} size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="icon"
|
||||
sx={{ p: 1 }}
|
||||
onClick={async () => {
|
||||
console.log(await db.user.getUser());
|
||||
await sync();
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
path={Icons.sync}
|
||||
rotate={syncStatus.key === "syncing"}
|
||||
size={16}
|
||||
/>
|
||||
{syncStatus.progress ? <Text>(${syncStatus.progress})</Text> : null}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</FlexScrollContainer>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "./dist"
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src", "global.d.ts"]
|
||||
"include": ["src", "global.d.ts", "build-utils"]
|
||||
}
|
||||
|
||||
93
extensions/web-clipper/vite.config.ts
Normal file
93
extensions/web-clipper/vite.config.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
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 { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { crx } from "@crxjs/vite-plugin";
|
||||
import path from "path";
|
||||
import { getManifest } from "./build-utils/manifest.js";
|
||||
import { version } from "./package.json";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const MANIFEST_VERSION = process.env.MANIFEST_VERSION || "2";
|
||||
const gitHash = (() => {
|
||||
try {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
} catch (e) {
|
||||
return process.env.GIT_HASH || "gitless";
|
||||
}
|
||||
})();
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react({
|
||||
jsxImportSource: "@emotion/react",
|
||||
babel: {
|
||||
plugins: ["@emotion/babel-plugin"]
|
||||
}
|
||||
}),
|
||||
crx({ manifest: getManifest(MANIFEST_VERSION) })
|
||||
],
|
||||
define: {
|
||||
APP_TITLE: `"Notesnook Web Clipper"`,
|
||||
GIT_HASH: `"${gitHash}"`,
|
||||
APP_VERSION: `"${version}"`,
|
||||
IS_DESKTOP_APP: false,
|
||||
PLATFORM: `"${process.env.PLATFORM}"`,
|
||||
IS_TESTING: false,
|
||||
IS_BETA: false,
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
process.env.NODE_ENV || "development"
|
||||
)
|
||||
},
|
||||
resolve: {
|
||||
dedupe: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"@mdi/js",
|
||||
"@mdi/react",
|
||||
"@emotion/react",
|
||||
"react-modal",
|
||||
"dayjs",
|
||||
"@streetwriters/kysely"
|
||||
]
|
||||
},
|
||||
build: {
|
||||
outDir: "build",
|
||||
emptyOutDir: true,
|
||||
target: "esnext"
|
||||
},
|
||||
worker: {
|
||||
format: "es",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: "assets/[name]-[hash:12][extname]",
|
||||
chunkFileNames: "assets/[name]-[hash:12].js",
|
||||
inlineDynamicImports: true
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3333,
|
||||
strictPort: true,
|
||||
hmr: {
|
||||
port: 3333
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -82,9 +82,13 @@ var options = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
type: "asset/resource" // ensures wasm is emitted as a file
|
||||
},
|
||||
{
|
||||
// look for .css or .scss files
|
||||
test: /\.(css|scss)$/,
|
||||
test: /\.(css)$/,
|
||||
// in the `src` directory
|
||||
use: [
|
||||
{
|
||||
@@ -92,12 +96,6 @@ var options = {
|
||||
},
|
||||
{
|
||||
loader: "css-loader"
|
||||
},
|
||||
{
|
||||
loader: "sass-loader",
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -133,7 +131,11 @@ var options = {
|
||||
alias: alias,
|
||||
extensions: fileExtensions
|
||||
.map((extension) => "." + extension)
|
||||
.concat([".js", ".jsx", ".ts", ".tsx", ".css"])
|
||||
.concat([".js", ".jsx", ".ts", ".tsx", ".css"]),
|
||||
fallback: {
|
||||
crypto: false,
|
||||
url: false
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin({
|
||||
|
||||
@@ -666,6 +666,15 @@ class UserManager {
|
||||
usesFallback: await this.usesFallbackPWHash(old_password)
|
||||
});
|
||||
|
||||
// retrieve user keys before deriving a new encryption key
|
||||
const oldUserKeys = {
|
||||
attachmentsKey: await this.getAttachmentsKey(),
|
||||
monographPasswordsKey: await this.getMonographPasswordsKey(),
|
||||
inboxKeys: (await this.hasInboxKeys())
|
||||
? await this.getInboxKeys()
|
||||
: undefined
|
||||
} as const;
|
||||
|
||||
await this.db.storage().deriveCryptoKey({
|
||||
password: new_password,
|
||||
salt
|
||||
@@ -678,27 +687,33 @@ class UserManager {
|
||||
const userEncryptionKey = await this.getEncryptionKey();
|
||||
if (userEncryptionKey) {
|
||||
const updateUserPayload: Partial<User> = {};
|
||||
const attachmentsKey = await this.getAttachmentsKey();
|
||||
if (attachmentsKey) {
|
||||
if (oldUserKeys.attachmentsKey) {
|
||||
user.attachmentsKey = await this.db
|
||||
.storage()
|
||||
.encrypt(userEncryptionKey, JSON.stringify(attachmentsKey));
|
||||
.encrypt(
|
||||
userEncryptionKey,
|
||||
JSON.stringify(oldUserKeys.attachmentsKey)
|
||||
);
|
||||
updateUserPayload.attachmentsKey = user.attachmentsKey;
|
||||
}
|
||||
const monographPasswordsKey = await this.getMonographPasswordsKey();
|
||||
if (monographPasswordsKey) {
|
||||
if (oldUserKeys.monographPasswordsKey) {
|
||||
user.monographPasswordsKey = await this.db
|
||||
.storage()
|
||||
.encrypt(userEncryptionKey, JSON.stringify(monographPasswordsKey));
|
||||
.encrypt(
|
||||
userEncryptionKey,
|
||||
JSON.stringify(oldUserKeys.monographPasswordsKey)
|
||||
);
|
||||
updateUserPayload.monographPasswordsKey = user.monographPasswordsKey;
|
||||
}
|
||||
const inboxKeys = await this.getInboxKeys();
|
||||
if (inboxKeys) {
|
||||
if (oldUserKeys.inboxKeys) {
|
||||
user.inboxKeys = {
|
||||
public: inboxKeys.publicKey,
|
||||
public: oldUserKeys.inboxKeys.publicKey,
|
||||
private: await this.db
|
||||
.storage()
|
||||
.encrypt(userEncryptionKey, JSON.stringify(inboxKeys.privateKey))
|
||||
.encrypt(
|
||||
userEncryptionKey,
|
||||
JSON.stringify(oldUserKeys.inboxKeys.privateKey)
|
||||
)
|
||||
};
|
||||
updateUserPayload.inboxKeys = user.inboxKeys;
|
||||
}
|
||||
|
||||
@@ -3,3 +3,13 @@
|
||||
exports[`collapse heading > heading collapsed 1`] = `"<h1 data-collapsed="true">Main Heading</h1><p>paragraph.</p><h2>Subheading</h2><p>subheading paragraph</p><h1>Main heading 2</h1><p>paragraph another</p>"`;
|
||||
|
||||
exports[`collapse heading > heading uncollapsed 1`] = `"<h1>Main Heading</h1><p>paragraph.</p><h2>Subheading</h2><p>subheading paragraph</p><h1>Main heading 2</h1><p>paragraph another</p>"`;
|
||||
|
||||
exports[`replacing collapsed heading with another heading level should not unhide content 1`] = `"<h2 data-collapsed="true">A collapsed heading</h2><p data-hidden="true">Hidden paragraph</p>"`;
|
||||
|
||||
exports[`replacing collapsed heading with another node (blockquote) should unhide content 1`] = `"<blockquote><h1 data-collapsed="true">A collpased heading</h1></blockquote><p>Hidden paragraph</p>"`;
|
||||
|
||||
exports[`replacing collapsed heading with another node (bulletList) should unhide content 1`] = `"<ul><li><p>A collpased heading</p></li></ul><p>Hidden paragraph</p>"`;
|
||||
|
||||
exports[`replacing collapsed heading with another node (codeBlock) should unhide content 1`] = `"<pre><code>A collpased heading</code></pre><p>Hidden paragraph</p>"`;
|
||||
|
||||
exports[`replacing collapsed heading with another node (paragraph) should unhide content 1`] = `"<p>A collpased heading</p><p>Hidden paragraph</p>"`;
|
||||
|
||||
@@ -18,8 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test, expect } from "vitest";
|
||||
import { createEditor } from "../../../../test-utils/index.js";
|
||||
import { createEditor, h } from "../../../../test-utils/index.js";
|
||||
import { Heading } from "../heading.js";
|
||||
import { Editor } from "@tiptap/core";
|
||||
|
||||
test("collapse heading", () => {
|
||||
const { editor } = createEditor({
|
||||
@@ -52,3 +53,59 @@ test("collapse heading", () => {
|
||||
|
||||
expect(editor.getHTML()).toMatchSnapshot("heading uncollapsed");
|
||||
});
|
||||
|
||||
test("replacing collapsed heading with another heading level should not unhide content", () => {
|
||||
const el = h("div", [
|
||||
h("h1", ["A collapsed heading"], { "data-collapsed": "true" }),
|
||||
h("p", ["Hidden paragraph"], { "data-hidden": "true" })
|
||||
]);
|
||||
const { editor } = createEditor({
|
||||
extensions: {
|
||||
heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] })
|
||||
},
|
||||
initialContent: el.outerHTML
|
||||
});
|
||||
|
||||
editor.commands.setTextSelection(0);
|
||||
editor.commands.setHeading({ level: 2 });
|
||||
|
||||
expect(editor.getHTML()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
const nodes: { name: string; setNode: (editor: Editor) => void }[] = [
|
||||
{
|
||||
name: "paragraph",
|
||||
setNode: (editor) => editor.commands.setParagraph()
|
||||
},
|
||||
{
|
||||
name: "codeBlock",
|
||||
setNode: (editor) => editor.commands.setCodeBlock()
|
||||
},
|
||||
{
|
||||
name: "bulletList",
|
||||
setNode: (editor) => editor.commands.toggleList("bulletList", "listItem")
|
||||
},
|
||||
{
|
||||
name: "blockquote",
|
||||
setNode: (editor) => editor.commands.toggleBlockquote()
|
||||
}
|
||||
];
|
||||
for (const { name, setNode } of nodes) {
|
||||
test(`replacing collapsed heading with another node (${name}) should unhide content`, () => {
|
||||
const el = h("div", [
|
||||
h("h1", ["A collpased heading"], { "data-collapsed": "true" }),
|
||||
h("p", ["Hidden paragraph"], { "data-hidden": "true" })
|
||||
]);
|
||||
const { editor } = createEditor({
|
||||
extensions: {
|
||||
heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] })
|
||||
},
|
||||
initialContent: el.outerHTML
|
||||
});
|
||||
|
||||
editor.commands.setTextSelection(0);
|
||||
setNode(editor);
|
||||
|
||||
expect(editor.getHTML()).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,10 +23,8 @@ import {
|
||||
textblockTypeInputRule
|
||||
} from "@tiptap/core";
|
||||
import { Heading as TiptapHeading } from "@tiptap/extension-heading";
|
||||
import { isClickWithinBounds } from "../../utils/prosemirror.js";
|
||||
import { Plugin, PluginKey, Selection, Transaction } from "@tiptap/pm/state";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
|
||||
|
||||
const COLLAPSIBLE_BLOCK_TYPES = [
|
||||
"paragraph",
|
||||
@@ -168,6 +166,14 @@ export const Heading = TiptapHeading.extend({
|
||||
addNodeView() {
|
||||
return ({ node, getPos, editor, HTMLAttributes }) => {
|
||||
const heading = document.createElement(`h${node.attrs.level}`);
|
||||
const contentWrapper = document.createElement("div");
|
||||
const icon = document.createElement("span");
|
||||
|
||||
// providing a minWidth so that empty headings show the blinking cursor
|
||||
contentWrapper.style.minWidth = "1px";
|
||||
|
||||
icon.className = "heading-collapse-icon";
|
||||
icon.contentEditable = "false";
|
||||
|
||||
for (const attr in HTMLAttributes) {
|
||||
heading.setAttribute(attr, HTMLAttributes[attr]);
|
||||
@@ -176,50 +182,46 @@ export const Heading = TiptapHeading.extend({
|
||||
if (node.attrs.collapsed) heading.dataset.collapsed = "true";
|
||||
else delete heading.dataset.collapsed;
|
||||
|
||||
function onClick(e: MouseEvent | TouchEvent) {
|
||||
if (e instanceof MouseEvent && e.button !== 0) return;
|
||||
if (!(e.target instanceof HTMLHeadingElement)) return;
|
||||
function onIconClick(e: MouseEvent | TouchEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||
if (typeof pos !== "number") return;
|
||||
|
||||
const resolvedPos = editor.state.doc.resolve(pos);
|
||||
const calloutAncestor = findParentNodeClosestToPos(
|
||||
resolvedPos,
|
||||
(node) => node.type.name === "callout"
|
||||
);
|
||||
if (calloutAncestor) return;
|
||||
|
||||
const forbiddenParents = ["callout"];
|
||||
if (
|
||||
isClickWithinBounds(
|
||||
e,
|
||||
resolvedPos,
|
||||
useToolbarStore.getState().isMobile ? "right" : "left"
|
||||
findParentNodeClosestToPos(resolvedPos, (node) =>
|
||||
forbiddenParents.includes(node.type.name)
|
||||
)
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
editor.commands.command(({ tr }) => {
|
||||
const currentNode = tr.doc.nodeAt(pos);
|
||||
if (currentNode && currentNode.type.name === "heading") {
|
||||
const shouldCollapse = !currentNode.attrs.collapsed;
|
||||
const headingLevel = currentNode.attrs.level;
|
||||
|
||||
tr.setNodeAttribute(pos, "collapsed", shouldCollapse);
|
||||
toggleNodesUnderHeading(tr, pos, headingLevel, shouldCollapse);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
editor.commands.command(({ tr }) => {
|
||||
const currentNode = tr.doc.nodeAt(pos);
|
||||
if (currentNode && currentNode.type.name === "heading") {
|
||||
const shouldCollapse = !currentNode.attrs.collapsed;
|
||||
const headingLevel = currentNode.attrs.level;
|
||||
|
||||
tr.setNodeAttribute(pos, "collapsed", shouldCollapse);
|
||||
toggleNodesUnderPos(tr, pos, headingLevel, shouldCollapse);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
heading.onmousedown = onClick;
|
||||
heading.ontouchstart = onClick;
|
||||
icon.onmousedown = onIconClick;
|
||||
icon.ontouchend = onIconClick;
|
||||
|
||||
heading.appendChild(contentWrapper);
|
||||
heading.appendChild(icon);
|
||||
|
||||
return {
|
||||
dom: heading,
|
||||
contentDOM: heading,
|
||||
contentDOM: contentWrapper,
|
||||
update: (updatedNode) => {
|
||||
if (updatedNode.type !== this.type) {
|
||||
return false;
|
||||
@@ -253,17 +255,17 @@ export const Heading = TiptapHeading.extend({
|
||||
}
|
||||
});
|
||||
|
||||
function toggleNodesUnderHeading(
|
||||
function toggleNodesUnderPos(
|
||||
tr: Transaction,
|
||||
headingPos: number,
|
||||
pos: number,
|
||||
headingLevel: number,
|
||||
isCollapsing: boolean
|
||||
) {
|
||||
const { doc } = tr;
|
||||
const headingNode = doc.nodeAt(headingPos);
|
||||
if (!headingNode || headingNode.type.name !== "heading") return;
|
||||
const node = doc.nodeAt(pos);
|
||||
if (!node) return;
|
||||
|
||||
let nextPos = headingPos + headingNode.nodeSize;
|
||||
let nextPos = pos + node.nodeSize;
|
||||
const cursorPos = tr.selection.from;
|
||||
let shouldMoveCursor = false;
|
||||
let insideCollapsedHeading = false;
|
||||
@@ -318,8 +320,8 @@ function toggleNodesUnderHeading(
|
||||
}
|
||||
|
||||
if (shouldMoveCursor) {
|
||||
const headingEndPos = headingPos + headingNode.nodeSize - 1;
|
||||
tr.setSelection(Selection.near(tr.doc.resolve(headingEndPos)));
|
||||
const endPos = pos + node.nodeSize - 1;
|
||||
tr.setSelection(Selection.near(tr.doc.resolve(endPos)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,26 +366,27 @@ const headingUpdatePlugin = new Plugin({
|
||||
let modified = false;
|
||||
|
||||
newDoc.descendants((newNode, pos) => {
|
||||
if (newNode.type.name === "heading") {
|
||||
if (pos >= oldDoc.content.size) return;
|
||||
if (pos >= oldDoc.content.size) return;
|
||||
|
||||
const oldNode = oldDoc.nodeAt(pos);
|
||||
if (
|
||||
oldNode &&
|
||||
oldNode.type.name === "heading" &&
|
||||
oldNode.attrs.level !== newNode.attrs.level
|
||||
) {
|
||||
/**
|
||||
* if the level of a collapsed heading is changed,
|
||||
* we need to reset visibility of all the nodes under it as there
|
||||
* might be a heading of same or higher level previously
|
||||
* hidden under this heading
|
||||
*/
|
||||
if (newNode.attrs.collapsed) {
|
||||
toggleNodesUnderHeading(tr, pos, oldNode.attrs.level, false);
|
||||
toggleNodesUnderHeading(tr, pos, newNode.attrs.level, true);
|
||||
modified = true;
|
||||
}
|
||||
const oldNode = oldDoc.nodeAt(pos);
|
||||
if (
|
||||
oldNode &&
|
||||
oldNode.type.name === "heading" &&
|
||||
oldNode.attrs.level !== newNode.attrs.level
|
||||
) {
|
||||
/**
|
||||
* if the level of a collapsed heading is changed,
|
||||
* we need to reset visibility of all the nodes under it as there
|
||||
* might be a heading of same or higher level previously
|
||||
* hidden under this heading
|
||||
*/
|
||||
if (newNode.type.name === "heading" && newNode.attrs.collapsed) {
|
||||
toggleNodesUnderPos(tr, pos, oldNode.attrs.level, false);
|
||||
toggleNodesUnderPos(tr, pos, newNode.attrs.level, true);
|
||||
modified = true;
|
||||
} else if (newNode.type.name !== "heading" && oldNode.attrs.collapsed) {
|
||||
toggleNodesUnderPos(tr, pos, oldNode.attrs.level, false);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -316,6 +316,31 @@ img.ProseMirror-separator {
|
||||
.ProseMirror table p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ProseMirror td > h1:first-child,
|
||||
.ProseMirror td > h2:first-child,
|
||||
.ProseMirror td > h3:first-child,
|
||||
.ProseMirror td > h4:first-child,
|
||||
.ProseMirror td > h5:first-child,
|
||||
.ProseMirror td > h6:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ProseMirror td > ol,
|
||||
.ProseMirror td > ul {
|
||||
padding-left: 20px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ProseMirror td > blockquote {
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ProseMirror td > blockquote > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
.resize-cursor {
|
||||
|
||||
@@ -739,6 +764,10 @@ p > *::selection {
|
||||
mask-size: cover;
|
||||
}
|
||||
|
||||
.simple-checklist > li.checked p {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Callout */
|
||||
.ProseMirror div.callout {
|
||||
padding: 15px;
|
||||
@@ -866,150 +895,133 @@ del.diffdel {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ProseMirror h1,
|
||||
.ProseMirror h2,
|
||||
.ProseMirror h3,
|
||||
.ProseMirror h4,
|
||||
.ProseMirror h5,
|
||||
.ProseMirror h1 ,
|
||||
.ProseMirror h2 ,
|
||||
.ProseMirror h3 ,
|
||||
.ProseMirror h4 ,
|
||||
.ProseMirror h5 ,
|
||||
.ProseMirror h6 {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ProseMirror h1::before,
|
||||
.ProseMirror h2::before,
|
||||
.ProseMirror h3::before,
|
||||
.ProseMirror h4::before,
|
||||
.ProseMirror h5::before,
|
||||
.ProseMirror h6::before {
|
||||
position: absolute;
|
||||
.ProseMirror h1 .heading-collapse-icon,
|
||||
.ProseMirror h2 .heading-collapse-icon,
|
||||
.ProseMirror h3 .heading-collapse-icon,
|
||||
.ProseMirror h4 .heading-collapse-icon,
|
||||
.ProseMirror h5 .heading-collapse-icon,
|
||||
.ProseMirror h6 .heading-collapse-icon {
|
||||
cursor: pointer;
|
||||
content: "";
|
||||
background-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-inline-start: 8px;
|
||||
|
||||
background-color: var(--icon);
|
||||
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik03LjQxIDguNThMMTIgMTMuMTdsNC41OS00LjU5TDE4IDEwbC02IDZsLTYtNmwxLjQxLTEuNDJaIi8+PC9zdmc+)
|
||||
no-repeat 50% 50%;
|
||||
mask-size: cover;
|
||||
border: 1px solid var(--background);
|
||||
|
||||
transform: rotate(0);
|
||||
transition: transform 250ms ease, opacity 200ms ease;
|
||||
left: -22px;
|
||||
opacity: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ProseMirror h1[dir="rtl"]::before,
|
||||
.ProseMirror h2[dir="rtl"]::before,
|
||||
.ProseMirror h3[dir="rtl"]::before,
|
||||
.ProseMirror h4[dir="rtl"]::before,
|
||||
.ProseMirror h5[dir="rtl"]::before,
|
||||
.ProseMirror h6[dir="rtl"]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ProseMirror h1[dir="rtl"]::after,
|
||||
.ProseMirror h2[dir="rtl"]::after,
|
||||
.ProseMirror h3[dir="rtl"]::after,
|
||||
.ProseMirror h4[dir="rtl"]::after,
|
||||
.ProseMirror h5[dir="rtl"]::after,
|
||||
.ProseMirror h6[dir="rtl"]::after {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
content: "";
|
||||
background-size: 18px;
|
||||
.ProseMirror h1 .heading-collapse-icon {
|
||||
margin-top: 3.5px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
background-color: var(--icon);
|
||||
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik03LjQxIDguNThMMTIgMTMuMTdsNC41OS00LjU5TDE4IDEwbC02IDZsLTYtNmwxLjQxLTEuNDJaIi8+PC9zdmc+)
|
||||
no-repeat 50% 50%;
|
||||
mask-size: cover;
|
||||
border: 1px solid var(--background);
|
||||
|
||||
transform: rotate(0deg);
|
||||
transition: transform 250ms ease, opacity 200ms ease;
|
||||
right: -22px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ProseMirror h1::before,
|
||||
.ProseMirror h1::after {
|
||||
top: 8px;
|
||||
.ProseMirror h2 .heading-collapse-icon {
|
||||
margin-top: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.ProseMirror h2::before,
|
||||
.ProseMirror h2::after
|
||||
{
|
||||
top: 3px;
|
||||
.ProseMirror h3 .heading-collapse-icon {
|
||||
margin-top: 2.3px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.ProseMirror h3::before,
|
||||
.ProseMirror h3::after {
|
||||
top: 0px;
|
||||
.ProseMirror h4 .heading-collapse-icon {
|
||||
margin-top: 1.8px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.ProseMirror h4::before,
|
||||
.ProseMirror h4::after {
|
||||
top: -1px;
|
||||
.ProseMirror h5 .heading-collapse-icon {
|
||||
margin-top: 1.3px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.ProseMirror h5::before,
|
||||
.ProseMirror h5::after {
|
||||
top: -2px;
|
||||
.ProseMirror h6 .heading-collapse-icon {
|
||||
margin-top: 0.3px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.ProseMirror h6::before,
|
||||
.ProseMirror h6::after {
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
.ProseMirror h1[data-collapsed="true"]::before,
|
||||
.ProseMirror h2[data-collapsed="true"]::before,
|
||||
.ProseMirror h3[data-collapsed="true"]::before,
|
||||
.ProseMirror h4[data-collapsed="true"]::before,
|
||||
.ProseMirror h5[data-collapsed="true"]::before,
|
||||
.ProseMirror h6[data-collapsed="true"]::before {
|
||||
.ProseMirror h1[data-collapsed="true"] .heading-collapse-icon,
|
||||
.ProseMirror h2[data-collapsed="true"] .heading-collapse-icon,
|
||||
.ProseMirror h3[data-collapsed="true"] .heading-collapse-icon,
|
||||
.ProseMirror h4[data-collapsed="true"] .heading-collapse-icon,
|
||||
.ProseMirror h5[data-collapsed="true"] .heading-collapse-icon,
|
||||
.ProseMirror h6[data-collapsed="true"] .heading-collapse-icon {
|
||||
transform: rotate(-90deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ProseMirror h1[data-collapsed="true"]::after,
|
||||
.ProseMirror h2[data-collapsed="true"]::after,
|
||||
.ProseMirror h3[data-collapsed="true"]::after,
|
||||
.ProseMirror h4[data-collapsed="true"]::after,
|
||||
.ProseMirror h5[data-collapsed="true"]::after,
|
||||
.ProseMirror h6[data-collapsed="true"]::after {
|
||||
.ProseMirror h1[data-collapsed="true"][dir="rtl"] .heading-collapse-icon,
|
||||
.ProseMirror h2[data-collapsed="true"][dir="rtl"] .heading-collapse-icon,
|
||||
.ProseMirror h3[data-collapsed="true"][dir="rtl"] .heading-collapse-icon,
|
||||
.ProseMirror h4[data-collapsed="true"][dir="rtl"] .heading-collapse-icon,
|
||||
.ProseMirror h5[data-collapsed="true"][dir="rtl"] .heading-collapse-icon,
|
||||
.ProseMirror h6[data-collapsed="true"][dir="rtl"] .heading-collapse-icon {
|
||||
transform: rotate(90deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.ProseMirror h1:hover::before,
|
||||
.ProseMirror h2:hover::before,
|
||||
.ProseMirror h3:hover::before,
|
||||
.ProseMirror h4:hover::before,
|
||||
.ProseMirror h5:hover::before,
|
||||
.ProseMirror h6:hover::before,
|
||||
.ProseMirror h1:hover::after,
|
||||
.ProseMirror h2:hover::after,
|
||||
.ProseMirror h3:hover::after,
|
||||
.ProseMirror h4:hover::after,
|
||||
.ProseMirror h5:hover::after,
|
||||
.ProseMirror h6:hover::after {
|
||||
.ProseMirror h1:hover .heading-collapse-icon,
|
||||
.ProseMirror h2:hover .heading-collapse-icon,
|
||||
.ProseMirror h3:hover .heading-collapse-icon,
|
||||
.ProseMirror h4:hover .heading-collapse-icon,
|
||||
.ProseMirror h5:hover .heading-collapse-icon,
|
||||
.ProseMirror h6:hover .heading-collapse-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ProseMirror div.callout h1::before,
|
||||
.ProseMirror div.callout h2::before,
|
||||
.ProseMirror div.callout h3::before,
|
||||
.ProseMirror div.callout h4::before,
|
||||
.ProseMirror div.callout h5::before,
|
||||
.ProseMirror div.callout h6::before {
|
||||
.ProseMirror div.callout h1 .heading-collapse-icon,
|
||||
.ProseMirror div.callout h2 .heading-collapse-icon,
|
||||
.ProseMirror div.callout h3 .heading-collapse-icon,
|
||||
.ProseMirror div.callout h4 .heading-collapse-icon,
|
||||
.ProseMirror div.callout h5 .heading-collapse-icon,
|
||||
.ProseMirror div.callout h6 .heading-collapse-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* hide collapse icon when heading is empty (only contains trailing break) */
|
||||
.ProseMirror h1:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon,
|
||||
.ProseMirror h2:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon,
|
||||
.ProseMirror h3:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon,
|
||||
.ProseMirror h4:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon,
|
||||
.ProseMirror h5:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon,
|
||||
.ProseMirror h6:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.ProseMirror h1 .heading-collapse-icon,
|
||||
.ProseMirror h2 .heading-collapse-icon,
|
||||
.ProseMirror h3 .heading-collapse-icon,
|
||||
.ProseMirror h4 .heading-collapse-icon,
|
||||
.ProseMirror h5 .heading-collapse-icon,
|
||||
.ProseMirror h6 .heading-collapse-icon {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-hidden="true"] {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -1028,3 +1040,4 @@ del.diffdel {
|
||||
pre[class*="language-"] {
|
||||
overflow: initial !important;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user