mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 11:39:21 +02:00
Compare commits
12 Commits
mobile/rel
...
v3.2.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d522c65d49 | ||
|
|
4b7cecccbe | ||
|
|
151abb94e1 | ||
|
|
b67a307453 | ||
|
|
bf83c87911 | ||
|
|
8e67d3dc23 | ||
|
|
4b24f2c4b6 | ||
|
|
6cd14a5a78 | ||
|
|
6edae50960 | ||
|
|
2a6a377d06 | ||
|
|
c569dbd778 | ||
|
|
36022df189 |
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0-beta.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0-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.1.0",
|
||||
"version": "3.2.0-beta.0",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" tools:node="remove" />
|
||||
|
||||
@@ -108,6 +108,12 @@ export class AppModel {
|
||||
return new TrashViewModel(this.page);
|
||||
}
|
||||
|
||||
async goToArchive() {
|
||||
await this.page.locator(getTestId("tab-home")).click();
|
||||
await this.navigateTo("Archive");
|
||||
return new NotesViewModel(this.page, "notes", "archive");
|
||||
}
|
||||
|
||||
async goToSettings() {
|
||||
await this.profileDropdown.open(
|
||||
this.page.locator(getTestId("profile-dropdown")),
|
||||
|
||||
@@ -37,6 +37,7 @@ abstract class BaseProperties {
|
||||
private readonly pinToggle: ToggleModel;
|
||||
private readonly favoriteToggle: ToggleModel;
|
||||
private readonly lockToggle: ToggleModel;
|
||||
private readonly archiveToggle: ToggleModel;
|
||||
|
||||
constructor(
|
||||
page: Page,
|
||||
@@ -47,6 +48,7 @@ abstract class BaseProperties {
|
||||
this.pinToggle = new ToggleModel(page, `${itemPrefix}-pin`);
|
||||
this.lockToggle = new ToggleModel(page, `${itemPrefix}-lock`);
|
||||
this.favoriteToggle = new ToggleModel(page, `${itemPrefix}-favorite`);
|
||||
this.archiveToggle = new ToggleModel(page, `${itemPrefix}-archive`);
|
||||
}
|
||||
|
||||
async isPinned() {
|
||||
@@ -126,6 +128,25 @@ abstract class BaseProperties {
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async isArchived() {
|
||||
await this.open();
|
||||
const state = await this.archiveToggle.isToggled();
|
||||
await this.close();
|
||||
return state;
|
||||
}
|
||||
|
||||
async archive() {
|
||||
await this.open();
|
||||
await this.archiveToggle.on();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async unarchive() {
|
||||
await this.open();
|
||||
await this.archiveToggle.off();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
abstract isColored(color: string): Promise<boolean>;
|
||||
abstract color(color: string): Promise<void>;
|
||||
abstract open(): Promise<void>;
|
||||
|
||||
@@ -201,6 +201,37 @@ for (const actor of actors) {
|
||||
expect(await note?.getDescription()).toContain(NOTE.content);
|
||||
expect(await note?.contextMenu.isLocked()).toBe(false);
|
||||
});
|
||||
|
||||
test(`archive a note using ${actor}`, async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const note = await notes.createNote(NOTE);
|
||||
|
||||
await note?.[actor].archive();
|
||||
|
||||
const archive = await app.goToArchive();
|
||||
const archivedNote = await archive.findNote(NOTE);
|
||||
expect(await archivedNote?.contextMenu.isArchived()).toBe(true);
|
||||
expect(await archivedNote?.properties.isArchived()).toBe(true);
|
||||
});
|
||||
|
||||
test(`unarchive a note using ${actor}`, async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
let notes = await app.goToNotes();
|
||||
let note = await notes.createNote(NOTE);
|
||||
await note?.contextMenu.archive();
|
||||
|
||||
const archive = await app.goToArchive();
|
||||
const archivedNote = await archive.findNote(NOTE);
|
||||
await archivedNote?.[actor].unarchive();
|
||||
|
||||
notes = await app.goToNotes();
|
||||
note = await notes.findNote(NOTE);
|
||||
expect(await note?.contextMenu.isArchived()).toBe(false);
|
||||
expect(await note?.properties.isArchived()).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
test("open a locked note", async ({ page }) => {
|
||||
@@ -339,3 +370,48 @@ test(`sort notes`, async ({ page }, info) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("archived favorite note shouldn't be in favorites note list", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const note = await notes.createNote(NOTE);
|
||||
|
||||
await note?.contextMenu.favorite();
|
||||
await note?.contextMenu.archive();
|
||||
|
||||
const favorites = await app.goToFavorites();
|
||||
expect(await favorites.findNote(NOTE)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("archived tag note shouldn't be in tags note list", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const tags = await app.goToTags();
|
||||
const tag = await tags.createItem({ title: "my-tag" });
|
||||
const notes = await tag?.open();
|
||||
|
||||
const note = await notes?.createNote(NOTE);
|
||||
expect(await notes?.findNote(NOTE)).toBeDefined();
|
||||
await note?.contextMenu.archive();
|
||||
|
||||
expect(await notes?.findNote(NOTE)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("archived notebook note shouldn't be in notebooks note list", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook = await notebooks.createNotebook({ title: "my-notebook" });
|
||||
const notes = await notebook?.openNotebook();
|
||||
|
||||
const note = await notes?.createNote(NOTE);
|
||||
expect(await notes?.findNote(NOTE)).toBeDefined();
|
||||
await note?.contextMenu.archive();
|
||||
|
||||
expect(await notes?.findNote(NOTE)).toBeUndefined();
|
||||
});
|
||||
|
||||
1412
apps/web/package-lock.json
generated
1412
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0-beta.0",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -19,7 +19,7 @@
|
||||
"@lingui/react": "5.1.2",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/react": "1.6.1",
|
||||
"@notesnook-importer/core": "^2.1.1",
|
||||
"@notesnook-importer/core": "^2.2.2",
|
||||
"@notesnook/common": "file:../../packages/common",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.tabsScroll,
|
||||
.titlebarLogo,
|
||||
.theme-scope-titleBar,
|
||||
.navigation-menu-header,
|
||||
.route-container-header {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
@@ -45,11 +45,7 @@ export default function Accordion(
|
||||
containerSx,
|
||||
...restProps
|
||||
} = props;
|
||||
const [isContentHidden, setIsContentHidden] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsContentHidden(isClosed);
|
||||
}, [isClosed]);
|
||||
const [isContentHidden, setIsContentHidden] = useState(isClosed);
|
||||
|
||||
return (
|
||||
<Flex sx={{ flexDirection: "column", ...sx }} {...restProps}>
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
mdiClose,
|
||||
mdiDotsVertical,
|
||||
mdiTrashCanOutline,
|
||||
mdiArchiveOutline,
|
||||
mdiBookRemoveOutline,
|
||||
mdiMagnify,
|
||||
mdiMenu,
|
||||
@@ -351,6 +352,7 @@ export const Cross = createIcon(mdiClose);
|
||||
export const MoreVertical = createIcon(mdiDotsVertical);
|
||||
export const MoreHorizontal = createIcon(mdiDotsHorizontal);
|
||||
export const Trash = createIcon(mdiTrashCanOutline);
|
||||
export const Archive = createIcon(mdiArchiveOutline);
|
||||
export const TopicRemove = createIcon(mdiBookmarkRemoveOutline);
|
||||
export const NotebookRemove = createIcon(mdiBookRemoveOutline);
|
||||
export const Search = createIcon(mdiMagnify);
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
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 {
|
||||
IFile,
|
||||
IFileProvider,
|
||||
ProviderSettings,
|
||||
transform
|
||||
} from "@notesnook-importer/core";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { Button, Flex, Input, Text } from "@theme-ui/components";
|
||||
import { xxhash64 } from "hash-wasm";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { importNote } from "../../../utils/importer";
|
||||
import Accordion from "../../accordion";
|
||||
import { TransformResult } from "../types";
|
||||
import { useStore as useAppStore } from "../../../stores/app-store";
|
||||
|
||||
type FileProviderHandlerProps = {
|
||||
provider: IFileProvider;
|
||||
onTransformFinished: (result: TransformResult) => void;
|
||||
};
|
||||
|
||||
type Progress = {
|
||||
total: number;
|
||||
done: number;
|
||||
};
|
||||
|
||||
export function FileProviderHandler(props: FileProviderHandlerProps) {
|
||||
const { provider, onTransformFinished } = props;
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [filesProgress, setFilesProgress] = useState<Progress>({
|
||||
done: 0,
|
||||
total: 0
|
||||
});
|
||||
const [totalNoteCount, setTotalNoteCount] = useState(0);
|
||||
const [_, setCounter] = useState<number>(0);
|
||||
const logs = useRef<string[]>([]);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setFiles((files) => {
|
||||
const newFiles = [...acceptedFiles, ...files];
|
||||
return newFiles;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
file: provider?.supportedExtensions?.concat([".zip"])
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setFiles([]);
|
||||
}, [provider]);
|
||||
|
||||
async function onStartImport() {
|
||||
let totalNotes = 0;
|
||||
const errors: Error[] = [];
|
||||
const settings: ProviderSettings = {
|
||||
clientType: "browser",
|
||||
hasher: { type: "xxh64", hash: xxhash64 },
|
||||
storage: {
|
||||
clear: async () => undefined,
|
||||
get: async () => [],
|
||||
write: async (data) => {
|
||||
logs.current.push(
|
||||
`[${new Date().toLocaleString()}] Pushing ${
|
||||
data.title
|
||||
} into database`
|
||||
);
|
||||
|
||||
await importNote(data);
|
||||
},
|
||||
iterate: async function* () {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
log: (message) => {
|
||||
logs.current.push(
|
||||
`[${new Date(message.date).toLocaleString()}] ${message.text}`
|
||||
);
|
||||
setCounter((s) => ++s);
|
||||
},
|
||||
reporter: () => {
|
||||
setTotalNoteCount(++totalNotes);
|
||||
}
|
||||
};
|
||||
|
||||
setTotalNoteCount(0);
|
||||
setFilesProgress({
|
||||
total: files.length,
|
||||
done: 0
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
setFilesProgress((p) => ({
|
||||
...p,
|
||||
done: p.done + 1
|
||||
}));
|
||||
|
||||
const providerFile: IFile = {
|
||||
name: file.name,
|
||||
modifiedAt: file.lastModified,
|
||||
size: file.size,
|
||||
data: file
|
||||
};
|
||||
errors.push(...(await transform(provider, [providerFile], settings)));
|
||||
}
|
||||
await useAppStore.getState().refresh();
|
||||
onTransformFinished({
|
||||
totalNotes,
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
if (filesProgress.done) {
|
||||
return (
|
||||
<Flex sx={{ flexDirection: "column", alignItems: "stretch" }}>
|
||||
<Text variant="subtitle">
|
||||
Processing {filesProgress.done} of {filesProgress.total} file(s)
|
||||
</Text>
|
||||
<Text variant="body" sx={{ mt: 4, textAlign: "center" }}>
|
||||
Found {totalNoteCount} notes
|
||||
</Text>
|
||||
{logs.current.length > 0 && (
|
||||
<Accordion
|
||||
title="Logs"
|
||||
isClosed={false}
|
||||
sx={{
|
||||
border: "1px solid var(--border)",
|
||||
mt: 2
|
||||
}}
|
||||
>
|
||||
<ScrollContainer>
|
||||
<Text
|
||||
as="pre"
|
||||
variant="body"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
maxHeight: 250,
|
||||
p: 2
|
||||
}}
|
||||
>
|
||||
{logs.current.map((c, index) => (
|
||||
<>
|
||||
<span key={index.toString()}>{c}</span>
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</Text>
|
||||
</ScrollContainer>
|
||||
</Accordion>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex sx={{ flexDirection: "column", alignItems: "stretch" }}>
|
||||
<Text variant="subtitle">Select {provider.name} files</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
as={"div"}
|
||||
sx={{ mt: 1, color: "paragraph", whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
Check out our step-by-step guide on{" "}
|
||||
<a href={provider.helpLink} target="_blank" rel="noreferrer">
|
||||
how to import from {provider?.name}.
|
||||
</a>
|
||||
</Text>
|
||||
<Flex
|
||||
{...getRootProps()}
|
||||
sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 100,
|
||||
border: "2px dashed var(--border)",
|
||||
borderRadius: "default",
|
||||
mt: 2,
|
||||
cursor: "pointer",
|
||||
":hover": {
|
||||
bg: "background-secondary"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Input {...getInputProps()} />
|
||||
<Text variant="body" sx={{ textAlign: "center" }}>
|
||||
{isDragActive
|
||||
? "Drop the files here"
|
||||
: "Drag & drop files here, or click to select files"}
|
||||
<br />
|
||||
<Text variant="subBody">
|
||||
Only {provider?.supportedExtensions.join(", ")} files are supported.{" "}
|
||||
{provider?.supportedExtensions.includes(".zip") ? null : (
|
||||
<>
|
||||
You can also select .zip files containing{" "}
|
||||
{provider?.supportedExtensions.join(", ")} files.
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
{provider.examples ? (
|
||||
<>For example, {provider.examples.join(", ")}</>
|
||||
) : null}
|
||||
</Text>
|
||||
</Text>
|
||||
</Flex>
|
||||
{files.length > 0 ? (
|
||||
<Accordion
|
||||
isClosed
|
||||
title={`${files.length} ${
|
||||
files.length > 1 ? "files" : "file"
|
||||
} selected`}
|
||||
sx={{
|
||||
border: "1px solid var(--border)",
|
||||
mt: 2,
|
||||
borderRadius: "default"
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{ flexDirection: "column", overflowY: "auto", maxHeight: 400 }}
|
||||
>
|
||||
{files.map((file, index) => (
|
||||
<Flex
|
||||
key={file.name}
|
||||
sx={{
|
||||
p: 2,
|
||||
bg: index % 2 ? "transparent" : "background-secondary",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
cursor: "pointer",
|
||||
":hover": {
|
||||
bg: "hover"
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
setFiles((files) => {
|
||||
const _files = files.slice();
|
||||
_files.splice(index, 1);
|
||||
return _files;
|
||||
});
|
||||
}}
|
||||
title="Click to remove"
|
||||
>
|
||||
<Text variant="body">{file.name}</Text>
|
||||
<Text variant="body">{formatBytes(file.size)}</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</Accordion>
|
||||
) : null}
|
||||
|
||||
{!!files.length && (
|
||||
<>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
bg: "primary",
|
||||
color: "static",
|
||||
mt: 2,
|
||||
borderRadius: 5,
|
||||
p: 1
|
||||
}}
|
||||
>
|
||||
Please make sure you have at least{" "}
|
||||
{formatBytes(files.reduce((prev, file) => prev + file.size, 0))} of
|
||||
free space before proceeding.
|
||||
</Text>
|
||||
{provider.requiresNetwork ? (
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
bg: "background-error",
|
||||
color: "paragraph-error",
|
||||
mt: 2,
|
||||
borderRadius: 5,
|
||||
p: 1
|
||||
}}
|
||||
>
|
||||
Please make sure you have good Internet access before proceeding.
|
||||
The importer may send network requests in order to download media
|
||||
resources such as images, files, and other attachments.
|
||||
</Text>
|
||||
) : null}
|
||||
<Button
|
||||
variant="accent"
|
||||
sx={{ alignSelf: "center", mt: 2, px: 4 }}
|
||||
onClick={onStartImport}
|
||||
>
|
||||
Start importing
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
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 { Button, Flex, Text } from "@theme-ui/components";
|
||||
import Accordion from "../../accordion";
|
||||
|
||||
type ImportErrorsProps = {
|
||||
errors: Error[];
|
||||
};
|
||||
|
||||
export function ImportErrors(props: ImportErrorsProps) {
|
||||
return (
|
||||
<Accordion
|
||||
isClosed={false}
|
||||
title={`${props.errors.length} errors occured`}
|
||||
sx={{ bg: "background-error", borderRadius: "default", mt: 2 }}
|
||||
color="paragraph-error"
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column", px: 2, pb: 2, overflowX: "auto" }}>
|
||||
{props.errors.map((error, index) => (
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ color: "paragraph-error", my: 1, fontFamily: "monospace" }}
|
||||
>
|
||||
{index + 1}. {error.message}
|
||||
<br />
|
||||
</Text>
|
||||
))}
|
||||
<Button
|
||||
variant="error"
|
||||
sx={{ alignSelf: "start", mt: 2 }}
|
||||
onClick={() =>
|
||||
window.open(
|
||||
"https://github.com/streetwriters/notesnook-importer/issues/new",
|
||||
"_blank"
|
||||
)
|
||||
}
|
||||
>
|
||||
Send us a bug report
|
||||
</Button>
|
||||
</Flex>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
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 { IProvider } from "@notesnook-importer/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { CheckCircleOutline } from "../../icons";
|
||||
import { TransformResult } from "../types";
|
||||
import { ImportErrors } from "./import-errors";
|
||||
|
||||
type ImportResultProps = {
|
||||
result: TransformResult;
|
||||
provider: IProvider;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
export function ImportResult(props: ImportResultProps) {
|
||||
const { result, onReset } = props;
|
||||
|
||||
if (result.totalNotes <= 0) {
|
||||
return (
|
||||
<Flex sx={{ flexDirection: "column", alignItems: "stretch" }}>
|
||||
<Text variant="title">Import unsuccessful</Text>
|
||||
<Text variant="body" sx={{ mt: 2 }}>
|
||||
We failed to import the selected files. Please try again.
|
||||
</Text>
|
||||
{result.errors.length > 0 && <ImportErrors errors={result.errors} />}
|
||||
<Button
|
||||
variant="accent"
|
||||
onClick={onReset}
|
||||
sx={{ alignSelf: "center", mt: 2, px: 4 }}
|
||||
>
|
||||
Start over
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CheckCircleOutline color="accent" />
|
||||
<Text variant="body" my={2} sx={{ textAlign: "center" }}>
|
||||
{strings.importCompleted()}. {props.result.totalNotes} notes
|
||||
successfully imported.
|
||||
{strings.errorsOccured(result.errors.length)}
|
||||
</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={async () => {
|
||||
onReset();
|
||||
}}
|
||||
>
|
||||
{strings.startOver()}
|
||||
</Button>
|
||||
{result.errors.length > 0 && (
|
||||
<Flex
|
||||
my={1}
|
||||
bg="var(--background-error)"
|
||||
p={1}
|
||||
sx={{ flexDirection: "column" }}
|
||||
>
|
||||
{result.errors.map((error) => (
|
||||
<Text
|
||||
key={error.message}
|
||||
variant="body"
|
||||
sx={{
|
||||
color: "var(--paragraph-error)"
|
||||
}}
|
||||
>
|
||||
{error.message}
|
||||
</Text>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
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 {
|
||||
INetworkProvider,
|
||||
OneNote,
|
||||
OneNoteSettings,
|
||||
ProviderSettings,
|
||||
transform
|
||||
} from "@notesnook-importer/core";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { xxhash64 } from "hash-wasm";
|
||||
import { useRef, useState } from "react";
|
||||
import { importNote } from "../../../utils/importer";
|
||||
import Accordion from "../../accordion";
|
||||
import { TransformResult } from "../types";
|
||||
|
||||
type NetworkProviderHandlerProps = {
|
||||
provider: INetworkProvider<ProviderSettings>;
|
||||
onTransformFinished: (result: TransformResult) => void;
|
||||
};
|
||||
|
||||
type Progress = {
|
||||
total: number;
|
||||
done: number;
|
||||
};
|
||||
|
||||
function getProviderSettings(
|
||||
provider: INetworkProvider<ProviderSettings>,
|
||||
settings: ProviderSettings
|
||||
) {
|
||||
if (provider instanceof OneNote) {
|
||||
return {
|
||||
...settings,
|
||||
cache: false,
|
||||
clientId: "6c32bdbd-c6c6-4cda-bcf0-0c8ec17e5804",
|
||||
redirectUri:
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: "https://app.notesnook.com"
|
||||
} as OneNoteSettings;
|
||||
}
|
||||
}
|
||||
|
||||
export function NetworkProviderHandler(props: NetworkProviderHandlerProps) {
|
||||
const { provider, onTransformFinished } = props;
|
||||
const [totalNoteCount, setTotalNoteCount] = useState(0);
|
||||
const [_, setCounter] = useState<number>(0);
|
||||
const logs = useRef<string[]>([]);
|
||||
|
||||
async function onStartImport() {
|
||||
let totalNotes = 0;
|
||||
const settings = getProviderSettings(provider, {
|
||||
clientType: "browser",
|
||||
hasher: { type: "xxh64", hash: xxhash64 },
|
||||
storage: {
|
||||
clear: async () => undefined,
|
||||
get: async () => [],
|
||||
write: async (data) => {
|
||||
logs.current.push(
|
||||
`[${new Date().toLocaleString()}] Pushing ${
|
||||
data.title
|
||||
} into database`
|
||||
);
|
||||
|
||||
await importNote(data);
|
||||
},
|
||||
iterate: async function* () {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
log: (message) => {
|
||||
logs.current.push(
|
||||
`[${new Date(message.date).toLocaleString()}] ${message.text}`
|
||||
);
|
||||
setCounter((s) => ++s);
|
||||
},
|
||||
reporter: () => {
|
||||
setTotalNoteCount(++totalNotes);
|
||||
}
|
||||
});
|
||||
if (!settings) return;
|
||||
|
||||
setTotalNoteCount(0);
|
||||
|
||||
const errors = await transform(provider, settings);
|
||||
console.log(errors);
|
||||
onTransformFinished({
|
||||
totalNotes,
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
alignItems: "stretch"
|
||||
}}
|
||||
>
|
||||
{totalNoteCount ? (
|
||||
<>
|
||||
<Text variant="title">Importing your notes from {provider.name}</Text>
|
||||
<Text variant="body" sx={{ mt: 4 }}>
|
||||
Found {totalNoteCount} notes
|
||||
</Text>
|
||||
{logs.current.length > 0 && (
|
||||
<Accordion
|
||||
isClosed={false}
|
||||
title="Logs"
|
||||
sx={{
|
||||
border: "1px solid var(--border)",
|
||||
mt: 2
|
||||
}}
|
||||
>
|
||||
<ScrollContainer>
|
||||
<Text
|
||||
as="pre"
|
||||
variant="body"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
maxHeight: 250,
|
||||
p: 2
|
||||
}}
|
||||
>
|
||||
{logs.current.map((c, index) => (
|
||||
<>
|
||||
<span key={index.toString()}>{c}</span>
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</Text>
|
||||
</ScrollContainer>
|
||||
</Accordion>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text variant="title">Connect your {provider.name} account</Text>
|
||||
<Text variant="body" sx={{ color: "fontTertiary", mt: [2, 0] }}>
|
||||
Check out our step-by-step guide on{" "}
|
||||
<a href={provider.helpLink} target="_blank" rel="noreferrer">
|
||||
how to import from {provider.name}.
|
||||
</a>
|
||||
</Text>
|
||||
<Button
|
||||
variant="accent"
|
||||
onClick={onStartImport}
|
||||
sx={{ my: 4, alignSelf: "center" }}
|
||||
>
|
||||
Start importing
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
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 {
|
||||
IProvider,
|
||||
ProviderFactory,
|
||||
Providers
|
||||
} from "@notesnook-importer/core";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
|
||||
type ProviderSelectorProps = {
|
||||
onProviderChanged: (provider: IProvider) => void;
|
||||
};
|
||||
|
||||
export function ProviderSelector(props: ProviderSelectorProps) {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "start",
|
||||
gap: 4
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column", flex: 1 }}>
|
||||
<Text variant="subtitle">Select a notes app to import from</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
as="div"
|
||||
sx={{ mt: 1, color: "paragraph", whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
Can't find your notes app in the list?{" "}
|
||||
<a href="https://github.com/streetwriters/notesnook-importer/issues/new">
|
||||
Send us a request.
|
||||
</a>
|
||||
</Text>
|
||||
</Flex>
|
||||
<select
|
||||
style={{
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
outline: "none",
|
||||
border: "1px solid var(--border-secondary)",
|
||||
borderRadius: "5px",
|
||||
color: "var(--paragraph)",
|
||||
padding: "5px",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "") return;
|
||||
const providerName: Providers = e.target.value as Providers;
|
||||
props.onProviderChanged(ProviderFactory.getProvider(providerName));
|
||||
}}
|
||||
>
|
||||
<option value="">Select notes app</option>
|
||||
{ProviderFactory.getAvailableProviders().map((provider) => (
|
||||
<option key={provider} value={provider}>
|
||||
{ProviderFactory.getProvider(provider as Providers).name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
82
apps/web/src/components/importer/importer.tsx
Normal file
82
apps/web/src/components/importer/importer.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
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 { Flex } from "@theme-ui/components";
|
||||
import { useState } from "react";
|
||||
import { ProviderSelector } from "./components/provider-selector";
|
||||
import { FileProviderHandler } from "./components/file-provider-handler";
|
||||
import { ImportResult } from "./components/import-result";
|
||||
import { IProvider } from "@notesnook-importer/core";
|
||||
import { NetworkProviderHandler } from "./components/network-provider-handler";
|
||||
import { TransformResult } from "./types";
|
||||
|
||||
export function Importer() {
|
||||
const [selectedProvider, setSelectedProvider] = useState<IProvider>();
|
||||
const [transformResult, setTransformResult] = useState<TransformResult>();
|
||||
const [instanceKey, setInstanceKey] = useState<string>(`${Math.random()}`);
|
||||
|
||||
return (
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
alignItems: "stretch",
|
||||
gap: 4
|
||||
}}
|
||||
>
|
||||
<ProviderSelector
|
||||
onProviderChanged={(provider) => {
|
||||
setInstanceKey(`${Math.random()}`);
|
||||
setSelectedProvider(provider);
|
||||
setTransformResult(undefined);
|
||||
}}
|
||||
/>
|
||||
{selectedProvider ? (
|
||||
<>
|
||||
{selectedProvider.type === "file" ? (
|
||||
<FileProviderHandler
|
||||
key={instanceKey}
|
||||
provider={selectedProvider}
|
||||
onTransformFinished={setTransformResult}
|
||||
/>
|
||||
) : selectedProvider.type === "network" ? (
|
||||
<NetworkProviderHandler
|
||||
key={instanceKey}
|
||||
provider={selectedProvider}
|
||||
onTransformFinished={setTransformResult}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
{transformResult && selectedProvider ? (
|
||||
<>
|
||||
<ImportResult
|
||||
result={transformResult}
|
||||
provider={selectedProvider}
|
||||
onReset={() => {
|
||||
setTransformResult(undefined);
|
||||
setInstanceKey(`${Math.random()}`);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
20
apps/web/src/components/importer/index.ts
Normal file
20
apps/web/src/components/importer/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
export * from "./importer";
|
||||
23
apps/web/src/components/importer/types.ts
Normal file
23
apps/web/src/components/importer/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
export type TransformResult = {
|
||||
totalNotes: number;
|
||||
errors: Error[];
|
||||
};
|
||||
@@ -36,7 +36,7 @@ export type Context =
|
||||
}
|
||||
| NotebookContext
|
||||
| {
|
||||
type: "favorite" | "monographs";
|
||||
type: "favorite" | "monographs" | "archive";
|
||||
};
|
||||
|
||||
export type WithDateEdited<T> = { items: T[]; dateEdited: number };
|
||||
|
||||
@@ -46,7 +46,8 @@ import {
|
||||
Reset,
|
||||
Rename,
|
||||
ExpandSidebar,
|
||||
HamburgerMenu
|
||||
HamburgerMenu,
|
||||
Archive
|
||||
} from "../icons";
|
||||
import { SortableNavigationItem } from "./navigation-item";
|
||||
import {
|
||||
@@ -111,7 +112,7 @@ import { showSortMenu } from "../group-header";
|
||||
import { Freeze } from "react-freeze";
|
||||
|
||||
type Route = {
|
||||
id: "notes" | "favorites" | "reminders" | "monographs" | "trash";
|
||||
id: "notes" | "favorites" | "reminders" | "monographs" | "trash" | "archive";
|
||||
title: string;
|
||||
path: string;
|
||||
icon: Icon;
|
||||
@@ -138,7 +139,13 @@ const routes: Route[] = [
|
||||
path: "/monographs",
|
||||
icon: Monographs
|
||||
},
|
||||
{ id: "trash", title: strings.routes.Trash(), path: "/trash", icon: Trash }
|
||||
{ id: "trash", title: strings.routes.Trash(), path: "/trash", icon: Trash },
|
||||
{
|
||||
id: "archive",
|
||||
title: strings.archive(),
|
||||
path: "/archive",
|
||||
icon: Archive
|
||||
}
|
||||
];
|
||||
|
||||
const tabs = [
|
||||
@@ -272,7 +279,9 @@ function NavigationMenu({ onExpand }: { onExpand?: () => void }) {
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
className="navigation-menu-header"
|
||||
sx={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 1
|
||||
@@ -704,6 +713,8 @@ function ItemCount({ item }: { item: Route | Color | Notebook | Tag }) {
|
||||
return trash?.length || 0;
|
||||
case "monographs":
|
||||
return monographs?.length || 0;
|
||||
case "archive":
|
||||
return db.notes.archived.count();
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
AddReminder,
|
||||
AddToNotebook,
|
||||
Alert,
|
||||
Archive,
|
||||
Attachment,
|
||||
AttachmentError,
|
||||
Circle,
|
||||
@@ -395,6 +396,15 @@ export const noteMenuItems: (
|
||||
await AddReminderDialog.show({ note });
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "archive",
|
||||
title: strings.archive(),
|
||||
isChecked: note.archived,
|
||||
icon: Archive.path,
|
||||
onClick: () => store.archive(!note.archived, ...ids),
|
||||
multiSelect: true
|
||||
},
|
||||
{ key: "sep1", type: "separator" },
|
||||
{
|
||||
type: "button",
|
||||
|
||||
@@ -31,7 +31,8 @@ import {
|
||||
ChevronRight,
|
||||
LinkedTo,
|
||||
ReferencedIn as ReferencedInIcon,
|
||||
Note as NoteIcon
|
||||
Note as NoteIcon,
|
||||
Archive
|
||||
} from "../icons";
|
||||
import { Box, Button, Flex, Text, FlexProps } from "@theme-ui/components";
|
||||
import {
|
||||
@@ -83,6 +84,12 @@ const tools = [
|
||||
label: strings.readOnly(),
|
||||
property: "readonly"
|
||||
},
|
||||
{
|
||||
key: "archive",
|
||||
icon: Archive,
|
||||
label: strings.archive(),
|
||||
property: "archived"
|
||||
},
|
||||
{
|
||||
key: "local-only",
|
||||
icon: SyncOff,
|
||||
@@ -837,10 +844,17 @@ export function Section({
|
||||
}
|
||||
|
||||
function changeToggleState(
|
||||
prop: "lock" | "readonly" | "local-only" | "pin" | "favorite",
|
||||
prop: "lock" | "readonly" | "local-only" | "pin" | "favorite" | "archive",
|
||||
session: ReadonlyEditorSession | DefaultEditorSession
|
||||
) {
|
||||
const { id: sessionId, readonly, localOnly, pinned, favorite } = session.note;
|
||||
const {
|
||||
id: sessionId,
|
||||
readonly,
|
||||
localOnly,
|
||||
pinned,
|
||||
favorite,
|
||||
archived
|
||||
} = session.note;
|
||||
if (!sessionId) return;
|
||||
switch (prop) {
|
||||
case "lock":
|
||||
@@ -855,6 +869,8 @@ function changeToggleState(
|
||||
return noteStore.pin(!pinned, sessionId);
|
||||
case "favorite":
|
||||
return noteStore.favorite(!favorite, sessionId);
|
||||
case "archive":
|
||||
return noteStore.archive(!archived, sessionId);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,28 +93,22 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "New sidebar UI/UX",
|
||||
title: "Archive",
|
||||
subtitle: (
|
||||
<>
|
||||
Your notebooks & tags are now displayed directly in the
|
||||
sidebar making organization & navigation much easier.
|
||||
You can now archive your notes to hide them from the main list.
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Hierarchical tree view for notebooks",
|
||||
title: "Integrated Notesnook Importer",
|
||||
subtitle: (
|
||||
<>
|
||||
Notebooks are now displayed in a hierarchical tree view so you
|
||||
can jump back and forth between them easily.
|
||||
Notesnook Importer is now integrated directly into the app
|
||||
allowing for faster and more seamless import of your notes. Just
|
||||
drag and drop your notes from other note-taking apps and viola!
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Set any tag, color or notebook as homepage",
|
||||
subtitle: (
|
||||
<>You can now set any tag, color or notebook as your homepage.</>
|
||||
)
|
||||
}
|
||||
],
|
||||
cta: {
|
||||
|
||||
@@ -48,6 +48,7 @@ import { ErrorText } from "../components/error-text";
|
||||
import { BuyDialog } from "./buy-dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { SettingsDialog } from "./settings";
|
||||
|
||||
type Step = {
|
||||
title: string;
|
||||
@@ -297,7 +298,9 @@ function Importer({ onClose }: { onClose: () => void }) {
|
||||
variant="accent"
|
||||
sx={{ borderRadius: 50, alignSelf: "center", px: 30 }}
|
||||
onClick={() => {
|
||||
window.open("https://importer.notesnook.com/", "_blank");
|
||||
SettingsDialog.show({
|
||||
activeSection: "importer"
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,318 +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 { strings } from "@notesnook/intl";
|
||||
import { Button, Flex, Input, Link, Text, Box } from "@theme-ui/components";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { db } from "../../../common/db";
|
||||
import { CheckCircleOutline } from "../../../components/icons";
|
||||
import Accordion from "../../../components/accordion";
|
||||
import { importFiles } from "../../../utils/importer";
|
||||
import { useStore as useAppStore } from "../../../stores/app-store";
|
||||
|
||||
type Provider = { title: string; link: string };
|
||||
const POPULAR_PROVIDERS: Provider[] = [
|
||||
{
|
||||
title: "Evernote",
|
||||
link: "https://help.notesnook.com/importing-notes/import-notes-from-evernote"
|
||||
},
|
||||
{
|
||||
title: "Simplenote",
|
||||
link: "https://help.notesnook.com/importing-notes/import-notes-from-simplenote"
|
||||
},
|
||||
{
|
||||
title: "Google Keep",
|
||||
link: "https://help.notesnook.com/importing-notes/import-notes-from-googlekeep"
|
||||
},
|
||||
{
|
||||
title: "Obsidian",
|
||||
link: "https://help.notesnook.com/importing-notes/import-notes-from-obsidian"
|
||||
},
|
||||
{
|
||||
title: "Joplin",
|
||||
link: "https://help.notesnook.com/importing-notes/import-notes-from-joplin"
|
||||
},
|
||||
{
|
||||
title: "Markdown files",
|
||||
link: "https://help.notesnook.com/importing-notes/import-notes-from-markdown-files"
|
||||
},
|
||||
{
|
||||
title: "other apps",
|
||||
link: "https://help.notesnook.com/importing-notes/"
|
||||
}
|
||||
];
|
||||
|
||||
export function Importer() {
|
||||
const [isDone, setIsDone] = useState(false);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [errors, setErrors] = useState<Error[]>([]);
|
||||
const notesCounter = useRef<HTMLSpanElement>(null);
|
||||
const importProgress = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setFiles((files) => {
|
||||
const newFiles = [...acceptedFiles, ...files];
|
||||
return newFiles;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
"application/zip": [".zip"]
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
// justifyContent: "center",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{isImporting ? (
|
||||
<>
|
||||
<Text variant="title" sx={{ textAlign: "center", mb: 4, mt: 150 }}>
|
||||
<span ref={notesCounter}>0</span> {strings.notesImported()}.
|
||||
</Text>
|
||||
|
||||
<Flex
|
||||
ref={importProgress}
|
||||
sx={{
|
||||
alignSelf: "start",
|
||||
borderRadius: "default",
|
||||
height: "5px",
|
||||
bg: "accent",
|
||||
width: `0%`
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : isDone ? (
|
||||
<>
|
||||
<CheckCircleOutline color="accent" sx={{ mt: 150 }} />
|
||||
<Text variant="body" my={2} sx={{ textAlign: "center" }}>
|
||||
{strings.importCompleted()}. {strings.errorsOccured(errors.length)}
|
||||
</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={async () => {
|
||||
setErrors([]);
|
||||
setFiles([]);
|
||||
setIsDone(false);
|
||||
setIsImporting(false);
|
||||
}}
|
||||
>
|
||||
{strings.startOver()}
|
||||
</Button>
|
||||
{errors.length > 0 && (
|
||||
<Flex
|
||||
my={1}
|
||||
bg="var(--background-error)"
|
||||
p={1}
|
||||
sx={{ flexDirection: "column" }}
|
||||
>
|
||||
{errors.map((error) => (
|
||||
<Text
|
||||
key={error.message}
|
||||
variant="body"
|
||||
sx={{
|
||||
color: "var(--paragraph-error)"
|
||||
}}
|
||||
>
|
||||
{error.message}
|
||||
</Text>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Accordion
|
||||
isClosed={false}
|
||||
title="How to import your notes from other apps?"
|
||||
containerSx={{
|
||||
px: 2,
|
||||
pb: 2,
|
||||
border: "1px solid var(--border)",
|
||||
borderTopWidth: 0,
|
||||
borderRadius: "default",
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0
|
||||
}}
|
||||
>
|
||||
<Text variant="subtitle" sx={{ mt: 2 }}>
|
||||
Quick start guide:
|
||||
</Text>
|
||||
<Box as="ol" sx={{ my: 1 }}>
|
||||
<Text as="li" variant="body">
|
||||
Go to{" "}
|
||||
<Link
|
||||
href="https://importer.notesnook.com/"
|
||||
target="_blank"
|
||||
sx={{ color: "accent" }}
|
||||
>
|
||||
https://importer.notesnook.com/
|
||||
</Link>
|
||||
</Text>
|
||||
<Text as="li" variant="body">
|
||||
Select the app you want to import from.
|
||||
</Text>
|
||||
<Text as="li" variant="body">
|
||||
Drag drop or select the files you exported from the other app.
|
||||
</Text>
|
||||
<Text as="li" variant="body">
|
||||
Start the importer and wait for it to complete processing.
|
||||
</Text>
|
||||
<Text as="li" variant="body">
|
||||
Download the .zip file from the Importer.
|
||||
</Text>
|
||||
<Text as="li" variant="body">
|
||||
Drop the .zip file below to complete your import.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Text variant={"body"} sx={{ fontWeight: "bold" }}>
|
||||
For detailed steps with screenshots, refer to the help article for
|
||||
each app:
|
||||
</Text>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr 1fr",
|
||||
gap: 1,
|
||||
mt: 1
|
||||
}}
|
||||
>
|
||||
{POPULAR_PROVIDERS.map((provider) => (
|
||||
<Button
|
||||
key={provider.link}
|
||||
variant="icon"
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
border: "1px solid var(--border)",
|
||||
textAlign: "left"
|
||||
}}
|
||||
onClick={() => window.open(provider.link, "_blank")}
|
||||
>
|
||||
Import from {provider.title}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
</Accordion>
|
||||
<Flex
|
||||
{...getRootProps()}
|
||||
data-test-id="import-dialog-select-files"
|
||||
sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: 200,
|
||||
flexShrink: 0,
|
||||
width: "full",
|
||||
border: "2px dashed var(--border)",
|
||||
borderRadius: "default",
|
||||
mt: 2,
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<Input {...getInputProps()} />
|
||||
<Text variant="body" sx={{ textAlign: "center" }}>
|
||||
{isDragActive
|
||||
? strings.dropFilesHere()
|
||||
: strings.dragAndDropFiles()}
|
||||
<br />
|
||||
<Text variant="subBody">{strings.onlyZipSupported()}</Text>
|
||||
</Text>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", mt: 2 }}>
|
||||
{files.map((file, i) => (
|
||||
<Text
|
||||
key={file.name}
|
||||
p={1}
|
||||
sx={{
|
||||
":hover": { bg: "hover" },
|
||||
cursor: "pointer",
|
||||
borderRadius: "default"
|
||||
}}
|
||||
onClick={() => {
|
||||
setFiles((files) => {
|
||||
const cloned = files.slice();
|
||||
cloned.splice(i, 1);
|
||||
return cloned;
|
||||
});
|
||||
}}
|
||||
variant="body"
|
||||
title="Click to remove"
|
||||
>
|
||||
{file.name}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</Flex>
|
||||
{/* <Flex my={1} sx={{ flexDirection: "column" }}>
|
||||
|
||||
</Flex> */}
|
||||
<Button
|
||||
variant="accent"
|
||||
sx={{ alignSelf: "end", mt: 1 }}
|
||||
onClick={async () => {
|
||||
setIsDone(false);
|
||||
setIsImporting(true);
|
||||
|
||||
await db.syncer?.acquireLock(async () => {
|
||||
try {
|
||||
for await (const message of importFiles(files)) {
|
||||
switch (message.type) {
|
||||
case "error":
|
||||
setErrors((errors) => [...errors, message.error]);
|
||||
break;
|
||||
case "progress": {
|
||||
const { count } = message;
|
||||
if (notesCounter.current)
|
||||
notesCounter.current.innerText = `${count}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error) {
|
||||
setErrors((errors) => [...errors, e as Error]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await useAppStore.getState().refresh();
|
||||
|
||||
setIsDone(true);
|
||||
setIsImporting(false);
|
||||
}}
|
||||
disabled={!files.length}
|
||||
>
|
||||
{files.length > 0
|
||||
? "Start import"
|
||||
: "Select files to start importing"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { SettingsGroup } from "./types";
|
||||
import { Importer } from "./components/importer";
|
||||
import { Importer } from "../../components/importer";
|
||||
|
||||
export const ImporterSettings: SettingsGroup[] = [
|
||||
{
|
||||
|
||||
@@ -48,6 +48,7 @@ export type TipContext =
|
||||
| "reminders"
|
||||
| "monographs"
|
||||
| "trash"
|
||||
| "archive"
|
||||
| "attachments";
|
||||
|
||||
export type Tip = {
|
||||
@@ -207,5 +208,8 @@ const DEFAULT_TIPS: Record<TipContext, Omit<Tip, "contexts">> = {
|
||||
trash: {
|
||||
text: ""
|
||||
},
|
||||
archive: {
|
||||
text: strings.yourArchiveIsEmpty()
|
||||
},
|
||||
search: { text: "" }
|
||||
};
|
||||
|
||||
@@ -105,6 +105,15 @@ const routes = defineRoutes({
|
||||
component: Trash
|
||||
});
|
||||
},
|
||||
"/archive": () => {
|
||||
useNoteStore.getState().setContext({ type: "archive" });
|
||||
return defineRoute({
|
||||
key: "notes",
|
||||
title: strings.archive(),
|
||||
type: "notes",
|
||||
component: Notes
|
||||
});
|
||||
},
|
||||
"/tags/:tagId": async ({ tagId }) => {
|
||||
const tag = await db.tags.tag(tagId);
|
||||
if (!tag) return NOT_FOUND_ROUTE;
|
||||
|
||||
@@ -382,6 +382,8 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
event.item.localOnly ?? session.note.localOnly;
|
||||
session.note.favorite =
|
||||
event.item.favorite ?? session.note.favorite;
|
||||
session.note.archived =
|
||||
event.item.archived ?? session.note.archived;
|
||||
session.note.dateEdited =
|
||||
event.item.dateEdited ?? session.note.dateEdited;
|
||||
});
|
||||
|
||||
@@ -62,7 +62,11 @@ class NoteStore extends BaseStore<NoteStore> {
|
||||
contextNotes: context
|
||||
? await notesFromContext(context).grouped(
|
||||
db.settings.getGroupOptions(
|
||||
context.type === "favorite" ? "favorites" : "notes"
|
||||
context.type === "favorite"
|
||||
? "favorites"
|
||||
: context.type === "archive"
|
||||
? "archive"
|
||||
: "notes"
|
||||
)
|
||||
)
|
||||
: undefined
|
||||
@@ -85,6 +89,11 @@ class NoteStore extends BaseStore<NoteStore> {
|
||||
await this.refresh();
|
||||
};
|
||||
|
||||
archive = async (state: boolean, ...ids: string[]) => {
|
||||
await db.notes.archive(state, ...ids);
|
||||
await this.refresh();
|
||||
};
|
||||
|
||||
unlock = async (id: string) => {
|
||||
return await Vault.unlockNote(id).then(async (res) => {
|
||||
await this.refresh();
|
||||
@@ -141,6 +150,8 @@ export function notesFromContext(context: Context) {
|
||||
.selector;
|
||||
case "favorite":
|
||||
return db.notes.favorites;
|
||||
case "archive":
|
||||
return db.notes.archived;
|
||||
case "monographs":
|
||||
return db.monographs.all;
|
||||
}
|
||||
|
||||
@@ -17,80 +17,21 @@ 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 { db } from "../common/db";
|
||||
import {
|
||||
Note,
|
||||
Notebook,
|
||||
ContentType,
|
||||
LegacyNotebook
|
||||
Note,
|
||||
Notebook
|
||||
} from "@notesnook-importer/core/dist/src/models";
|
||||
import {
|
||||
ATTACHMENTS_DIRECTORY_NAME,
|
||||
NOTE_DATA_FILENAME
|
||||
} from "@notesnook-importer/core/dist/src/utils/note-stream";
|
||||
import { path } from "@notesnook-importer/core/dist/src/utils/path";
|
||||
import { type ZipEntry } from "./streams/unzip-stream";
|
||||
import { hashBuffer, writeEncryptedFile } from "../interfaces/fs";
|
||||
import { Notebook as NotebookType } from "@notesnook/core";
|
||||
import { SerializedKey } from "@notesnook/crypto";
|
||||
import { db } from "../common/db";
|
||||
import { writeEncryptedFile } from "../interfaces/fs";
|
||||
|
||||
export async function* importFiles(zipFiles: File[]) {
|
||||
const { createUnzipIterator } = await import("./streams/unzip-stream");
|
||||
|
||||
for (const zip of zipFiles) {
|
||||
let count = 0;
|
||||
let filesRead = 0;
|
||||
|
||||
const attachments: Record<string, any> = {};
|
||||
|
||||
for await (const entry of createUnzipIterator(zip)) {
|
||||
++filesRead;
|
||||
|
||||
const isAttachment = entry.name.includes(
|
||||
`/${ATTACHMENTS_DIRECTORY_NAME}/`
|
||||
);
|
||||
const isNote = !isAttachment && entry.name.endsWith(NOTE_DATA_FILENAME);
|
||||
|
||||
try {
|
||||
if (isAttachment) {
|
||||
await processAttachment(entry, attachments);
|
||||
} else if (isNote) {
|
||||
await processNote(entry, attachments);
|
||||
++count;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) yield { type: "error" as const, error: e };
|
||||
}
|
||||
|
||||
yield {
|
||||
type: "progress" as const,
|
||||
count,
|
||||
filesRead
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processAttachment(
|
||||
entry: ZipEntry,
|
||||
attachments: Record<string, any>
|
||||
) {
|
||||
const name = path.basename(entry.name);
|
||||
if (!name || attachments[name] || (await db.attachments?.exists(name)))
|
||||
return;
|
||||
|
||||
const data = await entry.arrayBuffer();
|
||||
const { hash } = await hashBuffer(new Uint8Array(data));
|
||||
if (hash !== name) {
|
||||
throw new Error(`integrity check failed: ${name} !== ${hash}`);
|
||||
}
|
||||
|
||||
const file = new File([data], name, {
|
||||
type: "application/octet-stream"
|
||||
});
|
||||
const key = await db.attachments?.generateKey();
|
||||
const cipherData = await writeEncryptedFile(file, key, name);
|
||||
attachments[name] = { ...cipherData, key };
|
||||
}
|
||||
type EncryptedAttachmentFields = Awaited<
|
||||
ReturnType<typeof writeEncryptedFile>
|
||||
> & {
|
||||
key: SerializedKey;
|
||||
};
|
||||
|
||||
const colorMap: Record<string, string | undefined> = {
|
||||
default: undefined,
|
||||
@@ -107,19 +48,50 @@ const colorMap: Record<string, string | undefined> = {
|
||||
yellow: "#FFC107"
|
||||
};
|
||||
|
||||
async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
|
||||
const note = await fileToJson<Note>(entry);
|
||||
for (const attachment of note.attachments || []) {
|
||||
const cipherData = attachments[attachment.hash];
|
||||
if (!cipherData || (await db.attachments?.exists(attachment.hash)))
|
||||
export async function importNote(note: Note) {
|
||||
const encryptedAttachmentFieldsMap = await processAttachments(
|
||||
note.attachments
|
||||
);
|
||||
await processNote(note, encryptedAttachmentFieldsMap);
|
||||
}
|
||||
|
||||
async function processAttachments(attachments: Note["attachments"]) {
|
||||
if (!attachments) return {};
|
||||
|
||||
const map: Record<string, EncryptedAttachmentFields | undefined> = {};
|
||||
for (const { hash, filename, data } of attachments) {
|
||||
if (!data || !hash || map[hash]) {
|
||||
continue;
|
||||
}
|
||||
const exists = await db.attachments?.exists(hash);
|
||||
if (exists) continue;
|
||||
|
||||
const file = new File([data], filename, {
|
||||
type: "application/octet-stream"
|
||||
});
|
||||
const key = await db.attachments?.generateKey();
|
||||
const cipherData = await writeEncryptedFile(file, key, hash);
|
||||
map[hash] = { ...cipherData, key };
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async function processNote(
|
||||
note: Note,
|
||||
map: Record<string, EncryptedAttachmentFields | undefined>
|
||||
) {
|
||||
for (const attachment of note.attachments || []) {
|
||||
const cipherData = map[attachment.hash];
|
||||
if (!cipherData || (await db.attachments?.exists(attachment.hash))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await db.attachments?.add({
|
||||
...cipherData,
|
||||
hash: attachment.hash,
|
||||
hashType: attachment.hashType,
|
||||
filename: attachment.filename,
|
||||
type: attachment.mime
|
||||
mimeType: attachment.mime
|
||||
});
|
||||
}
|
||||
|
||||
@@ -185,39 +157,13 @@ async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
|
||||
}
|
||||
|
||||
for (const nb of notebooks) {
|
||||
if ("notebook" in nb) {
|
||||
const notebookId = await importLegacyNotebook(nb).catch(() => undefined);
|
||||
if (!notebookId) continue;
|
||||
const notebookIds = await importNotebook(nb).catch(() => undefined);
|
||||
if (!notebookIds) continue;
|
||||
for (const notebookId of notebookIds)
|
||||
await db.notes.addToNotebook(notebookId, noteId);
|
||||
} else {
|
||||
const notebookIds = await importNotebook(nb).catch(() => undefined);
|
||||
if (!notebookIds) continue;
|
||||
for (const notebookId of notebookIds)
|
||||
await db.notes.addToNotebook(notebookId, noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fileToJson<T>(file: ZipEntry) {
|
||||
const text = await file.text();
|
||||
return JSON.parse(text) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
async function importLegacyNotebook(
|
||||
notebook: LegacyNotebook | undefined
|
||||
): Promise<string | undefined> {
|
||||
if (!notebook) return;
|
||||
const nb = await db.notebooks.find(notebook.notebook);
|
||||
return nb
|
||||
? nb.id
|
||||
: await db.notebooks.add({
|
||||
title: notebook.notebook
|
||||
});
|
||||
}
|
||||
|
||||
async function importNotebook(
|
||||
notebook: Notebook,
|
||||
parent?: NotebookType
|
||||
|
||||
@@ -35,7 +35,12 @@ function Notes(props: NotesProps) {
|
||||
const context = useNotesStore((store) => store.context);
|
||||
const contextNotes = useNotesStore((store) => store.contextNotes);
|
||||
const refreshContext = useNotesStore((store) => store.refreshContext);
|
||||
const type = context?.type === "favorite" ? "favorites" : "notes";
|
||||
const type =
|
||||
context?.type === "favorite"
|
||||
? "favorites"
|
||||
: context?.type === "archive"
|
||||
? "archive"
|
||||
: "notes";
|
||||
const isCompact = useNotesStore((store) => store.viewMode === "compact");
|
||||
const filteredItems = useSearch(
|
||||
context?.type === "notebook" ? "notebook" : "notes",
|
||||
@@ -62,6 +67,8 @@ function Notes(props: NotesProps) {
|
||||
context={
|
||||
context.type === "favorite"
|
||||
? "favorites"
|
||||
: context.type === "archive"
|
||||
? "archive"
|
||||
: context.type === "monographs"
|
||||
? "monographs"
|
||||
: "notes"
|
||||
|
||||
@@ -682,3 +682,54 @@ for (const group of groups) {
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
test("get archived notes", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
await db.notes.archive(true, id);
|
||||
expect(await db.notes.archived.count()).toBeGreaterThan(0);
|
||||
}));
|
||||
|
||||
test("archive note", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
await db.notes.archive(true, id);
|
||||
const note = await db.notes.note(id);
|
||||
expect(note?.archived).toBe(true);
|
||||
}));
|
||||
|
||||
test("unarchive note", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
await db.notes.archive(true, id);
|
||||
await db.notes.archive(false, id);
|
||||
const note = await db.notes.note(id);
|
||||
expect(note?.archived).toBe(false);
|
||||
}));
|
||||
|
||||
test("archiving note should update cache.archived", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
await db.notes.archive(true, id);
|
||||
const note = await db.notes.note(id);
|
||||
expect(db.notes.cache.archived).toEqual([note?.id]);
|
||||
}));
|
||||
|
||||
test("un-archiving note should update cache.archived", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
await db.notes.archive(true, id);
|
||||
const note = await db.notes.note(id);
|
||||
expect(db.notes.cache.archived).toEqual([note?.id]);
|
||||
|
||||
await db.notes.archive(false, id);
|
||||
expect(db.notes.cache.archived).toEqual([]);
|
||||
}));
|
||||
|
||||
test("archived note shouldn't be in all notes", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
await db.notes.archive(true, id);
|
||||
expect(await db.notes.all.count()).toBe(0);
|
||||
}));
|
||||
|
||||
test("archived note shouldn't be in favorites", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
await db.notes.favorite(true, id);
|
||||
await db.notes.archive(true, id);
|
||||
expect(await db.notes.favorites.count()).toBe(0);
|
||||
}));
|
||||
|
||||
@@ -40,6 +40,7 @@ import { ICollection } from "./collection.js";
|
||||
import { SQLCollection } from "../database/sql-collection.js";
|
||||
import { isFalse } from "../database/index.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { addItems, deleteItems } from "../utils/array.js";
|
||||
|
||||
export type ExportOptions = {
|
||||
format: "html" | "md" | "txt" | "md-frontmatter";
|
||||
@@ -51,6 +52,7 @@ export type ExportOptions = {
|
||||
|
||||
export class Notes implements ICollection {
|
||||
name = "notes";
|
||||
cache: { archived: string[] } = { archived: [] };
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -69,6 +71,13 @@ export class Notes implements ICollection {
|
||||
async init() {
|
||||
await this.collection.init();
|
||||
this.totalNotes = await this.collection.count();
|
||||
await this.buildCache();
|
||||
}
|
||||
|
||||
async buildCache() {
|
||||
this.cache.archived = [];
|
||||
const archived = await this.archived.ids();
|
||||
this.cache.archived = archived;
|
||||
}
|
||||
|
||||
async add(
|
||||
@@ -234,7 +243,11 @@ export class Notes implements ICollection {
|
||||
|
||||
get all() {
|
||||
return this.collection.createFilter<Note>(
|
||||
(qb) => qb.where(isFalse("dateDeleted")).where(isFalse("deleted")),
|
||||
(qb) =>
|
||||
qb
|
||||
.where(isFalse("dateDeleted"))
|
||||
.where(isFalse("deleted"))
|
||||
.where(isFalse("archived")),
|
||||
this.db.options?.batchSize
|
||||
);
|
||||
}
|
||||
@@ -277,11 +290,23 @@ export class Notes implements ICollection {
|
||||
qb
|
||||
.where(isFalse("dateDeleted"))
|
||||
.where(isFalse("deleted"))
|
||||
.where(isFalse("archived"))
|
||||
.where("favorite", "==", true),
|
||||
this.db.options?.batchSize
|
||||
);
|
||||
}
|
||||
|
||||
get archived() {
|
||||
return this.collection.createFilter<Note>(
|
||||
(qb) =>
|
||||
qb
|
||||
.where(isFalse("dateDeleted"))
|
||||
.where(isFalse("deleted"))
|
||||
.where("archived", "==", true),
|
||||
this.db.options?.batchSize
|
||||
);
|
||||
}
|
||||
|
||||
exists(id: string) {
|
||||
return this.collection.exists(id);
|
||||
}
|
||||
@@ -300,6 +325,14 @@ export class Notes implements ICollection {
|
||||
favorite(state: boolean, ...ids: string[]) {
|
||||
return this.collection.update(ids, { favorite: state });
|
||||
}
|
||||
async archive(state: boolean, ...ids: string[]) {
|
||||
await this.collection.update(ids, { archived: state });
|
||||
if (state) {
|
||||
addItems(this.cache.archived, ...ids);
|
||||
} else {
|
||||
deleteItems(this.cache.archived, ...ids);
|
||||
}
|
||||
}
|
||||
readonly(state: boolean, ...ids: string[]) {
|
||||
return this.collection.update(ids, { readonly: state });
|
||||
}
|
||||
|
||||
@@ -395,6 +395,11 @@ class RelationsArray<TType extends keyof RelatableTable> {
|
||||
this.db.trash.cache.notes.length > 0,
|
||||
(b) => b.where("fromId", "not in", this.db.trash.cache.notes)
|
||||
)
|
||||
.$if(
|
||||
!!this.types?.includes("note" as TType) &&
|
||||
this.db.notes.cache.archived.length > 0,
|
||||
(b) => b.where("fromId", "not in", this.db.notes.cache.archived)
|
||||
)
|
||||
.$if(
|
||||
!!this.types?.includes("notebook" as TType) &&
|
||||
this.db.trash.cache.notebooks.length > 0,
|
||||
@@ -424,6 +429,11 @@ class RelationsArray<TType extends keyof RelatableTable> {
|
||||
this.db.trash.cache.notes.length > 0,
|
||||
(b) => b.where("toId", "not in", this.db.trash.cache.notes)
|
||||
)
|
||||
.$if(
|
||||
!!this.types?.includes("note" as TType) &&
|
||||
this.db.notes.cache.archived.length > 0,
|
||||
(b) => b.where("toId", "not in", this.db.notes.cache.archived)
|
||||
)
|
||||
.$if(
|
||||
!!this.types?.includes("notebook" as TType) &&
|
||||
this.db.trash.cache.notebooks.length > 0,
|
||||
|
||||
@@ -62,6 +62,7 @@ const defaultSettings: SettingItemMap = {
|
||||
"groupOptions:notes": DEFAULT_GROUP_OPTIONS("notes"),
|
||||
"groupOptions:notebooks": DEFAULT_GROUP_OPTIONS("notebooks"),
|
||||
"groupOptions:favorites": DEFAULT_GROUP_OPTIONS("favorites"),
|
||||
"groupOptions:archive": DEFAULT_GROUP_OPTIONS("archive"),
|
||||
"groupOptions:home": DEFAULT_GROUP_OPTIONS("home"),
|
||||
"groupOptions:reminders": DEFAULT_GROUP_OPTIONS("reminders"),
|
||||
|
||||
|
||||
@@ -232,7 +232,8 @@ const BooleanProperties: Set<BooleanFields> = new Set([
|
||||
"readonly",
|
||||
"remote",
|
||||
"synced",
|
||||
"isGeneratedTitle"
|
||||
"isGeneratedTitle",
|
||||
"archived"
|
||||
]);
|
||||
|
||||
const DataMappers: Partial<Record<ItemType, (row: any) => void>> = {
|
||||
|
||||
@@ -391,6 +391,14 @@ export class NNMigrationProvider implements MigrationProvider {
|
||||
.addColumn("isGeneratedTitle", "boolean")
|
||||
.execute();
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.alterTable("notes")
|
||||
.addColumn("archived", "boolean")
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ export const GroupingKey = [
|
||||
"tags",
|
||||
"trash",
|
||||
"favorites",
|
||||
"reminders"
|
||||
"reminders",
|
||||
"archive"
|
||||
] as const;
|
||||
export type GroupingKey = (typeof GroupingKey)[number];
|
||||
|
||||
@@ -200,6 +201,7 @@ export interface Note extends BaseItem<"note"> {
|
||||
deletedBy: null;
|
||||
|
||||
isGeneratedTitle?: boolean;
|
||||
archived?: boolean;
|
||||
}
|
||||
|
||||
export interface Notebook extends BaseItem<"notebook"> {
|
||||
|
||||
@@ -31,6 +31,13 @@ export function addItem<T>(array: T[], item: T) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function addItems<T>(array: T[], ...items: T[]) {
|
||||
for (const item of items) {
|
||||
addItem(array, item);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
export function deleteItem<T>(array: T[], item: T) {
|
||||
return deleteAtIndex(array, array.indexOf(item));
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
font-family: "Inter";
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: "Inter";
|
||||
/* p { */
|
||||
/* font-family: "Inter"; */
|
||||
/* color: var(--nn_primary_paragraph) ## TODO: use fixed color */
|
||||
}
|
||||
/* } */
|
||||
|
||||
::selection {
|
||||
color: white;
|
||||
@@ -37,6 +37,10 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#statusbar p {
|
||||
font-family: "Inter";
|
||||
}
|
||||
</style>
|
||||
<meta name="description" content="Notesnook editor for mobile" />
|
||||
<!--
|
||||
|
||||
@@ -866,6 +866,10 @@ msgstr "Apply changes"
|
||||
msgid "Applying changes"
|
||||
msgstr "Applying changes"
|
||||
|
||||
#: src/strings.ts:2468
|
||||
msgid "Archive"
|
||||
msgstr "Archive"
|
||||
|
||||
#: src/strings.ts:1431
|
||||
msgid "Are you scrolling a lot to find a specific note? Pin it to the top from Note properties."
|
||||
msgstr "Are you scrolling a lot to find a specific note? Pin it to the top from Note properties."
|
||||
@@ -7007,6 +7011,10 @@ msgstr "Your account password must be strong & unique."
|
||||
msgid "Your account will be downgraded in {days} days"
|
||||
msgstr "Your account will be downgraded in {days} days"
|
||||
|
||||
#: src/strings.ts:2469
|
||||
msgid "Your archive is empty"
|
||||
msgstr "Your archive is empty"
|
||||
|
||||
#: src/strings.ts:1914
|
||||
msgid "Your backup is ready to download"
|
||||
msgstr "Your backup is ready to download"
|
||||
|
||||
@@ -866,6 +866,10 @@ msgstr ""
|
||||
msgid "Applying changes"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2468
|
||||
msgid "Archive"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1431
|
||||
msgid "Are you scrolling a lot to find a specific note? Pin it to the top from Note properties."
|
||||
msgstr ""
|
||||
@@ -6953,6 +6957,10 @@ msgstr ""
|
||||
msgid "Your account will be downgraded in {days} days"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2469
|
||||
msgid "Your archive is empty"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1914
|
||||
msgid "Your backup is ready to download"
|
||||
msgstr ""
|
||||
|
||||
@@ -2464,5 +2464,7 @@ Use this if changes from other devices are not appearing on this device. This wi
|
||||
setAsHomepage: () => t`Set as homepage`,
|
||||
defaultSidebarTab: () => t`Default sidebar tab`,
|
||||
defaultSidebarTabDesc: () => t`Select the default sidebar tab`,
|
||||
unsetAsHomepage: () => t`Reset homepage`
|
||||
unsetAsHomepage: () => t`Reset homepage`,
|
||||
archive: () => t`Archive`,
|
||||
yourArchiveIsEmpty: () => t`Your archive is empty`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user