Compare commits

...

18 Commits

Author SHA1 Message Date
Ammar Ahmed
2ec4a25660 mobile: fix cursor render location incorrect when writing near top of editor 2025-05-01 10:39:49 +05:00
01zulfi
bb5d5a5731 core: segregate logic for note headline and note headline title generation (#7968)
* core: segregate logic for note headline and note headline title generation
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: minor refactor note headline title
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-04-29 09:27:35 +05:00
Ammar Ahmed
bf5e68a82e ci: fix build ios
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-04-28 14:03:48 +05:00
Ammar Ahmed
cc7f479627 ci: fix build ios
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-04-28 13:53:21 +05:00
Ammar Ahmed
9eb183f6b5 ci: fix build ios
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-04-28 13:42:39 +05:00
Ammar Ahmed
cb29d71619 mobile: fix crash when going from trash -> search 2025-04-28 12:30:50 +05:00
Ammar Ahmed
a6120ed90f mobile: release v3.1.1 2025-04-28 12:26:46 +05:00
Ammar Ahmed
48040c955d mobile: fix crash on delete item 2025-04-28 12:26:12 +05:00
Ammar Ahmed
18b43882e1 mobile: fix crash in list 2025-04-28 11:36:48 +05:00
Ammar Ahmed
37dc409b60 mobile: fix cursor bug on ios 2025-04-28 11:36:37 +05:00
Ammar Ahmed
8ab05c5a98 mobile: fix typing in editor title input 2025-04-28 11:36:27 +05:00
Abdullah Atta
38509c66c5 web: update @notesnook-importer/core to 2.2.0 2025-04-25 14:33:32 +05:00
Abdullah Atta
fc40a05254 web: open settings with importer page instead of importer.notesnook.com 2025-04-25 14:33:32 +05:00
Ammar Ahmed
8078c3ed3c mobile: fix default font setting not followed 2025-04-25 11:51:28 +05:00
01zulfi
57280f3ebb web: make nav menu header draggable (#7985)
* web: make part of nav menu header draggable
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* Update apps/web/src/app.css

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

* Update apps/web/src/components/navigation-menu/index.tsx

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

---------

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2025-04-25 11:21:16 +05:00
Ammar Ahmed
dc54644b4f mobile: remove READ_MEDIA_IMAGES permission 2025-04-24 15:29:58 +05:00
Abdullah Atta
ce31b0495d web: refresh app after import 2025-04-24 14:39:15 +05:00
Abdullah Atta
a62d4b5d4b web: move importer into web/desktop app (#7472)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-04-24 13:20:06 +05:00
41 changed files with 2333 additions and 818 deletions

View File

@@ -4,7 +4,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: macos-latest
runs-on: macos-14
timeout-minutes: 60
steps:
@@ -14,6 +14,11 @@ jobs:
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "16.1"
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit

View File

@@ -73,7 +73,7 @@ export default function List(props: ListProps) {
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: props.renderedInRoute === "Trash"
: props.renderedInRoute === "Trash" || props.dataType === "trash"
? "trash"
: `${props.dataType}s`;

View File

@@ -58,7 +58,6 @@ const Line = ({ top = 6, bottom = 6 }) => {
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
const { colors } = useThemeColors();
const isColor = !!ColorValues[item.title];
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>

View File

@@ -56,7 +56,7 @@ const Sort = ({
db.settings.getGroupOptions(
screen === "Notes"
? "home"
: screen === "Trash"
: screen === "Trash" || type === "trash"
? "trash"
: ((type + "s") as GroupingKey)
)
@@ -65,10 +65,11 @@ const Sort = ({
const groupType =
screen === "Notes"
? "home"
: screen === "Trash"
: screen === "Trash" || type === "trash"
? "trash"
: screen === "Favorites"
? "favorites"
: ((type + "s") as GroupingKey);
console.log("updateGroupOptions for group", groupType, "in", screen);
await db.settings.setGroupOptions(groupType, _groupOptions);
setGroupOptions(_groupOptions);
setTimeout(() => {

View File

@@ -19,13 +19,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [
{
title: "Notebook Tree View",
body: "Notebooks are now displayed in a tree view for better organization and user experience."
},
{
title: "New SideBar",
body: "Sidebar now contains tabs for Notebooks and Tags for easy navigation."
}
];
export const features: FeatureType[] = [];

View File

@@ -32,7 +32,7 @@ export function useGroupOptions(type: any) {
const onUpdate = (groupType: string) => {
if (groupType !== type) return;
const options = db.settings?.getGroupOptions(type) as any;
if (!options) return;
if (
groupOptions?.groupBy !== options.groupBy ||
groupOptions?.sortBy !== options.sortBy ||

View File

@@ -177,7 +177,7 @@ export const useEditorEvents = (
useEffect(() => {
const handleKeyboardDidShow: KeyboardEventListener = () => {
editor.commands.keyboardShown(true);
//editor.postMessage(NativeEvents.keyboardShown, undefined);
editor.postMessage(NativeEvents.keyboardShown, undefined);
};
const handleKeyboardDidHide: KeyboardEventListener = () => {
editor.commands.keyboardShown(false);

View File

@@ -352,7 +352,7 @@ export const useEditor = (
DatabaseLogger.log(`Note saved: ${id}...`);
clearTimeout(saveTimer);
const oldNote = currentNotes.current[id];
if (id) {
currentNotes.current[id] = await db.notes?.note(id);
}
@@ -376,11 +376,16 @@ export const useEditor = (
}
}
postMessage(
NativeEvents.title,
currentNotes.current[id]?.title,
tabId
);
if (
oldNote?.title !== currentNotes.current[id]?.title &&
!noteData.title
) {
postMessage(
NativeEvents.title,
currentNotes.current[id]?.title,
tabId
);
}
if (Notifications.isNotePinned(id as string)) {
Notifications.pinNote(id as string);

View File

@@ -121,7 +121,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3049
versionCode 3050
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -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" />

View File

@@ -1,8 +1,3 @@
- v3.1.0 brings new features and improvements to enhance your Notesnook experience.
- Notebooks and Tags are not located in the Sidebar
- Tree view for notebooks
- Improved list view UI for better user experience
- Improved notebook related actions by integrating tree view in most places
- You can set any notebook, tag or colors as home screen
- Bug fixes
Thank you for using Notesnook!

View File

@@ -1063,7 +1063,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2132;
CURRENT_PROJECT_VERSION = 2133;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1137,7 +1137,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.1.0;
MARKETING_VERSION = 3.1.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1168,7 +1168,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2132;
CURRENT_PROJECT_VERSION = 2133;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1242,7 +1242,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.1.0;
MARKETING_VERSION = 3.1.1;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1401,7 +1401,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2132;
CURRENT_PROJECT_VERSION = 2133;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1413,7 +1413,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.1.0;
MARKETING_VERSION = 3.1.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1444,7 +1444,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2132;
CURRENT_PROJECT_VERSION = 2133;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1457,7 +1457,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.1.0;
MARKETING_VERSION = 3.1.1;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1487,7 +1487,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2132;
CURRENT_PROJECT_VERSION = 2133;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1561,7 +1561,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.1.0;
MARKETING_VERSION = 3.1.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1592,7 +1592,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2132;
CURRENT_PROJECT_VERSION = 2133;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1667,7 +1667,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.1.0;
MARKETING_VERSION = 3.1.1;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.1.0",
"version": "3.1.1",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -56,4 +56,4 @@
"react": "18.2.0",
"react-native": "0.74.5"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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.0",
"@notesnook/common": "file:../../packages/common",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",

View File

@@ -1,6 +1,7 @@
.tabsScroll,
.titlebarLogo,
.theme-scope-titleBar,
.navigation-menu-header,
.route-container-header {
-webkit-app-region: drag;
}

View File

@@ -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}>

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
)}
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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&apos;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>
);
}

View 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>
);
}

View 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";

View 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[];
};

View File

@@ -272,7 +272,9 @@ function NavigationMenu({ onExpand }: { onExpand?: () => void }) {
}}
>
<Flex
className="navigation-menu-header"
sx={{
flex: 1,
flexDirection: "row",
alignItems: "center",
gap: 1

View File

@@ -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();
}}
>

View File

@@ -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>
);
}

View File

@@ -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[] = [
{

View File

@@ -17,80 +17,22 @@ 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
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,18 +49,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,
// todo: figure out typescript error
type: attachment.mime
});
}
@@ -198,11 +172,6 @@ async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
}
}
async function fileToJson<T>(file: ZipEntry) {
const text = await file.text();
return JSON.parse(text) as T;
}
/**
* @deprecated
*/

View File

@@ -0,0 +1,3 @@
- Bug fixes
Thank you for using Notesnook!

View File

@@ -134,7 +134,7 @@ test("changing content shouldn't reset the note title ", () =>
expect(note?.title).toBe("I am a note");
}));
test("note title with headline format should keep generating headline until title is edited", () =>
test("note title with headline format should keep generating headline title until title is edited", () =>
noteTest().then(async ({ db }) => {
await db.settings.setTitleFormat("$headline$");
const id = await db.notes.add({
@@ -191,19 +191,44 @@ test("note title with headline format should keep generating headline until titl
"headlineLorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam rutrum ex ac eros egestas, ut rhoncus felis faucibus. Mauris tempor orci nisl,"
]
].forEach(([testCase, content, expectedHeadline]) => {
test(`note should generate headline up to HEADLINE_CHARACTER_LIMIT characters - ${testCase}`, () =>
noteTest({
...TEST_NOTE,
content: {
type: TEST_NOTE.content.type,
data: content
}
}).then(async ({ db, id }) => {
test(`note title with headline format should generate headline title up to 150 characters - ${testCase}`, () =>
noteTest().then(async ({ db }) => {
await db.settings.setTitleFormat("$headline$");
const id = await db.notes.add({
content: {
type: TEST_NOTE.content.type,
data: content
}
});
const note = await db.notes.note(id);
expect(note?.headline).toBe(expectedHeadline);
expect(note?.title).toBe(expectedHeadline);
}));
});
test("note should get headline from first paragraph in content", () =>
noteTest({
...TEST_NOTE,
content: {
type: TEST_NOTE.content.type,
data: "<p>This is a very colorful existence.</p>"
}
}).then(async ({ db, id }) => {
const note = await db.notes.note(id);
expect(note?.headline).toBe("This is a very colorful existence.");
}));
test("note should not get headline if there is no p tag", () =>
noteTest({
...TEST_NOTE,
content: {
type: TEST_NOTE.content.type,
data: `<ol style="list-style-type: decimal;" data-mce-style="list-style-type: decimal;"><li>Hello I won't be a headline :(</li><li>Me too.</li><li>Gold.</li></ol>`
}
}).then(async ({ db, id }) => {
const note = await db.notes.note(id);
expect(note?.headline).toBe("");
}));
test("note title should allow trailing space", () =>
noteTest({ title: "Hello ", content: TEST_NOTE.content }).then(
async ({ db, id }) => {

View File

@@ -87,6 +87,7 @@ export class Notes implements ICollection {
let contentId = item.contentId;
let dateEdited = item.dateEdited;
let headline = item.headline;
let headlineTitle = "";
if (item.content && item.content.data && item.content.type) {
logger.debug("saving content", { id });
@@ -95,7 +96,8 @@ export class Notes implements ICollection {
const content = await getContentFromData(type, data);
if (!content) throw new Error("Invalid content type.");
headline = getNoteHeadline(content);
headline = content.toHeadline();
headlineTitle = content.toTitle();
dateEdited = Date.now();
contentId = await this.db.content.add({
noteId: id,
@@ -125,7 +127,7 @@ export class Notes implements ICollection {
this.db.settings.getTitleFormat(),
this.db.settings.getDateFormat(),
this.db.settings.getTimeFormat(),
headline ? headlineToTitle(headline) : "",
headlineTitle,
this.totalNotes
);
item.isGeneratedTitle = true;
@@ -147,13 +149,10 @@ export class Notes implements ICollection {
if (
item.isGeneratedTitle &&
HEADLINE_REGEX.test(titleFormat) &&
headline &&
currentNoteTitleFields?.title !== headlineToTitle(headline)
headlineTitle &&
currentNoteTitleFields?.title !== headlineTitle
) {
item.title = titleFormat.replace(
HEADLINE_REGEX,
headlineToTitle(headline)
);
item.title = titleFormat.replace(HEADLINE_REGEX, headlineTitle);
}
}
@@ -488,11 +487,3 @@ export class Notes implements ICollection {
).internalLinks;
}
}
function getNoteHeadline(content: Tiptap) {
return content.toHeadline();
}
function headlineToTitle(headline: string) {
return headline.split(" ").splice(0, 10).join(" ");
}

View File

@@ -31,6 +31,7 @@ import dataurl from "../utils/dataurl.js";
import {
HTMLParser,
extractHeadline,
extractTitle,
getDummyDocument
} from "../utils/html-parser.js";
import { HTMLRewriter } from "../utils/html-rewriter.js";
@@ -89,7 +90,11 @@ export class Tiptap {
}
toHeadline() {
return extractHeadline(this.data, 150);
return extractHeadline(this.data);
}
toTitle() {
return extractTitle(this.data, 150);
}
// isEmpty() {

View File

@@ -44,14 +44,42 @@ function wrapIntoHTMLDocument(input: string) {
return `<!doctype html><html lang="en"><head><title>Document Fragment</title></head><body>${input}</body></html>`;
}
export function extractHeadline(html: string, headlineCharacterLimit: number) {
export function extractHeadline(html: string) {
let text = "";
let start = false;
const parser = new Parser(
{
onopentag: (name) => {
if (name === "p") start = true;
},
onclosetag: (name) => {
if (name === "p") {
start = false;
parser.pause();
parser.end();
}
},
ontext: (data) => {
if (start) text += data;
}
},
{
lowerCaseTags: false,
decodeEntities: true
}
);
parser.end(html);
return text;
}
export function extractTitle(html: string, characterLimit: number) {
let text = "";
const parser = new Parser(
{
ontext: (data) => {
text += data;
if (text.length > headlineCharacterLimit) {
text = text.slice(0, headlineCharacterLimit);
if (text.length > characterLimit) {
text = text.slice(0, characterLimit);
parser.pause();
parser.end();
}

View File

@@ -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" />
<!--

View File

@@ -421,13 +421,6 @@ const Tiptap = ({
ref={editorRoot}
onDoubleClick={onClickEmptyArea}
>
<Header
hasRedo={redo}
hasUndo={undo}
settings={settings}
noHeader={settings.noHeader || false}
/>
<div
id="editor-saving-failed-overlay"
style={{
@@ -532,9 +525,17 @@ const Tiptap = ({
overflowY: controller.loading ? "hidden" : "scroll",
height: "100%",
display: "block",
position: "relative"
position: "relative",
overscrollBehavior: "none"
}}
>
<Header
hasRedo={redo}
hasUndo={undo}
settings={settings}
noHeader={settings.noHeader || false}
/>
{settings.noHeader || tab.session?.locked ? null : (
<>
<Tags settings={settings} loading={controller.loading} />
@@ -854,25 +855,24 @@ const Tiptap = ({
minHeight: 300
}}
/>
<TiptapEditorWrapper
key={tick + tab.id + "-editor"}
options={tiptapOptions}
settings={settings}
onEditorUpdate={(editor) => {
if (!editor) {
setUndo(false);
setRedo(false);
}
if (undo !== editor.can().undo()) {
setUndo(editor.can().undo());
}
if (redo !== editor.can().redo()) {
setRedo(editor.can().redo());
}
}}
/>
</div>
<TiptapEditorWrapper
key={tick + tab.id + "-editor"}
options={tiptapOptions}
settings={settings}
onEditorUpdate={(editor) => {
if (!editor) {
setUndo(false);
setRedo(false);
}
if (undo !== editor.can().undo()) {
setUndo(editor.can().undo());
}
if (redo !== editor.can().redo()) {
setRedo(editor.can().redo());
}
}}
/>
</div>
</>
);

View File

@@ -118,7 +118,8 @@ function Header({
backgroundColor: "var(--nn_primary_background)",
position: "sticky",
width: "100vw",
zIndex: 999
zIndex: 999,
top: 0
}}
>
{noHeader ? null : (

View File

@@ -43,7 +43,11 @@ export default function TiptapEditorWrapper(props: {
sx={{
display: props.settings.noToolbar ? "none" : "flex",
overflowY: "hidden",
minHeight: "50px"
minHeight: "50px",
position: "fixed",
bottom: 0,
width: "100%",
overscrollBehavior: "none"
}}
editor={editor}
location="bottom"

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Editor, scrollIntoViewById } from "@notesnook/editor";
import { keepLastLineInView } from "@notesnook/editor/extensions/keep-in-view/keep-in-view.js";
import { strings } from "@notesnook/intl";
import {
ThemeDefinition,
@@ -66,38 +67,12 @@ type Timers = {
scroll: NodeJS.Timeout | null;
};
function isInViewport(element: any) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
function scrollIntoView(editor: Editor) {
if (__PLATFORM__ == "android") return;
setTimeout(() => {
try {
const node = editor?.state.selection.$from;
const dom = node ? editor?.view?.domAtPos?.(node.pos) : null;
let domNode = dom?.node;
if (domNode) {
if (domNode.nodeType === Node.TEXT_NODE && domNode.parentNode) {
domNode = domNode.parentNode;
}
if (isInViewport(domNode)) return;
(domNode as HTMLElement).scrollIntoView({
behavior: "smooth",
block: "end"
});
}
} catch (e) {
/* empty */
}
}, 100);
if (!editor.isFocused) return;
keepLastLineInView(editor);
}, 1);
}
export type EditorController = {

View File

@@ -71,7 +71,7 @@ export const KeepInView = Extension.create<
export function keepLastLineInView(
editor: Editor,
THRESHOLD = 80,
THRESHOLD = 120,
SCROLL_THRESHOLD = 100
) {
if (