Compare commits
31 Commits
1.8.9-andr
...
2.1.8-andr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
286b2b39be | ||
|
|
6eb383c75a | ||
|
|
f6a93061fd | ||
|
|
96246a1e33 | ||
|
|
1f38f67871 | ||
|
|
203d7ad881 | ||
|
|
725fe41db2 | ||
|
|
6f6fc52c47 | ||
|
|
2e74f807d4 | ||
|
|
eb02332f37 | ||
|
|
829c27d456 | ||
|
|
b08abcf943 | ||
|
|
cddd6f3f5b | ||
|
|
8f90bbef59 | ||
|
|
dc621a2483 | ||
|
|
d5f9fb10d4 | ||
|
|
85efebbaca | ||
|
|
2fcd9d26e6 | ||
|
|
61bd7552ae | ||
|
|
8128f827a3 | ||
|
|
1b3b760b53 | ||
|
|
ffd7501e80 | ||
|
|
9f5d61250b | ||
|
|
a71b5a96f2 | ||
|
|
81bb053513 | ||
|
|
63796d5896 | ||
|
|
19a1ea2236 | ||
|
|
c72ce1ee69 | ||
|
|
ba88f28ae7 | ||
|
|
80186fd1cd | ||
|
|
ec07cab365 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Bug Report
|
||||
description: Are you facing a bug or a crash in Notesnook?
|
||||
labels: ["Type: Bug", "Status: Pending"]
|
||||
assignees:
|
||||
- thecodrr
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Tell us what you were trying to do & what happened
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction-steps
|
||||
attributes:
|
||||
label: Steps to reproduce the problem
|
||||
description: If we can reproduce it, we can fix it
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of Notesnook are you using?
|
||||
placeholder: v1.8.10 etc
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform/OS
|
||||
description: What platform are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
- iOS
|
||||
- Android
|
||||
- Chrome/Chromium
|
||||
- Firefox
|
||||
- Safari
|
||||
- Other browser
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Notesnook Discord Community
|
||||
url: https://discord.gg/6mHHyncHJE
|
||||
about: Reach out to us directly & discuss your issues, suggestions & other feedback!
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Feature request
|
||||
description: Suggest a feature you are missing in Notesnook
|
||||
labels: ["Type: Feature Request", "Status: Pending"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to suggest this idea!
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What problem are you facing?
|
||||
description: Also tell us how this idea will fix the problem & what's the use case.
|
||||
placeholder: e.g. I'm always frustrated when...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: What's the solution you'd like to see in Notesnook?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: What alternatives have you considered?
|
||||
- type: textarea
|
||||
id: general
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
3
.github/workflows/enex-tests.yml
vendored
@@ -27,6 +27,7 @@ jobs:
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: |
|
||||
cd packages/enex
|
||||
npm i
|
||||
npx turbo run build
|
||||
cd packages/enex
|
||||
npm run test
|
||||
|
||||
3
.github/workflows/importer-tests.yml
vendored
@@ -27,6 +27,7 @@ jobs:
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: |
|
||||
cd packages/importer
|
||||
npm i
|
||||
npx turbo run build
|
||||
cd packages/importer
|
||||
npm run test
|
||||
|
||||
72
README.md
@@ -1,50 +1,15 @@
|
||||
<p align="center">
|
||||
|
||||
<img style="align:center;" src="https://app.notesnook.com/apple-touch-icon.png" alt="Notesnook Logo" width="100" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Notesnook</h1>
|
||||
<h2 align="center">Take private notes, capture ideas, make lists & sync them anywhere</h2>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/5davZnhw3V"><img alt="Chat with us on Discord" src="https://img.shields.io/discord/796015620436787241?label=Chat%20with%20us&logo=discord&logoColor=white"/></a>
|
||||
<a href="https://GitHub.com/streetwriters/notesnook/issues/"><img src="https://img.shields.io/github/issues/streetwriters/notesnook.svg"/></a>
|
||||
<a href="https://twitter.com/notesnook"><img alt="Follow us on Twitter @notesnook" src="https://img.shields.io/twitter/follow/notesnook?label=Follow%20us%20%40notesnook&logo=twitter&style=social"></a>
|
||||
<a href="https://notesnook.com/">Website</a> | <a href="https://notesnook.com/roadmap">Roadmap</a> | <a href="https://notesnook.com/downloads">Downloads</a> | <a href="https://twitter.com/@notesnook">Twitter</a> | <a href="https://discord.gg/5davZnhw3V">Discord</a>
|
||||
</p>
|
||||
|
||||
<p align="center"><img src="https://github.com/streetwriters/notesnook/raw/main/banners/crossplatform.jpg" alt="Notesnook - Take private notes" width="75%"/></p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://play.google.com/store/apps/details?id=com.streetwriters.notesnook">
|
||||
<img alt="Get it on Google Play" src="https://github.com/streetwriters/notesnook/raw/main/badges/google.png" height="50px">
|
||||
</a>
|
||||
<a href="https://apps.apple.com/pk/app/notesnook-take-private-notes/id1544027013">
|
||||
<img alt="Download on the App Store" src="https://notesnook.com/static//apple.09f3b62d.png" height="50px">
|
||||
</a>
|
||||
<a href="https://app.notesnook.com/">
|
||||
<img alt="Try Notesnook on the Web" src="https://github.com/streetwriters/notesnook/raw/main/badges/web.png" height="50px" style="margin-left: 15px">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 😎 Features
|
||||
|
||||
Privacy isn't the only thing we have:
|
||||
|
||||
1. Cross platform - Notesnook has apps on all platforms
|
||||
2. End to end encrypted sync to unlimted devices
|
||||
3. Organize your notes using:
|
||||
1. Notebooks
|
||||
2. Topics
|
||||
3. Tags
|
||||
4. Colors
|
||||
4. Encrypt/lock your notes locally
|
||||
5. Full rich text editor with markdown support
|
||||
6. Focus mode (distraction-free mode)
|
||||
7. Export notes as HTML, MD, PDF & TXT
|
||||
8. Automatic backups
|
||||
9. Dark mode and custom accents
|
||||
|
||||
**_Note: You can checkout a preview of all these features on [the website](https://notesnook.com/)._**
|
||||
> ## Announcement:
|
||||
>
|
||||
> ### We will be open sourcing Notesnook in July-August, 2022. Follow our [Twitter](https://twitter.com/@notesnook) for updates.
|
||||
|
||||
## Support
|
||||
|
||||
@@ -55,11 +20,11 @@ You can reach out to us via:
|
||||
3. [Twitter](https://twitter.com/notesnook)
|
||||
4. [Create an issue](https://github.com/streetwriters/notesnook/issues/new)
|
||||
|
||||
We take all queries, issues, bug reports that you might have. Feel free to ask.
|
||||
We take all queries, issues and bug reports that you might have. Feel free to ask.
|
||||
|
||||
## 🛣️ Roadmap
|
||||
|
||||
This is just the beginning; we are not stopping here. **Read what we have planned for Notesnook in [our roadmap](https://github.com/streetwriters/notesnook/blob/main/ROADMAP.md).**
|
||||
This is just the beginning; we are not stopping here. **Read what we have planned for Notesnook in [our roadmap](https://notesnook.com/roadmap).**
|
||||
|
||||
## ❓ FAQs
|
||||
|
||||
@@ -69,8 +34,8 @@ There are [many](https://en.wikipedia.org/wiki/Comparison_of_note-taking_softwar
|
||||
|
||||
These are the 4 problems that Notesnook solves:
|
||||
|
||||
1. No compromise on security and user privacy
|
||||
2. No sacrifice on important note taking features (e.g. note locking, PDF exports, organize via notebooks, topics & tags)
|
||||
1. No compromise on security and user privacy.
|
||||
2. No sacrifice on important note taking features (e.g. note locking, PDF exports, organize via notebooks, topics & tags).
|
||||
3. Provides a simple, easy to use user interface.
|
||||
4. Works exactly the same on all platforms.
|
||||
|
||||
@@ -78,13 +43,11 @@ At Notesnook we believe in `simple privacy`. Simple privacy means giving you ful
|
||||
|
||||
### If you are so privacy focused, why did you not make Notesnook open source?
|
||||
|
||||
Notesnook is developed & maintained by a small team (only 3 people). The effort required to make it "open source-able" (writing good docs, managing pull requests etc) is currently out of our scope. We want to make a good product and ship it to you as fast as possible.
|
||||
|
||||
However, we have plans to open source the core parts of Notesnook (like the auth, encryption, storage etc.) that'll be reusable and auditable by the public.
|
||||
Notesnook is going open source in July-August 2022 (subject to change).
|
||||
|
||||
### Can I self host Notesnook?
|
||||
|
||||
Nope. Not right now. We might make the sync server available for self-hosting in the future (after certain procedures) but as of now, no.
|
||||
Nope. Not right now. We have plans to make the sync server available for self-hosting in the future (after certain procedures) after the client apps are open sourced.
|
||||
|
||||
### What happens to my notes if Notesnook is abandoned?
|
||||
|
||||
@@ -116,10 +79,10 @@ Anyone offering privacy for free is scamming you to sell your data. Why? Because
|
||||
`$4.49/mo` is worth it because:
|
||||
|
||||
1. It is cheaper than all other premium note taking apps (like Evernote, Standard Notes).
|
||||
2. It is the minimum viable price you can spend to ensure your notes' privacy. (If you self host, the cheapest VPS will cost you around $5).
|
||||
2. It is the minimum viable price you can spend to ensure your notes' privacy. (If you self host, the cheapest VPS will cost you around \$5).
|
||||
3. Your subscription is evidence that privacy is a global issue and should be addressed. That you care.
|
||||
|
||||
If you still think $4.49/mo is too much, you are welcome to spend >$4.49/mo on some other privacy invasive note taking app. :)
|
||||
If you still think $4.49/mo is too much, you are welcome to spend >$4.49/mo on some other privacy invasive note taking app :)
|
||||
|
||||
## Contribute
|
||||
|
||||
@@ -135,18 +98,13 @@ Bug reports are given utmost priority. Report it and we'll fix it ASAP.
|
||||
|
||||
### Join our Discord server & help out in the community
|
||||
|
||||
You can join [our Discord server](https://discord.gg/5davZnhw3V) and help other Notesnook users. We also post announcements, host polls, ask for early feedback there.
|
||||
You can join [our Discord server](https://discord.gg/5davZnhw3V) and help other Notesnook users. We also post announcements, host polls and ask for early feedback there.
|
||||
|
||||
### Help us out by giving social proof
|
||||
|
||||
Social proof helps other people know what they are getting. If you have tried out Notesnook and loved it, you can give social proof:
|
||||
|
||||
1. By tweeting about @notesnook on Twitter
|
||||
1. By tweeting about @notesnook on Twitter.
|
||||
2. Posting on Facebook, Reddit or any other social media.
|
||||
|
||||
We take all social proofs and publish them on the website for the public to see.
|
||||
|
||||
---
|
||||
|
||||
<sup>1</sup>Google Play and the Google Play logo are trademarks of Google LLC.<br/>
|
||||
<sup>2</sup>Apple logo® is a trademark of Apple Inc.
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
- [ ] **Two-Factor-Authentication** via Auth app & SMS.
|
||||
- [ ] **Stylus & Drawing Support** to take notes by drawing on the screen. We will be adding a dedicated editor where you can draw and take notes.
|
||||
- [ ] **Note Publishing** on a public url that is viewable by anyone with a shareable link. The note can be encrypted if required before publishing so only people with the password can view it.
|
||||
- [ ] **Links:** A dedicated links screen where user can list all links and import browser bookmarks
|
||||
- [ ] **Links** - A dedicated links screen where user can list all links and import browser bookmarks.
|
||||
- [ ] **Widgets on Android & iOS** - Initially we are working on adding a Quick Note widget to add notes without opening app.
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
1. 🥳 Added OneNote support! 🥳
|
||||
2. 🥳 Added Zoho Notebook support! 🥳
|
||||
1. 🥳 Added Joplin support! 🥳
|
||||
2. Optimize for mobile screens
|
||||
3. Add very basic telemetry (only sends the provider used)
|
||||
4. Fixed timestamp issues with Standard Notes provider
|
||||
5. Fixed issue where importer would always open in dark mode
|
||||
29301
apps/importer/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/importer-web",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.3",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
@@ -19,7 +19,7 @@
|
||||
"typescript": "^4.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@notesnook/importer": "^1.2.3",
|
||||
"@notesnook/importer": "^1.3.3",
|
||||
"colord": "^2.9.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"hash-wasm": "^4.9.0",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flex } from "@theme-ui/components";
|
||||
import { ThemeProvider } from "@theme-ui/theme-provider";
|
||||
import "./App.css";
|
||||
import { ThemeFactory } from "./theme";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { StepSeperator } from "./components/StepSeperator";
|
||||
import { ProviderSelector } from "./components/ProviderSelector";
|
||||
import { FileProviderHandler } from "./components/FileProviderHandler";
|
||||
@@ -12,11 +12,23 @@ import { Footer } from "./components/Footer";
|
||||
import { IProvider } from "@notesnook/importer";
|
||||
import { NetworkProviderHandler } from "./components/NetworkProviderHandler";
|
||||
import { ProviderResult } from "@notesnook/importer/dist/src/providers/provider";
|
||||
import { trackEvent } from "./utils/analytics";
|
||||
|
||||
function App() {
|
||||
const [selectedProvider, setSelectedProvider] = useState<IProvider>();
|
||||
const [providerResult, setProviderResult] = useState<ProviderResult>();
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProvider && providerResult) {
|
||||
(async () => {
|
||||
await trackEvent(
|
||||
{ name: selectedProvider.name, type: "event" },
|
||||
selectedProvider.name
|
||||
);
|
||||
})();
|
||||
}
|
||||
}, [providerResult, selectedProvider]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={ThemeFactory.construct()}>
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
@@ -27,7 +39,12 @@ function App() {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ProviderSelector onProviderChanged={setSelectedProvider} />
|
||||
<ProviderSelector
|
||||
onProviderChanged={(provider) => {
|
||||
setSelectedProvider(provider);
|
||||
setProviderResult(undefined);
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectedProvider ? (
|
||||
<>
|
||||
|
||||
@@ -49,7 +49,7 @@ export function FileProviderHandler(props: FileProviderHandlerProps) {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!provider || !files.length) return;
|
||||
if (!files.length) return;
|
||||
|
||||
setProgress({ total: 0, done: 0 });
|
||||
let result: ProviderResult = { notes: [], errors: [] };
|
||||
@@ -71,7 +71,11 @@ export function FileProviderHandler(props: FileProviderHandlerProps) {
|
||||
setProgress({ total: 0, done: 0 });
|
||||
onTransformFinished(result);
|
||||
})();
|
||||
}, [provider, files, onTransformFinished]);
|
||||
|
||||
// Disabled because we do not want to cause an update when provider changes
|
||||
// which only happens when the user selects a different provider.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [files, onTransformFinished]);
|
||||
|
||||
return (
|
||||
<StepContainer sx={{ flexDirection: "column", alignItems: "stretch" }}>
|
||||
|
||||
@@ -15,7 +15,9 @@ export function Footer() {
|
||||
}}
|
||||
>
|
||||
<Text variant="body">Notesnook Importer (v{appVersion})</Text>
|
||||
<Text variant="subBody">© 2021 Streetwriters LLC</Text>
|
||||
<Text variant="subBody">
|
||||
© {new Date().getFullYear()} Streetwriters LLC
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Flex, Link, Text } from "@theme-ui/components";
|
||||
import { Checkbox, Flex, Label, Link, Text } from "@theme-ui/components";
|
||||
import { useTelemetry } from "../utils/analytics";
|
||||
import { appVersion } from "../utils/version";
|
||||
|
||||
export function Hero() {
|
||||
const { isEnabled, setIsEnabled } = useTelemetry();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
mt: 150,
|
||||
mb: 100,
|
||||
mt: [50, 150],
|
||||
mb: [50, 100],
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
@@ -20,34 +23,52 @@ export function Hero() {
|
||||
fontSize: "title",
|
||||
textAlign: "center",
|
||||
color: "fontTertiary",
|
||||
mt: [2, 0],
|
||||
}}
|
||||
>
|
||||
Import your notes from any notes app into Notesnook.
|
||||
</Text>
|
||||
<Flex sx={{ mt: 2 }}>
|
||||
<Text variant="body" sx={{ px: 1 }}>
|
||||
<Flex sx={{ mt: 2, alignItems: "center" }}>
|
||||
<Text variant="body" sx={{ px: 2 }}>
|
||||
v{appVersion}
|
||||
</Text>
|
||||
<Link
|
||||
href="https://github.com/streetwriters/notesnook"
|
||||
variant="text.body"
|
||||
sx={{ px: 1, borderLeft: "1px solid var(--theme-ui-colors-border)" }}
|
||||
sx={{ px: 2, borderLeft: "1px solid var(--theme-ui-colors-border)" }}
|
||||
>
|
||||
Github
|
||||
</Link>
|
||||
<Link
|
||||
href="https://app.notesnook.com/"
|
||||
variant="text.body"
|
||||
sx={{ px: 1, borderLeft: "1px solid var(--theme-ui-colors-border)" }}
|
||||
sx={{ px: 2, borderLeft: "1px solid var(--theme-ui-colors-border)" }}
|
||||
>
|
||||
Notesnook Web
|
||||
</Link>
|
||||
<Label
|
||||
variant="text.body"
|
||||
sx={{
|
||||
width: "auto",
|
||||
alignItems: "center",
|
||||
px: 2,
|
||||
borderLeft: "1px solid var(--theme-ui-colors-border)",
|
||||
}}
|
||||
title={`Currently the only thing sent is which provider you used after a successful import. This data is completely anonymous, of course.`}
|
||||
>
|
||||
<Checkbox
|
||||
sx={{ mr: 1 }}
|
||||
checked={isEnabled}
|
||||
onChange={() => setIsEnabled((s) => !s)}
|
||||
/>
|
||||
Telemetry {isEnabled ? "enabled" : "disabled"}
|
||||
</Label>
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{
|
||||
bg: "bgSecondary",
|
||||
flexDirection: "column",
|
||||
width: 400,
|
||||
width: ["90%", 400],
|
||||
mt: 4,
|
||||
p: 2,
|
||||
borderRadius: "default",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { Accordion } from "./Accordion";
|
||||
import { TransformError } from "@notesnook/importer/dist/src/utils/transformerror";
|
||||
|
||||
type ImportErrorsProps = {
|
||||
errors: TransformError[];
|
||||
errors: Error[];
|
||||
};
|
||||
|
||||
export function ImportErrors(props: ImportErrorsProps) {
|
||||
@@ -13,14 +12,13 @@ export function ImportErrors(props: ImportErrorsProps) {
|
||||
sx={{ bg: "errorBg", borderRadius: "default", mt: 2 }}
|
||||
color="#E53935"
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column", px: 2, pb: 2 }}>
|
||||
<Flex sx={{ flexDirection: "column", px: 2, pb: 2, overflowX: "auto" }}>
|
||||
{props.errors.map((error, index) => (
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ color: "error", my: 1, fontFamily: "monospace" }}
|
||||
>
|
||||
{index + 1}. {error.message}{" "}
|
||||
{error.file ? `(${error.file.name})` : ""}
|
||||
{index + 1}. {error.message}
|
||||
<br />
|
||||
</Text>
|
||||
))}
|
||||
|
||||
@@ -10,11 +10,15 @@ type ProviderSelectorProps = {
|
||||
export function ProviderSelector(props: ProviderSelectorProps) {
|
||||
return (
|
||||
<StepContainer sx={{ flexDirection: "column" }}>
|
||||
<Flex sx={{ justifyContent: "space-between" }}>
|
||||
<Text variant="title">Choose a provider to import from</Text>
|
||||
<Flex
|
||||
sx={{
|
||||
justifyContent: ["stretch", "space-between"],
|
||||
flexDirection: ["column", "row"],
|
||||
}}
|
||||
>
|
||||
<Text variant="title">Select a notes app to import from</Text>
|
||||
<Select
|
||||
sx={{
|
||||
p: 0,
|
||||
m: 0,
|
||||
px: 2,
|
||||
border: "1px solid var(--theme-ui-colors-border)",
|
||||
@@ -27,7 +31,11 @@ export function ProviderSelector(props: ProviderSelectorProps) {
|
||||
},
|
||||
fontFamily: "body",
|
||||
fontSize: "body",
|
||||
width: 150,
|
||||
width: ["100%", 150],
|
||||
mt: [2, 0],
|
||||
p: [2, 0],
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const providerName: Providers = e.target.value as Providers;
|
||||
@@ -42,7 +50,7 @@ export function ProviderSelector(props: ProviderSelectorProps) {
|
||||
))}
|
||||
</Select>
|
||||
</Flex>
|
||||
<Text variant="body" sx={{ color: "fontTertiary" }}>
|
||||
<Text variant="body" sx={{ color: "fontTertiary", mt: [2, 0] }}>
|
||||
Can't find your notes app in the list?{" "}
|
||||
<a href="https://github.com/streetwriters/notesnook/issues/new">
|
||||
Send us a request.
|
||||
|
||||
@@ -8,7 +8,7 @@ export function StepContainer({
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
width: "40%",
|
||||
width: ["90%", "40%"],
|
||||
boxShadow: "0px 0px 20px 0px #00000011",
|
||||
p: 4,
|
||||
borderRadius: "default",
|
||||
|
||||
@@ -10,12 +10,7 @@ export class ThemeFactory {
|
||||
space: [0, 5, 10, 15, 20, 25, 30, 35],
|
||||
sizes: { full: "100%", half: "50%" },
|
||||
radii: { none: 0, default: 10 },
|
||||
colors: {
|
||||
modes: {
|
||||
dark: ColorSchemeFactory.construct("dark"),
|
||||
light: ColorSchemeFactory.construct("light"),
|
||||
},
|
||||
},
|
||||
colors: ColorSchemeFactory.construct("light"),
|
||||
rawColors: ColorSchemeFactory.construct("light"),
|
||||
...FontFactory.construct(),
|
||||
...new VariantsFactory(),
|
||||
|
||||
75
apps/importer/src/utils/analytics.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Config from "./config";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
umami?: {
|
||||
trackEvent: (
|
||||
value: string,
|
||||
type: string,
|
||||
url?: string,
|
||||
websiteId?: string
|
||||
) => void;
|
||||
trackView: (url: string, referrer?: string, websiteId?: string) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function loadTrackerScript() {
|
||||
if (Config.get("telemetry") === "false") return Promise.resolve(false);
|
||||
if (!!document.getElementById("analytics")) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
var script = document.createElement("script");
|
||||
script.id = "analytics";
|
||||
script.src = "https://analytics.streetwriters.co/umami.js";
|
||||
script.async = true;
|
||||
script.dataset.websiteId = "b84b000d-9fcb-48e3-bbc0-0adad1a960c0";
|
||||
|
||||
if (process.env.REACT_APP_PLATFORM !== "desktop")
|
||||
script.dataset.domains = "importer.notesnook.com";
|
||||
script.dataset.autoTrack = "false";
|
||||
script.dataset.doNotTrack = "true";
|
||||
script.dataset.hostUrl = "https://analytics.streetwriters.co";
|
||||
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
script.onload = () => resolve(true);
|
||||
script.onerror = (e) => reject(e);
|
||||
|
||||
var firstScriptElement = document.getElementsByTagName("script")[0];
|
||||
firstScriptElement.parentNode?.insertBefore(script, firstScriptElement);
|
||||
});
|
||||
}
|
||||
|
||||
type TrackerEvent = {
|
||||
name: string;
|
||||
// description: string;
|
||||
type?: "event" | "view";
|
||||
};
|
||||
|
||||
export async function trackEvent(event: TrackerEvent, eventMessage?: string) {
|
||||
if (
|
||||
!(await loadTrackerScript()) ||
|
||||
!Config.get("telemetry", true) ||
|
||||
!window.umami
|
||||
)
|
||||
return;
|
||||
|
||||
if (event.type === "view") trackVisit(event.name);
|
||||
else if (eventMessage) window.umami.trackEvent(eventMessage, event.name);
|
||||
}
|
||||
|
||||
function trackVisit(url: string = "/") {
|
||||
window.umami?.trackView(url, window.document.referrer);
|
||||
}
|
||||
|
||||
export function useTelemetry() {
|
||||
const [isEnabled, setIsEnabled] = useState(Config.get("telemetry", true));
|
||||
|
||||
useEffect(() => {
|
||||
Config.set("telemetry", isEnabled);
|
||||
}, [isEnabled]);
|
||||
|
||||
return { isEnabled, setIsEnabled };
|
||||
}
|
||||
27
apps/importer/src/utils/config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
function set(key: string, value: any) {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
function get<T>(key: string, def?: T): T | undefined {
|
||||
const value = window.localStorage.getItem(key);
|
||||
if (!value) return def;
|
||||
|
||||
return tryParse(value);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
|
||||
const Config = { set, get, clear };
|
||||
export default Config;
|
||||
|
||||
function tryParse(val: any) {
|
||||
if (val === "undefined" || val === "null") return;
|
||||
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (e) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
1832
apps/vericrypt/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/vericrypt",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
@@ -20,6 +20,7 @@
|
||||
"typescript": "^4.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/signalr-protocol-msgpack": "^6.0.7",
|
||||
"@notesnook/crypto": "^1.0.0",
|
||||
"clipboard-polyfill": "^3.0.3",
|
||||
"colord": "^2.9.2",
|
||||
|
||||
BIN
apps/vericrypt/src/assets/screenshots/devtools_requests_ws.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -26,7 +26,8 @@ export function ErrorsList(props: ErrorsListProps) {
|
||||
onClick={() =>
|
||||
window.open(
|
||||
"https://github.com/streetwriters/notesnook/issues/new",
|
||||
"_blank"
|
||||
"_blank",
|
||||
"noopener"
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -84,8 +84,8 @@ const steps = {
|
||||
<>
|
||||
On the right panel, you'll see an input with the placeholder "Start from
|
||||
key" at the top. Type <Code text="user" /> in that input. If you don't see
|
||||
the 'user' object, scroll down in key-value pair list and the search
|
||||
again.
|
||||
the 'user' object, scroll to end using <Code text="Page down" /> key in
|
||||
key-value pair list and then search again.
|
||||
</>,
|
||||
`At the top of the list, you'll see an entry with key "user". Expand the value.`,
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { Flex, Text, Image, Textarea } from "@theme-ui/components";
|
||||
import { StepContainer } from "./StepContainer";
|
||||
import DevtoolsRequestsFilter from "../assets/screenshots/devtools_requests_filter.png";
|
||||
import DevtoolsRequestsCopy from "../assets/screenshots/devtools_requests_copy.png";
|
||||
import DevtoolsRequestsWS from "../assets/screenshots/devtools_requests_ws.png";
|
||||
import DevtoolsRequestsWSMessages from "../assets/screenshots/devtools_requests_ws_messages.png";
|
||||
import DevtoolsRequestsWSMessagesSelect from "../assets/screenshots/devtools_requests_ws_messages_select.png";
|
||||
import DevtoolsRequestsWSMessagesCopy from "../assets/screenshots/devtools_requests_ws_messages_copy.png";
|
||||
|
||||
import DevtoolsRequestsFilterFirefox from "../assets/screenshots/firefox/network_tab_firefox.png";
|
||||
import DevtoolsRequestsCopyFirefox from "../assets/screenshots/firefox/copy_curl_request.png";
|
||||
// Firefox does not support extracting raw WebSocket response data yet.
|
||||
// Once it does we can start using these.
|
||||
// import FirefoxDevtoolsRequestsWS from "../assets/screenshots/firefox/devtools_requests_ws.png";
|
||||
// import FirefoxDevtoolsRequestsFilter from "../assets/screenshots/firefox/devtools_requests_filter.png";
|
||||
// import FirefoxDevtoolsRequestsWSResponse from "../assets/screenshots/firefox/devtools_requests_ws_response.png";
|
||||
// import FirefoxDevtoolsRequestsWSResponseSizeColumn from "../assets/screenshots/firefox/devtools_requests_ws_response_sizecolumn.png";
|
||||
// import FirefoxDevtoolsRequestsWSMessagesSelect from "../assets/screenshots/firefox/devtools_requests_ws_messages_select.png";
|
||||
// import FirefoxDevtoolsRequestsWSMessagesCopy from "../assets/screenshots/firefox/devtools_requests_ws_messages_copy.png";
|
||||
|
||||
import { Accordion } from "./Accordion";
|
||||
import { getCombo } from "../utils/keycombos";
|
||||
@@ -14,7 +23,7 @@ import { Code } from "./Code";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ErrorsList } from "./ErrorsList";
|
||||
import { getSourceUrl } from "../utils/links";
|
||||
import { parseCURL } from "../utils/curlparser";
|
||||
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
|
||||
|
||||
type PasteEncryptedDataProps = {
|
||||
onEncryptedDataPasted: (data?: SyncRequestBody) => void;
|
||||
@@ -37,16 +46,7 @@ export type SyncRequestBody = {
|
||||
attachments: EncryptedSyncItem[];
|
||||
};
|
||||
|
||||
const SAMPLE_CURL = `Paste your cURL request here.
|
||||
For example:
|
||||
curl 'https://api.notesnook.com/sync'
|
||||
-H 'authority: api.notesnook.com'
|
||||
-H 'authorization: Bearer 84374D98...'
|
||||
-H ...
|
||||
-H ...
|
||||
-H 'content-type: application/json'
|
||||
--data-raw '{"notes":[{...}], "notebooks": [{...}], "content": [{...}], "lastSynced":1443215713231}'
|
||||
--compressed`;
|
||||
const SAMPLE_CURL = `Paste raw base64 encoded data here.`;
|
||||
|
||||
const steps = {
|
||||
chromium: [
|
||||
@@ -58,6 +58,13 @@ const steps = {
|
||||
<>
|
||||
Switch to the <Code text="Network" /> tab.
|
||||
</>,
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">
|
||||
Make sure you have <Code text="WS" /> toggled instead of{" "}
|
||||
<Code text="Fetch/XHR" />
|
||||
</Text>
|
||||
<Image src={DevtoolsRequestsWS} width={400} sx={{ mt: 1 }} />
|
||||
</Flex>,
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">
|
||||
In the filter input, type <Code text="sync" /> to filter out sync
|
||||
@@ -66,63 +73,134 @@ const steps = {
|
||||
<Image src={DevtoolsRequestsFilter} width={400} sx={{ mt: 1 }} />
|
||||
</Flex>,
|
||||
<>
|
||||
At this point, you probably won't see anything useful. Try editing one of
|
||||
your notes and syncing. Make sure to keep the <Code text="Network" /> tab
|
||||
open.
|
||||
<b>Refresh the page by pressing F5</b> (this is done so we can capture the
|
||||
websocket request that happens on startup).
|
||||
</>,
|
||||
<>
|
||||
In the requests table in <Code text="Network" /> tab, you'll now see 3 or
|
||||
more new requests popup.
|
||||
Wait until a request starting with <Code text="sync?access_token=" /> pops
|
||||
up. It'll probably be the only one popping up and might take a bit so
|
||||
don't panic if you don't see anything.
|
||||
</>,
|
||||
<>Left-click on this request to open its details.</>,
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">
|
||||
Switch to the <Code text="Messages" /> tab.
|
||||
</Text>
|
||||
<Image src={DevtoolsRequestsWSMessages} width={400} sx={{ mt: 1 }} />
|
||||
</Flex>,
|
||||
<>
|
||||
At this point, you will a list of Binary messages. None of these will
|
||||
appear really useful. This is normal.
|
||||
</>,
|
||||
<>
|
||||
We are interested in only the <Code text="sync" /> request. Right click on
|
||||
this request to open the context menu.
|
||||
Now try editing one of your notes and syncing. (Make sure to keep the{" "}
|
||||
<Code text="Network" /> tab open.)
|
||||
</>,
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">
|
||||
From the context menu, go to <Code text="Copy" /> & click on{" "}
|
||||
<Code text="Copy as cURL" />.
|
||||
You will see a bunch of new <Code text="Binary Messages" /> appear in
|
||||
the list. Find the one with the largest Length (in KBs). Also make sure
|
||||
the output type is set to <Code text="Base64" /> as in the screenshot
|
||||
below.
|
||||
</Text>
|
||||
<Image src={DevtoolsRequestsCopy} width={400} sx={{ mt: 1 }} />
|
||||
</Flex>,
|
||||
],
|
||||
firefox: [
|
||||
"Focus the Notesnook tab in your browser.",
|
||||
<>
|
||||
Press <KeyCombo combo={getCombo("chromium", "developerTools")} /> to open
|
||||
Developer Tools.
|
||||
</>,
|
||||
<>
|
||||
Switch to the <Code text="Network" /> tab.
|
||||
</>,
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">
|
||||
In the filter input, type <Code text="sync" /> to filter out sync
|
||||
requests.
|
||||
</Text>
|
||||
<Image src={DevtoolsRequestsFilterFirefox} width={400} sx={{ mt: 1 }} />
|
||||
</Flex>,
|
||||
<>
|
||||
At this point, you probably won't see anything useful. Try editing one of
|
||||
your notes and syncing. Make sure to keep the <Code text="Network" /> tab
|
||||
open.
|
||||
</>,
|
||||
<>
|
||||
In the requests table in <Code text="Network" /> tab, you'll now see 3 or
|
||||
more new requests popup.
|
||||
</>,
|
||||
<>
|
||||
We are interested in only the <Code text="sync" /> request. Right click on
|
||||
this request to open the context menu.
|
||||
</>,
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">
|
||||
From the context menu, go to <Code text="Copy" /> & click on{" "}
|
||||
<Code text="Copy as cURL" />.
|
||||
</Text>
|
||||
<Image src={DevtoolsRequestsCopyFirefox} width={400} sx={{ mt: 1 }} />
|
||||
<Image
|
||||
src={DevtoolsRequestsWSMessagesSelect}
|
||||
width={700}
|
||||
sx={{ mt: 1, maxWidth: "95%" }}
|
||||
/>
|
||||
</Flex>,
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">Click on the copy button to copy the payload.</Text>
|
||||
<Image src={DevtoolsRequestsWSMessagesCopy} width={200} sx={{ mt: 1 }} />
|
||||
</Flex>,
|
||||
<> Paste it below to see the decrypted data.</>,
|
||||
],
|
||||
firefox: [],
|
||||
// firefox: [
|
||||
// "Focus the Notesnook tab in your browser.",
|
||||
// <>
|
||||
// Press <KeyCombo combo={getCombo("firefox", "developerTools")} /> to open
|
||||
// Developer Tools.
|
||||
// </>,
|
||||
// <>
|
||||
// Switch to the <Code text="Network" /> tab.
|
||||
// </>,
|
||||
// <Flex sx={{ flexDirection: "column" }}>
|
||||
// <Text as="p">
|
||||
// Make sure you have <Code text="WS" /> toggled instead of{" "}
|
||||
// <Code text="Fetch/XHR" />
|
||||
// </Text>
|
||||
// <Image src={FirefoxDevtoolsRequestsWS} width={400} sx={{ mt: 1 }} />
|
||||
// </Flex>,
|
||||
// <Flex sx={{ flexDirection: "column" }}>
|
||||
// <Text as="p">
|
||||
// In the filter input, type <Code text="sync" /> to filter out sync
|
||||
// requests.
|
||||
// </Text>
|
||||
// <Image src={FirefoxDevtoolsRequestsFilter} width={400} sx={{ mt: 1 }} />
|
||||
// </Flex>,
|
||||
// <>
|
||||
// <b>Refresh the page by pressing F5</b> (this is done so we can capture the
|
||||
// websocket request that happens on startup).
|
||||
// </>,
|
||||
// <>
|
||||
// Wait until a request starting with <Code text="sync?access_token=" /> pops
|
||||
// up. It'll probably be the only one popping up and might take a bit so
|
||||
// don't panic if you don't see anything.
|
||||
// </>,
|
||||
// <>Left-click on this request to open its details.</>,
|
||||
// <Flex sx={{ flexDirection: "column" }}>
|
||||
// <Text as="p">
|
||||
// Switch to the <Code text="Response" /> tab.
|
||||
// </Text>
|
||||
// <Image
|
||||
// src={FirefoxDevtoolsRequestsWSResponse}
|
||||
// width={400}
|
||||
// sx={{ mt: 1 }}
|
||||
// />
|
||||
// </Flex>,
|
||||
// <>
|
||||
// At this point, you will a list of Binary messages. None of these will
|
||||
// appear really useful. This is normal.
|
||||
// </>,
|
||||
// <Flex sx={{ flexDirection: "column" }}>
|
||||
// <Text as="p">
|
||||
// To help in finding the right request, <Code text="Right click" /> on the
|
||||
// table header & enable the <Code text="Size" /> column.
|
||||
// </Text>
|
||||
// <Image
|
||||
// src={FirefoxDevtoolsRequestsWSResponseSizeColumn}
|
||||
// width={300}
|
||||
// sx={{ mt: 1 }}
|
||||
// />
|
||||
// </Flex>,
|
||||
// <>
|
||||
// Now try editing one of your notes and syncing. (Make sure to keep the{" "}
|
||||
// <Code text="Network" /> tab open.)
|
||||
// </>,
|
||||
// <Flex sx={{ flexDirection: "column" }}>
|
||||
// <Text as="p">
|
||||
// You will see a bunch of new items appear in the table. Find the one with
|
||||
// the largest Length (in KBs).
|
||||
// </Text>
|
||||
// <Image
|
||||
// src={FirefoxDevtoolsRequestsWSMessagesSelect}
|
||||
// width={700}
|
||||
// sx={{ mt: 1, maxWidth: "95%" }}
|
||||
// />
|
||||
// </Flex>,
|
||||
// <Flex sx={{ flexDirection: "column" }}>
|
||||
// <Text as="p">
|
||||
// Right-click on the item & click on <Code text="Copy Message" />
|
||||
// </Text>
|
||||
// <Image
|
||||
// src={FirefoxDevtoolsRequestsWSMessagesCopy}
|
||||
// width={400}
|
||||
// sx={{ mt: 1 }}
|
||||
// />
|
||||
// </Flex>,
|
||||
// <> Paste it below to see the decrypted data.</>,
|
||||
// ],
|
||||
};
|
||||
|
||||
const isChromium = Platform.name === "Chrome";
|
||||
@@ -161,23 +239,37 @@ export function PasteEncryptedData(props: PasteEncryptedDataProps) {
|
||||
borderRadius: "default",
|
||||
}}
|
||||
>
|
||||
<Text variant="body" sx={{ mx: 2 }}>
|
||||
To make this whole process verifiable & trustworthy, we need to
|
||||
extract the raw data that Notesnook sends to its servers during sync.
|
||||
That way you can be sure that the data leaving your device is actually
|
||||
encrypted or not.
|
||||
</Text>
|
||||
<Text as="ol" variant="body" sx={{ mb: 2 }}>
|
||||
{instructions?.map((item) => (
|
||||
<Text as="li" sx={{ mt: 1 }}>
|
||||
{item}
|
||||
{isChromium ? (
|
||||
<>
|
||||
<Text variant="body" sx={{ mx: 2 }}>
|
||||
To make this whole process verifiable & trustworthy, we need
|
||||
to extract the raw data that Notesnook sends to its servers during
|
||||
sync. That way you can be sure that the data leaving your device
|
||||
is actually encrypted or not.
|
||||
</Text>
|
||||
))}
|
||||
</Text>
|
||||
<Text as="ol" variant="body" sx={{ mb: 2 }}>
|
||||
{instructions?.map((item) => (
|
||||
<Text as="li" sx={{ mt: 1 }}>
|
||||
{item}
|
||||
</Text>
|
||||
))}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Flex sx={{ bg: "errorBg", p: 1 }}>
|
||||
<Text as="p" variant="body" sx={{ color: "error" }}>
|
||||
Currently Firefox does not support pasting raw data from the
|
||||
WebSocket response view.{" "}
|
||||
<b>Please use a Chromium-based browser</b> for grabbing the
|
||||
necessary data.
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Accordion>
|
||||
<Textarea
|
||||
variant="forms.clean"
|
||||
placeholder={SAMPLE_CURL}
|
||||
spellCheck={false}
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontSize: "body",
|
||||
@@ -188,28 +280,67 @@ export function PasteEncryptedData(props: PasteEncryptedDataProps) {
|
||||
onChange={(e) => {
|
||||
try {
|
||||
setError(undefined);
|
||||
const curlRequest = e.target.value;
|
||||
if (!curlRequest) return setEncryptedData(undefined);
|
||||
const parsed = parseCURL(curlRequest);
|
||||
const base64Data = e.target.value;
|
||||
|
||||
if (parsed.url !== "https://api.notesnook.com/sync") {
|
||||
throw new Error(
|
||||
"Invalid cURL request. URL must be https://api.notesnook.com/sync."
|
||||
);
|
||||
const protocol = new MessagePackHubProtocol();
|
||||
const messages = protocol
|
||||
.parseMessages(toArrayBuffer(base64Data), {
|
||||
log: console.log,
|
||||
})
|
||||
.filter((m) => m.type === 1);
|
||||
|
||||
if (!messages.length) {
|
||||
setError("Invalid message.");
|
||||
return setEncryptedData(undefined);
|
||||
}
|
||||
|
||||
if (!parsed.data)
|
||||
throw new Error("Invalid cURL request. No data found.");
|
||||
const syncData: SyncRequestBody = {
|
||||
attachments: [],
|
||||
content: [],
|
||||
notebooks: [],
|
||||
notes: [],
|
||||
};
|
||||
for (const message of messages) {
|
||||
if (message.type === 1) {
|
||||
const { items, types } = message.arguments[0] as {
|
||||
items?: string[];
|
||||
types?: string[];
|
||||
};
|
||||
if (!items || !types) continue;
|
||||
|
||||
for (let i = 0; i < types.length; ++i) {
|
||||
const itemType = types[i];
|
||||
const item = JSON.parse(items[i]) as EncryptedSyncItem;
|
||||
|
||||
switch (itemType) {
|
||||
case "note":
|
||||
syncData.notes.push(item);
|
||||
break;
|
||||
case "notebook":
|
||||
syncData.notebooks.push(item);
|
||||
break;
|
||||
case "content":
|
||||
syncData.content.push(item);
|
||||
break;
|
||||
case "attachment":
|
||||
syncData.attachments.push(item);
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const syncData: SyncRequestBody = JSON.parse(parsed.data);
|
||||
if (syncData.notes.length <= 0 && syncData.content.length <= 0) {
|
||||
throw new Error(
|
||||
"Empty cURL request. Make sure the request body contains at least 1 note or 1 content."
|
||||
"The pasted data does not contain any notes. Please select another payload."
|
||||
);
|
||||
}
|
||||
|
||||
setEncryptedData(syncData);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const error = e as Error;
|
||||
setError(error.message);
|
||||
}
|
||||
@@ -239,3 +370,13 @@ export function PasteEncryptedData(props: PasteEncryptedDataProps) {
|
||||
</StepContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function toArrayBuffer(payload: string) {
|
||||
var binary_string = window.atob(payload);
|
||||
var len = binary_string.length;
|
||||
var bytes = new Uint8Array(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
const LITERAL_NAMES = {
|
||||
CURL: "curl",
|
||||
INCLUDE_HEADERS: "include-headers",
|
||||
SILENT: "run-silent",
|
||||
USE_GET: "use-get",
|
||||
COMPRESSED: "compressed",
|
||||
STATES: {
|
||||
HEADER: "header-state",
|
||||
CUSTOM_METHOD: "custom-method-state",
|
||||
DATA: "data-state",
|
||||
USER: "user-state",
|
||||
},
|
||||
STRING: "string",
|
||||
OTHERS: "others",
|
||||
};
|
||||
|
||||
const literals = [
|
||||
{
|
||||
regex: /^\bcurl\b/,
|
||||
name: LITERAL_NAMES.CURL,
|
||||
},
|
||||
{
|
||||
regex: /^(-H|--header)/,
|
||||
name: LITERAL_NAMES.STATES.HEADER,
|
||||
},
|
||||
{
|
||||
regex: /^-X/,
|
||||
name: LITERAL_NAMES.STATES.CUSTOM_METHOD,
|
||||
},
|
||||
{
|
||||
regex: /^(-u|--user)/,
|
||||
name: LITERAL_NAMES.STATES.USER,
|
||||
},
|
||||
{
|
||||
regex: /^(-d|--data-raw|--data-ascii|--data)/,
|
||||
name: LITERAL_NAMES.STATES.DATA,
|
||||
},
|
||||
{
|
||||
regex: /^--compressed/,
|
||||
name: LITERAL_NAMES.COMPRESSED,
|
||||
},
|
||||
{
|
||||
regex: /^(-i|--include)/,
|
||||
name: LITERAL_NAMES.INCLUDE_HEADERS,
|
||||
},
|
||||
{
|
||||
regex: /^(-s|--silent)/,
|
||||
name: LITERAL_NAMES.SILENT,
|
||||
},
|
||||
{
|
||||
regex: /^(-G|--get)/,
|
||||
name: LITERAL_NAMES.USE_GET,
|
||||
},
|
||||
{
|
||||
regex: /^'[^']*'/,
|
||||
name: LITERAL_NAMES.STRING,
|
||||
},
|
||||
{
|
||||
regex: /^"[^"]*"/,
|
||||
name: LITERAL_NAMES.STRING,
|
||||
},
|
||||
{
|
||||
regex: /^[^\n\r\s]*/,
|
||||
name: LITERAL_NAMES.STRING,
|
||||
},
|
||||
{
|
||||
regex: /^.*/,
|
||||
name: LITERAL_NAMES.OTHERS,
|
||||
},
|
||||
];
|
||||
|
||||
const trimValue = (val: string) =>
|
||||
val
|
||||
.trim()
|
||||
.replace(/^('|")\s*/, "")
|
||||
.replace(/\s*('|")$/, "");
|
||||
|
||||
const isURL = (val: string) =>
|
||||
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www\.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w\-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[.!/\\\w]*))?)/.test(
|
||||
val
|
||||
);
|
||||
|
||||
type Token = { name: string; value: RegExpMatchArray | null };
|
||||
type ParsedCurlRequest = {
|
||||
url: string;
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
data?: string;
|
||||
};
|
||||
|
||||
export function parseCURL(curlCmd: string) {
|
||||
let cmd = curlCmd.trim();
|
||||
const tokens: Token[] = [];
|
||||
const result: ParsedCurlRequest = {
|
||||
url: "",
|
||||
method: "GET",
|
||||
headers: {} as Record<string, string>,
|
||||
data: "",
|
||||
};
|
||||
while (cmd) {
|
||||
for (const literal of literals) {
|
||||
if (literal.regex.test(cmd)) {
|
||||
tokens.push({
|
||||
name: literal.name,
|
||||
value: cmd.match(literal.regex),
|
||||
});
|
||||
|
||||
cmd = cmd.replace(literal.regex, "").trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tokens.length) {
|
||||
if (tokens.shift()?.name !== "curl") {
|
||||
throw new Error("Not a curl command.");
|
||||
}
|
||||
|
||||
let state = "";
|
||||
tokens.forEach((token) => {
|
||||
if (!token.value) return;
|
||||
|
||||
if (!state) {
|
||||
switch (token.name) {
|
||||
case LITERAL_NAMES.STATES.HEADER:
|
||||
state = LITERAL_NAMES.STATES.HEADER;
|
||||
break;
|
||||
case LITERAL_NAMES.STATES.DATA:
|
||||
state = LITERAL_NAMES.STATES.DATA;
|
||||
break;
|
||||
case LITERAL_NAMES.STATES.CUSTOM_METHOD:
|
||||
state = LITERAL_NAMES.STATES.CUSTOM_METHOD;
|
||||
break;
|
||||
case LITERAL_NAMES.STATES.USER:
|
||||
state = LITERAL_NAMES.STATES.USER;
|
||||
break;
|
||||
case LITERAL_NAMES.STRING: {
|
||||
const val = trimValue(token.value[0]);
|
||||
if (isURL(val)) {
|
||||
result.url = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (state) {
|
||||
case LITERAL_NAMES.STATES.HEADER: {
|
||||
const pair = trimValue(token.value[0]).split(/\s*:\s*/);
|
||||
result.headers[pair[0]] = pair[1];
|
||||
break;
|
||||
}
|
||||
case LITERAL_NAMES.STATES.DATA: {
|
||||
console.log(token);
|
||||
const pair = token.value[0];
|
||||
result.data = trimValue(pair).trim();
|
||||
break;
|
||||
}
|
||||
case LITERAL_NAMES.STATES.CUSTOM_METHOD: {
|
||||
const pair = token.value[0];
|
||||
result.method = trimValue(pair).trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
state = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"downlevelIteration": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
|
||||
BIN
badges/app-store-badge.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
@@ -1,21 +1,23 @@
|
||||
---
|
||||
icon: pencil
|
||||
order: 990
|
||||
label: Creating a note
|
||||
label: Create a note
|
||||
---
|
||||
|
||||
# Creating a note
|
||||
You are in a notes app, the first thing you want to do is make a note.
|
||||
# Create a note
|
||||
Once you have successfully installed [Notesnook](https://notesnook.com/downloads/), the first thing you may want to do is create a note.
|
||||
|
||||
To create a note on
|
||||
|
||||
+++ Desktop/Web
|
||||
1. Click on the `+` button on top right corner or click anywhere inside the editor to focus it.
|
||||
2. Start writing and your note will be created.
|
||||
3. As you write, note will be saved automatically whenever you stop for a second.
|
||||
1. Click on the green colored `+` button in the top right corner of the `Notes` section.
|
||||
2. Start writing in the editor and your note will be created automatically with your current date and time as note title. Alternatively, you can name your note title yourself.
|
||||
3. Notes will be saved automatically whenever you stop writing for a time interval of more than one second.
|
||||
+++ Mobile/Tablet
|
||||
1. On mobile, just like desktop, regardless of what screen you have opened, you can always swipe from right to left and open the editor.
|
||||
2. You can also press the `+` button on bottom right corner to open the editor when you are in `Notes`, a `Notebook` or `Tag`.
|
||||
3. Start writing in editor and your note will be created.
|
||||
4. As you write, note will be saved automatically whenever you stop for a second.
|
||||
1. On mobile, regardless of what section of the app you have opened, you can always swipe from right to left and open the editor.
|
||||
2. Alternatively, you can also press the `+` button in the bottom right corner to open the editor when you are in `Notes`, a `Notebook`, or `Tag` section.
|
||||
3. Start writing in the editor and your note will be created automatically with your current date and time as note title. Alternatively, you can name your note title yourself.
|
||||
4. Notes will be saved automatically whenever you stop writing for a time interval of more than one second.
|
||||
+++
|
||||
|
||||
And that is how you create your first private note on Notesnook!
|
||||
Congratulations, You have successfully created your first private note on Notesnook!
|
||||
|
||||
@@ -11,28 +11,28 @@ We are currently working on writing detailed docs for our app. The current versi
|
||||
!!!
|
||||
|
||||
|
||||
Notesnook is a simple and cross-platform **private notes app** that keeps all your notes organized and synced on your phone, tablet and computer.
|
||||
Notesnook is a simple and cross-platform **private notes app** that keeps all your notes organized and synced between your phone, tablet and computer.
|
||||
|
||||
<img src="https://github.com/streetwriters/notesnook/raw/main/banners/crossplatform.jpg" alt="Notesnook - Take private notes"/>
|
||||
|
||||
We use best-in-class encryption algorithms **XChaCha20-Poly1305 & Argon2** that are stronger & safer than AES256, impenetrable & future-proof. Your notes are encrypted on your device. All your notes are 100% private. **No one but you can read your notes (not even us).**
|
||||
We use best-in-class encryption algorithms **XChaCha20-Poly1305 & Argon2** that are stronger & safer than AES256, impenetrable & future-proof. Your notes are always encrypted on your device first and then stored on servers. All your notes are 100% private. **No one but only you can read your notes (not even us).**
|
||||
|
||||
Take notes, capture ideas, create to-do lists, keep a journal, write your assignments and manage projects, we have got you covered everywhere.
|
||||
|
||||
## Features
|
||||
|
||||
Privacy isn't the only thing that Notesnook has to offer:
|
||||
Privacy isn't the only thing that Notesnook offers:
|
||||
|
||||
1. Cross platform - Notesnook has apps on all platforms and works even on your browser.
|
||||
2. End to end encrypted private note syncing to unlimted devices
|
||||
3. Organize your notes with notebooks, tags and colors
|
||||
4. Encrypt/lock your notes in a private vault locally
|
||||
5. Full rich text editor with markdown support
|
||||
6. Focus mode (distraction-free mode)
|
||||
7. Export notes in HTML, Markdown, PDF & Text formats
|
||||
8. Automatic backups
|
||||
9. Dark mode and custom accents
|
||||
10. Notesnook Monograph - Share your notes on public url with one click in a safe and secure way
|
||||
1. Cross platform - Notesnook supports apps on all platforms and works even on your browser.
|
||||
2. End to end encrypted private note syncing between unlimited devices.
|
||||
3. Organize your notes with notebooks, tags and colors.
|
||||
4. Encrypt/lock your notes in a private vault locally.
|
||||
5. Full rich text editor with markdown support.
|
||||
6. Focus mode (distraction-free mode).
|
||||
7. Export notes in HTML, Markdown, PDF & other Text formats.
|
||||
8. Automatic backups.
|
||||
9. Dark mode and custom accents.
|
||||
10. Notesnook Monograph - Share your notes on public url with just one click in a safe and secure way.
|
||||
|
||||
Notesnook is specifically designed for each platform to give you the best private note taking experience.
|
||||
|
||||
@@ -42,8 +42,8 @@ Notesnook is available to download on Android, iOS, Windows, MacOS, and Linux. P
|
||||
|
||||
## Support
|
||||
|
||||
Technical support questions are best asked on our [Discord](https://discord.com/invite/5davZnhw3V). We are available there 24/7 to respond to your queries. You can also chat with us there and other members who are using notesnook and share your experience.
|
||||
For technical questions, doubts or support, join our [Discord](https://discord.com/invite/5davZnhw3V). We are available 24/7 to respond to your queries. You can also join to communicate our ever growing community members and share your experiences on Discord.
|
||||
|
||||
If you find a bug or would like to submit a feature request, please create an [Issue](https://github.com/streetwriters/notesnook/issues) and we will investigate right away.
|
||||
If you found any bug or like to submit a feature request, please create an [Issue](https://github.com/streetwriters/notesnook/issues) and we will review right away.
|
||||
|
||||
Do you have a general inquiry? Please feel free to contact us at support@notesnook.com.
|
||||
If you have any other general inquiry, feel free to contact us at support@notesnook.com.
|
||||
|
||||
48719
package-lock.json
generated
@@ -22,8 +22,7 @@
|
||||
"turbo": "^1.0.19"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"apps/*"
|
||||
"packages/*"
|
||||
],
|
||||
"turbo": {
|
||||
"baseBranch": "origin/main",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/crypto",
|
||||
"private": false,
|
||||
"description": "An easy to use wrapper around libsodium exposing essential cryptographic methods. Currently only Web is supported.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
@@ -12,10 +12,10 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"libsodium-wrappers": "^0.7.9"
|
||||
"libsodium-wrappers": "0.7.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/libsodium-wrappers": "^0.7.9"
|
||||
"@types/libsodium-wrappers": "0.7.9"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
8501
packages/enex/package-lock.json
generated
@@ -18,14 +18,14 @@
|
||||
},
|
||||
"author": "Abdullah Atta <abdullahatta@streetwriters.co>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"node-html-parser": "github:thecodrr/node-html-parser",
|
||||
"spark-md5": "^3.0.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/streetwriters/notesnook.git",
|
||||
"directory": "packages/enex"
|
||||
},
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"node-html-parser": "github:thecodrr/node-html-parser",
|
||||
"spark-md5": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@ Supported providers:
|
||||
6. Standard Notes
|
||||
7. OneNote
|
||||
8. Zoho Notebook
|
||||
9. Joplin
|
||||
|
||||
BIN
packages/importer/__tests__/data/joplin/13_05_2022.jex
Normal file
9884
packages/importer/package-lock.json
generated
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"name": "@notesnook/importer",
|
||||
"private": false,
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.3",
|
||||
"main": "dist/index.js",
|
||||
"description": "Convert notes from various note taking apps into a simple & standard format.",
|
||||
"scripts": {
|
||||
"test": "tap --ts --no-check-coverage --no-coverage",
|
||||
"build": "tsc"
|
||||
"build": "tsc",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@notesnook/enex": "^1.0.3",
|
||||
"@notesnook/onenote": "^1.0.2",
|
||||
"@notesnook/znel": "^1.0.0",
|
||||
"@standardnotes/common": "^1.8.0",
|
||||
"@standardnotes/features": "^1.25.0",
|
||||
"@standardnotes/common": "^1.19.8",
|
||||
"@standardnotes/features": "^1.42.1",
|
||||
"fflate": "^0.7.3",
|
||||
"highlight.js": "^11.4.0",
|
||||
"node-html-parser": "github:thecodrr/node-html-parser",
|
||||
|
||||
83
packages/importer/src/providers/joplin/helpers.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ModelType } from "./types";
|
||||
|
||||
// Taken from https://github.com/laurent22/joplin/blob/6f1a806e5c7159a544de5d997b189e0e83a9d8ea/packages/lib/models/BaseItem.ts#L477-L523
|
||||
export async function unserialize(content: string) {
|
||||
const lines = content.split("\n");
|
||||
let output: any = {};
|
||||
let state = "readingProps";
|
||||
const body: string[] = [];
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
let line = lines[i];
|
||||
|
||||
if (state == "readingProps") {
|
||||
line = line.trim();
|
||||
|
||||
if (line == "") {
|
||||
state = "readingBody";
|
||||
continue;
|
||||
}
|
||||
|
||||
const p = line.indexOf(":");
|
||||
if (p < 0)
|
||||
throw new Error(`Invalid property format: ${line}: ${content}`);
|
||||
const key = line.substr(0, p).trim();
|
||||
const value = line.substr(p + 1).trim();
|
||||
output[key] = value;
|
||||
} else if (state == "readingBody") {
|
||||
body.splice(0, 0, line);
|
||||
}
|
||||
}
|
||||
|
||||
if (!output.type_)
|
||||
throw new Error(`Missing required property: type_: ${content}`);
|
||||
output.type_ = Number(output.type_);
|
||||
|
||||
if (body.length) {
|
||||
const title = body.splice(0, 2);
|
||||
output.title = title[0];
|
||||
}
|
||||
|
||||
if (output.type_ === ModelType.Note) output.body = body.join("\n");
|
||||
|
||||
for (const n in output) {
|
||||
if (!output.hasOwnProperty(n)) continue;
|
||||
output[n] = await unserialize_format(output.type_, n, output[n]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// Taken from https://github.com/laurent22/joplin/blob/dev/packages/lib/models/BaseItem.ts#L325-L351
|
||||
// slightly modified to remove all database related code
|
||||
function unserialize_format(type: ModelType, propName: string, propValue: any) {
|
||||
if (propName[propName.length - 1] == "_") return propValue; // Private property
|
||||
|
||||
if (["title_diff", "body_diff"].indexOf(propName) >= 0) {
|
||||
if (!propValue) return "";
|
||||
propValue = JSON.parse(propValue);
|
||||
} else if (["longitude", "latitude", "altitude"].indexOf(propName) >= 0) {
|
||||
const places = propName === "altitude" ? 4 : 8;
|
||||
propValue = Number(propValue).toFixed(places);
|
||||
} else {
|
||||
if (
|
||||
[
|
||||
"created_time",
|
||||
"updated_time",
|
||||
"user_created_time",
|
||||
"user_updated_time",
|
||||
].indexOf(propName) >= 0
|
||||
) {
|
||||
propValue = !propValue ? "0" : new Date(propValue).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
if (propName === "body") return propValue;
|
||||
|
||||
return typeof propValue === "string"
|
||||
? propValue
|
||||
.replace(/\\n/g, "\n")
|
||||
.replace(/\\r/g, "\r")
|
||||
.replace(/\\\n/g, "\\n")
|
||||
.replace(/\\\r/g, "\\r")
|
||||
: propValue;
|
||||
}
|
||||
181
packages/importer/src/providers/joplin/index.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { ContentType, Note, Notebook } from "../../models/note";
|
||||
import { File } from "../../utils/file";
|
||||
import {
|
||||
IFileProvider,
|
||||
iterate,
|
||||
ProviderResult,
|
||||
ProviderSettings,
|
||||
} from "../provider";
|
||||
import { parse } from "node-html-parser";
|
||||
import {
|
||||
NoteEntity,
|
||||
FolderEntity,
|
||||
TagEntity,
|
||||
NoteTagEntity,
|
||||
ResourceEntity,
|
||||
} from "./types";
|
||||
import showdown from "showdown";
|
||||
import { Attachment, attachmentToHTML } from "../../models/attachment";
|
||||
import { HTMLRootElement } from "node-html-parser/dist/nodes/html";
|
||||
import { IHasher } from "../../utils/hasher";
|
||||
import { unserialize } from "./helpers";
|
||||
import { ModelType } from "./types";
|
||||
|
||||
const converter = new showdown.Converter();
|
||||
|
||||
type JoplinData = {
|
||||
notes: NoteEntity[];
|
||||
noteTags: NoteTagEntity[];
|
||||
folders: FolderEntity[];
|
||||
tags: TagEntity[];
|
||||
resources: ResourceEntity[];
|
||||
};
|
||||
|
||||
export class Joplin implements IFileProvider {
|
||||
type: "file" = "file";
|
||||
public supportedExtensions = [".jex", ".md"];
|
||||
public validExtensions = [...this.supportedExtensions];
|
||||
public version = "1.0.0";
|
||||
public name = "Joplin";
|
||||
|
||||
async process(
|
||||
files: File[],
|
||||
settings: ProviderSettings
|
||||
): Promise<ProviderResult> {
|
||||
const data: JoplinData = {
|
||||
folders: [],
|
||||
noteTags: [],
|
||||
notes: [],
|
||||
resources: [],
|
||||
tags: [],
|
||||
};
|
||||
const notes: Note[] = [];
|
||||
await iterate(this, files, async (file) => {
|
||||
const item = await unserialize(file.text);
|
||||
switch (item.type_) {
|
||||
case ModelType.Note:
|
||||
data.notes.push(item);
|
||||
break;
|
||||
case ModelType.NoteTag:
|
||||
data.noteTags.push(item);
|
||||
break;
|
||||
case ModelType.Resource:
|
||||
data.resources.push(item);
|
||||
break;
|
||||
case ModelType.Tag:
|
||||
data.tags.push(item);
|
||||
break;
|
||||
case ModelType.Folder:
|
||||
data.folders.push(item);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
for (const note of data.notes) {
|
||||
if (!note.id || !note.body) continue;
|
||||
const tags = this.resolveTags(note.id, data.tags, data.noteTags);
|
||||
const parentFolder = data.folders.find((a) => a.id === note.parent_id);
|
||||
const notebooks: Notebook[] = [];
|
||||
|
||||
const html = converter.makeHtml(note.body);
|
||||
const document = parse(html);
|
||||
const title =
|
||||
note.title ||
|
||||
document.querySelector("h1,h2")?.textContent ||
|
||||
"Untitled note";
|
||||
|
||||
const attachments = await this.resolveResources(
|
||||
data.resources,
|
||||
document,
|
||||
files,
|
||||
settings.hasher
|
||||
);
|
||||
|
||||
if (parentFolder) {
|
||||
const notebook = this.resolveFolders(data.folders, parentFolder);
|
||||
if (notebook) notebooks.push(notebook);
|
||||
}
|
||||
|
||||
notes.push({
|
||||
title,
|
||||
dateCreated: note.user_created_time || note.created_time,
|
||||
dateEdited: note.user_updated_time || note.updated_time,
|
||||
tags,
|
||||
attachments,
|
||||
notebooks,
|
||||
content: {
|
||||
data: document.outerHTML,
|
||||
type: ContentType.HTML,
|
||||
},
|
||||
});
|
||||
}
|
||||
return { errors: [], notes };
|
||||
}
|
||||
|
||||
private resolveTags(
|
||||
noteId: string,
|
||||
tags: TagEntity[],
|
||||
noteTags: NoteTagEntity[]
|
||||
) {
|
||||
return tags
|
||||
.filter((t) => {
|
||||
return noteTags.some(
|
||||
(noteTag) => noteTag.tag_id === t.id && noteTag.note_id === noteId
|
||||
);
|
||||
})
|
||||
.map((tag) => tag.title!);
|
||||
}
|
||||
|
||||
private async resolveResources(
|
||||
resources: ResourceEntity[],
|
||||
document: HTMLRootElement,
|
||||
files: File[],
|
||||
hasher: IHasher
|
||||
) {
|
||||
const attachments: Attachment[] = [];
|
||||
for (let resource of resources) {
|
||||
if (!resource.id) continue;
|
||||
const element = document.querySelector(
|
||||
`[src=":/${resource.id}"],[href=":/${resource.id}"]`
|
||||
);
|
||||
if (!element) continue;
|
||||
const resourceFile = files.find((f) =>
|
||||
f.path?.includes(`resources/${resource.id!}`)
|
||||
);
|
||||
if (!resourceFile) continue;
|
||||
|
||||
const dataHash = await hasher.hash(resourceFile.bytes);
|
||||
const attachment: Attachment = {
|
||||
data: resourceFile?.bytes,
|
||||
size: resourceFile?.bytes.length,
|
||||
hash: dataHash,
|
||||
filename: resource.title || resource.filename || dataHash,
|
||||
hashType: hasher.type,
|
||||
mime: resource.mime || "application/octet-stream",
|
||||
};
|
||||
attachments.push(attachment);
|
||||
element.replaceWith(attachmentToHTML(attachment));
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
private resolveFolders(
|
||||
folders: FolderEntity[],
|
||||
parentFolder: FolderEntity
|
||||
): Notebook | null {
|
||||
let folder: FolderEntity | undefined = parentFolder;
|
||||
let path: string[] = [parentFolder.title!];
|
||||
while (folder?.parent_id) {
|
||||
folder = folders.find((f) => f.id === folder?.parent_id);
|
||||
if (!folder) break;
|
||||
path.push(folder.title!);
|
||||
}
|
||||
const topMost = path.pop();
|
||||
if (!topMost) return null;
|
||||
return {
|
||||
notebook: topMost,
|
||||
topic: path.reverse().join(".") || "All notes",
|
||||
};
|
||||
}
|
||||
}
|
||||
125
packages/importer/src/providers/joplin/types.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* We cannot use @joplin/lib directly because Typescript throws errors
|
||||
* during compilation. Until those errors are fixed upstream, we'll have to
|
||||
* use extracted types.
|
||||
*/
|
||||
|
||||
// Extracted from https://github.com/laurent22/joplin/blob/2acd55eb810d9d6f6e4b2e1c6adff9ce72dde956/packages/lib/BaseModel.ts
|
||||
export enum ModelType {
|
||||
Note = 1,
|
||||
Folder = 2,
|
||||
Setting = 3,
|
||||
Resource = 4,
|
||||
Tag = 5,
|
||||
NoteTag = 6,
|
||||
Search = 7,
|
||||
Alarm = 8,
|
||||
MasterKey = 9,
|
||||
ItemChange = 10,
|
||||
NoteResource = 11,
|
||||
ResourceLocalState = 12,
|
||||
Revision = 13,
|
||||
Migration = 14,
|
||||
SmartFilter = 15,
|
||||
Command = 16,
|
||||
}
|
||||
|
||||
/**
|
||||
* All the below types have been extracted from: https://github.com/laurent22/joplin/blob/2acd55eb810d9d6f6e4b2e1c6adff9ce72dde956/packages/lib/services/database/types.ts
|
||||
*/
|
||||
|
||||
export interface FolderEntity {
|
||||
id?: string | null;
|
||||
title?: string;
|
||||
created_time?: number;
|
||||
updated_time?: number;
|
||||
user_created_time?: number;
|
||||
user_updated_time?: number;
|
||||
encryption_cipher_text?: string;
|
||||
encryption_applied?: number;
|
||||
parent_id?: string;
|
||||
is_shared?: number;
|
||||
share_id?: string;
|
||||
master_key_id?: string;
|
||||
icon?: string;
|
||||
type_?: number;
|
||||
}
|
||||
|
||||
export interface NoteTagEntity {
|
||||
id?: string | null;
|
||||
note_id?: string;
|
||||
tag_id?: string;
|
||||
created_time?: number;
|
||||
updated_time?: number;
|
||||
user_created_time?: number;
|
||||
user_updated_time?: number;
|
||||
encryption_cipher_text?: string;
|
||||
encryption_applied?: number;
|
||||
is_shared?: number;
|
||||
type_?: number;
|
||||
}
|
||||
export interface NoteEntity {
|
||||
id?: string | null;
|
||||
parent_id?: string;
|
||||
title?: string;
|
||||
body?: string;
|
||||
created_time?: number;
|
||||
updated_time?: number;
|
||||
is_conflict?: number;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
altitude?: number;
|
||||
author?: string;
|
||||
source_url?: string;
|
||||
is_todo?: number;
|
||||
todo_due?: number;
|
||||
todo_completed?: number;
|
||||
source?: string;
|
||||
source_application?: string;
|
||||
application_data?: string;
|
||||
order?: number;
|
||||
user_created_time?: number;
|
||||
user_updated_time?: number;
|
||||
encryption_cipher_text?: string;
|
||||
encryption_applied?: number;
|
||||
markup_language?: number;
|
||||
is_shared?: number;
|
||||
share_id?: string;
|
||||
conflict_original_id?: string;
|
||||
master_key_id?: string;
|
||||
type_?: number;
|
||||
}
|
||||
|
||||
export interface ResourceEntity {
|
||||
id?: string | null;
|
||||
title?: string;
|
||||
mime?: string;
|
||||
filename?: string;
|
||||
created_time?: number;
|
||||
updated_time?: number;
|
||||
user_created_time?: number;
|
||||
user_updated_time?: number;
|
||||
file_extension?: string;
|
||||
encryption_cipher_text?: string;
|
||||
encryption_applied?: number;
|
||||
encryption_blob_encrypted?: number;
|
||||
size?: number;
|
||||
is_shared?: number;
|
||||
share_id?: string;
|
||||
master_key_id?: string;
|
||||
type_?: number;
|
||||
}
|
||||
|
||||
export interface TagEntity {
|
||||
id?: string | null;
|
||||
title?: string;
|
||||
created_time?: number;
|
||||
updated_time?: number;
|
||||
user_created_time?: number;
|
||||
user_updated_time?: number;
|
||||
encryption_cipher_text?: string;
|
||||
encryption_applied?: number;
|
||||
is_shared?: number;
|
||||
parent_id?: string;
|
||||
type_?: number;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { GoogleKeep } from "./keep";
|
||||
import { Simplenote } from "./simplenote";
|
||||
import { ZohoNotebook } from "./zohonotebook";
|
||||
import { OneNote } from "./onenote";
|
||||
import { Joplin } from "./joplin";
|
||||
|
||||
const providerMap = {
|
||||
evernote: Evernote,
|
||||
@@ -16,18 +17,10 @@ const providerMap = {
|
||||
onenote: OneNote,
|
||||
standardnotes: StandardNotes,
|
||||
zohonotebook: ZohoNotebook,
|
||||
joplin: Joplin,
|
||||
};
|
||||
|
||||
type ProvidersMap = {
|
||||
evernote: Evernote;
|
||||
md: Markdown;
|
||||
html: HTML;
|
||||
keep: GoogleKeep;
|
||||
simplenote: Simplenote;
|
||||
onenote: OneNote;
|
||||
standardnotes: StandardNotes;
|
||||
zohonotebook: ZohoNotebook;
|
||||
};
|
||||
type ProvidersMap = typeof providerMap;
|
||||
|
||||
export type Providers = keyof ProvidersMap;
|
||||
|
||||
@@ -38,8 +31,8 @@ export class ProviderFactory {
|
||||
|
||||
static getProvider<TProvider extends Providers>(
|
||||
provider: TProvider
|
||||
): ProvidersMap[TProvider] {
|
||||
const Provider = <unknown>new providerMap[provider]();
|
||||
return <ProvidersMap[TProvider]>Provider;
|
||||
): InstanceType<ProvidersMap[TProvider]> {
|
||||
const Provider = new providerMap[provider]();
|
||||
return Provider as InstanceType<ProvidersMap[TProvider]>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const defaultEditorDescription = (item: SNNote): EditorDescription => {
|
||||
|
||||
export class StandardNotes implements IFileProvider {
|
||||
public type: "file" = "file";
|
||||
public supportedExtensions = [".txt"];
|
||||
public supportedExtensions = [".txt", ".zip"];
|
||||
public validExtensions = [...this.supportedExtensions];
|
||||
public version = "1.0.0";
|
||||
public name = "Standard Notes";
|
||||
@@ -81,10 +81,11 @@ export class StandardNotes implements IFileProvider {
|
||||
});
|
||||
|
||||
for (let item of snnotes) {
|
||||
const { createdAt, updatedAt } = this.getTimestamps(item);
|
||||
let note: Note = {
|
||||
title: item.content.title,
|
||||
dateCreated: item.created_at_timestamp,
|
||||
dateEdited: item.updated_at_timestamp || item.created_at_timestamp,
|
||||
dateCreated: createdAt,
|
||||
dateEdited: updatedAt,
|
||||
pinned: <boolean>item.content.appData[DefaultAppDomain]?.pinned,
|
||||
tags: this.getTags(item, tags),
|
||||
content: this.parseContent(item, components),
|
||||
@@ -224,4 +225,23 @@ export class StandardNotes implements IFileProvider {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getTimestamps(note: SNNote) {
|
||||
let createdAt =
|
||||
typeof note.created_at === "string"
|
||||
? new Date(note.created_at).getTime()
|
||||
: note.created_at_timestamp
|
||||
? note.created_at_timestamp / 1000
|
||||
: undefined;
|
||||
let updatedAt =
|
||||
typeof note.updated_at === "string"
|
||||
? new Date(note.updated_at).getTime()
|
||||
: note.updated_at_timestamp
|
||||
? note.updated_at_timestamp / 1000
|
||||
: undefined;
|
||||
|
||||
if (updatedAt === 0) updatedAt = undefined;
|
||||
if (createdAt === 0) createdAt = undefined;
|
||||
return { createdAt, updatedAt };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import type { AppDataField, BackupFile } from "@standardnotes/snjs";
|
||||
import { ComponentArea, EditorFeatureDescription, NoteType } from "@standardnotes/features";
|
||||
import {
|
||||
AppDataField,
|
||||
BackupFile,
|
||||
BackupFileDecryptedContextualPayload,
|
||||
ItemContent,
|
||||
} from "@standardnotes/snjs";
|
||||
import {
|
||||
ComponentArea,
|
||||
EditorFeatureDescription,
|
||||
NoteType,
|
||||
} from "@standardnotes/features";
|
||||
import { ContentType } from "@standardnotes/common";
|
||||
import {RawPayload, NoteContent, TagContent, ComponentContent} from "@standardnotes/snjs";
|
||||
import { NoteContent, TagContent, ComponentContent } from "@standardnotes/snjs";
|
||||
|
||||
enum ProtocolVersion {
|
||||
V001 = "001",
|
||||
V002 = "002",
|
||||
V003 = "003",
|
||||
V004 = "004"
|
||||
V004 = "004",
|
||||
}
|
||||
const ComponentDataDomain = 'org.standardnotes.sn.components';
|
||||
const ComponentDataDomain = "org.standardnotes.sn.components";
|
||||
const DefaultAppDomain = "org.standardnotes.sn";
|
||||
|
||||
type SNBackupItem<TContentType extends ContentType, TContent> = RawPayload & {
|
||||
type SNBackupItem<
|
||||
TContentType extends ContentType,
|
||||
TContent extends ItemContent
|
||||
> = BackupFileDecryptedContextualPayload<TContent> & {
|
||||
content_type: TContentType;
|
||||
content: TContent & {
|
||||
appData: {
|
||||
@@ -20,14 +32,14 @@ type SNBackupItem<TContentType extends ContentType, TContent> = RawPayload & {
|
||||
[ComponentDataDomain]?: Record<string, any | null | undefined>;
|
||||
};
|
||||
};
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type SNNote = SNBackupItem<ContentType.Note, NoteContent>;
|
||||
export type SNComponent = SNBackupItem<ContentType.Component, ComponentContent>;
|
||||
export type SNTag = SNBackupItem<ContentType.Tag, TagContent>;
|
||||
export type SNBackup = {
|
||||
items: RawPayload[];
|
||||
} & BackupFile;
|
||||
export type SNBackup = BackupFile;
|
||||
export type EditorDescription = Pick<
|
||||
EditorFeatureDescription,
|
||||
"note_type" | "file_type"
|
||||
@@ -41,7 +53,10 @@ export type Spreadsheet = {
|
||||
};
|
||||
|
||||
export {
|
||||
DefaultAppDomain, ComponentDataDomain, ProtocolVersion,
|
||||
DefaultAppDomain,
|
||||
ComponentDataDomain,
|
||||
ProtocolVersion,
|
||||
ContentType,
|
||||
ComponentArea, NoteType
|
||||
ComponentArea,
|
||||
NoteType,
|
||||
};
|
||||
|
||||
@@ -25,17 +25,24 @@ type Unpacker = (file: IFile) => IFile[];
|
||||
|
||||
const unpackers: Record<string, Unpacker> = {
|
||||
".zip": unzip,
|
||||
".jex": untar,
|
||||
".znote": untar,
|
||||
};
|
||||
|
||||
function isArchive(file: IFile) {
|
||||
return file.name.endsWith(".zip") || file.name.endsWith(".znote");
|
||||
for (let unpacker in unpackers) {
|
||||
if (file.name.endsWith(unpacker)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function unpack(files: IFile[], root?: string): File[] {
|
||||
const extracted: File[] = [];
|
||||
|
||||
for (let file of files) {
|
||||
if (file.path && root) file.path = path.join(root, file.path);
|
||||
extracted.push(new File(file));
|
||||
|
||||
if (isArchive(file)) {
|
||||
try {
|
||||
const root = path.basename(file.name, "");
|
||||
@@ -45,9 +52,6 @@ export function unpack(files: IFile[], root?: string): File[] {
|
||||
console.error(e);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (file.path && root) file.path = path.join(root, file.path);
|
||||
extracted.push(new File(file));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
9916
packages/onenote/package-lock.json
generated
@@ -28,6 +28,7 @@
|
||||
"@azure/msal-node-extensions": "^1.0.0-alpha.15",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"isomorphic-fetch": "^3.0.0"
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"node-html-parser": "github:thecodrr/node-html-parser"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"description": "All the custom made or modified plugins for TinyMCE used in Notesnook & other apps maintained by Streetwriters team.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node scripts/testHarness.js"
|
||||
"test": "node scripts/testHarness.js",
|
||||
"build": "echo \"Noop.\""
|
||||
},
|
||||
"keywords": [
|
||||
"tinymce",
|
||||
|
||||
8501
packages/znel/package-lock.json
generated
@@ -18,14 +18,14 @@
|
||||
},
|
||||
"author": "Abdullah Atta <abdullahatta@streetwriters.co>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"node-html-parser": "github:thecodrr/node-html-parser",
|
||||
"spark-md5": "^3.0.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/streetwriters/notesnook.git",
|
||||
"directory": "packages/znel"
|
||||
},
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"node-html-parser": "github:thecodrr/node-html-parser",
|
||||
"spark-md5": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||