mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
65 Commits
fix/loadin
...
fix-github
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e68dd129e | ||
|
|
2350d0e41d | ||
|
|
fbd5c8e3cd | ||
|
|
ee5c4077fe | ||
|
|
47bbfc618e | ||
|
|
b377329a8b | ||
|
|
ae8bcf3865 | ||
|
|
2f67611829 | ||
|
|
058c612f4f | ||
|
|
be8df90ccb | ||
|
|
aa730b5b93 | ||
|
|
34bbcf6387 | ||
|
|
33c3c78e2f | ||
|
|
bf144dcda4 | ||
|
|
284ebdb86f | ||
|
|
9dac4cf7f4 | ||
|
|
fb973a5514 | ||
|
|
f9226510f9 | ||
|
|
5c0a964699 | ||
|
|
41ffaab3dc | ||
|
|
764c721f1d | ||
|
|
3d267f91ef | ||
|
|
f10f28a7f8 | ||
|
|
8d04f89a18 | ||
|
|
9d1374dcd4 | ||
|
|
094a65e514 | ||
|
|
372da979e1 | ||
|
|
d3371326f2 | ||
|
|
57452bd0e6 | ||
|
|
993b8460bf | ||
|
|
081201aa42 | ||
|
|
270e6778d1 | ||
|
|
2074320583 | ||
|
|
cd90030616 | ||
|
|
807bd683b7 | ||
|
|
35929026a4 | ||
|
|
41eb94ee47 | ||
|
|
ba51e110ad | ||
|
|
ecad06d249 | ||
|
|
19a151f637 | ||
|
|
7c4a6f1e17 | ||
|
|
bc88e953d1 | ||
|
|
7d89ec676a | ||
|
|
c8f8bcca93 | ||
|
|
9fa904e849 | ||
|
|
05cc947cb4 | ||
|
|
a20a683b17 | ||
|
|
5fc43c931e | ||
|
|
0879c3b4d6 | ||
|
|
2eded71de3 | ||
|
|
9e30ee50b8 | ||
|
|
08621a5873 | ||
|
|
17f5bd28ba | ||
|
|
4558c0b2eb | ||
|
|
a442677e91 | ||
|
|
95666bee36 | ||
|
|
c4232ac95a | ||
|
|
0283037289 | ||
|
|
b6fd0df715 | ||
|
|
f53fb58024 | ||
|
|
21d452308d | ||
|
|
823608cca1 | ||
|
|
f3a0b998e4 | ||
|
|
a47a38b16b | ||
|
|
a184c53dd5 |
28
.github/workflows/android.publish.yml
vendored
28
.github/workflows/android.publish.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Publish @notesnook/mobile
|
||||
name: Publish @notesnook/android
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
@@ -28,31 +28,6 @@ jobs:
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1.1.1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: production
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
|
||||
- name: Build apks for Github release
|
||||
run: yarn release:android
|
||||
|
||||
@@ -65,6 +40,7 @@ jobs:
|
||||
alias: ${{ secrets.PUBLIC_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
|
||||
- name: Rename apk files
|
||||
|
||||
30
.github/workflows/desktop.publish.yml
vendored
30
.github/workflows/desktop.publish.yml
vendored
@@ -18,6 +18,21 @@ on:
|
||||
required: true
|
||||
default: true
|
||||
description: "Publish on GitHub releases?"
|
||||
build-windows:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for Windows?"
|
||||
build-linux:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for Linux?"
|
||||
build-mac:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for macOS?"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -71,6 +86,7 @@ jobs:
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
if: inputs.build-mac
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
@@ -163,6 +179,7 @@ jobs:
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
needs: build
|
||||
if: inputs.build-linux
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
@@ -221,12 +238,22 @@ jobs:
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
if: inputs.build-windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET Core SDK
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
|
||||
- name: Install AzureCodeSigning
|
||||
run: Install-Module -Name AzureCodeSigning -RequiredVersion 0.3.0 -Force -Repository PSGallery
|
||||
shell: pwsh
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
@@ -247,6 +274,9 @@ jobs:
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
run: |
|
||||
if ($${{ inputs.publish-github }} -eq $true) {
|
||||
yarn electron-builder --win --publish always
|
||||
|
||||
67
.github/workflows/ios.publish.yml
vendored
Normal file
67
.github/workflows/ios.publish.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Publish @notesnook/ios
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-13
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Build packages
|
||||
run: npx nx run @notesnook/mobile:build
|
||||
|
||||
- name: Install Pods
|
||||
run: npm run prepare:ios
|
||||
|
||||
- name: Build iOS App
|
||||
uses: yukiarrr/ios-build-action@v1.11.2
|
||||
with:
|
||||
bundle-identifier: org.streetwriters.notesnook
|
||||
scheme: Notesnook
|
||||
configuration: "Release"
|
||||
export-options: apps/mobile/native/ios/ExportOptions.plist
|
||||
project-path: apps/mobile/native/ios/Notesnook.xcodeproj
|
||||
workspace-path: apps/mobile/native/ios/Notesnook.xcworkspace
|
||||
update-targets: |
|
||||
Notesnook
|
||||
Make Note
|
||||
NotesWidgetExtension
|
||||
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
|
||||
code-signing-identity: Apple Distribution
|
||||
team-id: ${{ secrets.APPLE_TEAM_ID }}
|
||||
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
|
||||
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
|
||||
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
|
||||
output-path: Notesnook.ipa
|
||||
mobileprovision-base64: |
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_APP }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
|
||||
|
||||
- name: 'Upload app to TestFlight'
|
||||
uses: apple-actions/upload-testflight-build@v1
|
||||
with:
|
||||
app-path: Notesnook.ipa
|
||||
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
api-key-id: ${{ secrets.API_KEY_ID }}
|
||||
api-private-key: ${{ secrets.API_KEY }}
|
||||
|
||||
- name: Upload to Github
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Notesnook.ipa
|
||||
path: Notesnook.ipa
|
||||
4
.github/workflows/web.beta.publish.yml
vendored
4
.github/workflows/web.beta.publish.yml
vendored
@@ -1,10 +1,11 @@
|
||||
name: Publish @notesnook/web Beta
|
||||
|
||||
on:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- '!v3-beta'
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
@@ -22,6 +23,7 @@ on:
|
||||
- synchronize
|
||||
branches:
|
||||
- master
|
||||
- '!v3-beta'
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
|
||||
40
.github/workflows/web.v3-beta.publish.yml
vendored
Normal file
40
.github/workflows/web.v3-beta.publish.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Publish @notesnook/web v3 Beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate beta build
|
||||
run: npm run build:beta:web
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
|
||||
id: pages
|
||||
with:
|
||||
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
cloudflare-project-name: notesnook-v3-app
|
||||
directory: ./apps/web/build
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,4 +9,5 @@ nx-cloud.env
|
||||
.env.local
|
||||
.nyc_output
|
||||
site
|
||||
node_modules.backup
|
||||
node_modules.backup
|
||||
.nx
|
||||
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -135,6 +135,10 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"sign": "./sign.js",
|
||||
"icon": "assets/icons/app.ico"
|
||||
},
|
||||
"portable": {
|
||||
|
||||
60
apps/desktop/sign.js
Normal file
60
apps/desktop/sign.js
Normal 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/>.
|
||||
*/
|
||||
const { writeFileSync, rmSync, readFileSync } = require("fs");
|
||||
const { execSync } = require("child_process");
|
||||
const { relative, join } = require("path");
|
||||
|
||||
module.exports = async function (configuration) {
|
||||
const Endpoint = "https://weu.codesigning.azure.net";
|
||||
const CodeSigningAccountName = "Notesnook";
|
||||
const CertificateProfileName = "Notesnook";
|
||||
const FileDigest = configuration.hash.toUpperCase();
|
||||
const TimestampRfc3161 = "http://timestamp.acs.microsoft.com";
|
||||
const TimestampDigest = configuration.hash.toUpperCase();
|
||||
const Description = "The Notesnook app";
|
||||
const DescriptionUrl = "https://notesnook.com/";
|
||||
const FilesCatalog = createCatalog(configuration.path);
|
||||
|
||||
const command = `Invoke-AzureCodeSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
|
||||
|
||||
console.debug("Signing", configuration.path, "using command", command);
|
||||
|
||||
psexec(command);
|
||||
|
||||
console.debug("Signed", configuration.path);
|
||||
|
||||
deleteCatalog();
|
||||
};
|
||||
|
||||
function createCatalog(path) {
|
||||
writeFileSync("_catalog", relative(__dirname, path));
|
||||
return join(__dirname, "_catalog");
|
||||
}
|
||||
|
||||
function deleteCatalog() {
|
||||
rmSync("_catalog");
|
||||
}
|
||||
|
||||
function psexec(cmd) {
|
||||
return execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: "pwsh"
|
||||
});
|
||||
}
|
||||
@@ -17,13 +17,12 @@ 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 "@azure/core-asynciterator-polyfill";
|
||||
import SettingsService from "./services/settings";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
@@ -32,11 +31,16 @@ import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { ApplicationHolder } from "./navigation";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
import Notifications from "./services/notifications";
|
||||
import SettingsService from "./services/settings";
|
||||
import { TipManager } from "./services/tip-manager";
|
||||
import { useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
|
||||
SettingsService.checkOrientation();
|
||||
const App = () => {
|
||||
|
||||
@@ -34,7 +34,8 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir } from "./io";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -181,7 +182,9 @@ export default async function downloadAttachment(
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined
|
||||
groupId: undefined,
|
||||
base64: false,
|
||||
text: false
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
@@ -212,10 +215,17 @@ export default async function downloadAttachment(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (
|
||||
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
|
||||
)
|
||||
|
||||
if (!(await exists(attachment.metadata.hash))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.base64 || options.text) {
|
||||
return await db.attachments.read(
|
||||
attachment.metadata.hash,
|
||||
options.base64 ? "base64" : "text"
|
||||
);
|
||||
}
|
||||
|
||||
let filename = getFileNameWithExtension(
|
||||
attachment.metadata.filename,
|
||||
@@ -233,7 +243,8 @@ export default async function downloadAttachment(
|
||||
mime: attachment.metadata.type,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
};
|
||||
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
|
||||
@@ -36,9 +36,13 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
}
|
||||
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const isPng = /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
console.log("decrypting....");
|
||||
const isPng = !attachment.metadata.type
|
||||
? false
|
||||
: /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = !attachment.metadata.type
|
||||
? false
|
||||
: /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
|
||||
const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const contextId = attachment.metadata.hash;
|
||||
const [filename, setFilename] = useState(attachment.metadata.filename);
|
||||
@@ -64,7 +64,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
name: "Download",
|
||||
onPress: async () => {
|
||||
if (currentProgress) {
|
||||
await db.fs.cancel(attachment.metadata.hash, "download");
|
||||
await db.fs.cancel(attachment.metadata.hash);
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
}
|
||||
downloadAttachment(attachment.metadata.hash, false);
|
||||
@@ -117,7 +117,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
});
|
||||
}
|
||||
|
||||
setAttachments([...db.attachments.all]);
|
||||
setAttachments();
|
||||
setLoading({
|
||||
name: null
|
||||
});
|
||||
@@ -140,7 +140,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments([...db.attachments.all]);
|
||||
setAttachments();
|
||||
}
|
||||
},
|
||||
positiveText: "Rename"
|
||||
@@ -152,8 +152,8 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
name: "Delete",
|
||||
onPress: async () => {
|
||||
await db.attachments.remove(attachment.metadata.hash, false);
|
||||
setAttachments([...db.attachments.all]);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
setAttachments();
|
||||
close();
|
||||
},
|
||||
icon: "delete-outline"
|
||||
}
|
||||
@@ -362,8 +362,13 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
Actions.present = (attachment, set, context) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref) => (
|
||||
<Actions fwdRef={ref} setAttachments={set} attachment={attachment} />
|
||||
component: (ref, close) => (
|
||||
<Actions
|
||||
fwdRef={ref}
|
||||
setAttachments={set}
|
||||
close={close}
|
||||
attachment={attachment}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -132,7 +132,7 @@ export const AttachmentItem = ({
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption || !pressable) return;
|
||||
db.fs.cancel(attachment.metadata.hash, currentProgress.type);
|
||||
db.fs.cancel(attachment.metadata.hash);
|
||||
setCurrentProgress(null);
|
||||
}}
|
||||
style={{
|
||||
|
||||
@@ -68,7 +68,7 @@ export const AttachmentDialog = ({ note }) => {
|
||||
!attachmentSearchValue.current ||
|
||||
attachmentSearchValue.current === ""
|
||||
) {
|
||||
setAttachments([...attachments]);
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
}
|
||||
clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(() => {
|
||||
@@ -77,13 +77,16 @@ export const AttachmentDialog = ({ note }) => {
|
||||
attachmentSearchValue.current
|
||||
);
|
||||
if (results.length === 0) return;
|
||||
setAttachments(results);
|
||||
|
||||
setAttachments(filterAttachments(currentFilter, results));
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<AttachmentItem
|
||||
setAttachments={setAttachments}
|
||||
setAttachments={() => {
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
}}
|
||||
attachment={item}
|
||||
context="attachments-list"
|
||||
/>
|
||||
@@ -133,11 +136,12 @@ export const AttachmentDialog = ({ note }) => {
|
||||
}
|
||||
];
|
||||
|
||||
const filterAttachments = (type) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
isDocument;
|
||||
const filterAttachments = (type, _attachments) => {
|
||||
const attachments =
|
||||
_attachments || note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
switch (type) {
|
||||
case "all":
|
||||
return attachments;
|
||||
|
||||
@@ -101,6 +101,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
setVisible(false);
|
||||
setData(null);
|
||||
}}
|
||||
keyboardHandlerDisabled={data?.keyboardHandlerDisabled}
|
||||
bottomPadding={!data.noBottomPadding}
|
||||
enableGesturesInScrollView={
|
||||
typeof data.enableGesturesInScrollView === "undefined"
|
||||
|
||||
@@ -43,6 +43,7 @@ import Paragraph from "../../ui/typography/paragraph";
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
import { Dialog } from "../../dialog";
|
||||
|
||||
const ExportNotesSheet = ({ notes, update }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -157,6 +158,8 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Dialog context="export-notes" />
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
{!exporting && !complete ? (
|
||||
actions.map((item) => (
|
||||
@@ -346,7 +349,8 @@ ExportNotesSheet.present = (notes, allNotes) => {
|
||||
notes={allNotes ? db.notes.all : notes}
|
||||
update={update}
|
||||
/>
|
||||
)
|
||||
),
|
||||
keyboardHandlerDisabled: true
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
|
||||
const PublishNoteSheet = ({ note: item, update }) => {
|
||||
const PublishNoteSheet = ({ note: item }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const actionSheetRef = useRef();
|
||||
|
||||
@@ -48,8 +48,7 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
const [note, setNote] = useState(item);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const publishUrl =
|
||||
note &&
|
||||
`https://monograph.notesnook.com/${db?.monographs.monograph(note?.id)}`;
|
||||
note && `https://monogr.ph/${db?.monographs.monograph(note?.id)}`;
|
||||
const isPublished = note && db?.monographs.isPublished(note?.id);
|
||||
const pwdInput = useRef();
|
||||
const passwordValue = useRef();
|
||||
|
||||
@@ -37,11 +37,11 @@ const SheetWrapper = ({
|
||||
onOpen,
|
||||
closeOnTouchBackdrop = true,
|
||||
onHasReachedTop,
|
||||
keyboardMode,
|
||||
overlay,
|
||||
overlayOpacity = 0.3,
|
||||
enableGesturesInScrollView = false,
|
||||
bottomPadding = true
|
||||
bottomPadding = true,
|
||||
keyboardHandlerDisabled
|
||||
}) => {
|
||||
const localRef = useRef(null);
|
||||
const { colors } = useThemeColors("sheet");
|
||||
@@ -121,8 +121,9 @@ const SheetWrapper = ({
|
||||
initialOffsetFromBottom={1}
|
||||
onPositionChanged={onHasReachedTop}
|
||||
closeOnTouchBackdrop={closeOnTouchBackdrop}
|
||||
keyboardMode={keyboardMode}
|
||||
keyboardHandlerEnabled={sheetKeyboardHandler}
|
||||
keyboardHandlerEnabled={
|
||||
keyboardHandlerDisabled ? false : sheetKeyboardHandler
|
||||
}
|
||||
closeOnPressBack={closeOnTouchBackdrop}
|
||||
indicatorColor={colors.secondary.background}
|
||||
onOpen={_onOpen}
|
||||
|
||||
@@ -550,15 +550,6 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
}
|
||||
|
||||
async function exportNote() {
|
||||
if (item.locked) {
|
||||
ToastEvent.show({
|
||||
heading: "Note is locked",
|
||||
type: "error",
|
||||
message: "Locked notes cannot be exported",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
ExportNotesSheet.present([item]);
|
||||
}
|
||||
|
||||
|
||||
@@ -481,7 +481,7 @@ export const useAppEvents = () => {
|
||||
|
||||
subscribeToIAPListeners();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(error);
|
||||
DatabaseLogger.error(e);
|
||||
|
||||
ToastEvent.error(e, "An error occurred", "global");
|
||||
}
|
||||
@@ -623,15 +623,20 @@ export const useAppEvents = () => {
|
||||
}, [loading, onUserUpdated]);
|
||||
|
||||
const initializeDatabase = useCallback(async () => {
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
DatabaseLogger.info("Initializing database");
|
||||
await db.init();
|
||||
try {
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
DatabaseLogger.info("Initializing database");
|
||||
await db.init();
|
||||
}
|
||||
if (IsDatabaseMigrationRequired()) return;
|
||||
initialize();
|
||||
setLoading(false);
|
||||
Walkthrough.init();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
ToastEvent.error(e, "Error initializing database", "global");
|
||||
}
|
||||
if (IsDatabaseMigrationRequired()) return;
|
||||
initialize();
|
||||
setLoading(false);
|
||||
Walkthrough.init();
|
||||
}, [IsDatabaseMigrationRequired]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"url": "^0.11.0",
|
||||
"validator": "^13.5.2",
|
||||
"zustand": "^3.6.0",
|
||||
"katex": "0.16.2"
|
||||
"katex": "0.16.2",
|
||||
"@readme/data-urls": "3.0.0"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import { EditorProps, useEditorType } from "./tiptap/types";
|
||||
import { useEditor } from "./tiptap/use-editor";
|
||||
import { useEditorEvents } from "./tiptap/use-editor-events";
|
||||
import { editorController } from "./tiptap/utils";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
const style: ViewStyle = {
|
||||
height: "100%",
|
||||
@@ -93,35 +94,6 @@ const Editor = React.memo(
|
||||
get: () => editor
|
||||
}));
|
||||
|
||||
const onMediaDownloaded = useCallback(
|
||||
({
|
||||
hash,
|
||||
groupId,
|
||||
src,
|
||||
attachmentType
|
||||
}: {
|
||||
hash: string;
|
||||
groupId: string;
|
||||
src: string;
|
||||
attachmentType: string;
|
||||
}) => {
|
||||
if (groupId !== editor.note.current?.id) return;
|
||||
editorController.current.markImageLoaded(hash);
|
||||
if (attachmentType === "webclip") {
|
||||
editor.commands.updateWebclip({
|
||||
hash: hash,
|
||||
src: src
|
||||
});
|
||||
} else {
|
||||
editor.commands.updateImage({
|
||||
hash: hash,
|
||||
dataurl: src
|
||||
});
|
||||
}
|
||||
},
|
||||
[editor.commands, editor.note]
|
||||
);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
renderKey.current =
|
||||
renderKey.current === `editor-0` ? `editor-1` : `editor-0`;
|
||||
@@ -130,16 +102,11 @@ const Editor = React.memo(
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = [
|
||||
eSubscribeEvent("webview_reset", onError),
|
||||
EV.subscribe(EVENTS.mediaAttachmentDownloaded, onMediaDownloaded)
|
||||
];
|
||||
|
||||
const sub = [eSubscribeEvent("webview_reset", onError)];
|
||||
return () => {
|
||||
sub.forEach((s) => s.unsubscribe());
|
||||
EV.unsubscribe(EVENTS.mediaAttachmentDownloaded, onMediaDownloaded);
|
||||
};
|
||||
}, [onError, onMediaDownloaded]);
|
||||
}, [onError]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setImmediate(() => {
|
||||
@@ -210,6 +177,7 @@ export default Editor;
|
||||
const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
|
||||
const readonly = useEditorStore((state) => state.readonly);
|
||||
const keyboard = useKeyboard();
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const onPress = async () => {
|
||||
if (editor.note.current) {
|
||||
@@ -225,7 +193,7 @@ const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
|
||||
name="pencil-lock"
|
||||
type="grayBg"
|
||||
onPress={onPress}
|
||||
color="accent"
|
||||
color={colors.primary.accent}
|
||||
customStyle={{
|
||||
position: "absolute",
|
||||
bottom: 60,
|
||||
|
||||
@@ -17,10 +17,7 @@ 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 type {
|
||||
Attachment,
|
||||
AttachmentProgress
|
||||
} from "@notesnook/editor/dist/extensions/attachment/index";
|
||||
import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index";
|
||||
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
|
||||
import { createRef, RefObject } from "react";
|
||||
import { Platform } from "react-native";
|
||||
@@ -203,11 +200,16 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
|
||||
);
|
||||
};
|
||||
|
||||
setAttachmentProgress = async (attachmentProgress: AttachmentProgress) => {
|
||||
setAttachmentProgress = async (attachmentProgress: Partial<Attachment>) => {
|
||||
await this.doAsync(
|
||||
`editor && editor.commands.setAttachmentProgress(${JSON.stringify(
|
||||
`editor && editor.commands.updateAttachment(${JSON.stringify(
|
||||
attachmentProgress
|
||||
)})`
|
||||
)}, {
|
||||
preventUpdate: true,
|
||||
query: (attachment) => {
|
||||
return attachment.hash === "${attachmentProgress.hash}";
|
||||
}
|
||||
})`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -36,5 +36,6 @@ export const EventTypes = {
|
||||
contentchange: "editor-event:content-change",
|
||||
reminders: "editor-event:reminders",
|
||||
previewAttachment: "editor-event:preview-attachment",
|
||||
copyToClipboard: "editor-events:copy-to-clipboard"
|
||||
copyToClipboard: "editor-events:copy-to-clipboard",
|
||||
getAttachmentData: "editor-events:get-attachment-data"
|
||||
};
|
||||
|
||||
@@ -97,6 +97,7 @@ export type SavePayload = {
|
||||
type?: Content["type"];
|
||||
sessionId?: string | null;
|
||||
sessionHistoryId?: number;
|
||||
ignoreEdit: boolean;
|
||||
};
|
||||
|
||||
export type AppState = {
|
||||
|
||||
@@ -66,6 +66,7 @@ import { EditorMessage, EditorProps, useEditorType } from "./types";
|
||||
import { EditorEvents, editorState } from "./utils";
|
||||
import { useNoteStore } from "../../../stores/use-notes-store";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import downloadAttachment from "../../../common/filesystem/download-attachment";
|
||||
|
||||
const publishNote = async (editor: useEditorType) => {
|
||||
const user = useUserStore.getState().user;
|
||||
@@ -127,6 +128,8 @@ const showActionsheet = async (editor: useEditorType) => {
|
||||
}
|
||||
};
|
||||
|
||||
type ContentMessage = { html: string; ignoreEdit: boolean };
|
||||
|
||||
export const useEditorEvents = (
|
||||
editor: useEditorType,
|
||||
{ readonly: editorPropReadonly, noHeader, noToolbar }: Partial<EditorProps>
|
||||
@@ -330,14 +333,16 @@ export const useEditorEvents = (
|
||||
if (editorMessage.type === EventTypes.content) {
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
content: editorMessage.value as string,
|
||||
content: (editorMessage.value as ContentMessage).html,
|
||||
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
|
||||
forSessionId: editorMessage.sessionId
|
||||
});
|
||||
} else if (editorMessage.type === EventTypes.title) {
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
title: editorMessage.value as string,
|
||||
forSessionId: editorMessage.sessionId
|
||||
forSessionId: editorMessage.sessionId,
|
||||
ignoreEdit: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,6 +421,44 @@ export const useEditorEvents = (
|
||||
break;
|
||||
}
|
||||
|
||||
case EventTypes.getAttachmentData: {
|
||||
const attachment = (editorMessage.value as any)
|
||||
.attachment as Attachment;
|
||||
|
||||
console.log(
|
||||
"Getting attachment data:",
|
||||
attachment.hash,
|
||||
attachment.type
|
||||
);
|
||||
downloadAttachment(attachment.hash, true, {
|
||||
base64: attachment.type === "image",
|
||||
text: attachment.type === "web-clip",
|
||||
silent: true,
|
||||
groupId: editor.note.current?.id,
|
||||
cache: true
|
||||
} as any)
|
||||
.then((data: any) => {
|
||||
console.log(
|
||||
"Got attachment data:",
|
||||
!!data,
|
||||
(editorMessage.value as any).resolverId
|
||||
);
|
||||
editor.postMessage(EditorEvents.attachmentData, {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
console.log("Error downloading attachment data");
|
||||
editor.postMessage(EditorEvents.attachmentData, {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data: undefined
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case EventTypes.pro:
|
||||
if (editor.state.current?.isFocused) {
|
||||
editor.state.current.isFocused = true;
|
||||
|
||||
@@ -78,7 +78,6 @@ export const useEditor = (
|
||||
const saveCount = useRef(0);
|
||||
const lastContentChangeTime = useRef<number>(0);
|
||||
const lock = useRef(false);
|
||||
const loadedImages = useRef<{ [name: string]: boolean }>({});
|
||||
const lockedSessionId = useRef<string>();
|
||||
const loadingState = useRef<string>();
|
||||
|
||||
@@ -129,12 +128,9 @@ export const useEditor = (
|
||||
|
||||
const reset = useCallback(
|
||||
async (resetState = true, resetContent = true) => {
|
||||
currentNote.current?.id &&
|
||||
db.fs?.cancel(currentNote.current.id, "download");
|
||||
currentNote.current?.id && db.fs?.cancel(currentNote.current.id);
|
||||
currentNote.current = null;
|
||||
loadedImages.current = {};
|
||||
currentContent.current = null;
|
||||
clearTimeout(timers.current["loading-images"]);
|
||||
sessionHistoryId.current = undefined;
|
||||
saveCount.current = 0;
|
||||
loadingState.current = undefined;
|
||||
@@ -166,7 +162,8 @@ export const useEditor = (
|
||||
data,
|
||||
type,
|
||||
sessionId: currentSessionId,
|
||||
sessionHistoryId: currentSessionHistoryId
|
||||
sessionHistoryId: currentSessionHistoryId,
|
||||
ignoreEdit
|
||||
}: SavePayload) => {
|
||||
if (
|
||||
readonly ||
|
||||
@@ -198,6 +195,12 @@ export const useEditor = (
|
||||
};
|
||||
|
||||
noteData.title = title;
|
||||
|
||||
if (ignoreEdit) {
|
||||
console.log("Ignoring edits...");
|
||||
noteData.dateEdited = note?.dateEdited;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
noteData.content = {
|
||||
data: data,
|
||||
@@ -287,58 +290,6 @@ export const useEditor = (
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getMediaToLoad = (previousContent?: string) => {
|
||||
if (!currentNote.current?.id) return [];
|
||||
|
||||
const previousAttachments =
|
||||
previousContent?.matchAll(/data-hash="(.+?)"/gm) || [];
|
||||
const attachments =
|
||||
currentContent.current?.data?.matchAll(/data-hash="(.+?)"/gm) || [];
|
||||
|
||||
const media: string[] = [];
|
||||
|
||||
const oldMatches = Array.from(previousAttachments).map((match) => match[1]);
|
||||
const matches = Array.from(attachments).map((match) => match[1]);
|
||||
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
const currentHash = matches[i];
|
||||
const oldHash = oldMatches[i];
|
||||
if (currentHash !== oldHash) {
|
||||
media.push(currentHash);
|
||||
loadedImages.current[currentHash] = false;
|
||||
}
|
||||
}
|
||||
return media;
|
||||
};
|
||||
|
||||
const markImageLoaded = (hash: string) => {
|
||||
const attachment = loadedImages.current[hash];
|
||||
if (typeof attachment === "boolean") {
|
||||
loadedImages.current[hash] = true;
|
||||
}
|
||||
};
|
||||
|
||||
const loadImages = useCallback((previousContent?: string) => {
|
||||
if (!currentNote.current?.id) return;
|
||||
const timerId = "loading-images";
|
||||
clearTimeout(timers.current[timerId]);
|
||||
timers.current[timerId] = setTimeout(() => {
|
||||
if (!currentNote.current?.id) return;
|
||||
if (currentNote.current?.content?.isPreview) {
|
||||
db.content?.downloadMedia(
|
||||
currentNote.current?.id,
|
||||
currentNote.current.content,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
const media = getMediaToLoad(previousContent);
|
||||
if (media.length > 0) {
|
||||
db.attachments?.downloadMedia(currentNote.current?.id, media);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const loadNote = useCallback(
|
||||
async (
|
||||
item: Omit<NoteType, "type"> & {
|
||||
@@ -425,18 +376,9 @@ export const useEditor = (
|
||||
}
|
||||
}, 300);
|
||||
overlay(false);
|
||||
loadImages();
|
||||
}
|
||||
},
|
||||
[
|
||||
commands,
|
||||
isDefaultEditor,
|
||||
loadContent,
|
||||
loadImages,
|
||||
overlay,
|
||||
postMessage,
|
||||
reset
|
||||
]
|
||||
[commands, isDefaultEditor, loadContent, overlay, postMessage, reset]
|
||||
);
|
||||
|
||||
const lockNoteWithVault = useCallback((note: NoteType) => {
|
||||
@@ -466,8 +408,6 @@ export const useEditor = (
|
||||
|
||||
lock.current = true;
|
||||
|
||||
const previousContent = currentContent.current?.data;
|
||||
|
||||
if (data.type === "tiptap") {
|
||||
if (!currentNote.current.locked && isContentEncrypted) {
|
||||
lockNoteWithVault(note);
|
||||
@@ -500,18 +440,8 @@ export const useEditor = (
|
||||
}
|
||||
|
||||
lock.current = false;
|
||||
if (data.type === "tiptap") {
|
||||
loadImages(previousContent);
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.syncCompleted,
|
||||
() => {
|
||||
loadImages(previousContent);
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
},
|
||||
[loadImages, lockNoteWithVault, postMessage, commands]
|
||||
[lockNoteWithVault, postMessage, commands]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -531,12 +461,14 @@ export const useEditor = (
|
||||
title,
|
||||
content,
|
||||
type,
|
||||
forSessionId
|
||||
forSessionId,
|
||||
ignoreEdit
|
||||
}: {
|
||||
title?: string;
|
||||
content?: string;
|
||||
type: string;
|
||||
forSessionId: string;
|
||||
ignoreEdit: boolean;
|
||||
}) => {
|
||||
if (lock.current || lockedSessionId.current === forSessionId) return;
|
||||
lastContentChangeTime.current = Date.now();
|
||||
@@ -567,7 +499,8 @@ export const useEditor = (
|
||||
type: "tiptap",
|
||||
sessionId: forSessionId,
|
||||
id: noteId,
|
||||
sessionHistoryId: sessionHistoryId.current
|
||||
sessionHistoryId: sessionHistoryId.current,
|
||||
ignoreEdit
|
||||
};
|
||||
withTimer(
|
||||
noteId || "newnote",
|
||||
@@ -585,7 +518,7 @@ export const useEditor = (
|
||||
}
|
||||
saveNote(params);
|
||||
},
|
||||
150
|
||||
ignoreEdit ? 0 : 150
|
||||
);
|
||||
},
|
||||
[withTimer, onChange, saveNote]
|
||||
@@ -689,7 +622,6 @@ export const useEditor = (
|
||||
saveContent,
|
||||
onContentChanged,
|
||||
editorId: editorId,
|
||||
markImageLoaded,
|
||||
overlay,
|
||||
postMessage
|
||||
};
|
||||
|
||||
@@ -50,7 +50,8 @@ export const EditorEvents: { [name: string]: string } = {
|
||||
titleplaceholder: "native:titleplaceholder",
|
||||
logger: "native:logger",
|
||||
status: "native:status",
|
||||
keyboardShown: "native:keyboardShown"
|
||||
keyboardShown: "native:keyboardShown",
|
||||
attachmentData: "native:attachment-data"
|
||||
};
|
||||
|
||||
export function randId(prefix: string) {
|
||||
|
||||
@@ -115,7 +115,7 @@ export const EditorWrapper = ({ width }) => {
|
||||
style={{ height: 1, padding: 0, width: 1, position: "absolute" }}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<ProgressBar />
|
||||
{/* <ProgressBar /> */}
|
||||
<Editor key="editor" withController={true} />
|
||||
</KeyboardAvoidingViewIOS>
|
||||
)}
|
||||
|
||||
@@ -69,10 +69,10 @@ export const TrashIntervalPicker = createSettingsPicker({
|
||||
db.settings.setTrashCleanupInterval(item);
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return item === -1 ? "Never" : item + " days";
|
||||
return item === -1 ? "Never" : item === 1 ? "Daily" : item + " days";
|
||||
},
|
||||
getItemKey: (item) => item.toString(),
|
||||
options: [-1, 7, 30, 365],
|
||||
options: [-1, 1, 7, 30, 365],
|
||||
compareValue: (current, item) => current === item,
|
||||
premium: true
|
||||
});
|
||||
|
||||
@@ -65,7 +65,11 @@ const PLACEHOLDER_DATA = (trashCleanupInterval = 7) => ({
|
||||
paragraph:
|
||||
trashCleanupInterval === -1
|
||||
? "Set automatic trash cleanup interval from Settings > Behaviour > Clean trash interval."
|
||||
: `Items in the trash will be permanently deleted after after ${trashCleanupInterval} days.`,
|
||||
: `Trash gets automatically cleaned up ${
|
||||
trashCleanupInterval === 1
|
||||
? "daily."
|
||||
: `after ${trashCleanupInterval} days.`
|
||||
}`,
|
||||
button: null,
|
||||
loading: "Loading trash items"
|
||||
});
|
||||
|
||||
@@ -18,17 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { decode, EntityLevel } from "entities";
|
||||
import { zipSync } from "fflate";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNHTMLtoPDF from "react-native-html-to-pdf-lite";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { zip } from "react-native-zip-archive";
|
||||
import { DatabaseLogger, db } from "../common/database/index";
|
||||
import Storage from "../common/database/storage";
|
||||
import { convertNoteToText } from "../utils/note-to-text";
|
||||
|
||||
import { sleep } from "../utils/time";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import BiometicService from "./biometrics";
|
||||
import { ToastEvent } from "./event-manager";
|
||||
|
||||
const MIMETypes = {
|
||||
txt: "text/plain",
|
||||
@@ -99,8 +101,8 @@ async function save(path, data, fileName, extension) {
|
||||
return uri || path;
|
||||
}
|
||||
|
||||
async function makeHtml(note) {
|
||||
let html = await db.notes.note(note.id).export("html");
|
||||
async function makeHtml(note, content) {
|
||||
let html = await db.notes.note(note.id).export("html", content);
|
||||
html = decode(html, {
|
||||
level: EntityLevel.HTML
|
||||
});
|
||||
@@ -111,23 +113,25 @@ async function makeHtml(note) {
|
||||
*
|
||||
* @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type
|
||||
*/
|
||||
async function exportAs(type, note, bulk) {
|
||||
async function exportAs(type, note, bulk, content) {
|
||||
let data;
|
||||
switch (type) {
|
||||
case "html":
|
||||
{
|
||||
data = await makeHtml(note);
|
||||
data = await makeHtml(note, content);
|
||||
}
|
||||
break;
|
||||
case "md":
|
||||
data = await db.notes.note(note.id).export("md");
|
||||
data = await db.notes.note(note.id).export("md", content);
|
||||
break;
|
||||
case "md-frontmatter":
|
||||
data = await db.notes.note(note.id).export("md-frontmatter");
|
||||
data = await db.notes
|
||||
.note(note.id)
|
||||
.export("md-frontmatter", content?.data);
|
||||
break;
|
||||
case "pdf":
|
||||
{
|
||||
let html = await makeHtml(note);
|
||||
let html = await makeHtml(note, content);
|
||||
let fileName = sanitizeFilename(note.title + Date.now(), {
|
||||
replacement: "_"
|
||||
});
|
||||
@@ -153,21 +157,84 @@ async function exportAs(type, note, bulk) {
|
||||
}
|
||||
break;
|
||||
case "txt":
|
||||
data = await convertNoteToText(note);
|
||||
{
|
||||
data = await db.notes?.note(note.id).export("txt", content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function unlockVault() {
|
||||
let biometry = await BiometicService.isBiometryAvailable();
|
||||
let fingerprint = await BiometicService.hasInternetCredentials("nn_vault");
|
||||
if (biometry && fingerprint) {
|
||||
let credentials = await BiometicService.getCredentials(
|
||||
"Unlock vault",
|
||||
"Unlock vault to export locked notes"
|
||||
);
|
||||
if (credentials) {
|
||||
return db.vault.unlock(credentials.password);
|
||||
}
|
||||
}
|
||||
useSettingStore.getState().setSheetKeyboardHandler(false);
|
||||
return new Promise((resolve) => {
|
||||
setImmediate(() => {
|
||||
presentDialog({
|
||||
context: "export-notes",
|
||||
input: true,
|
||||
secureTextEntry: true,
|
||||
positiveText: "Unlock",
|
||||
title: "Unlock vault",
|
||||
paragraph: "Some exported notes are locked, Unlock to export them",
|
||||
inputPlaceholder: "Enter password",
|
||||
positivePress: async (value) => {
|
||||
const unlocked = await db.vault.unlock(value);
|
||||
if (!unlocked) {
|
||||
ToastEvent.show({
|
||||
heading: "Invalid password",
|
||||
message: "Please enter a valid password",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
resolve(unlocked);
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
return true;
|
||||
},
|
||||
onClose: () => {
|
||||
resolve(false);
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type
|
||||
*/
|
||||
async function exportNote(note, type) {
|
||||
let content;
|
||||
|
||||
if (note.locked) {
|
||||
try {
|
||||
let unlocked = await unlockVault();
|
||||
if (!unlocked) return null;
|
||||
const unlockedNote = await db.vault.open(note.id);
|
||||
content = unlockedNote.content;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
let path = await getPath(FolderNames[type]);
|
||||
if (!path) return;
|
||||
let result = await exportAs(type, note);
|
||||
|
||||
let result = await exportAs(type, note, false, content);
|
||||
if (!result) return null;
|
||||
let fileName = sanitizeFilename(note.title + Date.now(), {
|
||||
replacement: "_"
|
||||
@@ -195,11 +262,6 @@ function copyFileAsync(source, dest) {
|
||||
});
|
||||
}
|
||||
|
||||
function zipsync(results) {
|
||||
let data = zipSync(results);
|
||||
return Buffer.from(data.buffer).toString("base64");
|
||||
}
|
||||
|
||||
function getUniqueFileName(fileName, results) {
|
||||
const chunks = fileName.split(".");
|
||||
const ext = chunks.pop();
|
||||
@@ -221,19 +283,108 @@ async function bulkExport(notes, type, callback) {
|
||||
let path = await getPath(FolderNames[type]);
|
||||
if (!path) return;
|
||||
|
||||
const exportCacheFolder =
|
||||
RNFetchBlob.fs.dirs.CacheDir + `/export_${Date.now()}`;
|
||||
|
||||
await RNFetchBlob.fs.mkdir(exportCacheFolder).catch((e) => console.log(e));
|
||||
|
||||
const mkdir = async (dir) => {
|
||||
const folder = `${exportCacheFolder}/${dir}`;
|
||||
if (!(await RNFetchBlob.fs.exists(folder))) {
|
||||
await RNFetchBlob.fs.mkdir(folder);
|
||||
}
|
||||
};
|
||||
|
||||
const writeFile = async (path, result) => {
|
||||
const cacheFilePath = exportCacheFolder + path;
|
||||
await RNFetchBlob.fs.writeFile(
|
||||
cacheFilePath,
|
||||
result,
|
||||
type === "pdf" ? "base64" : "utf8"
|
||||
);
|
||||
};
|
||||
|
||||
const results = {};
|
||||
for (var i = 0; i < notes.length; i++) {
|
||||
try {
|
||||
await sleep(1);
|
||||
let note = notes[i];
|
||||
if (note.locked) continue;
|
||||
let result = await exportAs(type, note);
|
||||
let content;
|
||||
if (note.locked) {
|
||||
try {
|
||||
let unlocked = !db.vault.unlocked ? await unlockVault() : true;
|
||||
if (!unlocked) {
|
||||
continue;
|
||||
}
|
||||
const unlockedNote = await db.vault.open(note.id);
|
||||
content = unlockedNote.content;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let result = await exportAs(type, note, true, content);
|
||||
let fileName = sanitizeFilename(note.title, {
|
||||
replacement: "_"
|
||||
});
|
||||
if (result) {
|
||||
results[getUniqueFileName(fileName + `.${type}`, results)] =
|
||||
Buffer.from(result, type === "pdf" ? "base64" : "utf-8");
|
||||
const notebooks = [
|
||||
...(db.relations
|
||||
?.to({ id: note.id, type: "note" }, "notebook")
|
||||
.map((notebook) => ({
|
||||
title: notebook.title
|
||||
})) || []),
|
||||
...(note.notebooks || []).map((ref) => {
|
||||
const notebook = db.notebooks?.notebook(ref.id);
|
||||
const topics = notebook?.topics.all || [];
|
||||
|
||||
return {
|
||||
title: notebook?.title,
|
||||
topics: ref.topics
|
||||
.map((topicId) => topics.find((topic) => topic.id === topicId))
|
||||
.filter(Boolean)
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
for (const notebook of notebooks) {
|
||||
results[notebook.title] = results[notebook.title] || {};
|
||||
await mkdir(notebook.title);
|
||||
|
||||
if (notebook.topics && notebook.topics.length) {
|
||||
for (const topic of notebook.topics) {
|
||||
results[notebook.title][topic.title] =
|
||||
results[notebook.title][topic.title] || {};
|
||||
|
||||
await mkdir(`${notebook.title}/${topic.title}`);
|
||||
const exportedNoteName = getUniqueFileName(
|
||||
fileName + `.${type}`,
|
||||
results[notebook.title][topic.title]
|
||||
);
|
||||
results[notebook.title][topic.title][exportedNoteName] = true;
|
||||
|
||||
writeFile(
|
||||
`/${notebook.title}/${topic.title}/${exportedNoteName}`,
|
||||
result
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const exportedNoteName = getUniqueFileName(
|
||||
fileName + `.${type}`,
|
||||
results[notebook.title]
|
||||
);
|
||||
results[notebook.title][exportedNoteName] = true;
|
||||
writeFile(`/${notebook.title}/${exportedNoteName}`, result);
|
||||
}
|
||||
}
|
||||
|
||||
if (!notebooks.length) {
|
||||
const exportedNoteName = getUniqueFileName(
|
||||
fileName + `.${type}`,
|
||||
results
|
||||
);
|
||||
results[exportedNoteName] = true;
|
||||
writeFile(`/${exportedNoteName}`, result);
|
||||
}
|
||||
}
|
||||
callback(`${i + 1}/${notes.length}`);
|
||||
} catch (e) {
|
||||
@@ -244,26 +395,24 @@ async function bulkExport(notes, type, callback) {
|
||||
|
||||
try {
|
||||
callback("zipping");
|
||||
await sleep(1);
|
||||
const zipped = zipsync(results);
|
||||
const zipOutputPath =
|
||||
Platform.OS === "ios"
|
||||
? path + `/${fileName}`
|
||||
: RNFetchBlob.fs.dirs.CacheDir + `/${fileName}`;
|
||||
await zip(exportCacheFolder, zipOutputPath);
|
||||
callback("saving to disk");
|
||||
await sleep(1);
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
RNFetchBlob.fs.writeFile(path + `/${fileName}`, zipped, "base64");
|
||||
} else {
|
||||
const templFile = RNFetchBlob.fs.dirs.CacheDir + `/${fileName}`;
|
||||
await RNFetchBlob.fs.writeFile(templFile, zipped, "base64");
|
||||
if (Platform.OS === "android") {
|
||||
const file = await ScopedStorage.createFile(
|
||||
path,
|
||||
fileName,
|
||||
"application/zip"
|
||||
);
|
||||
path = file.uri;
|
||||
await copyFileAsync("file://" + templFile, path);
|
||||
await RNFetchBlob.fs.unlink(templFile);
|
||||
await copyFileAsync("file://" + zipOutputPath, path);
|
||||
await RNFetchBlob.fs.unlink(zipOutputPath);
|
||||
callback();
|
||||
}
|
||||
RNFetchBlob.fs.unlink(exportCacheFolder);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
|
||||
if (!progress) return;
|
||||
editorController.current?.commands.setAttachmentProgress({
|
||||
hash: hash,
|
||||
progress: 100,
|
||||
type: progress[hash]?.type || "download"
|
||||
progress: 100
|
||||
});
|
||||
progress[hash] = null;
|
||||
set({ progress: { ...progress } });
|
||||
@@ -80,10 +79,11 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
|
||||
progress[hash] = { sent, total, hash, recieved, type };
|
||||
const progressPercentage =
|
||||
type === "upload" ? sent / total : recieved / total;
|
||||
|
||||
editorController.current?.commands.setAttachmentProgress({
|
||||
hash: hash,
|
||||
progress: Math.round(Math.max(progressPercentage * 100, 0)),
|
||||
type: type
|
||||
//@ts-ignore
|
||||
progress: Math.round(Math.max(progressPercentage * 100, 0))
|
||||
});
|
||||
set({ progress: { ...progress } });
|
||||
},
|
||||
|
||||
@@ -61,7 +61,10 @@ export const getGithubVersion = async () => {
|
||||
if (!res?.ok) return null;
|
||||
const data = (await res?.json()) as GithubRelease[];
|
||||
|
||||
const versions = data?.filter((tag) => tag.tag_name.endsWith("android"));
|
||||
const versions = data?.filter(
|
||||
(tag) =>
|
||||
tag.tag_name.endsWith("android") && !tag.tag_name.endsWith("beta-android")
|
||||
);
|
||||
const latestVersion = versions[0];
|
||||
const version = latestVersion.tag_name.replace("-android", "");
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,7 @@ import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { isImage } from "@notesnook/core/dist/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { db } from "../common/database";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import { IOS_APPGROUPID } from "./constants";
|
||||
|
||||
const santizeUri = (uri) => {
|
||||
@@ -57,6 +57,7 @@ export async function attachFile(uri, hash, type, filename, options) {
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (Platform.OS === "ios") RNFetchBlob.fs.unlink(uri).catch(console.log);
|
||||
DatabaseLogger.error(e, "Attach file error");
|
||||
console.log("attach file error: ", e);
|
||||
return false;
|
||||
}
|
||||
@@ -107,26 +108,31 @@ async function createNotes(bundle) {
|
||||
uri: uri,
|
||||
type: "cache"
|
||||
});
|
||||
await attachFile(uri, hash, file.type, file.name, {
|
||||
const attached = await attachFile(uri, hash, file.type, file.name, {
|
||||
type: "cache",
|
||||
id: id,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
});
|
||||
let content = ``;
|
||||
if (isImage(file.type)) {
|
||||
content = `<img data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" />`;
|
||||
} else {
|
||||
content = `<p><span data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" data-size="${file.size}" /></p>`;
|
||||
|
||||
if (attached) {
|
||||
if (isImage(file.type)) {
|
||||
content = `<img data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" />`;
|
||||
} else {
|
||||
content = `<p><span data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" data-size="${file.size}" /></p>`;
|
||||
}
|
||||
const rawContent = await db.content.raw(
|
||||
db.notes.note(id).data?.contentId
|
||||
);
|
||||
await db.notes.add({
|
||||
id: id,
|
||||
content: {
|
||||
type: "tiptap",
|
||||
data: rawContent?.data ? rawContent?.data + content : content
|
||||
},
|
||||
sessionId: sessionId
|
||||
});
|
||||
}
|
||||
const rawContent = await db.content.raw(db.notes.note(id).data?.contentId);
|
||||
await db.notes.add({
|
||||
id: id,
|
||||
content: {
|
||||
type: "tiptap",
|
||||
data: rawContent?.data ? rawContent?.data + content : content
|
||||
},
|
||||
sessionId: sessionId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 2074
|
||||
versionCode 2076
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowDisablePreview">true</item>
|
||||
<item name="android:editTextBackground">@drawable/edit_text</item>
|
||||
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:datePickerDialogTheme">@style/DialogDatePicker.Theme</item>
|
||||
</style>
|
||||
@@ -20,6 +20,7 @@
|
||||
<style name="BootTheme" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/bootsplash_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/bootsplash_logo_dark</item>
|
||||
<item name="android:navigationBarColor">@color/bootsplash_background</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
|
||||
@@ -31,6 +32,7 @@
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
<item name="android:editTextBackground">@drawable/edit_text</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="DialogDatePicker.Theme" parent="Theme.AppCompat.Light.Dialog">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowDisablePreview">true</item>
|
||||
<item name="android:editTextBackground">@drawable/edit_text</item>
|
||||
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:datePickerDialogTheme">@style/DialogDatePicker.Theme</item>
|
||||
</style>
|
||||
@@ -20,6 +20,7 @@
|
||||
<style name="BootTheme" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/bootsplash_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/bootsplash_logo</item>
|
||||
<item name="android:navigationBarColor">@color/bootsplash_background</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
|
||||
@@ -31,6 +32,7 @@
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
<item name="android:editTextBackground">@drawable/edit_text</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="DialogDatePicker.Theme" parent="Theme.AppCompat.Light.Dialog">
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
- Bug fixes and performance improvements
|
||||
- Support for exporting locked notes
|
||||
- Maintain directory structure in exports based on Notebooks/Topics
|
||||
- Match color of navigation bar in sheets with app theme
|
||||
- Improved image loading and show progress when loading images
|
||||
- Support for setting trash interval to daily
|
||||
- Fix android app UI issues with RTL languages
|
||||
- Fix pasting links in editor
|
||||
- Other Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
15
apps/mobile/native/ios/ExportOptions.plist
Normal file
15
apps/mobile/native/ios/ExportOptions.plist
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>org.streetwriters.notesnook</key>
|
||||
<string>Notesnook App Distribution 2024</string>
|
||||
<key>org.streetwriters.notesnook.notewidget</key>
|
||||
<string>Notesnook Widget Distribution 2024</string>
|
||||
<key>org.streetwriters.notesnook.share</key>
|
||||
<string>Notesnook Share Distribution 2024</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -14,13 +14,13 @@
|
||||
2D02E4BD1E0B4A84006451C7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
2D02E4BF1E0B4AB3006451C7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
2DCD954D1E0B4F2C00145EB5 /* NotesnookTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* NotesnookTests.m */; };
|
||||
6510626F27042891009661C3 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6552012F27019F7700A43C51 /* OpenSans-Regular.ttf */; };
|
||||
6510627627042895009661C3 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6552012E27019F6E00A43C51 /* OpenSans-SemiBold.ttf */; };
|
||||
6510E6D72877215700DACAA9 /* build.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 6510E6D62877215700DACAA9 /* build.bundle */; };
|
||||
6515C42F2580AA3000E83E39 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6515C42E2580AA2F00E83E39 /* StoreKit.framework */; };
|
||||
6517B7C12B6838EB0079FF37 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */; };
|
||||
6517B7C22B6838EB0079FF37 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */; };
|
||||
6517B7C32B6838EB0079FF37 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */; };
|
||||
6529A13E279BC4C70048D4A8 /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6529A13D279BC4C70048D4A8 /* BootSplash.storyboard */; };
|
||||
6593E4A3281C345400492C50 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6593E4A2281C345400492C50 /* AppDelegate.mm */; };
|
||||
659670B22A2754FD00C5D2AF /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 659670B12A2754FD00C5D2AF /* OpenSans-Bold.ttf */; };
|
||||
659BE46725E11A5100E05671 /* notesnook-text.png in Resources */ = {isa = PBXBuildFile; fileRef = 659BE46625E11A5100E05671 /* notesnook-text.png */; };
|
||||
65AA857925E6DDEC00772A01 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65AA857825E6DDEC00772A01 /* WidgetKit.framework */; };
|
||||
65AA857B25E6DDEC00772A01 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65AA857A25E6DDEC00772A01 /* SwiftUI.framework */; };
|
||||
@@ -110,12 +110,12 @@
|
||||
4C4AF691C324D95337F2FB0A /* libPods-Make Note.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Make Note.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6510E6D62877215700DACAA9 /* build.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = build.bundle; path = "../../../../packages/editor-mobile/build.bundle"; sourceTree = "<group>"; };
|
||||
6515C42E2580AA2F00E83E39 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Regular.ttf"; path = "../../../../packages/editor-mobile/public/fonts/OpenSans-Regular.ttf"; sourceTree = "<group>"; };
|
||||
6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-SemiBold.ttf"; path = "../../../../packages/editor-mobile/public/fonts/OpenSans-SemiBold.ttf"; sourceTree = "<group>"; };
|
||||
6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Bold.ttf"; path = "../../../../packages/editor-mobile/public/fonts/OpenSans-Bold.ttf"; sourceTree = "<group>"; };
|
||||
6529A13D279BC4C70048D4A8 /* BootSplash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = BootSplash.storyboard; path = Notesnook/BootSplash.storyboard; sourceTree = "<group>"; };
|
||||
6529D2B0257B4A2900B49BC3 /* NotesnookDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NotesnookDebug.entitlements; path = Notesnook/NotesnookDebug.entitlements; sourceTree = "<group>"; };
|
||||
6552012E27019F6E00A43C51 /* OpenSans-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-SemiBold.ttf"; path = "../android/app/src/main/assets/fonts/OpenSans-SemiBold.ttf"; sourceTree = "<group>"; };
|
||||
6552012F27019F7700A43C51 /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Regular.ttf"; path = "../android/app/src/main/assets/fonts/OpenSans-Regular.ttf"; sourceTree = "<group>"; };
|
||||
6593E4A2281C345400492C50 /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Notesnook/AppDelegate.mm; sourceTree = "<group>"; };
|
||||
659670B12A2754FD00C5D2AF /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Bold.ttf"; path = "../android/app/src/main/assets/fonts/OpenSans-Bold.ttf"; sourceTree = "<group>"; };
|
||||
659BE46625E11A5100E05671 /* notesnook-text.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "notesnook-text.png"; path = "Notesnook/Images.xcassets/notesnook-text.png"; sourceTree = "<group>"; };
|
||||
65A7F34F255689AD00699170 /* Notesnook.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Notesnook.entitlements; path = Notesnook/Notesnook.entitlements; sourceTree = "<group>"; };
|
||||
65AA857725E6DDEC00772A01 /* NotesWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotesWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -293,13 +293,13 @@
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */,
|
||||
6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */,
|
||||
6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */,
|
||||
65C400DD2A80B68D00AA3DF5 /* MaterialCommunityIcons.ttf */,
|
||||
65C149862A61151B005C40F1 /* extension.bundle */,
|
||||
6510E6D62877215700DACAA9 /* build.bundle */,
|
||||
65EC5B71272A7EE200FB3748 /* NotesWidgetExtensionDebug.entitlements */,
|
||||
6552012F27019F7700A43C51 /* OpenSans-Regular.ttf */,
|
||||
659670B12A2754FD00C5D2AF /* OpenSans-Bold.ttf */,
|
||||
6552012E27019F6E00A43C51 /* OpenSans-SemiBold.ttf */,
|
||||
65D145C429DC30470056FE7D /* MaterialCommunityIcons.ttf */,
|
||||
13B07FAE1A68108700A75B9A /* Notesnook */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
@@ -542,12 +542,12 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
65C400DE2A80B68D00AA3DF5 /* MaterialCommunityIcons.ttf in Resources */,
|
||||
6510626F27042891009661C3 /* OpenSans-Regular.ttf in Resources */,
|
||||
659670B22A2754FD00C5D2AF /* OpenSans-Bold.ttf in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
6517B7C32B6838EB0079FF37 /* OpenSans-Bold.ttf in Resources */,
|
||||
6517B7C22B6838EB0079FF37 /* OpenSans-SemiBold.ttf in Resources */,
|
||||
6529A13E279BC4C70048D4A8 /* BootSplash.storyboard in Resources */,
|
||||
6510E6D72877215700DACAA9 /* build.bundle in Resources */,
|
||||
6510627627042895009661C3 /* OpenSans-SemiBold.ttf in Resources */,
|
||||
6517B7C12B6838EB0079FF37 /* OpenSans-Regular.ttf in Resources */,
|
||||
659BE46725E11A5100E05671 /* notesnook-text.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -997,7 +997,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2066;
|
||||
CURRENT_PROJECT_VERSION = 2067;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1102,7 +1102,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2066;
|
||||
CURRENT_PROJECT_VERSION = 2067;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1335,7 +1335,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2066;
|
||||
CURRENT_PROJECT_VERSION = 2067;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1378,7 +1378,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2066;
|
||||
CURRENT_PROJECT_VERSION = 2067;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1421,7 +1421,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2066;
|
||||
CURRENT_PROJECT_VERSION = 2067;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1526,7 +1526,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2066;
|
||||
CURRENT_PROJECT_VERSION = 2067;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
|
||||
33
apps/mobile/package-lock.json
generated
33
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -62,7 +62,8 @@
|
||||
"@microsoft/signalr": "^7.0.10",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.2-alpha",
|
||||
"@readme/data-urls": "^3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.4-alpha",
|
||||
"async-mutex": "^0.3.2",
|
||||
"dayjs": "1.11.9",
|
||||
"entities": "^4.3.1",
|
||||
@@ -81,7 +82,7 @@
|
||||
"@types/html-to-text": "^9.0.0",
|
||||
"@types/katex": "^0.16.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -117,7 +118,7 @@
|
||||
"@tiptap/extension-font-family": "2.1.12",
|
||||
"@tiptap/extension-history": "2.1.12",
|
||||
"@tiptap/extension-horizontal-rule": "2.1.12",
|
||||
"@tiptap/extension-link": "2.1.12",
|
||||
"@tiptap/extension-link": "^2.1.16",
|
||||
"@tiptap/extension-list-keymap": "2.1.12",
|
||||
"@tiptap/extension-placeholder": "2.1.12",
|
||||
"@tiptap/extension-subscript": "2.1.12",
|
||||
@@ -248,6 +249,7 @@
|
||||
"dependencies": {
|
||||
"@flyerhq/react-native-link-preview": "^1.6.0",
|
||||
"@mdi/js": "^6.7.96",
|
||||
"@readme/data-urls": "3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.1-alpha.2",
|
||||
"absolutify": "^0.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -7095,6 +7097,14 @@
|
||||
"nanoid": "^3.1.23"
|
||||
}
|
||||
},
|
||||
"node_modules/@readme/data-urls": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@readme/data-urls/-/data-urls-3.0.0.tgz",
|
||||
"integrity": "sha512-b0L7VWqbLZqOSSAFUrxS5ZwUfec35WDsAwwCH481vYnhk0dWO3nvmNVNCbP8CY4cXqwL1W4uCAnhDz+CUmXM3g==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sayem314/react-native-keep-awake": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sayem314/react-native-keep-awake/-/react-native-keep-awake-1.2.0.tgz",
|
||||
@@ -26831,11 +26841,12 @@
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/crypto": "file:../crypto",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.2-alpha",
|
||||
"@readme/data-urls": "^3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.4-alpha",
|
||||
"@types/html-to-text": "^9.0.0",
|
||||
"@types/katex": "^0.16.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"async-mutex": "^0.3.2",
|
||||
@@ -26882,7 +26893,7 @@
|
||||
"@tiptap/extension-font-family": "2.1.12",
|
||||
"@tiptap/extension-history": "2.1.12",
|
||||
"@tiptap/extension-horizontal-rule": "2.1.12",
|
||||
"@tiptap/extension-link": "2.1.12",
|
||||
"@tiptap/extension-link": "^2.1.16",
|
||||
"@tiptap/extension-list-keymap": "2.1.12",
|
||||
"@tiptap/extension-placeholder": "2.1.12",
|
||||
"@tiptap/extension-subscript": "2.1.12",
|
||||
@@ -26971,6 +26982,7 @@
|
||||
"requires": {
|
||||
"@flyerhq/react-native-link-preview": "^1.6.0",
|
||||
"@mdi/js": "^6.7.96",
|
||||
"@readme/data-urls": "3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.1-alpha.2",
|
||||
"absolutify": "^0.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -27640,6 +27652,11 @@
|
||||
"nanoid": "^3.1.23"
|
||||
}
|
||||
},
|
||||
"@readme/data-urls": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@readme/data-urls/-/data-urls-3.0.0.tgz",
|
||||
"integrity": "sha512-b0L7VWqbLZqOSSAFUrxS5ZwUfec35WDsAwwCH481vYnhk0dWO3nvmNVNCbP8CY4cXqwL1W4uCAnhDz+CUmXM3g=="
|
||||
},
|
||||
"@sayem314/react-native-keep-awake": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sayem314/react-native-keep-awake/-/react-native-keep-awake-1.2.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.6.15",
|
||||
"version": "2.6.16",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -10,6 +10,7 @@
|
||||
"scripts": {
|
||||
"postinstall": "patch-package && node ./scripts/optimize-fonts.mjs",
|
||||
"run-android": "cd native && react-native run-android --active-arch-only",
|
||||
"build": "echo 'No build script defined'",
|
||||
"run-ios": "cd native && react-native run-ios",
|
||||
"start": "cd native && react-native start",
|
||||
"repack": "cd native && react-native webpack-start",
|
||||
|
||||
2
apps/theme-builder/package-lock.json
generated
2
apps/theme-builder/package-lock.json
generated
@@ -1377,7 +1377,7 @@
|
||||
},
|
||||
"../web": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@aws-sdk/util-base64-browser": "^3.208.0",
|
||||
|
||||
13
apps/web/package-lock.json
generated
13
apps/web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@aws-sdk/util-base64-browser": "^3.208.0",
|
||||
@@ -215,7 +215,8 @@
|
||||
"@microsoft/signalr": "^7.0.10",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.2-alpha",
|
||||
"@readme/data-urls": "^3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.4-alpha",
|
||||
"async-mutex": "^0.3.2",
|
||||
"dayjs": "1.11.9",
|
||||
"entities": "^4.3.1",
|
||||
@@ -234,7 +235,7 @@
|
||||
"@types/html-to-text": "^9.0.0",
|
||||
"@types/katex": "^0.16.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -279,7 +280,7 @@
|
||||
"@tiptap/extension-font-family": "2.1.12",
|
||||
"@tiptap/extension-history": "2.1.12",
|
||||
"@tiptap/extension-horizontal-rule": "2.1.12",
|
||||
"@tiptap/extension-link": "2.1.12",
|
||||
"@tiptap/extension-link": "^2.1.16",
|
||||
"@tiptap/extension-list-keymap": "2.1.12",
|
||||
"@tiptap/extension-placeholder": "2.1.12",
|
||||
"@tiptap/extension-subscript": "2.1.12",
|
||||
@@ -438,7 +439,7 @@
|
||||
},
|
||||
"../desktop": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
|
||||
@@ -20,11 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { lazify } from "../utils/lazify";
|
||||
import { db } from "./db";
|
||||
|
||||
async function download(hash: string) {
|
||||
async function download(hash: string, groupId?: string) {
|
||||
const attachment = db.attachments?.attachment(hash);
|
||||
if (!attachment) return;
|
||||
const downloadResult = await db.fs?.downloadFile(
|
||||
attachment.metadata.hash,
|
||||
groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash,
|
||||
attachment.chunkSize,
|
||||
attachment.metadata
|
||||
@@ -61,8 +61,12 @@ type OutputTypeToReturnType = {
|
||||
export async function downloadAttachment<
|
||||
TType extends "blob" | "base64" | "text",
|
||||
TOutputType = OutputTypeToReturnType[TType]
|
||||
>(hash: string, type: TType): Promise<TOutputType | undefined> {
|
||||
const response = await download(hash);
|
||||
>(
|
||||
hash: string,
|
||||
type: TType,
|
||||
groupId?: string
|
||||
): Promise<TOutputType | undefined> {
|
||||
const response = await download(hash, groupId);
|
||||
if (!response) return;
|
||||
const { attachment, key } = response;
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ import { TaskManager } from "./task-manager";
|
||||
import { ExportStream } from "../utils/streams/export-stream";
|
||||
import { createZipStream } from "../utils/streams/zip-stream";
|
||||
import { createWriteStream } from "../utils/stream-saver";
|
||||
import { showToast } from "../utils/toast";
|
||||
import Vault from "./vault";
|
||||
import { db } from "./db";
|
||||
import Note from "@notesnook/core/dist/models/note";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
|
||||
export async function exportToPDF(
|
||||
title: string,
|
||||
@@ -74,12 +79,91 @@ export async function exportNotes(
|
||||
title: "Exporting notes",
|
||||
subtitle: "Please wait while your notes are exported.",
|
||||
action: async (report) => {
|
||||
await new ExportStream(noteIds, format, undefined, (c, text) =>
|
||||
report({ total: noteIds.length, current: c, text })
|
||||
)
|
||||
.pipeThrough(createZipStream())
|
||||
.pipeTo(await createWriteStream("notes.zip"));
|
||||
return true;
|
||||
try {
|
||||
let progress = 0;
|
||||
await createNoteStream(noteIds)
|
||||
.pipeThrough(
|
||||
new TransformStream<Note, Note>({
|
||||
transform(note, controller) {
|
||||
controller.enqueue(note);
|
||||
report({
|
||||
total: noteIds.length,
|
||||
current: progress++,
|
||||
text: `Exporting "${note?.title || "Unknown note"}"`
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipeThrough(new ExportStream(format))
|
||||
.pipeThrough(createZipStream())
|
||||
.pipeTo(await createWriteStream("notes.zip"));
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) showToast("error", e.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createNoteStream(noteIds: string[]) {
|
||||
let i = 0;
|
||||
return new ReadableStream<Note>({
|
||||
start() {},
|
||||
async pull(controller) {
|
||||
const noteId = noteIds[i++];
|
||||
if (!noteId) controller.close();
|
||||
else controller.enqueue(db.notes?.note(noteId));
|
||||
},
|
||||
async cancel(reason) {
|
||||
throw new Error(reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const FORMAT_TO_EXT = {
|
||||
pdf: "pdf",
|
||||
md: "md",
|
||||
txt: "txt",
|
||||
html: "html",
|
||||
"md-frontmatter": "md"
|
||||
} as const;
|
||||
|
||||
export async function exportNote(
|
||||
note: Note,
|
||||
format: keyof typeof FORMAT_TO_EXT,
|
||||
disableTemplate = false
|
||||
) {
|
||||
if (!db.vault?.unlocked && note.data.locked && !(await Vault.unlockVault())) {
|
||||
showToast("error", `Skipping note "${note.title}" as it is locked.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const rawContent = note.data.contentId
|
||||
? await db.content?.raw(note.data.contentId)
|
||||
: undefined;
|
||||
|
||||
const content =
|
||||
rawContent &&
|
||||
!rawContent.deleted &&
|
||||
(typeof rawContent.data === "object"
|
||||
? await db.vault?.decryptContent(rawContent)
|
||||
: rawContent);
|
||||
|
||||
const exported = await note
|
||||
.export(format === "pdf" ? "html" : format, content, disableTemplate)
|
||||
.catch((e: Error) => {
|
||||
console.error(note.data, e);
|
||||
showToast("error", `Failed to export note "${note.title}": ${e.message}`);
|
||||
return false as const;
|
||||
});
|
||||
|
||||
if (typeof exported === "boolean" && !exported) return false;
|
||||
|
||||
const filename = sanitizeFilename(note.title, { replacement: "-" });
|
||||
const ext = FORMAT_TO_EXT[format];
|
||||
return {
|
||||
filename: [filename, ext].join("."),
|
||||
content: exported
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,11 +72,12 @@ type DocumentPreview = {
|
||||
function onEditorChange(
|
||||
noteId: string | undefined,
|
||||
sessionId: number,
|
||||
content: string
|
||||
content: string,
|
||||
ignoreEdit: boolean
|
||||
) {
|
||||
if (!content) return;
|
||||
|
||||
editorstore.get().saveSessionContent(noteId, sessionId, {
|
||||
editorstore.get().saveSessionContent(noteId, sessionId, ignoreEdit, {
|
||||
type: "tiptap",
|
||||
data: content
|
||||
});
|
||||
@@ -135,26 +136,7 @@ export default function EditorManager({
|
||||
if (result) item.data = result.data;
|
||||
else EV.publish(EVENTS.vaultLocked);
|
||||
}
|
||||
const oldHashes = editorInstance.current.getMediaHashes();
|
||||
|
||||
editorInstance.current.updateContent(item.data as string);
|
||||
|
||||
const newHashes = editorInstance.current.getMediaHashes();
|
||||
const hashesToLoad = newHashes.filter(
|
||||
(hash, index) => hash !== oldHashes[index]
|
||||
);
|
||||
|
||||
if (appstore.get().isSyncing()) {
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.syncCompleted,
|
||||
async () => {
|
||||
await db.attachments?.downloadMedia(id, hashesToLoad);
|
||||
},
|
||||
true
|
||||
);
|
||||
} else {
|
||||
await db.attachments?.downloadMedia(id, hashesToLoad);
|
||||
}
|
||||
} else if (isNote) {
|
||||
if (!locked && item.locked) return EV.publish(EVENTS.vaultLocked);
|
||||
|
||||
@@ -180,18 +162,6 @@ export default function EditorManager({
|
||||
setTimestamp(Date.now());
|
||||
}, []);
|
||||
|
||||
const loadMedia = useCallback(async () => {
|
||||
if (previewSession.current) {
|
||||
await db.content?.downloadMedia(
|
||||
noteId,
|
||||
previewSession.current.content,
|
||||
true
|
||||
);
|
||||
} else if (noteId && editorstore.get().session.content) {
|
||||
await db.attachments?.downloadMedia(noteId);
|
||||
}
|
||||
}, [noteId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewSession) return;
|
||||
|
||||
@@ -237,6 +207,7 @@ export default function EditorManager({
|
||||
/>
|
||||
)}
|
||||
<Editor
|
||||
id={noteId}
|
||||
nonce={timestamp}
|
||||
content={() =>
|
||||
previewSession.current?.content?.data ||
|
||||
@@ -247,7 +218,6 @@ export default function EditorManager({
|
||||
options={{
|
||||
readonly: isReadonly || isPreviewSession,
|
||||
onRequestFocus: () => toggleProperties(false),
|
||||
onLoadMedia: loadMedia,
|
||||
focusMode: isFocusMode,
|
||||
isMobile: isMobile || isTablet
|
||||
}}
|
||||
@@ -351,9 +321,9 @@ type EditorOptions = {
|
||||
readonly?: boolean;
|
||||
focusMode?: boolean;
|
||||
onRequestFocus?: () => void;
|
||||
onLoadMedia?: () => void;
|
||||
};
|
||||
type EditorProps = {
|
||||
id: string | number;
|
||||
content: () => string | undefined;
|
||||
nonce?: number;
|
||||
options?: EditorOptions;
|
||||
@@ -361,8 +331,9 @@ type EditorProps = {
|
||||
onPreviewDocument?: (preview: DocumentPreview) => void;
|
||||
};
|
||||
export function Editor(props: EditorProps) {
|
||||
const { content, nonce, options, onContentChange, onPreviewDocument } = props;
|
||||
const { readonly, headless, onLoadMedia, isMobile } = options || {
|
||||
const { id, content, nonce, options, onContentChange, onPreviewDocument } =
|
||||
props;
|
||||
const { readonly, headless, isMobile } = options || {
|
||||
headless: false,
|
||||
readonly: false,
|
||||
focusMode: false,
|
||||
@@ -375,40 +346,16 @@ export function Editor(props: EditorProps) {
|
||||
useEffect(() => {
|
||||
const event = AppEventManager.subscribe(
|
||||
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
|
||||
({ hash, loaded, total, type }: AttachmentProgress) => {
|
||||
({ hash, loaded, total }: AttachmentProgress) => {
|
||||
editor.current?.sendAttachmentProgress(
|
||||
hash,
|
||||
type,
|
||||
Math.round((loaded / total) * 100)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const mediaAttachmentDownloadedEvent = EV.subscribe(
|
||||
EVENTS.mediaAttachmentDownloaded,
|
||||
({
|
||||
groupId,
|
||||
hash,
|
||||
attachmentType,
|
||||
src
|
||||
}: {
|
||||
groupId?: string;
|
||||
attachmentType: "image" | "webclip" | "generic";
|
||||
hash: string;
|
||||
src: string;
|
||||
}) => {
|
||||
if (groupId?.startsWith("monograph")) return;
|
||||
if (attachmentType === "image") {
|
||||
editor.current?.loadImage(hash, src);
|
||||
} else if (attachmentType === "webclip") {
|
||||
editor.current?.loadWebClip(hash, src);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
event.unsubscribe();
|
||||
mediaAttachmentDownloadedEvent.unsubscribe();
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
@@ -424,7 +371,6 @@ export function Editor(props: EditorProps) {
|
||||
corsHost: Config.get("corsProxy", "https://cors.notesnook.com")
|
||||
}}
|
||||
onLoad={() => {
|
||||
if (onLoadMedia) onLoadMedia();
|
||||
if (nonce && nonce > 0) setIsLoading(false);
|
||||
}}
|
||||
onContentChange={onContentChange}
|
||||
@@ -437,8 +383,11 @@ export function Editor(props: EditorProps) {
|
||||
const container = document.getElementById("dialogContainer");
|
||||
if (!(container instanceof HTMLElement)) return;
|
||||
|
||||
const dataurl =
|
||||
data.bloburl || (await downloadAttachment(hash, "base64"));
|
||||
const dataurl = await downloadAttachment(
|
||||
hash,
|
||||
"base64",
|
||||
id.toString()
|
||||
);
|
||||
if (!dataurl)
|
||||
return showToast("error", "This image cannot be previewed.");
|
||||
|
||||
@@ -455,7 +404,7 @@ export function Editor(props: EditorProps) {
|
||||
);
|
||||
} else if (attachment && onPreviewDocument) {
|
||||
onPreviewDocument({ hash });
|
||||
const blob = await downloadAttachment(hash, "blob");
|
||||
const blob = await downloadAttachment(hash, "blob", id.toString());
|
||||
if (!blob) return;
|
||||
onPreviewDocument({ url: URL.createObjectURL(blob), hash });
|
||||
}
|
||||
@@ -467,6 +416,13 @@ export function Editor(props: EditorProps) {
|
||||
editor.current?.attachFile(file);
|
||||
});
|
||||
}}
|
||||
onGetAttachmentData={(attachment) => {
|
||||
return downloadAttachment(
|
||||
attachment.hash,
|
||||
attachment.type === "web-clip" ? "text" : "base64",
|
||||
id.toString()
|
||||
);
|
||||
}}
|
||||
onAttachFile={async (file) => {
|
||||
const result = await attachFile(file);
|
||||
if (!result) return;
|
||||
@@ -580,7 +536,7 @@ function PreviewModeNotice(props: PreviewModeNoticeProps) {
|
||||
async (cancelled: boolean) => {
|
||||
const { id, sessionId } = editorstore.get().session;
|
||||
if (!cancelled) {
|
||||
await editorstore.saveSessionContent(id, sessionId, content);
|
||||
await editorstore.saveSessionContent(id, sessionId, false, content);
|
||||
}
|
||||
onDiscard();
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ import { TaskManager } from "../../common/task-manager";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { showFilePicker } from "../../utils/file-picker";
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
|
||||
const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
|
||||
const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
|
||||
@@ -80,33 +81,54 @@ export async function reuploadAttachment(
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {File} selectedFile
|
||||
* @param {File} file
|
||||
* @returns
|
||||
*/
|
||||
async function pickFile(selectedFile: File, options?: AddAttachmentOptions) {
|
||||
async function pickFile(
|
||||
file: File,
|
||||
options?: AddAttachmentOptions
|
||||
): Promise<Attachment | undefined> {
|
||||
try {
|
||||
if (selectedFile.size > FILE_SIZE_LIMIT)
|
||||
if (file.size > FILE_SIZE_LIMIT)
|
||||
throw new Error("File too big. You cannot add files over 500 MB.");
|
||||
if (!selectedFile) return;
|
||||
if (!file) return;
|
||||
|
||||
return await addAttachment(selectedFile, undefined, options);
|
||||
const hash = await addAttachment(file, options);
|
||||
return {
|
||||
type: "file",
|
||||
filename: file.name,
|
||||
hash,
|
||||
mime: file.type,
|
||||
size: file.size
|
||||
};
|
||||
} catch (e) {
|
||||
showToast("error", `${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {File} selectedImage
|
||||
* @param {File} file
|
||||
* @returns
|
||||
*/
|
||||
async function pickImage(selectedImage: File, options?: AddAttachmentOptions) {
|
||||
async function pickImage(
|
||||
file: File,
|
||||
options?: AddAttachmentOptions
|
||||
): Promise<Attachment | undefined> {
|
||||
try {
|
||||
if (selectedImage.size > IMAGE_SIZE_LIMIT)
|
||||
if (file.size > IMAGE_SIZE_LIMIT)
|
||||
throw new Error("Image too big. You cannot add images over 50 MB.");
|
||||
if (!selectedImage) return;
|
||||
if (!file) return;
|
||||
|
||||
const dataurl = await toDataURL(selectedImage);
|
||||
return await addAttachment(selectedImage, dataurl, options);
|
||||
const hash = await addAttachment(file, options);
|
||||
const dimensions = await getImageDimensions(file);
|
||||
return {
|
||||
type: "image",
|
||||
filename: file.name,
|
||||
hash,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
...dimensions
|
||||
};
|
||||
} catch (e) {
|
||||
showToast("error", (e as Error).message);
|
||||
}
|
||||
@@ -118,12 +140,6 @@ async function getEncryptionKey(): Promise<SerializedKey> {
|
||||
return key;
|
||||
}
|
||||
|
||||
async function toDataURL(file: File): Promise<string> {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const base64 = Buffer.from(buffer).toString("base64");
|
||||
return `data:${file.type};base64,${base64}`;
|
||||
}
|
||||
|
||||
export type AttachmentProgress = {
|
||||
hash: string;
|
||||
type: "encrypt" | "download" | "upload";
|
||||
@@ -131,15 +147,6 @@ export type AttachmentProgress = {
|
||||
loaded: number;
|
||||
};
|
||||
|
||||
export type Attachment = {
|
||||
hash: string;
|
||||
filename: string;
|
||||
mime: string;
|
||||
size: number;
|
||||
dataurl?: string;
|
||||
bloburl?: string;
|
||||
};
|
||||
|
||||
type AddAttachmentOptions = {
|
||||
expectedFileHash?: string;
|
||||
showProgress?: boolean;
|
||||
@@ -148,9 +155,8 @@ type AddAttachmentOptions = {
|
||||
|
||||
async function addAttachment(
|
||||
file: File,
|
||||
dataurl: string | undefined,
|
||||
options: AddAttachmentOptions = {}
|
||||
): Promise<Attachment> {
|
||||
): Promise<string> {
|
||||
const { default: FS } = await import("../../interfaces/fs");
|
||||
const { expectedFileHash, showProgress = true } = options;
|
||||
let forceWrite = options.forceWrite;
|
||||
@@ -165,7 +171,7 @@ async function addAttachment(
|
||||
`Please select the same file for reuploading. Expected hash ${expectedFileHash} but got ${hash}.`
|
||||
);
|
||||
|
||||
const exists = db.attachments?.exists(hash);
|
||||
const exists = db.attachments?.attachment(hash);
|
||||
if (!forceWrite && exists) {
|
||||
forceWrite = (await FS.getUploadedFileSize(hash)) <= 0;
|
||||
}
|
||||
@@ -181,19 +187,13 @@ async function addAttachment(
|
||||
...output,
|
||||
hash,
|
||||
hashType,
|
||||
filename: file.name,
|
||||
type: file.type,
|
||||
filename: exists?.metadata.filename || file.name,
|
||||
type: exists?.metadata.type || file.type,
|
||||
key
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hash: hash,
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
dataurl
|
||||
};
|
||||
return hash;
|
||||
};
|
||||
|
||||
const result = showProgress
|
||||
@@ -204,7 +204,10 @@ async function addAttachment(
|
||||
return result;
|
||||
}
|
||||
|
||||
function withProgress<T>(file: File, action: () => Promise<T>): Promise<T> {
|
||||
function withProgress<T>(
|
||||
file: File,
|
||||
action: () => Promise<T>
|
||||
): Promise<T | Error> {
|
||||
return TaskManager.startTask({
|
||||
type: "modal",
|
||||
title: "Encrypting attachment",
|
||||
@@ -226,3 +229,19 @@ function withProgress<T>(file: File, action: () => Promise<T>): Promise<T> {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getImageDimensions(file: File) {
|
||||
return new Promise<{ width: number; height: number } | undefined>(
|
||||
(resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const { naturalWidth: width, naturalHeight: height } = img;
|
||||
resolve({ width, height });
|
||||
};
|
||||
img.onerror = () => {
|
||||
resolve(undefined);
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@ import {
|
||||
getTotalWords,
|
||||
countWords,
|
||||
getFontById,
|
||||
TiptapOptions
|
||||
TiptapOptions,
|
||||
Attachment
|
||||
} from "@notesnook/editor";
|
||||
import { Box, Flex } from "@theme-ui/components";
|
||||
import { PropsWithChildren, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Attachment } from "./picker";
|
||||
import { IEditor } from "./types";
|
||||
import {
|
||||
useConfigureEditor,
|
||||
@@ -57,12 +57,12 @@ import { store as editorstore } from "../../stores/editor-store";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
|
||||
|
||||
type OnChangeHandler = (
|
||||
id: string | undefined,
|
||||
sessionId: number,
|
||||
content: string
|
||||
content: string,
|
||||
ignoreEdit: boolean
|
||||
) => void;
|
||||
type TipTapProps = {
|
||||
editorContainer: HTMLElement;
|
||||
@@ -72,6 +72,7 @@ type TipTapProps = {
|
||||
onInsertAttachment?: (type: AttachmentType) => void;
|
||||
onDownloadAttachment?: (attachment: Attachment) => void;
|
||||
onPreviewAttachment?: (attachment: Attachment) => void;
|
||||
onGetAttachmentData?: (attachment: Attachment) => Promise<string | undefined>;
|
||||
onAttachFile?: (file: File) => void;
|
||||
onFocus?: () => void;
|
||||
content?: () => string | undefined;
|
||||
@@ -92,6 +93,7 @@ function save(
|
||||
editor: Editor,
|
||||
content: Fragment,
|
||||
preventSave: boolean,
|
||||
ignoreEdit: boolean,
|
||||
onChange?: OnChangeHandler
|
||||
) {
|
||||
configureEditor({
|
||||
@@ -105,7 +107,7 @@ function save(
|
||||
|
||||
if (preventSave) return;
|
||||
const html = getHTMLFromFragment(content, editor.schema);
|
||||
onChange?.(noteId, sessionId, html);
|
||||
onChange?.(noteId, sessionId, html, ignoreEdit);
|
||||
}
|
||||
|
||||
const deferredSave = debounceWithId(save, SAVE_INTERVAL);
|
||||
@@ -117,6 +119,7 @@ function TipTap(props: TipTapProps) {
|
||||
onInsertAttachment,
|
||||
onDownloadAttachment,
|
||||
onPreviewAttachment,
|
||||
onGetAttachmentData,
|
||||
onAttachFile,
|
||||
onContentChange,
|
||||
onFocus = () => {},
|
||||
@@ -210,7 +213,8 @@ function TipTap(props: TipTapProps) {
|
||||
onUpdate: ({ editor, transaction }) => {
|
||||
onContentChange?.();
|
||||
|
||||
const preventSave = transaction?.getMeta("preventSave") as boolean;
|
||||
const preventSave = transaction.getMeta("preventSave") as boolean;
|
||||
const ignoreEdit = transaction.getMeta("ignoreEdit") as boolean;
|
||||
const { id, sessionId } = editorstore.get().session;
|
||||
const content = editor.state.doc.content;
|
||||
deferredSave(
|
||||
@@ -220,6 +224,7 @@ function TipTap(props: TipTapProps) {
|
||||
editor as Editor,
|
||||
content,
|
||||
preventSave || !editor.isEditable,
|
||||
ignoreEdit,
|
||||
onChange
|
||||
);
|
||||
},
|
||||
@@ -282,7 +287,8 @@ function TipTap(props: TipTapProps) {
|
||||
onOpenLink: (url) => {
|
||||
window.open(url, "_blank");
|
||||
return true;
|
||||
}
|
||||
},
|
||||
getAttachmentData: onGetAttachmentData
|
||||
};
|
||||
}, [readonly, nonce, doubleSpacedLines, dateFormat, timeFormat]);
|
||||
|
||||
@@ -420,14 +426,6 @@ function toIEditor(editor: Editor): IEditor {
|
||||
}),
|
||||
undo: () => editor.current?.commands.undo(),
|
||||
redo: () => editor.current?.commands.redo(),
|
||||
getMediaHashes: () => {
|
||||
if (!editor.current) return [];
|
||||
const hashes: string[] = [];
|
||||
editor.current.state.doc.descendants((n) => {
|
||||
if (typeof n.attrs.hash === "string") hashes.push(n.attrs.hash);
|
||||
});
|
||||
return hashes;
|
||||
},
|
||||
updateContent: (content) => {
|
||||
const { from, to } = editor.state.selection;
|
||||
editor.current
|
||||
@@ -443,27 +441,17 @@ function toIEditor(editor: Editor): IEditor {
|
||||
})
|
||||
.run();
|
||||
},
|
||||
attachFile: (file: Attachment) => {
|
||||
if (file.dataurl) {
|
||||
editor.current?.commands.insertImage({
|
||||
...file,
|
||||
bloburl: toBlobURL(file.dataurl, file.hash)
|
||||
});
|
||||
} else editor.current?.commands.insertAttachment(file);
|
||||
},
|
||||
loadWebClip: (hash, src) =>
|
||||
editor.current?.commands.updateWebClip({ hash }, { src }),
|
||||
loadImage: (hash, dataurl) =>
|
||||
editor.current?.commands.updateImage(
|
||||
{ hash },
|
||||
{ hash, bloburl: toBlobURL(dataurl, hash), preventUpdate: true }
|
||||
),
|
||||
sendAttachmentProgress: (hash, type, progress) =>
|
||||
editor.current?.commands.setAttachmentProgress({
|
||||
hash,
|
||||
type,
|
||||
progress
|
||||
})
|
||||
attachFile: (file: Attachment) =>
|
||||
file.type === "image"
|
||||
? editor.current?.commands.insertImage(file)
|
||||
: editor.current?.commands.insertAttachment(file),
|
||||
sendAttachmentProgress: (hash, progress) =>
|
||||
editor.current?.commands.updateAttachment(
|
||||
{
|
||||
progress
|
||||
},
|
||||
{ query: (a) => a.hash === hash, preventUpdate: true }
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import {
|
||||
Published,
|
||||
@@ -63,6 +63,7 @@ function Toolbar() {
|
||||
const theme = useThemeStore((store) => store.colorScheme);
|
||||
const toggleNightMode = useThemeStore((store) => store.toggleColorScheme);
|
||||
const [isTitleVisible, setIsTitleVisible] = useState(false);
|
||||
const onScrollTitleRef = useRef(null);
|
||||
|
||||
const monographs = useMonographStore((store) => store.monographs);
|
||||
const { canRedo, canUndo, redo, undo } = useHistory();
|
||||
@@ -92,6 +93,10 @@ function Toolbar() {
|
||||
};
|
||||
}, [isTitleVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onScrollTitleRef.current) onScrollTitleRef.current.value = title;
|
||||
}, [title]);
|
||||
|
||||
const tools = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -216,6 +221,7 @@ function Toolbar() {
|
||||
}}
|
||||
/>
|
||||
<AnimatedInput
|
||||
ref={onScrollTitleRef}
|
||||
variant="clean"
|
||||
ml={[2, 2, 0]}
|
||||
initial={{
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 { Attachment } from "./picker";
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
|
||||
export type NoteStatistics = {
|
||||
words: {
|
||||
@@ -33,14 +33,7 @@ export interface IEditor {
|
||||
}) => void;
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
getMediaHashes: () => string[];
|
||||
updateContent: (content: string) => void;
|
||||
attachFile: (file: Attachment) => void;
|
||||
loadWebClip: (hash: string, html: string) => void;
|
||||
loadImage: (hash: string, src: string) => void;
|
||||
sendAttachmentProgress: (
|
||||
hash: string,
|
||||
type: "download" | "upload" | "encrypt",
|
||||
progress: number
|
||||
) => void;
|
||||
sendAttachmentProgress: (hash: string, progress: number) => void;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ import { hashNavigate, navigate } from "../../navigation";
|
||||
import { showPublishView } from "../publish-view";
|
||||
import IconTag from "../icon-tag";
|
||||
import { COLORS } from "../../common/constants";
|
||||
import { exportNotes } from "../../common/export";
|
||||
import { exportNote, exportNotes, exportToPDF } from "../../common/export";
|
||||
import { Multiselect } from "../../common/multi-select";
|
||||
import { store as selectionStore } from "../../stores/selection-store";
|
||||
import {
|
||||
@@ -86,7 +86,7 @@ import {
|
||||
ReferencesWithDateEdited
|
||||
} from "../list-container/types";
|
||||
import { SchemeColors } from "@notesnook/theme";
|
||||
import Vault from "../../common/vault";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
type NoteProps = {
|
||||
tags: Item[];
|
||||
@@ -369,10 +369,9 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
|
||||
onClick: async () => {
|
||||
const { unlock, lock } = store.get();
|
||||
if (!note.locked) {
|
||||
await lock(note.id);
|
||||
showToast("success", "Note locked successfully!");
|
||||
} else {
|
||||
await unlock(note.id);
|
||||
if (await lock(note.id))
|
||||
showToast("success", "Note locked successfully!");
|
||||
} else if (await unlock(note.id)) {
|
||||
showToast("success", "Note unlocked successfully!");
|
||||
}
|
||||
},
|
||||
@@ -422,7 +421,12 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
|
||||
isDisabled: !isSynced,
|
||||
icon: Print.path,
|
||||
onClick: async () => {
|
||||
await exportNotes("pdf", [note.id]);
|
||||
const item = db.notes?.note(note);
|
||||
if (!item) return;
|
||||
|
||||
const result = await exportNote(item, "pdf");
|
||||
if (!result) return;
|
||||
await exportToPDF(note.title, result.content);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -456,7 +460,24 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
|
||||
// ? "Multiple notes cannot be exported as PDF."
|
||||
// : false,
|
||||
isPro: format.type !== "txt",
|
||||
onClick: () => exportNotes(format.type, ids)
|
||||
onClick: async () => {
|
||||
if (ids.length === 1) {
|
||||
const item = db.notes?.note(note);
|
||||
if (!item) return;
|
||||
|
||||
const result = await exportNote(item, format.type);
|
||||
if (!result) return;
|
||||
if (format.type === "pdf")
|
||||
return exportToPDF(note.title, result.content);
|
||||
|
||||
return FileSaver.saveAs(
|
||||
new Blob([new TextEncoder().encode(result.content)]),
|
||||
result.filename
|
||||
);
|
||||
}
|
||||
|
||||
await exportNotes(format.type, ids);
|
||||
}
|
||||
}))
|
||||
},
|
||||
multiSelect: true,
|
||||
@@ -706,17 +727,11 @@ async function copyNote(noteId: string, format: "md" | "txt") {
|
||||
try {
|
||||
const note = db.notes?.note(noteId);
|
||||
if (!note) throw new Error("No note with this id exists.");
|
||||
if (note?.data.locked && !(await Vault.unlockVault()))
|
||||
throw new Error("Please unlock this note to copy it.");
|
||||
|
||||
const rawContent = await db.content?.raw(note.data.contentId);
|
||||
const content = note?.data.locked
|
||||
? await db.vault?.decryptContent(rawContent)
|
||||
: rawContent;
|
||||
const result = await exportNote(note, format, true);
|
||||
if (!result) return;
|
||||
|
||||
const text = await note.export(format, content, false);
|
||||
if (!text) throw new Error(`Could not convert note to ${format}.`);
|
||||
await navigator.clipboard.writeText(text);
|
||||
await navigator.clipboard.writeText(result.content);
|
||||
showToast("success", "Copied!");
|
||||
} catch (e) {
|
||||
if (e instanceof Error)
|
||||
|
||||
@@ -125,7 +125,7 @@ function PublishView(props) {
|
||||
variant="body"
|
||||
as="a"
|
||||
target="_blank"
|
||||
href={`https://monograph.notesnook.com/${publishId}`}
|
||||
href={`https://monogr.ph/${publishId}`}
|
||||
sx={{
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
@@ -134,13 +134,13 @@ function PublishView(props) {
|
||||
mr: 2
|
||||
}}
|
||||
>
|
||||
{`https://monograph.notesnook.com/${publishId}`}
|
||||
{`https://monogr.ph/${publishId}`}
|
||||
</Text>
|
||||
<Button
|
||||
variant="anchor"
|
||||
className="copyPublishLink"
|
||||
onClick={() => {
|
||||
writeText(`https://monograph.notesnook.com/${publishId}`);
|
||||
writeText(`https://monogr.ph/${publishId}`);
|
||||
}}
|
||||
>
|
||||
<Copy size={20} color="accent" />
|
||||
|
||||
@@ -89,7 +89,23 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
)
|
||||
}
|
||||
]
|
||||
: [],
|
||||
: [
|
||||
{
|
||||
title: "Daily trash cleanup interval",
|
||||
subtitle:
|
||||
"You can now set the trash cleanup interval to Daily in addition to Weekly, Monthly & Yearly."
|
||||
},
|
||||
{
|
||||
title: "Organized bulk exports",
|
||||
subtitle:
|
||||
"Bulk exports are now automatically sorted/organized into folders based on your Notebook/Topic organization."
|
||||
},
|
||||
{
|
||||
title: "New domain for Monographs",
|
||||
subtitle:
|
||||
"Monographs will now be published to monogr.ph instead of monograph.notesnook.com. Don't worry, all your published notes will automatically redirect."
|
||||
}
|
||||
],
|
||||
cta: {
|
||||
title: "Got it",
|
||||
icon: Checkmark,
|
||||
|
||||
@@ -125,6 +125,7 @@ export const BehaviourSettings: SettingsGroup[] = [
|
||||
selectedOption: () =>
|
||||
useSettingStore.getState().trashCleanupInterval.toString(),
|
||||
options: [
|
||||
{ value: "1", title: "Daily" },
|
||||
{ value: "7", title: "Weekly" },
|
||||
{ value: "30", title: "Monthly" },
|
||||
{ value: "365", title: "Yearly" },
|
||||
|
||||
@@ -306,8 +306,17 @@ class EditorStore extends BaseStore {
|
||||
return this.saveSession(noteId, { [name]: value });
|
||||
};
|
||||
|
||||
saveSessionContent = (noteId, sessionId, content) => {
|
||||
return this.saveSession(noteId, { sessionId, content });
|
||||
/**
|
||||
*
|
||||
* @param {*} noteId
|
||||
* @param {*} sessionId
|
||||
* @param {boolean} ignoreEdit if this is set to true, we save the content but do not update the dateEdited. Useful for metadata only changes in content.
|
||||
* @param {*} content
|
||||
* @returns
|
||||
*/
|
||||
saveSessionContent = (noteId, sessionId, ignoreEdit, content) => {
|
||||
const dateEdited = ignoreEdit ? this.get().session.dateEdited : undefined;
|
||||
return this.saveSession(noteId, { sessionId, content, dateEdited });
|
||||
};
|
||||
|
||||
setTag = (tag) => {
|
||||
|
||||
@@ -123,10 +123,11 @@ class NoteStore extends BaseStore {
|
||||
};
|
||||
|
||||
lock = async (id) => {
|
||||
await Vault.lockNote(id);
|
||||
if (!(await Vault.lockNote(id))) return false;
|
||||
this.refreshItem(id);
|
||||
if (editorStore.get().session.id === id)
|
||||
await editorStore.openSession(id, true);
|
||||
return true;
|
||||
};
|
||||
|
||||
readonly = async (id) => {
|
||||
|
||||
@@ -17,97 +17,78 @@ 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 { sanitizeFilename } from "@notesnook/common";
|
||||
import Note from "@notesnook/core/dist/models/note";
|
||||
import { db } from "../../common/db";
|
||||
import { exportToPDF } from "../../common/export";
|
||||
import Vault from "../../common/vault";
|
||||
import { showToast } from "../toast";
|
||||
import { exportNote } from "../../common/export";
|
||||
import { makeUniqueFilename } from "./utils";
|
||||
import { ZipFile } from "./zip-stream";
|
||||
|
||||
const FORMAT_TO_EXT = {
|
||||
pdf: "pdf",
|
||||
md: "md",
|
||||
txt: "txt",
|
||||
html: "html",
|
||||
"md-frontmatter": "md"
|
||||
} as const;
|
||||
|
||||
export class ExportStream extends ReadableStream<ZipFile> {
|
||||
export class ExportStream extends TransformStream<Note, ZipFile> {
|
||||
constructor(
|
||||
noteIds: string[],
|
||||
format: "pdf" | "md" | "txt" | "html" | "md-frontmatter",
|
||||
signal?: AbortSignal,
|
||||
onProgress?: (current: number, text: string) => void
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const textEncoder = new TextEncoder();
|
||||
let index = 0;
|
||||
const counters: Record<string, number> = {};
|
||||
let vaultUnlocked = false;
|
||||
|
||||
super({
|
||||
async start() {
|
||||
if (noteIds.length === 1 && db.notes?.note(noteIds[0])?.data.locked) {
|
||||
vaultUnlocked = await Vault.unlockVault();
|
||||
if (!vaultUnlocked) return false;
|
||||
} else if (noteIds.length > 1 && (await db.vault?.exists())) {
|
||||
vaultUnlocked = await Vault.unlockVault();
|
||||
if (!vaultUnlocked)
|
||||
showToast(
|
||||
"error",
|
||||
"Failed to unlock vault. Locked notes will be skipped."
|
||||
);
|
||||
}
|
||||
},
|
||||
async pull(controller) {
|
||||
if (signal?.aborted) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
async transform(note, controller) {
|
||||
try {
|
||||
if (signal?.aborted) {
|
||||
controller.terminate();
|
||||
return;
|
||||
}
|
||||
|
||||
const note = db.notes?.note(noteIds[index++]);
|
||||
if (!note) return;
|
||||
if (!vaultUnlocked && note.data.locked) return;
|
||||
if (!note || format === "pdf") return;
|
||||
|
||||
onProgress && onProgress(index, `Exporting "${note.title}"...`);
|
||||
const result = await exportNote(note, format);
|
||||
if (!result) return;
|
||||
|
||||
const rawContent = await db.content?.raw(note.data.contentId);
|
||||
const content = note.data.locked
|
||||
? await db.vault?.decryptContent(rawContent)
|
||||
: rawContent;
|
||||
const { filename, content } = result;
|
||||
|
||||
const exported = await note
|
||||
.export(format === "pdf" ? "html" : format, content)
|
||||
.catch((e: Error) => {
|
||||
console.error(note.data, e);
|
||||
showToast(
|
||||
"error",
|
||||
`Failed to export note "${note.title}": ${e.message}`
|
||||
);
|
||||
const notebooks = [
|
||||
...(
|
||||
db.relations?.to({ id: note.id, type: "note" }, "notebook") || []
|
||||
).map((n) => ({ title: n.title, topics: [] })),
|
||||
...(note.notebooks || []).map(
|
||||
(ref: { id: string; topics: string[] }) => {
|
||||
const notebook = db.notebooks?.notebook(ref.id);
|
||||
const topics: any[] = notebook?.topics.all || [];
|
||||
|
||||
return {
|
||||
title: notebook?.title,
|
||||
topics: ref.topics
|
||||
.map((topicId: string) =>
|
||||
topics.find((topic) => topic.id === topicId)
|
||||
)
|
||||
.filter(Boolean)
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
|
||||
const filePaths: Array<string> =
|
||||
notebooks.length > 0
|
||||
? notebooks
|
||||
.map((notebook) => {
|
||||
if (notebook.topics.length > 0)
|
||||
return notebook.topics.map((topic: { title: string }) =>
|
||||
[notebook.title, topic.title, filename].join("/")
|
||||
);
|
||||
return [notebook.title, filename].join("/");
|
||||
})
|
||||
.flat()
|
||||
: [filename];
|
||||
|
||||
filePaths.forEach((filePath) => {
|
||||
controller.enqueue({
|
||||
path: makeUniqueFilename(filePath, counters),
|
||||
data: content,
|
||||
mtime: new Date(note.data.dateEdited),
|
||||
ctime: new Date(note.data.dateCreated)
|
||||
});
|
||||
});
|
||||
|
||||
if (typeof exported !== "string") {
|
||||
showToast("error", `Failed to export note "${note.title}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === "pdf") {
|
||||
await exportToPDF(note.title, exported);
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = sanitizeFilename(note.title, { replacement: "-" });
|
||||
const ext = FORMAT_TO_EXT[format];
|
||||
controller.enqueue({
|
||||
path: makeUniqueFilename([filename, ext].join("."), counters),
|
||||
data: textEncoder.encode(exported),
|
||||
mtime: new Date(note.data.dateEdited),
|
||||
ctime: new Date(note.data.dateCreated)
|
||||
});
|
||||
|
||||
if (index === noteIds.length) {
|
||||
controller.close();
|
||||
} catch (e) {
|
||||
controller.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,16 +17,19 @@ 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 { path } from "@notesnook-importer/core/dist/src/utils/path";
|
||||
|
||||
export function makeUniqueFilename(
|
||||
filePath: string,
|
||||
counters: Record<string, number>
|
||||
) {
|
||||
filePath = filePath.toLowerCase();
|
||||
counters[filePath] = (counters[filePath] || 0) + 1;
|
||||
if (counters[filePath] === 1) return filePath;
|
||||
const matchablePath = filePath.toLowerCase();
|
||||
const count = (counters[matchablePath] = (counters[matchablePath] || 0) + 1);
|
||||
if (count === 1) return filePath;
|
||||
|
||||
const parts = filePath.split(".");
|
||||
return `${parts.slice(0, -1).join(".")}-${counters[filePath]}.${
|
||||
parts[parts.length - 1]
|
||||
}`;
|
||||
const ext = path.extname(filePath);
|
||||
const basename = ext
|
||||
? `${path.basename(filePath, ext)}-${count}${ext}`
|
||||
: `${path.basename(filePath)}-${count}`;
|
||||
return path.join(path.dirname(filePath), basename);
|
||||
}
|
||||
|
||||
@@ -18,13 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Deflate, Inflate } from "./fflate-shim";
|
||||
import { Uint8ArrayReader, ZipWriter, configure } from "@zip.js/zip.js";
|
||||
import {
|
||||
Uint8ArrayReader,
|
||||
TextReader,
|
||||
ZipWriter,
|
||||
configure
|
||||
} from "@zip.js/zip.js";
|
||||
|
||||
configure({ Deflate, Inflate });
|
||||
|
||||
export type ZipFile = {
|
||||
path: string;
|
||||
data: Uint8Array;
|
||||
data: string | Uint8Array;
|
||||
mtime?: Date;
|
||||
ctime?: Date;
|
||||
};
|
||||
@@ -43,10 +48,16 @@ export function createZipStream(signal?: AbortSignal) {
|
||||
if (written.has(chunk.path)) return;
|
||||
|
||||
await writer
|
||||
.add(chunk.path, new Uint8ArrayReader(chunk.data), {
|
||||
creationDate: chunk.ctime,
|
||||
lastModDate: chunk.mtime
|
||||
})
|
||||
.add(
|
||||
chunk.path,
|
||||
typeof chunk.data === "string"
|
||||
? new TextReader(chunk.data)
|
||||
: new Uint8ArrayReader(chunk.data),
|
||||
{
|
||||
creationDate: chunk.ctime,
|
||||
lastModDate: chunk.mtime
|
||||
}
|
||||
)
|
||||
.catch(async (e) => {
|
||||
await ts.writable.abort(e);
|
||||
await ts.readable.cancel(e);
|
||||
|
||||
BIN
docs/help/contents/_include/static/skiff-importer/1.png
Normal file
BIN
docs/help/contents/_include/static/skiff-importer/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/help/contents/_include/static/skiff-importer/2.png
Normal file
BIN
docs/help/contents/_include/static/skiff-importer/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/help/contents/_include/static/skiff-importer/3.png
Normal file
BIN
docs/help/contents/_include/static/skiff-importer/3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
docs/help/contents/_include/static/skiff-importer/4.png
Normal file
BIN
docs/help/contents/_include/static/skiff-importer/4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Skiff Pages
|
||||
---
|
||||
|
||||
# How do I import notes from Skiff Pages?
|
||||
|
||||
The following steps will help you quickly import your notes from Skiff Pages into Notesnook.
|
||||
|
||||
## Exporting your Skiff Pages
|
||||
|
||||
1. Open the [Skiff Pages](https://app.skiff.com) app
|
||||
2. Open Settings > Export or just go directly to [https://app.skiff.com/dashboard/?settingTab=export](https://app.skiff.com/dashboard/?settingTab=export)
|
||||

|
||||
3. Click on the Export button next to `Pages and Files` — this might take a few minutes depending on how many pages you have.
|
||||
4. Once the export is complete, save the `Skiff.zip` file at your preferred location.
|
||||

|
||||
|
||||
## Importing Skiff.zip file into Notesnook
|
||||
|
||||
Once you have the `Skiff.zip` file containing your Skiff pages, its time to import them into Notesnook.
|
||||
|
||||
1. Go to [https://importer.notesnook.com/](https://importer.notesnook.com/) on your PC/Laptop.
|
||||
2. From the list of note apps to import from, select "Markdown".
|
||||

|
||||
3. Drop your Skiff.zip file, or click anywhere inside the box to browse and select your Skiff.zip file. Then click "Start processing".
|
||||

|
||||
4. Once the Importer finishes processing your files, download the .zip file.
|
||||

|
||||
5. After you have downloaded the `.zip` file, [go to the Notesnook Web App](https://app.notesnook.com/) > Settings > Notesnook Importer. Select the .zip you downloaded earlier and click "Start import" button.
|
||||

|
||||
6. Once importing completes you should see all your notes in Notesnook. If you face any issues during importing, feel free to [report them on GitHub](https://github.com/streetwriters/notesnook-importer).
|
||||
|
||||
## Supported formats
|
||||
|
||||
- [x] Images
|
||||
- [x] Code blocks
|
||||
- [ ] Math blocks (Skiff Pages doesn't mark them properly so there's no way to detect them.)
|
||||
- [x] Tables
|
||||
- [x] Rich text (bold, italic, headings, lists etc.)
|
||||
- [x] Task lists
|
||||
@@ -38,6 +38,7 @@ navigation:
|
||||
- path: importing-notes/import-notes-from-simplenote.md
|
||||
- path: importing-notes/import-notes-from-standardnotes.md
|
||||
- path: importing-notes/import-notes-from-zoho-notebook.md
|
||||
- path: importing-notes/import-notes-from-skiff-pages.md
|
||||
- path: importing-notes/import-notes-from-obsidian.md
|
||||
- path: importing-notes/import-notes-from-html-files.md
|
||||
- path: importing-notes/import-notes-from-markdown-files.md
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
- Bug fixes and performance improvements
|
||||
- Support for exporting locked notes
|
||||
- Maintain directory structure in exports based on Notebooks/Topics
|
||||
- Match color of navigation bar in sheets with app theme
|
||||
- Improved image loading and show progress when loading images
|
||||
- Support for setting trash interval to daily
|
||||
- Fix android app UI issues with RTL languages
|
||||
- Fix pasting links in editor
|
||||
- Other Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
10
fastlane/metadata/android/en-US/changelogs/10379.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/10379.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
- Support for exporting locked notes
|
||||
- Maintain directory structure in exports based on Notebooks/Topics
|
||||
- Match color of navigation bar in sheets with app theme
|
||||
- Improved image loading and show progress when loading images
|
||||
- Support for setting trash interval to daily
|
||||
- Fix android app UI issues with RTL languages
|
||||
- Fix pasting links in editor
|
||||
- Other Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
10
packages/common/package-lock.json
generated
10
packages/common/package-lock.json
generated
@@ -31,7 +31,8 @@
|
||||
"@microsoft/signalr": "^7.0.10",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.2-alpha",
|
||||
"@readme/data-urls": "^3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.4-alpha",
|
||||
"async-mutex": "^0.3.2",
|
||||
"dayjs": "1.11.9",
|
||||
"entities": "^4.3.1",
|
||||
@@ -50,7 +51,7 @@
|
||||
"@types/html-to-text": "^9.0.0",
|
||||
"@types/katex": "^0.16.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -147,11 +148,12 @@
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/crypto": "file:../crypto",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.2-alpha",
|
||||
"@readme/data-urls": "^3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.4-alpha",
|
||||
"@types/html-to-text": "^9.0.0",
|
||||
"@types/katex": "^0.16.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"async-mutex": "^0.3.2",
|
||||
|
||||
@@ -75,7 +75,7 @@ function sanitize(input: string, replacement: string) {
|
||||
.replace(windowsReservedRe, replacement)
|
||||
.replace(windowsTrailingRe, replacement);
|
||||
|
||||
return sanitized.slice(0, 254).toLowerCase();
|
||||
return sanitized.slice(0, 254);
|
||||
}
|
||||
|
||||
export function sanitizeFilename(
|
||||
|
||||
109
packages/core/package-lock.json
generated
109
packages/core/package-lock.json
generated
@@ -12,7 +12,8 @@
|
||||
"@microsoft/signalr": "^7.0.10",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.2-alpha",
|
||||
"@readme/data-urls": "^3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.4-alpha",
|
||||
"async-mutex": "^0.3.2",
|
||||
"dayjs": "1.11.9",
|
||||
"entities": "^4.3.1",
|
||||
@@ -31,7 +32,7 @@
|
||||
"@types/html-to-text": "^9.0.0",
|
||||
"@types/katex": "^0.16.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -576,6 +577,14 @@
|
||||
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@readme/data-urls": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@readme/data-urls/-/data-urls-3.0.0.tgz",
|
||||
"integrity": "sha512-b0L7VWqbLZqOSSAFUrxS5ZwUfec35WDsAwwCH481vYnhk0dWO3nvmNVNCbP8CY4cXqwL1W4uCAnhDz+CUmXM3g==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@selderee/plugin-htmlparser2": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||
@@ -589,9 +598,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@streetwriters/showdown": {
|
||||
"version": "3.0.2-alpha",
|
||||
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.2-alpha.tgz",
|
||||
"integrity": "sha512-j6IuhMgCBKnOPruierM9Lbrd+Zgj9bc80K6jYXaKcqvxd9hFnNNvbzm24IMxfuEGxv+Tqv+ukr9gxOBZU+5WuA==",
|
||||
"version": "3.0.4-alpha",
|
||||
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.4-alpha.tgz",
|
||||
"integrity": "sha512-R2UJzMXyJz312RxekXGQIQvLsdZ0eSa4Z7dRXzf/fL2XsPPtyEzkZce0RoZn/mmKfDym5XKnjcVxnF3hXgjVaw==",
|
||||
"bin": {
|
||||
"showdown": "bin/showdown.js"
|
||||
},
|
||||
@@ -663,10 +672,11 @@
|
||||
"integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/showdown": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.0.tgz",
|
||||
"integrity": "sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA==",
|
||||
"node_modules/@types/streetwriters__showdown": {
|
||||
"name": "@types/showdown",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.6.tgz",
|
||||
"integrity": "sha512-pTvD/0CIeqe4x23+YJWlX2gArHa8G0J0Oh6GKaVXV7TAeickpkkZiNOgFcFcmLQ5lB/K0qBJL1FtRYltBfbGCQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
@@ -1176,20 +1186,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
|
||||
"integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"abab": "^2.0.6",
|
||||
"whatwg-mimetype": "^3.0.0",
|
||||
"whatwg-url": "^12.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.9",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
|
||||
@@ -1788,6 +1784,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/data-urls": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
|
||||
"integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"abab": "^2.0.6",
|
||||
"whatwg-mimetype": "^3.0.0",
|
||||
"whatwg-url": "^12.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
@@ -2269,9 +2279,9 @@
|
||||
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -3253,6 +3263,11 @@
|
||||
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||
}
|
||||
},
|
||||
"@readme/data-urls": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@readme/data-urls/-/data-urls-3.0.0.tgz",
|
||||
"integrity": "sha512-b0L7VWqbLZqOSSAFUrxS5ZwUfec35WDsAwwCH481vYnhk0dWO3nvmNVNCbP8CY4cXqwL1W4uCAnhDz+CUmXM3g=="
|
||||
},
|
||||
"@selderee/plugin-htmlparser2": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||
@@ -3263,9 +3278,9 @@
|
||||
}
|
||||
},
|
||||
"@streetwriters/showdown": {
|
||||
"version": "3.0.2-alpha",
|
||||
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.2-alpha.tgz",
|
||||
"integrity": "sha512-j6IuhMgCBKnOPruierM9Lbrd+Zgj9bc80K6jYXaKcqvxd9hFnNNvbzm24IMxfuEGxv+Tqv+ukr9gxOBZU+5WuA=="
|
||||
"version": "3.0.4-alpha",
|
||||
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.4-alpha.tgz",
|
||||
"integrity": "sha512-R2UJzMXyJz312RxekXGQIQvLsdZ0eSa4Z7dRXzf/fL2XsPPtyEzkZce0RoZn/mmKfDym5XKnjcVxnF3hXgjVaw=="
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
@@ -3327,10 +3342,10 @@
|
||||
"integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/showdown": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.0.tgz",
|
||||
"integrity": "sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA==",
|
||||
"@types/streetwriters__showdown": {
|
||||
"version": "npm:@types/showdown@2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.6.tgz",
|
||||
"integrity": "sha512-pTvD/0CIeqe4x23+YJWlX2gArHa8G0J0Oh6GKaVXV7TAeickpkkZiNOgFcFcmLQ5lB/K0qBJL1FtRYltBfbGCQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/unist": {
|
||||
@@ -3716,17 +3731,6 @@
|
||||
"rrweb-cssom": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"data-urls": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
|
||||
"integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"abab": "^2.0.6",
|
||||
"whatwg-mimetype": "^3.0.0",
|
||||
"whatwg-url": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.11.9",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
|
||||
@@ -4170,6 +4174,17 @@
|
||||
"xml-name-validator": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"data-urls": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
|
||||
"integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"abab": "^2.0.6",
|
||||
"whatwg-mimetype": "^3.0.0",
|
||||
"whatwg-url": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
@@ -4521,9 +4536,9 @@
|
||||
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA=="
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
|
||||
},
|
||||
"qclone": {
|
||||
"version": "1.2.0",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@types/html-to-text": "^9.0.0",
|
||||
"@types/katex": "^0.16.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -41,7 +41,8 @@
|
||||
"@microsoft/signalr": "^7.0.10",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.2-alpha",
|
||||
"@readme/data-urls": "^3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.4-alpha",
|
||||
"async-mutex": "^0.3.2",
|
||||
"dayjs": "1.11.9",
|
||||
"entities": "^4.3.1",
|
||||
|
||||
@@ -136,7 +136,7 @@ class Settings {
|
||||
}
|
||||
/**
|
||||
* Setting to -1 means never clear trash.
|
||||
* @param {7 | 30 | 365 | -1} time
|
||||
* @param {1 | 7 | 30 | 365 | -1} time
|
||||
*/
|
||||
async setTrashCleanupInterval(time) {
|
||||
this._settings.trashCleanupInterval = time;
|
||||
|
||||
@@ -61,6 +61,10 @@ export default class Vault {
|
||||
});
|
||||
}
|
||||
|
||||
get unlocked() {
|
||||
return !!this._vaultPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new vault
|
||||
* @param {string} password The password
|
||||
|
||||
@@ -83,6 +83,8 @@ export default class Content extends Collection {
|
||||
}
|
||||
|
||||
async raw(id) {
|
||||
if (!id) return;
|
||||
|
||||
const content = await this._collection.getItem(id);
|
||||
if (!content) return;
|
||||
return content;
|
||||
@@ -118,6 +120,7 @@ export default class Content extends Collection {
|
||||
async downloadMedia(groupId, contentItem, notify = true) {
|
||||
if (!contentItem) return contentItem;
|
||||
const content = getContentFromData(contentItem.type, contentItem.data);
|
||||
if (!content) console.log(contentItem);
|
||||
contentItem.data = await content.insertMedia(async (hashes) => {
|
||||
const attachments = hashes.map((h) => this._db.attachments.attachment(h));
|
||||
await this._db.fs.queueDownloads(
|
||||
|
||||
@@ -129,7 +129,8 @@ export default class Notes extends Collection {
|
||||
readonly: !!note.readonly,
|
||||
|
||||
dateCreated: note.dateCreated,
|
||||
dateEdited: note.dateEdited || note.dateCreated || Date.now(),
|
||||
dateEdited:
|
||||
noteArg.dateEdited || note.dateEdited || note.dateCreated || Date.now(),
|
||||
dateModified: note.dateModified
|
||||
};
|
||||
|
||||
|
||||
@@ -148,6 +148,12 @@ exports[`convert HTML to markdown with outlinelists > html-to-md-outlinelists.md
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert HTML to markdown with singleSpacedParagraphs > html-to-md-singleSpacedParagraphs.md 1`] = `
|
||||
"hello world
|
||||
hello world 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert HTML to markdown with tables > html-to-md-tables.md 1`] = `
|
||||
"| Goal | To introduce various features of the app to the user and to convert a user on trial or basic plan to upgrade. |
|
||||
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -394,6 +400,11 @@ exports[`convert HTML to text with outlinelists > html-to-txt-outlinelists.txt 1
|
||||
* Yes it doesn't!"
|
||||
`;
|
||||
|
||||
exports[`convert HTML to text with singleSpacedParagraphs > html-to-txt-singleSpacedParagraphs.txt 1`] = `
|
||||
"hello world
|
||||
hello world 2"
|
||||
`;
|
||||
|
||||
exports[`convert HTML to text with tables > html-to-txt-tables.txt 1`] = `
|
||||
"Goal To introduce various features of the app to the user and to
|
||||
convert a user on trial or basic plan to upgrade.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,6 +24,7 @@ import { toChunks } from "../utils/array.js";
|
||||
import { migrateItem } from "../migrations.js";
|
||||
import Indexer from "./indexer.js";
|
||||
import setManipulator from "../utils/set.js";
|
||||
import { logger } from "../logger.js";
|
||||
|
||||
const COLORS = [
|
||||
"red",
|
||||
@@ -87,6 +88,7 @@ export default class Backup {
|
||||
constructor(db) {
|
||||
this._db = db;
|
||||
this._migrator = new Migrator();
|
||||
this.logger = logger.scope("Backup");
|
||||
}
|
||||
|
||||
lastBackupTime() {
|
||||
@@ -267,18 +269,27 @@ export default class Backup {
|
||||
? "color"
|
||||
: item.itemType || item.type;
|
||||
|
||||
// items should sync immediately after getting restored
|
||||
item.dateModified = Date.now();
|
||||
item.synced = false;
|
||||
|
||||
if (itemType === "attachment" && item.metadata && item.metadata.hash) {
|
||||
const attachment = this._db.attachments.attachment(item.metadata.hash);
|
||||
if (attachment) {
|
||||
const isNewGeneric =
|
||||
item.metadata.type === "application/octet-stream";
|
||||
const isOldGeneric =
|
||||
attachment.metadata.type === "application/octet-stream";
|
||||
item = {
|
||||
...attachment,
|
||||
metadata: {
|
||||
...attachment.metadata,
|
||||
type: item.metadata.type || attachment.metadata.type
|
||||
type:
|
||||
// we keep whichever mime type is more specific
|
||||
isNewGeneric && !isOldGeneric
|
||||
? attachment.metadata.type
|
||||
: item.metadata.type,
|
||||
filename:
|
||||
// we keep the filename based on which item's mime type we kept
|
||||
isNewGeneric && !isOldGeneric
|
||||
? attachment.metadata.filename
|
||||
: item.metadata.filename
|
||||
},
|
||||
noteIds: setManipulator.union(attachment.noteIds, item.noteIds)
|
||||
};
|
||||
@@ -288,6 +299,10 @@ export default class Backup {
|
||||
}
|
||||
}
|
||||
|
||||
// items should sync immediately after getting restored
|
||||
item.dateModified = Date.now();
|
||||
item.synced = false;
|
||||
|
||||
const collectionKey = itemTypeToCollectionKey[itemType];
|
||||
if (collectionKey) {
|
||||
toAdd[collectionKey] = toAdd[collectionKey] || [];
|
||||
|
||||
@@ -133,23 +133,26 @@ export default class FileStorage {
|
||||
return result;
|
||||
}
|
||||
|
||||
async cancel(groupId, type) {
|
||||
const queue =
|
||||
type === "download"
|
||||
? this.downloads.get(groupId)
|
||||
: this.uploads.get(groupId);
|
||||
if (!queue) return;
|
||||
for (let i = 0; i < queue.length; ++i) {
|
||||
const file = queue[i];
|
||||
if (file.cancel) await file.cancel("Operation canceled.");
|
||||
queue.splice(i, 1);
|
||||
}
|
||||
if (type === "download") {
|
||||
this.downloads.delete(groupId);
|
||||
EV.publish(EVENTS.downloadCanceled, { groupId, canceled: true });
|
||||
} else if (type === "upload") {
|
||||
this.uploads.delete(groupId);
|
||||
EV.publish(EVENTS.uploadCanceled, { groupId, canceled: true });
|
||||
async cancel(groupId) {
|
||||
const queues = [
|
||||
{ type: "download", files: this.downloads.get(groupId) },
|
||||
{ type: "upload", files: this.uploads.get(groupId) }
|
||||
].filter((a) => !!a.files);
|
||||
|
||||
for (const queue of queues) {
|
||||
for (let i = 0; i < queue.files.length; ++i) {
|
||||
const file = queue.files[i];
|
||||
if (file.cancel) await file.cancel("Operation canceled.");
|
||||
queue.files.splice(i, 1);
|
||||
}
|
||||
|
||||
if (queue.type === "download") {
|
||||
this.downloads.delete(groupId);
|
||||
EV.publish(EVENTS.downloadCanceled, { groupId, canceled: true });
|
||||
} else if (queue.type === "upload") {
|
||||
this.uploads.delete(groupId);
|
||||
EV.publish(EVENTS.uploadCanceled, { groupId, canceled: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,19 +17,30 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const REGEX = /^data:(image\/.+);base64,(.+)/;
|
||||
import { parse, validate } from "@readme/data-urls";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} dataurl
|
||||
* @returns {{mime?: string; data?: string}}
|
||||
*/
|
||||
function toObject(dataurl) {
|
||||
const regexResult = REGEX.exec(dataurl);
|
||||
if (!regexResult || regexResult.length < 3) return {};
|
||||
const [, mime, data] = regexResult;
|
||||
return { mime, data };
|
||||
const result = parse(dataurl);
|
||||
if (!result) return {};
|
||||
return {
|
||||
mime: result.contentType,
|
||||
data: result.data
|
||||
};
|
||||
}
|
||||
|
||||
function fromObject({ type, data }) {
|
||||
if (REGEX.test(data)) return data;
|
||||
if (validate(data)) return data;
|
||||
return `data:${type};base64,${data}`;
|
||||
}
|
||||
|
||||
const dataurl = { toObject, fromObject };
|
||||
function isValid(url) {
|
||||
return validate(url);
|
||||
}
|
||||
|
||||
const dataurl = { toObject, fromObject, isValid };
|
||||
export default dataurl;
|
||||
|
||||
4
packages/editor-mobile/package-lock.json
generated
4
packages/editor-mobile/package-lock.json
generated
@@ -57,7 +57,7 @@
|
||||
"@tiptap/extension-font-family": "2.1.12",
|
||||
"@tiptap/extension-history": "2.1.12",
|
||||
"@tiptap/extension-horizontal-rule": "2.1.12",
|
||||
"@tiptap/extension-link": "2.1.12",
|
||||
"@tiptap/extension-link": "^2.1.16",
|
||||
"@tiptap/extension-list-keymap": "2.1.12",
|
||||
"@tiptap/extension-placeholder": "2.1.12",
|
||||
"@tiptap/extension-subscript": "2.1.12",
|
||||
@@ -21707,7 +21707,7 @@
|
||||
"@tiptap/extension-font-family": "2.1.12",
|
||||
"@tiptap/extension-history": "2.1.12",
|
||||
"@tiptap/extension-horizontal-rule": "2.1.12",
|
||||
"@tiptap/extension-link": "2.1.12",
|
||||
"@tiptap/extension-link": "^2.1.16",
|
||||
"@tiptap/extension-list-keymap": "2.1.12",
|
||||
"@tiptap/extension-placeholder": "2.1.12",
|
||||
"@tiptap/extension-subscript": "2.1.12",
|
||||
|
||||
@@ -44,7 +44,7 @@ import Tags from "./tags";
|
||||
import Title from "./title";
|
||||
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
|
||||
|
||||
globalThis.toBlobURL = toBlobURL;
|
||||
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
|
||||
|
||||
const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
const [tick, setTick] = useState(0);
|
||||
@@ -61,8 +61,11 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
});
|
||||
const _editor = useTiptap(
|
||||
{
|
||||
onUpdate: ({ editor }) => {
|
||||
global.editorController.contentChange(editor as Editor);
|
||||
onUpdate: ({ editor, transaction }) => {
|
||||
global.editorController.contentChange(
|
||||
editor as Editor,
|
||||
transaction.getMeta("ignoreEdit")
|
||||
);
|
||||
},
|
||||
onOpenAttachmentPicker: (editor, type) => {
|
||||
global.editorController.openFilePicker(type);
|
||||
@@ -76,6 +79,9 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
global.editorController.previewAttachment(attachment);
|
||||
return true;
|
||||
},
|
||||
getAttachmentData(attachment) {
|
||||
return global.editorController.getAttachmentData(attachment);
|
||||
},
|
||||
element: !layout ? undefined : contentRef.current || undefined,
|
||||
editable: !settings.readonly,
|
||||
editorProps: {
|
||||
@@ -226,7 +232,10 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
/>
|
||||
|
||||
<div
|
||||
onClick={onClickBottomArea}
|
||||
onTouchEnd={(e) => {
|
||||
e.preventDefault();
|
||||
onClickBottomArea();
|
||||
}}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
width: "100%",
|
||||
@@ -246,7 +255,7 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
}}
|
||||
editor={_editor}
|
||||
location="bottom"
|
||||
tools={settings.tools}
|
||||
tools={[...settings.tools]}
|
||||
defaultFontFamily={settings.fontFamily}
|
||||
defaultFontSize={settings.fontSize}
|
||||
/>
|
||||
|
||||
@@ -30,9 +30,8 @@ import {
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { EventTypes, isReactNative, post, saveTheme } from "../utils";
|
||||
import { EventTypes, isReactNative, post, randId, saveTheme } from "../utils";
|
||||
import { injectCss, transform } from "../utils/css";
|
||||
|
||||
type Attachment = {
|
||||
hash: string;
|
||||
filename: string;
|
||||
@@ -88,7 +87,7 @@ function scrollIntoView(editor: Editor) {
|
||||
export type EditorController = {
|
||||
selectionChange: (editor: Editor) => void;
|
||||
titleChange: (title: string) => void;
|
||||
contentChange: (editor: Editor) => void;
|
||||
contentChange: (editor: Editor, ignoreEdit?: boolean) => void;
|
||||
scroll: (event: React.UIEvent<HTMLDivElement, UIEvent>) => void;
|
||||
title: string;
|
||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||
@@ -102,6 +101,7 @@ export type EditorController = {
|
||||
setTitlePlaceholder: React.Dispatch<React.SetStateAction<string>>;
|
||||
countWords: (ms: number) => void;
|
||||
copyToClipboard: (text: string) => void;
|
||||
getAttachmentData: (attachment: Attachment) => Promise<string>;
|
||||
};
|
||||
|
||||
export function useEditorController(update: () => void): EditorController {
|
||||
@@ -138,17 +138,27 @@ export function useEditorController(update: () => void): EditorController {
|
||||
}, [colors]);
|
||||
|
||||
const contentChange = useCallback(
|
||||
(editor: Editor) => {
|
||||
(editor: Editor, ignoreEdit) => {
|
||||
const currentSessionId = globalThis.sessionId;
|
||||
post(EventTypes.contentchange);
|
||||
if (!editor) return;
|
||||
if (typeof timers.current.change === "number") {
|
||||
clearTimeout(timers.current?.change);
|
||||
}
|
||||
timers.current.change = setTimeout(() => {
|
||||
htmlContentRef.current = editor.getHTML();
|
||||
post(EventTypes.content, htmlContentRef.current, currentSessionId);
|
||||
}, 300);
|
||||
timers.current.change = setTimeout(
|
||||
() => {
|
||||
htmlContentRef.current = editor.getHTML();
|
||||
post(
|
||||
EventTypes.content,
|
||||
{
|
||||
html: htmlContentRef.current,
|
||||
ignoreEdit: ignoreEdit
|
||||
},
|
||||
currentSessionId
|
||||
);
|
||||
},
|
||||
ignoreEdit ? 0 : 300
|
||||
);
|
||||
|
||||
countWords(5000);
|
||||
},
|
||||
@@ -211,6 +221,12 @@ export function useEditorController(update: () => void): EditorController {
|
||||
scrollIntoView(editor?.current as any);
|
||||
}
|
||||
break;
|
||||
case "native:attachment-data":
|
||||
if (pendingResolvers[value.resolverId]) {
|
||||
logger("info", "resolved data for attachment", value.resolverId);
|
||||
pendingResolvers[value.resolverId](value.data);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -252,6 +268,20 @@ export function useEditorController(update: () => void): EditorController {
|
||||
post(EventTypes.copyToClipboard, text);
|
||||
};
|
||||
|
||||
const getAttachmentData = (attachment: Attachment) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const resolverId = randId("get_attachment_data");
|
||||
pendingResolvers[resolverId] = (data) => {
|
||||
delete pendingResolvers[resolverId];
|
||||
resolve(data);
|
||||
};
|
||||
post(EventTypes.getAttachmentData, {
|
||||
attachment,
|
||||
resolverId: resolverId
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
contentChange,
|
||||
selectionChange,
|
||||
@@ -268,6 +298,7 @@ export function useEditorController(update: () => void): EditorController {
|
||||
openLink,
|
||||
onUpdate: onUpdate,
|
||||
countWords,
|
||||
copyToClipboard
|
||||
copyToClipboard,
|
||||
getAttachmentData
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,13 @@ import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
|
||||
import { useEditorController } from "../hooks/useEditorController";
|
||||
import { ThemeDefinition } from "@notesnook/theme";
|
||||
|
||||
globalThis.pendingResolvers = {};
|
||||
export function randId(prefix: string) {
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
export type SafeAreaType = {
|
||||
top: number;
|
||||
left: number;
|
||||
@@ -49,6 +56,9 @@ export type Settings = {
|
||||
|
||||
/* eslint-disable no-var */
|
||||
declare global {
|
||||
var pendingResolvers: {
|
||||
[key: string]: (value: any) => void;
|
||||
};
|
||||
var statusBar: React.MutableRefObject<{
|
||||
set: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
@@ -154,7 +164,8 @@ export const EventTypes = {
|
||||
contentchange: "editor-event:content-change",
|
||||
reminders: "editor-event:reminders",
|
||||
previewAttachment: "editor-event:preview-attachment",
|
||||
copyToClipboard: "editor-events:copy-to-clipboard"
|
||||
copyToClipboard: "editor-events:copy-to-clipboard",
|
||||
getAttachmentData: "editor-events:get-attachment-data"
|
||||
} as const;
|
||||
|
||||
export function isReactNative(): boolean {
|
||||
|
||||
136
packages/editor/package-lock.json
generated
136
packages/editor/package-lock.json
generated
@@ -95,7 +95,8 @@
|
||||
"@microsoft/signalr": "^7.0.10",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.2-alpha",
|
||||
"@readme/data-urls": "^3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.4-alpha",
|
||||
"async-mutex": "^0.3.2",
|
||||
"dayjs": "1.11.9",
|
||||
"entities": "^4.3.1",
|
||||
@@ -114,7 +115,7 @@
|
||||
"@types/html-to-text": "^9.0.0",
|
||||
"@types/katex": "^0.16.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -3122,6 +3123,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
@@ -3142,6 +3144,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -3274,6 +3277,7 @@
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
|
||||
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
@@ -4090,8 +4094,7 @@
|
||||
"@emotion/use-insertion-effect-with-fallbacks": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
|
||||
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw=="
|
||||
},
|
||||
"@emotion/utils": {
|
||||
"version": "1.2.1",
|
||||
@@ -4344,11 +4347,12 @@
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/crypto": "file:../crypto",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.2-alpha",
|
||||
"@readme/data-urls": "^3.0.0",
|
||||
"@streetwriters/showdown": "^3.0.4-alpha",
|
||||
"@types/html-to-text": "^9.0.0",
|
||||
"@types/katex": "^0.16.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"async-mutex": "^0.3.2",
|
||||
@@ -4565,104 +4569,87 @@
|
||||
"@tiptap/core": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.1.12.tgz",
|
||||
"integrity": "sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ=="
|
||||
},
|
||||
"@tiptap/extension-blockquote": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.1.12.tgz",
|
||||
"integrity": "sha512-Qb3YRlCfugx9pw7VgLTb+jY37OY4aBJeZnqHzx4QThSm13edNYjasokbX0nTwL1Up4NPTcY19JUeHt6fVaVVGg==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-Qb3YRlCfugx9pw7VgLTb+jY37OY4aBJeZnqHzx4QThSm13edNYjasokbX0nTwL1Up4NPTcY19JUeHt6fVaVVGg=="
|
||||
},
|
||||
"@tiptap/extension-bold": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.1.12.tgz",
|
||||
"integrity": "sha512-AZGxIxcGU1/y6V2YEbKsq6BAibL8yQrbRm6EdcBnby41vj1WziewEKswhLGmZx5IKM2r2ldxld03KlfSIlKQZg==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-AZGxIxcGU1/y6V2YEbKsq6BAibL8yQrbRm6EdcBnby41vj1WziewEKswhLGmZx5IKM2r2ldxld03KlfSIlKQZg=="
|
||||
},
|
||||
"@tiptap/extension-bullet-list": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.12.tgz",
|
||||
"integrity": "sha512-vtD8vWtNlmAZX8LYqt2yU9w3mU9rPCiHmbp4hDXJs2kBnI0Ju/qAyXFx6iJ3C3XyuMnMbJdDI9ee0spAvFz7cQ==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-vtD8vWtNlmAZX8LYqt2yU9w3mU9rPCiHmbp4hDXJs2kBnI0Ju/qAyXFx6iJ3C3XyuMnMbJdDI9ee0spAvFz7cQ=="
|
||||
},
|
||||
"@tiptap/extension-character-count": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-character-count/-/extension-character-count-2.1.12.tgz",
|
||||
"integrity": "sha512-+GFbBG13nvF8mFIeisSERG/Q3CuRsTNwVZIRbJTLgGdbHXFqPhJh4Xfm7cv7OaOYevUlVyO+z5pGD7wIl1bLqQ==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-+GFbBG13nvF8mFIeisSERG/Q3CuRsTNwVZIRbJTLgGdbHXFqPhJh4Xfm7cv7OaOYevUlVyO+z5pGD7wIl1bLqQ=="
|
||||
},
|
||||
"@tiptap/extension-code": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.1.12.tgz",
|
||||
"integrity": "sha512-CRiRq5OTC1lFgSx6IMrECqmtb93a0ZZKujEnaRhzWliPBjLIi66va05f/P1vnV6/tHaC3yfXys6dxB5A4J8jxw==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-CRiRq5OTC1lFgSx6IMrECqmtb93a0ZZKujEnaRhzWliPBjLIi66va05f/P1vnV6/tHaC3yfXys6dxB5A4J8jxw=="
|
||||
},
|
||||
"@tiptap/extension-code-block": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.1.12.tgz",
|
||||
"integrity": "sha512-RXtSYCVsnk8D+K80uNZShClfZjvv1EgO42JlXLVGWQdIgaNyuOv/6I/Jdf+ZzhnpsBnHufW+6TJjwP5vJPSPHA==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-RXtSYCVsnk8D+K80uNZShClfZjvv1EgO42JlXLVGWQdIgaNyuOv/6I/Jdf+ZzhnpsBnHufW+6TJjwP5vJPSPHA=="
|
||||
},
|
||||
"@tiptap/extension-color": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-2.1.12.tgz",
|
||||
"integrity": "sha512-Myd6iSbPJvvclr+NRBEdE0k52QlQrXZnJljk4JKn0b25cl60ERA40FH9QLBjkpTed7SDbI3oX7LWIzTUoCj39w==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-Myd6iSbPJvvclr+NRBEdE0k52QlQrXZnJljk4JKn0b25cl60ERA40FH9QLBjkpTed7SDbI3oX7LWIzTUoCj39w=="
|
||||
},
|
||||
"@tiptap/extension-document": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.1.12.tgz",
|
||||
"integrity": "sha512-0QNfAkCcFlB9O8cUNSwTSIQMV9TmoEhfEaLz/GvbjwEq4skXK3bU+OQX7Ih07waCDVXIGAZ7YAZogbvrn/WbOw==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-0QNfAkCcFlB9O8cUNSwTSIQMV9TmoEhfEaLz/GvbjwEq4skXK3bU+OQX7Ih07waCDVXIGAZ7YAZogbvrn/WbOw=="
|
||||
},
|
||||
"@tiptap/extension-dropcursor": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.1.12.tgz",
|
||||
"integrity": "sha512-0tT/q8nL4NBCYPxr9T0Brck+RQbWuczm9nV0bnxgt0IiQXoRHutfPWdS7GA65PTuVRBS/3LOco30fbjFhkfz/A==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-0tT/q8nL4NBCYPxr9T0Brck+RQbWuczm9nV0bnxgt0IiQXoRHutfPWdS7GA65PTuVRBS/3LOco30fbjFhkfz/A=="
|
||||
},
|
||||
"@tiptap/extension-font-family": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-2.1.12.tgz",
|
||||
"integrity": "sha512-1PAvmtilBD1OWfr1I+oKND1MLNGWMEx/Qa7xC2YDzonxhiBd56Xog6fDE/+2Bbud0vPEIrMHrg8QOjCDH413vw==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-1PAvmtilBD1OWfr1I+oKND1MLNGWMEx/Qa7xC2YDzonxhiBd56Xog6fDE/+2Bbud0vPEIrMHrg8QOjCDH413vw=="
|
||||
},
|
||||
"@tiptap/extension-gapcursor": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.1.12.tgz",
|
||||
"integrity": "sha512-zFYdZCqPgpwoB7whyuwpc8EYLYjUE5QYKb8vICvc+FraBUDM51ujYhFSgJC3rhs8EjI+8GcK8ShLbSMIn49YOQ==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-zFYdZCqPgpwoB7whyuwpc8EYLYjUE5QYKb8vICvc+FraBUDM51ujYhFSgJC3rhs8EjI+8GcK8ShLbSMIn49YOQ=="
|
||||
},
|
||||
"@tiptap/extension-hard-break": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.1.12.tgz",
|
||||
"integrity": "sha512-nqKcAYGEOafg9D+2cy1E4gHNGuL12LerVa0eS2SQOb+PT8vSel9OTKU1RyZldsWSQJ5rq/w4uIjmLnrSR2w6Yw==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-nqKcAYGEOafg9D+2cy1E4gHNGuL12LerVa0eS2SQOb+PT8vSel9OTKU1RyZldsWSQJ5rq/w4uIjmLnrSR2w6Yw=="
|
||||
},
|
||||
"@tiptap/extension-heading": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.1.12.tgz",
|
||||
"integrity": "sha512-MoANP3POAP68Ko9YXarfDKLM/kXtscgp6m+xRagPAghRNujVY88nK1qBMZ3JdvTVN6b/ATJhp8UdrZX96TLV2w==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-MoANP3POAP68Ko9YXarfDKLM/kXtscgp6m+xRagPAghRNujVY88nK1qBMZ3JdvTVN6b/ATJhp8UdrZX96TLV2w=="
|
||||
},
|
||||
"@tiptap/extension-history": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.1.12.tgz",
|
||||
"integrity": "sha512-6b7UFVkvPjq3LVoCTrYZAczt5sQrQUaoDWAieVClVZoFLfjga2Fwjcfgcie8IjdPt8YO2hG/sar/c07i9vM0Sg==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-6b7UFVkvPjq3LVoCTrYZAczt5sQrQUaoDWAieVClVZoFLfjga2Fwjcfgcie8IjdPt8YO2hG/sar/c07i9vM0Sg=="
|
||||
},
|
||||
"@tiptap/extension-horizontal-rule": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.1.12.tgz",
|
||||
"integrity": "sha512-RRuoK4KxrXRrZNAjJW5rpaxjiP0FJIaqpi7nFbAua2oHXgsCsG8qbW2Y0WkbIoS8AJsvLZ3fNGsQ8gpdliuq3A==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-RRuoK4KxrXRrZNAjJW5rpaxjiP0FJIaqpi7nFbAua2oHXgsCsG8qbW2Y0WkbIoS8AJsvLZ3fNGsQ8gpdliuq3A=="
|
||||
},
|
||||
"@tiptap/extension-italic": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.1.12.tgz",
|
||||
"integrity": "sha512-/XYrW4ZEWyqDvnXVKbgTXItpJOp2ycswk+fJ3vuexyolO6NSs0UuYC6X4f+FbHYL5VuWqVBv7EavGa+tB6sl3A==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-/XYrW4ZEWyqDvnXVKbgTXItpJOp2ycswk+fJ3vuexyolO6NSs0UuYC6X4f+FbHYL5VuWqVBv7EavGa+tB6sl3A=="
|
||||
},
|
||||
"@tiptap/extension-link": {
|
||||
"version": "2.1.16",
|
||||
@@ -4675,110 +4662,92 @@
|
||||
"@tiptap/extension-list-item": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.1.12.tgz",
|
||||
"integrity": "sha512-Gk7hBFofAPmNQ8+uw8w5QSsZOMEGf7KQXJnx5B022YAUJTYYxO3jYVuzp34Drk9p+zNNIcXD4kc7ff5+nFOTrg==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-Gk7hBFofAPmNQ8+uw8w5QSsZOMEGf7KQXJnx5B022YAUJTYYxO3jYVuzp34Drk9p+zNNIcXD4kc7ff5+nFOTrg=="
|
||||
},
|
||||
"@tiptap/extension-list-keymap": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-2.1.12.tgz",
|
||||
"integrity": "sha512-f19nGaqhIZhssM2k8nYR+zcoMc7UCLcW6YCNhTXSrybUsb6SMFVob9OL7+sy1x2n5Was5IqsvyAGakLjdTEwAw==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-f19nGaqhIZhssM2k8nYR+zcoMc7UCLcW6YCNhTXSrybUsb6SMFVob9OL7+sy1x2n5Was5IqsvyAGakLjdTEwAw=="
|
||||
},
|
||||
"@tiptap/extension-ordered-list": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.1.12.tgz",
|
||||
"integrity": "sha512-tF6VGl+D2avCgn9U/2YLJ8qVmV6sPE/iEzVAFZuOSe6L0Pj7SQw4K6AO640QBob/d8VrqqJFHCb6l10amJOnXA==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-tF6VGl+D2avCgn9U/2YLJ8qVmV6sPE/iEzVAFZuOSe6L0Pj7SQw4K6AO640QBob/d8VrqqJFHCb6l10amJOnXA=="
|
||||
},
|
||||
"@tiptap/extension-paragraph": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.1.12.tgz",
|
||||
"integrity": "sha512-hoH/uWPX+KKnNAZagudlsrr4Xu57nusGekkJWBcrb5MCDE91BS+DN2xifuhwXiTHxnwOMVFjluc0bPzQbkArsw==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-hoH/uWPX+KKnNAZagudlsrr4Xu57nusGekkJWBcrb5MCDE91BS+DN2xifuhwXiTHxnwOMVFjluc0bPzQbkArsw=="
|
||||
},
|
||||
"@tiptap/extension-placeholder": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.1.12.tgz",
|
||||
"integrity": "sha512-K52o7B1zkP4vaVy3z4ZwHn+tQy6KlXtedj1skLg+796ImwH2GYS5z6MFOTfKzBO2hLncUzLco/s0C5PLCD6SDw==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-K52o7B1zkP4vaVy3z4ZwHn+tQy6KlXtedj1skLg+796ImwH2GYS5z6MFOTfKzBO2hLncUzLco/s0C5PLCD6SDw=="
|
||||
},
|
||||
"@tiptap/extension-strike": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.1.12.tgz",
|
||||
"integrity": "sha512-HlhrzIjYUT8oCH9nYzEL2QTTn8d1ECnVhKvzAe6x41xk31PjLMHTUy8aYjeQEkWZOWZ34tiTmslV1ce6R3Dt8g==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-HlhrzIjYUT8oCH9nYzEL2QTTn8d1ECnVhKvzAe6x41xk31PjLMHTUy8aYjeQEkWZOWZ34tiTmslV1ce6R3Dt8g=="
|
||||
},
|
||||
"@tiptap/extension-subscript": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.1.12.tgz",
|
||||
"integrity": "sha512-tb1jysEvf4SIiXwEOgDTXiyrG39RVNHvn/zsGMg5wy5t9qUp9m1k7kKYTH084ktuKDAPQonCcpn3hwc+ngTFzg==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-tb1jysEvf4SIiXwEOgDTXiyrG39RVNHvn/zsGMg5wy5t9qUp9m1k7kKYTH084ktuKDAPQonCcpn3hwc+ngTFzg=="
|
||||
},
|
||||
"@tiptap/extension-superscript": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.1.12.tgz",
|
||||
"integrity": "sha512-ek6L+DNsrjiJieArlgTvQt1VfJ56d8V19WAPW/ciRhq88YRlTEY9nSO3QuUCSUO1nGmE5OWQpgrsiW/XZbONVw==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-ek6L+DNsrjiJieArlgTvQt1VfJ56d8V19WAPW/ciRhq88YRlTEY9nSO3QuUCSUO1nGmE5OWQpgrsiW/XZbONVw=="
|
||||
},
|
||||
"@tiptap/extension-table": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.1.12.tgz",
|
||||
"integrity": "sha512-q/DuKZ4j1ycRfuFdb9rBJ3MglGNxlM2BQ1csScX/BrVIsAQI5B8sdzy1BrIlepQ6DRu4DCzHcKMI8u4/edUSWA==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-q/DuKZ4j1ycRfuFdb9rBJ3MglGNxlM2BQ1csScX/BrVIsAQI5B8sdzy1BrIlepQ6DRu4DCzHcKMI8u4/edUSWA=="
|
||||
},
|
||||
"@tiptap/extension-table-cell": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.1.12.tgz",
|
||||
"integrity": "sha512-hextcfVTdwX8G7s8Q/V6LW2aUhGvPgu1dfV+kVVO42AFHxG+6PIkDOUuHphGajG3Nrs129bjMDWb8jphj38dUg==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-hextcfVTdwX8G7s8Q/V6LW2aUhGvPgu1dfV+kVVO42AFHxG+6PIkDOUuHphGajG3Nrs129bjMDWb8jphj38dUg=="
|
||||
},
|
||||
"@tiptap/extension-table-header": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.1.12.tgz",
|
||||
"integrity": "sha512-a4WZ5Z7gqQ/QlK8cK2d1ONYdma/J5+yH/0SNtQhkfELoS45GsLJh89OyKO0W0FnY6Mg0RoH1FsoBD+cqm0yazA==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-a4WZ5Z7gqQ/QlK8cK2d1ONYdma/J5+yH/0SNtQhkfELoS45GsLJh89OyKO0W0FnY6Mg0RoH1FsoBD+cqm0yazA=="
|
||||
},
|
||||
"@tiptap/extension-table-row": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.1.12.tgz",
|
||||
"integrity": "sha512-0kPr+zngQC1YQRcU6+Fl3CpIW/SdJhVJ5qOLpQleXrLPdjmZQd3Z1DXvOSDphYjXCowGPCxeUa++6bo7IoEMJw==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-0kPr+zngQC1YQRcU6+Fl3CpIW/SdJhVJ5qOLpQleXrLPdjmZQd3Z1DXvOSDphYjXCowGPCxeUa++6bo7IoEMJw=="
|
||||
},
|
||||
"@tiptap/extension-task-item": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.1.12.tgz",
|
||||
"integrity": "sha512-uqrDTO4JwukZUt40GQdvB6S+oDhdp4cKNPMi0sbteWziQugkSMLlkYvxU0Hfb/YeziaWWwFI7ssPu/hahyk6dQ==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-uqrDTO4JwukZUt40GQdvB6S+oDhdp4cKNPMi0sbteWziQugkSMLlkYvxU0Hfb/YeziaWWwFI7ssPu/hahyk6dQ=="
|
||||
},
|
||||
"@tiptap/extension-task-list": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.1.12.tgz",
|
||||
"integrity": "sha512-BUpYlEWK+Q3kw9KIiOqvhd0tUPhMcOf1+fJmCkluJok+okAxMbP1umAtCEQ3QkoCwLr+vpHJov7h3yi9+dwgeQ==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-BUpYlEWK+Q3kw9KIiOqvhd0tUPhMcOf1+fJmCkluJok+okAxMbP1umAtCEQ3QkoCwLr+vpHJov7h3yi9+dwgeQ=="
|
||||
},
|
||||
"@tiptap/extension-text": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.1.12.tgz",
|
||||
"integrity": "sha512-rCNUd505p/PXwU9Jgxo4ZJv4A3cIBAyAqlx/dtcY6cjztCQuXJhuQILPhjGhBTOLEEL4kW2wQtqzCmb7O8i2jg==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-rCNUd505p/PXwU9Jgxo4ZJv4A3cIBAyAqlx/dtcY6cjztCQuXJhuQILPhjGhBTOLEEL4kW2wQtqzCmb7O8i2jg=="
|
||||
},
|
||||
"@tiptap/extension-text-align": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.1.12.tgz",
|
||||
"integrity": "sha512-siMlwrkgVrAxxgmZn8GOc75J7UZi2CVrP9vDHkUPPyKm/fjssYekXwGCEk4Vswii1BbOh2gt+MDsRkeYRGyDlQ==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-siMlwrkgVrAxxgmZn8GOc75J7UZi2CVrP9vDHkUPPyKm/fjssYekXwGCEk4Vswii1BbOh2gt+MDsRkeYRGyDlQ=="
|
||||
},
|
||||
"@tiptap/extension-text-style": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.1.12.tgz",
|
||||
"integrity": "sha512-nfjWXX0JSRHLcscfiMESh+RN+Z7bG8nio/C9+8yQASM90VxU9f8oKgF8HnnSYsSrD4lLf44Q6XjmB7aMVUuikg==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-nfjWXX0JSRHLcscfiMESh+RN+Z7bG8nio/C9+8yQASM90VxU9f8oKgF8HnnSYsSrD4lLf44Q6XjmB7aMVUuikg=="
|
||||
},
|
||||
"@tiptap/extension-underline": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.1.12.tgz",
|
||||
"integrity": "sha512-NwwdhFT8gDD0VUNLQx85yFBhP9a8qg8GPuxlGzAP/lPTV8Ubh3vSeQ5N9k2ZF/vHlEvnugzeVCbmYn7wf8vn1g==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-NwwdhFT8gDD0VUNLQx85yFBhP9a8qg8GPuxlGzAP/lPTV8Ubh3vSeQ5N9k2ZF/vHlEvnugzeVCbmYn7wf8vn1g=="
|
||||
},
|
||||
"@tiptap/pm": {
|
||||
"version": "2.1.12",
|
||||
@@ -5839,8 +5808,7 @@
|
||||
}
|
||||
},
|
||||
"prosemirror-codemark": {
|
||||
"version": "0.4.2",
|
||||
"requires": {}
|
||||
"version": "0.4.2"
|
||||
},
|
||||
"prosemirror-collab": {
|
||||
"version": "1.3.0",
|
||||
@@ -5988,26 +5956,26 @@
|
||||
}
|
||||
},
|
||||
"re-resizable": {
|
||||
"version": "6.9.9",
|
||||
"requires": {}
|
||||
"version": "6.9.9"
|
||||
},
|
||||
"react": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"react-colorful": {
|
||||
"version": "5.6.1",
|
||||
"requires": {}
|
||||
"version": "5.6.1"
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -6094,6 +6062,7 @@
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
|
||||
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
@@ -6320,8 +6289,7 @@
|
||||
}
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"requires": {}
|
||||
"version": "1.2.0"
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
|
||||
@@ -21,30 +21,17 @@ import { Node, mergeAttributes, findChildren, Editor } from "@tiptap/core";
|
||||
import { Attribute } from "@tiptap/core";
|
||||
import { createSelectionBasedNodeView } from "../react";
|
||||
import { AttachmentComponent } from "./component";
|
||||
import { Attachment } from "./types";
|
||||
|
||||
export type AttachmentType = "image" | "file" | "camera";
|
||||
export interface AttachmentOptions {
|
||||
types: string[];
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
onDownloadAttachment: (editor: Editor, attachment: Attachment) => boolean;
|
||||
onOpenAttachmentPicker: (editor: Editor, type: AttachmentType) => boolean;
|
||||
onPreviewAttachment: (editor: Editor, attachment: Attachment) => boolean;
|
||||
}
|
||||
|
||||
export type AttachmentWithProgress = AttachmentProgress & Attachment;
|
||||
|
||||
export type Attachment = {
|
||||
hash: string;
|
||||
filename: string;
|
||||
mime: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type AttachmentProgress = {
|
||||
progress: number;
|
||||
type: "upload" | "download" | "encrypt";
|
||||
hash: string;
|
||||
};
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
attachment: {
|
||||
@@ -52,8 +39,15 @@ declare module "@tiptap/core" {
|
||||
insertAttachment: (attachment: Attachment) => ReturnType;
|
||||
removeAttachment: () => ReturnType;
|
||||
downloadAttachment: (attachment: Attachment) => ReturnType;
|
||||
setAttachmentProgress: (progress: AttachmentProgress) => ReturnType;
|
||||
previewAttachment: (options: Attachment) => ReturnType;
|
||||
updateAttachment: (
|
||||
attachment: Partial<Attachment>,
|
||||
options: {
|
||||
preventUpdate?: boolean;
|
||||
ignoreEdit?: boolean;
|
||||
query: (attachment: Attachment) => boolean;
|
||||
}
|
||||
) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -67,6 +61,7 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: [this.name],
|
||||
HTMLAttributes: {},
|
||||
onDownloadAttachment: () => false,
|
||||
onOpenAttachmentPicker: () => false,
|
||||
@@ -82,6 +77,7 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
type: { default: "file", rendered: false },
|
||||
progress: {
|
||||
default: 0,
|
||||
rendered: false
|
||||
@@ -153,23 +149,28 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
|
||||
({ editor }) => {
|
||||
return this.options.onOpenAttachmentPicker(editor, type);
|
||||
},
|
||||
setAttachmentProgress:
|
||||
(options) =>
|
||||
updateAttachment:
|
||||
(attachment, options) =>
|
||||
({ state, tr, dispatch }) => {
|
||||
const { hash, progress } = options;
|
||||
const attachments = findChildren(
|
||||
state.doc,
|
||||
(node) =>
|
||||
(node.type.name === this.name || node.type.name === "image") &&
|
||||
node.attrs.hash === hash
|
||||
this.options.types.includes(node.type.name) &&
|
||||
options.query(node.attrs as Attachment)
|
||||
);
|
||||
for (const attachment of attachments) {
|
||||
tr.setNodeMarkup(attachment.pos, attachment.node.type, {
|
||||
...attachment.node.attrs,
|
||||
progress: progress === 100 ? null : progress
|
||||
if (!attachments.length) return false;
|
||||
|
||||
for (const { node, pos } of attachments) {
|
||||
const progress = attachment.progress || node.attrs.progress;
|
||||
tr.setNodeMarkup(pos, node.type, {
|
||||
...node.attrs,
|
||||
...attachment,
|
||||
progress:
|
||||
progress !== undefined && progress < 100 ? progress : undefined
|
||||
});
|
||||
}
|
||||
tr.setMeta("preventUpdate", true);
|
||||
tr.setMeta("preventUpdate", options.preventUpdate || false);
|
||||
tr.setMeta("ignoreEdit", options.ignoreEdit || false);
|
||||
tr.setMeta("addToHistory", false);
|
||||
if (dispatch) dispatch(tr);
|
||||
return true;
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Box, Text } from "@theme-ui/components";
|
||||
import { AttachmentWithProgress } from "./attachment";
|
||||
import { FileAttachment } from "./types";
|
||||
import { useRef, useState } from "react";
|
||||
import { Icon } from "@notesnook/ui";
|
||||
import { Icons } from "../../toolbar/icons";
|
||||
@@ -27,7 +27,7 @@ import { ToolbarGroup } from "../../toolbar/components/toolbar-group";
|
||||
import { DesktopOnly } from "../../components/responsive";
|
||||
|
||||
export function AttachmentComponent(
|
||||
props: SelectionBasedReactNodeViewProps<AttachmentWithProgress>
|
||||
props: SelectionBasedReactNodeViewProps<FileAttachment>
|
||||
) {
|
||||
const { editor, node, selected } = props;
|
||||
const { filename, size, progress } = node.attrs;
|
||||
|
||||
@@ -18,3 +18,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./attachment";
|
||||
export * from "./types";
|
||||
|
||||
53
packages/editor/src/extensions/attachment/types.ts
Normal file
53
packages/editor/src/extensions/attachment/types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
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 BaseAttachment = {
|
||||
hash: string;
|
||||
filename: string;
|
||||
mime: string;
|
||||
size: number;
|
||||
progress?: number;
|
||||
};
|
||||
|
||||
export type FileAttachment = BaseAttachment & {
|
||||
type: "file";
|
||||
};
|
||||
|
||||
export type WebClipAttachment = BaseAttachment & {
|
||||
type: "web-clip";
|
||||
src: string;
|
||||
title: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
};
|
||||
|
||||
export type ImageAttachment = BaseAttachment & {
|
||||
type: "image";
|
||||
width?: number;
|
||||
height?: number;
|
||||
src?: string;
|
||||
aspectRatio?: number;
|
||||
} & ImageAlignmentOptions;
|
||||
|
||||
export type ImageAlignmentOptions = {
|
||||
float?: boolean;
|
||||
align?: "center" | "left" | "right";
|
||||
};
|
||||
|
||||
export type Attachment = FileAttachment | WebClipAttachment | ImageAttachment;
|
||||
@@ -17,9 +17,10 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Box, Flex, Image } from "@theme-ui/components";
|
||||
import { ImageAlignmentOptions, ImageAttributes } from "./image";
|
||||
import { useRef } from "react";
|
||||
import { ThemeUIStyleObject } from "@theme-ui/core";
|
||||
import { Box, Flex, Image, Text } from "@theme-ui/components";
|
||||
import { ImageAttributes } from "./image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SelectionBasedReactNodeViewProps } from "../react";
|
||||
import { DesktopOnly } from "../../components/responsive";
|
||||
import { Icon } from "@notesnook/ui";
|
||||
@@ -33,61 +34,72 @@ import { Resizer } from "../../components/resizer";
|
||||
import {
|
||||
corsify,
|
||||
downloadImage,
|
||||
isDataUrl,
|
||||
revokeBloburl,
|
||||
toBlobURL,
|
||||
toDataURL
|
||||
} from "../../utils/downloader";
|
||||
import { motion } from "framer-motion";
|
||||
import { useObserver } from "../../hooks/use-observer";
|
||||
import { Attachment, ImageAlignmentOptions } from "../attachment";
|
||||
import DataURL from "@notesnook/core/dist/utils/dataurl";
|
||||
|
||||
export const AnimatedImage = motion(Image);
|
||||
|
||||
export function ImageComponent(
|
||||
props: SelectionBasedReactNodeViewProps<
|
||||
Partial<ImageAttributes & ImageAlignmentOptions>
|
||||
>
|
||||
props: SelectionBasedReactNodeViewProps<Partial<ImageAttributes>>
|
||||
) {
|
||||
const { editor, node, selected } = props;
|
||||
const { src, alt, title, textDirection, hash, aspectRatio, mime, progress } =
|
||||
node.attrs;
|
||||
const [bloburl, setBloburl] = useState<string | undefined>(
|
||||
toBlobURL("", "image", mime, hash)
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const {
|
||||
bloburl,
|
||||
src,
|
||||
alt,
|
||||
title,
|
||||
width,
|
||||
height,
|
||||
textDirection,
|
||||
hash,
|
||||
aspectRatio,
|
||||
mime
|
||||
} = node.attrs;
|
||||
const { inView, ref: imageRef } = useObserver<HTMLImageElement>({
|
||||
threshold: 0.2,
|
||||
once: true
|
||||
});
|
||||
const float = isMobile ? false : node.attrs.float;
|
||||
|
||||
let align = node.attrs.align;
|
||||
if (!align) align = textDirection ? "right" : "left";
|
||||
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const downloadOptions = useToolbarStore((store) => store.downloadOptions);
|
||||
const isReadonly = !editor.current?.isEditable;
|
||||
const isSVG = !!mime && mime.includes("/svg");
|
||||
const relativeHeight = aspectRatio
|
||||
? editor.view.dom.clientWidth / aspectRatio
|
||||
: undefined;
|
||||
|
||||
const { height, width } = clampSize(
|
||||
node.attrs,
|
||||
editor.view.dom.clientWidth,
|
||||
aspectRatio
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inView) return;
|
||||
if (src || !hash || bloburl) return;
|
||||
(async function () {
|
||||
const data = await editor.current?.storage
|
||||
.getAttachmentData?.(node.attrs)
|
||||
.catch(() => null);
|
||||
if (typeof data !== "string" || !data) return; // TODO: show error
|
||||
|
||||
setBloburl(toBlobURL(data, "image", node.attrs.mime, hash));
|
||||
})();
|
||||
}, [inView]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!hash) return;
|
||||
revokeBloburl(hash);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: float ? "inline" : "flex",
|
||||
ml: float ? (align === "right" ? 2 : 0) : 0,
|
||||
mr: float ? (align === "left" ? 2 : 0) : 0,
|
||||
float: float ? (align as "left" | "right") : "none",
|
||||
justifyContent: float
|
||||
? "stretch"
|
||||
: align === "center"
|
||||
? "center"
|
||||
: align === "left"
|
||||
? "start"
|
||||
: "end",
|
||||
...getAlignmentStyles(node.attrs),
|
||||
position: "relative",
|
||||
mt: isSVG ? `24px` : 0,
|
||||
":hover .drag-handle, :active .drag-handle": {
|
||||
@@ -95,41 +107,12 @@ export function ImageComponent(
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!src && !bloburl && hash && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: width || "100%",
|
||||
height: height || relativeHeight || "100%",
|
||||
maxWidth: "100%",
|
||||
minWidth: 135,
|
||||
bg: "background-secondary",
|
||||
border: selected
|
||||
? "2px solid var(--accent)"
|
||||
: "2px solid transparent",
|
||||
borderRadius: "default",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
mt: 1,
|
||||
py: 50
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
path={Icons.image}
|
||||
size={width ? width * 0.2 : 72}
|
||||
color="gray"
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<Resizer
|
||||
style={{ marginTop: 5 }}
|
||||
enabled={editor.isEditable && !float}
|
||||
selected={selected}
|
||||
width={width}
|
||||
height={height || relativeHeight}
|
||||
height={height}
|
||||
onResize={(width, height) => {
|
||||
editor.commands.setImageSize({ width, height });
|
||||
}}
|
||||
@@ -158,8 +141,9 @@ export function ImageComponent(
|
||||
hash ? "previewAttachment" : "none",
|
||||
hash ? "downloadAttachment" : "none",
|
||||
"imageAlignLeft",
|
||||
float ? "none" : "imageAlignCenter",
|
||||
"imageAlignCenter",
|
||||
"imageAlignRight",
|
||||
"imageFloat",
|
||||
"imageProperties"
|
||||
]
|
||||
}
|
||||
@@ -172,6 +156,24 @@ export function ImageComponent(
|
||||
</Flex>
|
||||
)}
|
||||
</DesktopOnly>
|
||||
{progress ? (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 2,
|
||||
right: 2,
|
||||
bg: "background",
|
||||
borderRadius: 100,
|
||||
p: 1,
|
||||
px: 2,
|
||||
border: "1px solid var(--border)",
|
||||
zIndex: 2
|
||||
}}
|
||||
>
|
||||
<Icon path={Icons.loading} rotate size={14} sx={{ mr: 1 }} />
|
||||
<Text variant="body">{progress}%</Text>
|
||||
</Flex>
|
||||
) : null}
|
||||
{!isReadonly && selected && (
|
||||
<Icon
|
||||
className="drag-handle"
|
||||
@@ -211,28 +213,50 @@ export function ImageComponent(
|
||||
}}
|
||||
></Box>
|
||||
) : null}
|
||||
|
||||
{!src && !bloburl && hash && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: editor.isEditable ? "100%" : width,
|
||||
height: editor.isEditable ? "100%" : height,
|
||||
bg: "background-secondary",
|
||||
border: selected
|
||||
? "2px solid var(--accent)"
|
||||
: "2px solid transparent",
|
||||
borderRadius: "default",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
// mt: 1,
|
||||
py: 50
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
path={Icons.image}
|
||||
size={width ? width * 0.2 : 72}
|
||||
color="gray"
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<AnimatedImage
|
||||
as={isSVG ? "object" : "img"}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: bloburl || src ? 1 : 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeIn" }}
|
||||
transition={{ duration: 0.2, ease: "easeIn" }}
|
||||
data-drag-image
|
||||
ref={imageRef}
|
||||
alt={alt}
|
||||
crossOrigin="anonymous"
|
||||
{...(isSVG
|
||||
? {
|
||||
data:
|
||||
toBlobURL("", hash) ||
|
||||
bloburl ||
|
||||
corsify(src, downloadOptions?.corsHost),
|
||||
data: bloburl || corsify(src, downloadOptions?.corsHost),
|
||||
type: mime
|
||||
}
|
||||
: {
|
||||
src:
|
||||
toBlobURL("", hash) ||
|
||||
bloburl ||
|
||||
corsify(src, downloadOptions?.corsHost)
|
||||
src: bloburl || corsify(src, downloadOptions?.corsHost)
|
||||
})}
|
||||
title={title}
|
||||
sx={{
|
||||
@@ -248,6 +272,7 @@ export function ImageComponent(
|
||||
const { hash, filename, mime, size } = node.attrs;
|
||||
if (!!hash && !!filename && !!mime && !!size)
|
||||
editor.current?.commands.previewAttachment({
|
||||
type: "image",
|
||||
hash,
|
||||
filename,
|
||||
mime,
|
||||
@@ -256,36 +281,49 @@ export function ImageComponent(
|
||||
}}
|
||||
onLoad={async function onLoad() {
|
||||
if (!imageRef.current) return;
|
||||
const { clientHeight, clientWidth } = imageRef.current;
|
||||
if (src && !isDataUrl(src) && canParse(src)) {
|
||||
|
||||
const { naturalWidth, naturalHeight, clientHeight, clientWidth } =
|
||||
imageRef.current;
|
||||
const orignalWidth = naturalWidth || clientWidth;
|
||||
const orignalHeight = naturalHeight || clientHeight;
|
||||
const naturalAspectRatio = orignalWidth / orignalHeight;
|
||||
const fixedDimensions = fixAspectRatio(width, naturalAspectRatio);
|
||||
|
||||
if (src && !DataURL.isValid(src) && canParse(src)) {
|
||||
const image = await downloadImage(src, downloadOptions);
|
||||
if (!image) return;
|
||||
const { url, size, blob, mimeType } = image;
|
||||
imageRef.current.src = url;
|
||||
const dataurl = await toDataURL(blob);
|
||||
await editor.threadsafe((editor) =>
|
||||
editor.commands.updateImage(
|
||||
{ src, hash },
|
||||
editor.commands.updateAttachment(
|
||||
{
|
||||
...fixedDimensions,
|
||||
src: dataurl,
|
||||
bloburl: url,
|
||||
size: size,
|
||||
mime: mimeType,
|
||||
aspectRatio:
|
||||
!height && !width && !aspectRatio
|
||||
? clientWidth / clientHeight
|
||||
: undefined
|
||||
}
|
||||
aspectRatio: naturalAspectRatio
|
||||
},
|
||||
{ query: makeImageQuery(src, hash) }
|
||||
)
|
||||
);
|
||||
} else if (!height && !width && !aspectRatio) {
|
||||
} else if (!aspectRatio || aspectRatio != naturalAspectRatio) {
|
||||
await editor.threadsafe((editor) =>
|
||||
editor.commands.updateImage(
|
||||
{ src, hash },
|
||||
editor.commands.updateAttachment(
|
||||
{
|
||||
aspectRatio: clientWidth / clientHeight
|
||||
}
|
||||
...fixedDimensions,
|
||||
aspectRatio: naturalAspectRatio
|
||||
},
|
||||
{ query: makeImageQuery(src, hash), ignoreEdit: true }
|
||||
)
|
||||
);
|
||||
} else if (height !== fixedDimensions.height) {
|
||||
await editor.threadsafe((editor) =>
|
||||
editor.commands.updateAttachment(fixedDimensions, {
|
||||
query: makeImageQuery(src, hash),
|
||||
ignoreEdit: true
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -296,6 +334,12 @@ export function ImageComponent(
|
||||
);
|
||||
}
|
||||
|
||||
function makeImageQuery(src?: string, hash?: string) {
|
||||
return (a: Attachment) =>
|
||||
a.type === "image" &&
|
||||
((!!a.src && !!src && a.src === src) ||
|
||||
(!!a.hash && !!hash && a.hash === hash));
|
||||
}
|
||||
function canParse(src: string) {
|
||||
if (!src) return false;
|
||||
try {
|
||||
@@ -304,3 +348,52 @@ function canParse(src: string) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function clampSize(
|
||||
size: { width?: number; height?: number },
|
||||
maxWidth: number,
|
||||
aspectRatio?: number
|
||||
): { width: number; height: number } {
|
||||
if (typeof aspectRatio === "string" && isNaN(aspectRatio)) aspectRatio = 1;
|
||||
|
||||
// if no size we set the image to maximum size.
|
||||
if (!size.width || !size.height) return { width: maxWidth, height: maxWidth };
|
||||
|
||||
if (!aspectRatio) aspectRatio = size.width / size.height;
|
||||
|
||||
if (size.width > maxWidth)
|
||||
return { width: maxWidth, height: maxWidth / aspectRatio };
|
||||
|
||||
return {
|
||||
height: size.height,
|
||||
width: size.width
|
||||
};
|
||||
}
|
||||
|
||||
function fixAspectRatio(width: number, aspectRatio: number) {
|
||||
return {
|
||||
width,
|
||||
height: width / aspectRatio
|
||||
};
|
||||
}
|
||||
|
||||
function getAlignmentStyles(
|
||||
options: ImageAlignmentOptions
|
||||
): ThemeUIStyleObject {
|
||||
const { align, float } = options;
|
||||
if (float && align !== "center") {
|
||||
return {
|
||||
display: "inline",
|
||||
ml: align === "right" ? 2 : 0,
|
||||
mr: align === "left" ? 2 : 0,
|
||||
float: align,
|
||||
justifyContent: "stretch"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
display: "flex",
|
||||
justifyContent:
|
||||
align === "center" ? "center" : align === "right" ? "end" : "start"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,63 +17,28 @@ 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 {
|
||||
Node,
|
||||
nodeInputRule,
|
||||
mergeAttributes,
|
||||
findChildren
|
||||
} from "@tiptap/core";
|
||||
import { Attrs } from "@tiptap/pm/model";
|
||||
import { Plugin } from "@tiptap/pm/state";
|
||||
import { Node, nodeInputRule, mergeAttributes } from "@tiptap/core";
|
||||
import { hasSameAttributes } from "../../utils/prosemirror";
|
||||
import { Attachment, getDataAttribute } from "../attachment";
|
||||
import {
|
||||
ImageAlignmentOptions,
|
||||
ImageAttachment,
|
||||
getDataAttribute
|
||||
} from "../attachment";
|
||||
import { createSelectionBasedNodeView } from "../react";
|
||||
import { TextDirections } from "../text-direction";
|
||||
import { ImageComponent } from "./component";
|
||||
|
||||
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
export interface ImageOptions {
|
||||
inline: boolean;
|
||||
allowBase64: boolean;
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* We have two attributes that store the source of an image:
|
||||
*
|
||||
* 1. `src`
|
||||
* 2. `dataurl`
|
||||
*
|
||||
* `src` is the image's inherent source. This can contain a URL, a base64-dataurl,
|
||||
* a blob...etc. We should never touch this attribute. This is also where we store
|
||||
* the data that we want to upload so after we download a pasted image, its base64
|
||||
* dataurl goes in this attribute.
|
||||
*
|
||||
* `dataurl` should never get added to the final HTML. This attribute is where we
|
||||
* restore an image's data after loading a note.
|
||||
*
|
||||
* The reason we have 2 instead of a single attribute is to avoid unnecessary processing.
|
||||
* Keeping everything in the `src` attribute requires us to always send the rendered image
|
||||
* along with everything else. This is pointless because we already have the image's rendered
|
||||
* data.
|
||||
*/
|
||||
export type ImageAttributes = Partial<ImageSizeOptions> &
|
||||
Attachment & {
|
||||
src: string;
|
||||
bloburl?: string;
|
||||
alt?: string;
|
||||
title?: string;
|
||||
textDirection?: TextDirections;
|
||||
aspectRatio?: number;
|
||||
};
|
||||
|
||||
export type ImageAlignmentOptions = {
|
||||
float?: boolean;
|
||||
align?: "center" | "left" | "right";
|
||||
export type ImageAttributes = ImageAttachment & {
|
||||
textDirection?: TextDirections;
|
||||
};
|
||||
|
||||
export type ImageSizeOptions = {
|
||||
export type ImageSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
@@ -85,12 +50,8 @@ declare module "@tiptap/core" {
|
||||
* Add an image
|
||||
*/
|
||||
insertImage: (options: Partial<ImageAttributes>) => ReturnType;
|
||||
updateImage: (
|
||||
query: { src?: string; hash?: string },
|
||||
options: Partial<ImageAttributes> & { preventUpdate?: boolean }
|
||||
) => ReturnType;
|
||||
setImageAlignment: (options: ImageAlignmentOptions) => ReturnType;
|
||||
setImageSize: (options: ImageSizeOptions) => ReturnType;
|
||||
setImageSize: (size: ImageSize) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -120,15 +81,15 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
type: { default: "image", rendered: false },
|
||||
progress: {
|
||||
default: 0,
|
||||
rendered: false
|
||||
},
|
||||
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
default: null
|
||||
},
|
||||
title: {
|
||||
default: null
|
||||
},
|
||||
width: { default: null },
|
||||
height: { default: null },
|
||||
|
||||
@@ -142,7 +103,10 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
size: getDataAttribute("size"),
|
||||
aspectRatio: {
|
||||
default: undefined,
|
||||
parseHTML: (element) => element.dataset.aspectRatio,
|
||||
parseHTML: (element) =>
|
||||
element.dataset.aspectRatio
|
||||
? parseFloat(element.dataset.aspectRatio)
|
||||
: 1,
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.aspectRatio) {
|
||||
return {};
|
||||
@@ -152,11 +116,6 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
[`data-aspect-ratio`]: attributes.aspectRatio
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
bloburl: {
|
||||
...getDataAttribute("bloburl"),
|
||||
rendered: false
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -184,28 +143,6 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
});
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
transformCopied: (content) => {
|
||||
content.content.descendants((node) => {
|
||||
if (
|
||||
node.type.name === this.name &&
|
||||
typeof node.attrs.bloburl === "string"
|
||||
) {
|
||||
const attrs = node.attrs as Writeable<Attrs>;
|
||||
attrs.src = attrs.bloburl;
|
||||
delete attrs.bloburl;
|
||||
}
|
||||
});
|
||||
return content;
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
insertImage:
|
||||
@@ -242,32 +179,6 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
.updateAttributes(this.name, { ...options })
|
||||
.setNodeSelection(from)
|
||||
.run();
|
||||
},
|
||||
updateImage:
|
||||
(query, options) =>
|
||||
({ state, tr }) => {
|
||||
const keyedQuery = query.hash
|
||||
? { key: "hash", value: query.hash }
|
||||
: query.src
|
||||
? { key: "src", value: query.src }
|
||||
: null;
|
||||
if (!keyedQuery) return false;
|
||||
|
||||
const images = findChildren(
|
||||
state.doc,
|
||||
(node) =>
|
||||
node.type.name === this.name &&
|
||||
node.attrs[keyedQuery.key] === keyedQuery.value
|
||||
);
|
||||
for (const image of images) {
|
||||
tr.setNodeMarkup(image.pos, image.node.type, {
|
||||
...image.node.attrs,
|
||||
...options
|
||||
});
|
||||
}
|
||||
tr.setMeta("preventUpdate", options.preventUpdate || false);
|
||||
tr.setMeta("addToHistory", false);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -41,18 +41,25 @@ export function WebClipComponent(
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const embedRef = useRef<HTMLIFrameElement>(null);
|
||||
const resizeObserverRef = useRef<ResizeObserver>();
|
||||
const { src, title, fullscreen, html } = node.attrs;
|
||||
const { src, title, fullscreen, progress } = node.attrs;
|
||||
|
||||
useEffect(() => {
|
||||
const iframe = embedRef.current;
|
||||
if (!iframe || !iframe.contentDocument || !isLoading || !html) return;
|
||||
iframe.contentDocument.open();
|
||||
iframe.contentDocument.write(html || FAILED_CONTENT);
|
||||
iframe.contentDocument.close();
|
||||
iframe.contentDocument.head.innerHTML += `<base target="_blank">`;
|
||||
(async function () {
|
||||
const iframe = embedRef.current;
|
||||
if (!iframe || !iframe.contentDocument || !isLoading) return;
|
||||
|
||||
setIsLoading(false);
|
||||
}, [html]);
|
||||
const html = await editor.current?.storage
|
||||
.getAttachmentData?.(node.attrs)
|
||||
.catch(() => null);
|
||||
iframe.contentDocument.open();
|
||||
iframe.contentDocument.write(
|
||||
typeof html !== "string" || !html ? FAILED_CONTENT : html
|
||||
);
|
||||
iframe.contentDocument.close();
|
||||
iframe.contentDocument.head.innerHTML += `<base target="_blank">`;
|
||||
setIsLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function fullscreenchanged() {
|
||||
@@ -193,10 +200,14 @@ export function WebClipComponent(
|
||||
width: "100%",
|
||||
height: "calc(100% - 20px)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
justifyContent: "center",
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<Icon path={Icons.loading} rotate size={32} />
|
||||
{progress ? (
|
||||
<Text sx={{ mt: 2 }}>Loading web clip ({progress}%)</Text>
|
||||
) : null}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -17,9 +17,9 @@ 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 { Node, mergeAttributes, findChildren } from "@tiptap/core";
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { hasSameAttributes } from "../../utils/prosemirror";
|
||||
import { getDataAttribute } from "../attachment";
|
||||
import { WebClipAttachment, getDataAttribute } from "../attachment";
|
||||
import { createSelectionBasedNodeView } from "../react";
|
||||
import { WebClipComponent } from "./component";
|
||||
|
||||
@@ -27,28 +27,10 @@ export interface WebClipOptions {
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type WebClipAttributes = {
|
||||
export type WebClipAttributes = WebClipAttachment & {
|
||||
fullscreen: boolean;
|
||||
src: string;
|
||||
html: string;
|
||||
title: string;
|
||||
hash: string;
|
||||
type: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
};
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
webclip: {
|
||||
updateWebClip: (
|
||||
query: { hash?: string },
|
||||
options: { src: string }
|
||||
) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const WebClipNode = Node.create<WebClipOptions>({
|
||||
name: "webclip",
|
||||
content: "",
|
||||
@@ -68,14 +50,15 @@ export const WebClipNode = Node.create<WebClipOptions>({
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
type: { default: "web-clip", rendered: false },
|
||||
progress: {
|
||||
default: 0,
|
||||
rendered: false
|
||||
},
|
||||
fullscreen: {
|
||||
rendered: false,
|
||||
default: false
|
||||
},
|
||||
html: {
|
||||
rendered: false,
|
||||
default: null
|
||||
},
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
@@ -89,7 +72,7 @@ export const WebClipNode = Node.create<WebClipOptions>({
|
||||
default: null
|
||||
},
|
||||
hash: getDataAttribute("hash"),
|
||||
type: getDataAttribute("mime")
|
||||
mime: getDataAttribute("mime")
|
||||
};
|
||||
},
|
||||
|
||||
@@ -113,30 +96,5 @@ export const WebClipNode = Node.create<WebClipOptions>({
|
||||
shouldUpdate: (prev, next) => !hasSameAttributes(prev.attrs, next.attrs),
|
||||
forceEnableSelection: true
|
||||
});
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
updateWebClip:
|
||||
(query, options) =>
|
||||
({ state, tr, dispatch }) => {
|
||||
const clips = findChildren(
|
||||
state.doc,
|
||||
(node) =>
|
||||
node.type.name === this.name && node.attrs["hash"] === query.hash
|
||||
);
|
||||
|
||||
for (const clip of clips) {
|
||||
tr.setNodeMarkup(clip.pos, clip.node.type, {
|
||||
...clip.node.attrs,
|
||||
html: options.src
|
||||
});
|
||||
}
|
||||
tr.setMeta("preventUpdate", true);
|
||||
tr.setMeta("addToHistory", false);
|
||||
if (dispatch) dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
79
packages/editor/src/hooks/use-observer.ts
Normal file
79
packages/editor/src/hooks/use-observer.ts
Normal 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 { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { debounce } from "../utils/debounce";
|
||||
|
||||
type ObserverType = {
|
||||
threshold: number;
|
||||
rootMargin?: string;
|
||||
once?: boolean;
|
||||
};
|
||||
|
||||
export function useObserver<T extends Element = Element>({
|
||||
threshold,
|
||||
rootMargin = "0px",
|
||||
once = false
|
||||
}: ObserverType) {
|
||||
const [inView, setInView] = useState<boolean>();
|
||||
const ref = useRef<T>(null);
|
||||
const observer = useRef<IntersectionObserver>();
|
||||
|
||||
const updateInView = useCallback(
|
||||
(val: boolean) => {
|
||||
if (inView && once) {
|
||||
return;
|
||||
}
|
||||
setInView(val);
|
||||
},
|
||||
[inView, once]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const options = {
|
||||
root: null,
|
||||
rootMargin: rootMargin,
|
||||
threshold: threshold
|
||||
};
|
||||
|
||||
observer.current = new IntersectionObserver(
|
||||
debounce((entries) => {
|
||||
updateInView(entries[0].isIntersecting);
|
||||
}, 500) as IntersectionObserverCallback,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
observer?.current?.observe(ref.current);
|
||||
}
|
||||
|
||||
const reference = ref.current;
|
||||
|
||||
return () => {
|
||||
if (reference) {
|
||||
observer.current?.unobserve(reference);
|
||||
observer.current?.disconnect();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return { inView, ref };
|
||||
}
|
||||
@@ -39,7 +39,11 @@ import StarterKit from "@tiptap/starter-kit";
|
||||
import ListKeymap from "@tiptap/extension-list-keymap";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import "./extensions";
|
||||
import { AttachmentNode, AttachmentOptions } from "./extensions/attachment";
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentNode,
|
||||
AttachmentOptions
|
||||
} from "./extensions/attachment";
|
||||
import BulletList from "./extensions/bullet-list";
|
||||
import { CodeBlock } from "./extensions/code-block";
|
||||
import { Codemark } from "./extensions/code-mark";
|
||||
@@ -107,6 +111,7 @@ export type TiptapOptions = EditorOptions &
|
||||
downloadOptions?: DownloadOptions;
|
||||
isMobile?: boolean;
|
||||
doubleSpacedLines?: boolean;
|
||||
getAttachmentData: (attachment: Attachment) => Promise<string | undefined>;
|
||||
};
|
||||
|
||||
const useTiptap = (
|
||||
@@ -120,6 +125,7 @@ const useTiptap = (
|
||||
onOpenAttachmentPicker,
|
||||
onPreviewAttachment,
|
||||
onOpenLink,
|
||||
getAttachmentData,
|
||||
onBeforeCreate,
|
||||
downloadOptions,
|
||||
dateFormat,
|
||||
@@ -259,7 +265,8 @@ const useTiptap = (
|
||||
AttachmentNode.configure({
|
||||
onDownloadAttachment,
|
||||
onOpenAttachmentPicker,
|
||||
onPreviewAttachment
|
||||
onPreviewAttachment,
|
||||
types: [AttachmentNode.name, ImageNode.name, WebClipNode.name]
|
||||
}),
|
||||
OutlineListItem,
|
||||
OutlineList.configure({ keepAttributes: true, keepMarks: true }),
|
||||
@@ -310,6 +317,7 @@ const useTiptap = (
|
||||
editor.storage.portalProviderAPI = PortalProviderAPI;
|
||||
editor.storage.dateFormat = dateFormat;
|
||||
editor.storage.timeFormat = timeFormat;
|
||||
editor.storage.getAttachmentData = getAttachmentData;
|
||||
if (onBeforeCreate) onBeforeCreate({ editor });
|
||||
},
|
||||
injectCSS: false,
|
||||
@@ -319,6 +327,7 @@ const useTiptap = (
|
||||
onPreviewAttachment,
|
||||
onDownloadAttachment,
|
||||
onOpenAttachmentPicker,
|
||||
getAttachmentData,
|
||||
PortalProviderAPI,
|
||||
onBeforeCreate,
|
||||
onOpenLink,
|
||||
@@ -342,6 +351,7 @@ const useTiptap = (
|
||||
|
||||
export { type Fragment } from "prosemirror-model";
|
||||
export { type Attachment, type AttachmentType } from "./extensions/attachment";
|
||||
export { type ImageAttributes } from "./extensions/image";
|
||||
export * from "./extensions/react";
|
||||
export * from "./toolbar";
|
||||
export * from "./types";
|
||||
|
||||
@@ -119,7 +119,8 @@ import {
|
||||
mdiPencilOff,
|
||||
mdiPencil,
|
||||
mdiCheckboxMultipleBlankOutline,
|
||||
mdiCheckboxMultipleMarked
|
||||
mdiCheckboxMultipleMarked,
|
||||
mdiFormatFloatLeft
|
||||
} from "@mdi/js";
|
||||
|
||||
export const Icons = {
|
||||
@@ -167,6 +168,7 @@ export const Icons = {
|
||||
imageDownload: mdiProgressDownload,
|
||||
imageFailed: mdiProgressAlert,
|
||||
imageSettings: mdiImageEditOutline,
|
||||
imageFloat: mdiFormatFloatLeft,
|
||||
colorClear: mdiInvertColorsOff,
|
||||
save: mdiContentSaveOutline,
|
||||
copy: mdiClipboardOutline,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user