mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 03:29:21 +02:00
Compare commits
18 Commits
v3.2.0-bet
...
fix-cursor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ec4a25660 | ||
|
|
bb5d5a5731 | ||
|
|
bf5e68a82e | ||
|
|
cc7f479627 | ||
|
|
9eb183f6b5 | ||
|
|
cb29d71619 | ||
|
|
a6120ed90f | ||
|
|
48040c955d | ||
|
|
18b43882e1 | ||
|
|
37dc409b60 | ||
|
|
8ab05c5a98 | ||
|
|
38509c66c5 | ||
|
|
fc40a05254 | ||
|
|
8078c3ed3c | ||
|
|
57280f3ebb | ||
|
|
dc54644b4f | ||
|
|
ce31b0495d | ||
|
|
a62d4b5d4b |
7
.github/workflows/ios.publish.yml
vendored
7
.github/workflows/ios.publish.yml
vendored
@@ -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
|
||||
|
||||
@@ -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`;
|
||||
|
||||
|
||||
@@ -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" }}>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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!
|
||||
@@ -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)";
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
1534
apps/web/package-lock.json
generated
1534
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
@@ -272,7 +272,9 @@ function NavigationMenu({ onExpand }: { onExpand?: () => void }) {
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
className="navigation-menu-header"
|
||||
sx={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 1
|
||||
|
||||
@@ -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[] = [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
3
fastlane/metadata/android/en-US/changelogs/15254.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15254.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Bug fixes
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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(" ");
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
<!--
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -118,7 +118,8 @@ function Header({
|
||||
backgroundColor: "var(--nn_primary_background)",
|
||||
position: "sticky",
|
||||
width: "100vw",
|
||||
zIndex: 999
|
||||
zIndex: 999,
|
||||
top: 0
|
||||
}}
|
||||
>
|
||||
{noHeader ? null : (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -71,7 +71,7 @@ export const KeepInView = Extension.create<
|
||||
|
||||
export function keepLastLineInView(
|
||||
editor: Editor,
|
||||
THRESHOLD = 80,
|
||||
THRESHOLD = 120,
|
||||
SCROLL_THRESHOLD = 100
|
||||
) {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user