Compare commits

...

87 Commits

Author SHA1 Message Date
Ammar Ahmed
5e68dd129e Update android.publish.yml
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-02-27 01:03:48 +05:00
Ammar Ahmed
2350d0e41d mobile: minor fix 2024-02-27 01:02:39 +05:00
Ammar Ahmed
fbd5c8e3cd mobile: fix github release 2024-02-27 01:01:07 +05:00
Abdullah Atta
ee5c4077fe docs: improve skiff pages import guide 2024-02-13 01:07:14 +05:00
Abdullah Atta
47bbfc618e docs: add guide for migrating from skiff pages 2024-02-12 10:49:40 +05:00
Abdullah Atta
b377329a8b core: fix attachment mime type on read 2024-02-02 16:02:20 +05:00
Abdullah Atta
ae8bcf3865 ci: fix you may only define one of branches and branches-ignore for a single event
Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-31 12:46:32 +05:00
Abdullah Atta
2f67611829 ci: add workflows for v3 beta publishing 2024-01-31 12:41:08 +05:00
Ammar Ahmed
058c612f4f ci: update default build tools version android
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-01-30 11:41:20 +05:00
Ammar Ahmed
be8df90ccb iOS Publish Workflow (#4194)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-01-30 11:32:48 +05:00
Abdullah Atta
aa730b5b93 web: fix title only txt exports 2024-01-30 00:29:46 +05:00
Abdullah Atta
34bbcf6387 web: add feature highlights for v2.6.15 2024-01-29 23:33:15 +05:00
Abdullah Atta
33c3c78e2f desktop: get rid of configuration.win.signingHashAlgorithms should be an array 2024-01-29 23:27:45 +05:00
Ammar Ahmed
bf144dcda4 mobile: add ExportOptions.plist 2024-01-29 23:23:48 +05:00
Ammar Ahmed
284ebdb86f ci: @notesnook/ios
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-01-29 23:23:01 +05:00
Abdullah Atta
9dac4cf7f4 desktop: set signing has to sha256 2024-01-29 23:22:29 +05:00
Abdullah Atta
fb973a5514 ci: run build job no matter what 2024-01-29 23:22:29 +05:00
Abdullah Atta
f9226510f9 desktop: use full path for FilesCatalog 2024-01-29 23:22:29 +05:00
Abdullah Atta
5c0a964699 ci: add inputs to disable build for specific platforms 2024-01-29 23:22:29 +05:00
Abdullah Atta
41ffaab3dc ci: install azurecodesigning module 2024-01-29 23:22:29 +05:00
Abdullah Atta
764c721f1d desktop: add windows code signing 2024-01-29 23:22:29 +05:00
Ammar Ahmed
3d267f91ef mobile: bump version code 2024-01-29 19:55:41 +05:00
Ammar Ahmed
f10f28a7f8 mobile: release v2.6.15 2024-01-29 19:32:14 +05:00
Ammar Ahmed
8d04f89a18 mobile: remove progress bar 2024-01-29 19:06:01 +05:00
Ammar Ahmed
9d1374dcd4 editor: fix progress not showing in image on mobile 2024-01-29 19:05:49 +05:00
Abdullah Atta
094a65e514 editor: hide progress on image if 0 2024-01-29 18:26:44 +05:00
Ammar Ahmed
372da979e1 mobile: support ignoring edits 2024-01-29 18:26:44 +05:00
Abdullah Atta
d3371326f2 editor: add progress indicator to image 2024-01-29 18:26:44 +05:00
Abdullah Atta
57452bd0e6 core: allow setting custom date edited 2024-01-29 18:26:44 +05:00
Abdullah Atta
993b8460bf web: do not update dateEdited if ignoring edits 2024-01-29 18:26:44 +05:00
Abdullah Atta
081201aa42 editor: add support for ignoring edits 2024-01-29 18:26:44 +05:00
Abdullah Atta
270e6778d1 editor: optimize image loading when fast scrolling 2024-01-29 16:23:59 +05:00
Abdullah Atta
2074320583 editor: revoke blob urls on image unmount 2024-01-29 16:09:05 +05:00
Abdullah Atta
cd90030616 web: fix note title in toolbar not changing 2024-01-29 15:45:13 +05:00
Abdullah Atta
807bd683b7 web: do not show success prompt on unlock cancel 2024-01-29 15:36:01 +05:00
Abdullah Atta
35929026a4 web: monograph.notesnook.com > monogr.ph 2024-01-29 15:22:22 +05:00
Ammar Ahmed
41eb94ee47 mobile: fix exporting locked notes 2024-01-29 11:02:32 +05:00
Ammar Ahmed
ba51e110ad mobile: fix opening webclips 2024-01-29 11:02:32 +05:00
Ammar Ahmed
ecad06d249 mobile: disable rtl mode 2024-01-29 11:02:32 +05:00
Abdullah Atta
19a151f637 editor: fix image query matching all images in editor 2024-01-28 17:17:45 +05:00
Ammar Ahmed
7c4a6f1e17 global: update deps 2024-01-27 23:06:24 +05:00
Ammar Ahmed
bc88e953d1 mobile: make sure tools render in toolbar 2024-01-27 23:05:30 +05:00
Ammar Ahmed
7d89ec676a mobile: fix attachments not loading when added from share ext 2024-01-27 22:50:41 +05:00
Ammar Ahmed
c8f8bcca93 mobile: organize bulk exports 2024-01-27 21:23:29 +05:00
Abdullah Atta
9fa904e849 web: fix exporting of single note 2024-01-27 21:23:29 +05:00
Abdullah Atta
05cc947cb4 web: fix pdf exports 2024-01-27 21:23:29 +05:00
alihamuh
a20a683b17 web: code refactoring 2024-01-27 21:23:29 +05:00
alihamuh
5fc43c931e web: added directory paths for exports 2024-01-27 21:23:29 +05:00
Abdullah Atta
0879c3b4d6 core: fix formatting of single spaced paragarphs in markdown exports (#4186)
* core: update showdown library to support single spaced paragraphs

* core: add tests

* core: update showdown library
2024-01-27 18:59:35 +05:00
Abdullah Atta
2eded71de3 core: fix restored attachments not syncing 2024-01-27 16:06:04 +05:00
Abdullah Atta
9e30ee50b8 core: add logs 2024-01-27 16:06:04 +05:00
Abdullah Atta
08621a5873 core: try to preserve mime type on backup restore 2024-01-27 16:06:04 +05:00
Abdullah Atta
17f5bd28ba web: do not overwrite filename & mimetype on attachment reupload 2024-01-27 16:06:04 +05:00
Ammar Ahmed
4558c0b2eb mobile: fix downloading attachments from editor 2024-01-27 16:06:04 +05:00
Abdullah Atta
a442677e91 editor: fix loading of images that have wrong mime type 2024-01-27 16:06:04 +05:00
Abdullah Atta
95666bee36 editor: remove attachment progress once it reaches 100 2024-01-27 16:06:04 +05:00
Abdullah Atta
c4232ac95a web: do not send hash when updating attachment 2024-01-27 16:06:04 +05:00
Abdullah Atta
0283037289 mobile: changes related to download cancellation 2024-01-27 16:06:04 +05:00
Abdullah Atta
b6fd0df715 web: get rid of all attachment loading logic 2024-01-27 16:06:04 +05:00
Abdullah Atta
f53fb58024 editor: eagerly load image/webclip instead of waiting on client 2024-01-27 16:06:04 +05:00
Abdullah Atta
21d452308d core: simplify download cancellation 2024-01-27 16:06:04 +05:00
Ammar Ahmed
823608cca1 mobile: daily trash cleanup 2024-01-27 16:05:28 +05:00
alihamuh
f3a0b998e4 web: added daily trash interval option 2024-01-27 16:05:28 +05:00
theProton
a47a38b16b web: fix incorrect title on scrolling note (#4137)
Signed-off-by: Parthiv-M <parthivmenon.dev@gmail.com>
2024-01-27 11:44:58 +05:00
Ammar Ahmed
a184c53dd5 mobile: fix loading empty conent in editor on tablets 2024-01-26 23:02:06 +05:00
Abdullah Atta
a3464255ec core: always use attachment mime type from backup if possible 2024-01-26 19:21:55 +05:00
Abdullah Atta
088c6670a9 core: fix crash if note title only contains whitespace 2024-01-26 19:12:41 +05:00
Abdullah Atta
27840b9bd6 editor: fix link pasting (#4178) 2024-01-26 18:57:59 +05:00
Muhammad Ali
ae819504f0 editor: fix input rules not working at the end of note (#4130)
* web: added condition in getDeletedNodes

* editor: some corrections

* editor: merge if conditions

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-26 16:43:19 +05:00
Muhammad Ali
af792e394c web: add support for moving locked notes to trash (#4134)
* web: added support for single/multi trash notes

* web: show error when trying to preview a locked note in trash

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-26 16:37:54 +05:00
Muhammad Ali
76a4d5c2a5 editor: fix subtasks getting unchecked on drag/drop reorder (#4135)
* editor: add a condition to check nested item state

* editor: force coerce value to boolean

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-26 16:31:23 +05:00
Muhammad Ali
4e23acd608 web: fix realtime editor sync cannot be disabled (#4140) 2024-01-26 16:22:56 +05:00
Ammar Ahmed
3c65dcdd8b ci: bump android build tools version 2024-01-15 12:31:37 +05:00
Ammar Ahmed
efea6e3972 theme: add missing alpha colors 2024-01-15 12:31:11 +05:00
Ammar Ahmed
185cca7880 mobile: release v2.6.15 2024-01-12 20:10:58 +05:00
Abdullah Atta
fbded93405 global: update lockfiles 2024-01-05 22:10:05 +05:00
Abdullah Atta
674881b873 web: add zip64 support for backups, export & imports (#4079)
* web: use @zip.js/zip.js for reading zip files

* web: always use zip streams for writing zips

* core: fix types

* theme: update lockfile
2024-01-05 22:05:04 +05:00
Abdullah Atta
248fb6dd2d web: bump version to 2.6.14 2024-01-05 21:55:18 +05:00
Abdullah Atta
3211e7a149 global: update lockfiles 2024-01-05 21:54:06 +05:00
Abdullah Atta
08496c699e editor: fix font size changes back after exiting list (fixes #3602) 2024-01-05 21:48:00 +05:00
Abdullah Atta
060fa25814 core: support underline when exporting as md (fix #3990) 2024-01-05 20:53:14 +05:00
Abdullah Atta
a6b1effc77 core: remove relations auto cleanup 2024-01-05 16:24:02 +05:00
Abdullah Atta
5cf9c4808e web: show resubscribe button if subscription is canceled 2024-01-05 15:55:36 +05:00
Muhammad Ali
d5a44067fd web: fix editor header title input not becoming readonly (#4064) 2024-01-05 11:08:16 +05:00
Muhammad Ali
0de463526f editor: add support for +/- font-size using shortcut keys (#4029) 2024-01-05 10:40:29 +05:00
Muhammad Ali
5fd79875ef editor: fix math/formula block getting cut off on mobile (#4023) 2024-01-05 10:39:20 +05:00
Muhammad Ali
bdb19ad325 web: add license to settings (#4020) 2024-01-05 10:38:13 +05:00
134 changed files with 3985 additions and 1831 deletions

View File

@@ -1,4 +1,4 @@
name: Publish @notesnook/mobile
name: Publish @notesnook/android
on: workflow_dispatch
@@ -28,30 +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 }}
- 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
@@ -64,6 +40,8 @@ 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
run: |

View File

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

View File

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

View 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
View File

@@ -9,4 +9,5 @@ nx-cloud.env
.env.local
.nyc_output
site
node_modules.backup
node_modules.backup
.nx

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "2.6.13",
"version": "2.6.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "2.6.13",
"version": "2.6.15",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "2.6.13",
"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
View File

@@ -0,0 +1,60 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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"
});
}

View File

@@ -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 = () => {

View File

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

View File

@@ -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,
{

View File

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

View File

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

View File

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

View File

@@ -101,6 +101,7 @@ const SheetProvider = ({ context = "global" }) => {
setVisible(false);
setData(null);
}}
keyboardHandlerDisabled={data?.keyboardHandlerDisabled}
bottomPadding={!data.noBottomPadding}
enableGesturesInScrollView={
typeof data.enableGesturesInScrollView === "undefined"

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -97,6 +97,7 @@ export type SavePayload = {
type?: Content["type"];
sessionId?: string | null;
sessionHistoryId?: number;
ignoreEdit: boolean;
};
export type AppState = {

View File

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

View File

@@ -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"> & {
@@ -408,13 +359,13 @@ export const useEditor = (
await commands.setStatus(getFormattedDate(item.dateEdited), "Saved");
await postMessage(EditorEvents.title, item.title);
loadingState.current = currentContent.current?.data;
if (currentContent.current?.data) {
await postMessage(
EditorEvents.html,
currentContent.current?.data,
10000
);
}
await postMessage(
EditorEvents.html,
currentContent.current?.data || "",
10000
);
loadingState.current = undefined;
useEditorStore.getState().setReadonly(item.readonly);
await commands.setTags(currentNote.current);
@@ -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
};

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,10 @@
- Added support to check/uncheck all items in a checklist
- Now you can open pdfs/images in readonly mode
- Fixed a bug where users are unable to login when app lock is enabled
- 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!

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

View File

@@ -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 = 2065;
CURRENT_PROJECT_VERSION = 2067;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1071,7 +1071,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.14;
MARKETING_VERSION = 2.6.15;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1102,7 +1102,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2065;
CURRENT_PROJECT_VERSION = 2067;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1176,7 +1176,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.14;
MARKETING_VERSION = 2.6.15;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1335,7 +1335,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2065;
CURRENT_PROJECT_VERSION = 2067;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1347,7 +1347,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.14;
MARKETING_VERSION = 2.6.15;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1378,7 +1378,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2065;
CURRENT_PROJECT_VERSION = 2067;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1391,7 +1391,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.14;
MARKETING_VERSION = 2.6.15;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1421,7 +1421,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2065;
CURRENT_PROJECT_VERSION = 2067;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1495,7 +1495,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.14;
MARKETING_VERSION = 2.6.15;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1526,7 +1526,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2065;
CURRENT_PROJECT_VERSION = 2067;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1601,7 +1601,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.14;
MARKETING_VERSION = 2.6.15;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -931,4 +931,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: b0cadd188aa428b98d1eaa0b84b3b6208e714119
COCOAPODS: 1.12.1
COCOAPODS: 1.14.2

File diff suppressed because one or more lines are too long

View File

@@ -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.1-alpha.2",
"@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.1-alpha.2",
"@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",

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "2.6.14",
"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",
@@ -44,4 +45,4 @@
"react-native": "0.72.0",
"react-native-iap": "12.11.0"
}
}
}

View File

@@ -1377,7 +1377,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "2.6.12",
"version": "2.6.15",
"license": "GPL-3.0-or-later",
"dependencies": {
"@aws-sdk/util-base64-browser": "^3.208.0",
@@ -1388,7 +1388,7 @@
"@emotion/react": "11.11.1",
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.1",
"@notesnook-importer/core": "^1.7.1",
"@notesnook-importer/core": "^2.0.0",
"@notesnook/common": "file:../../packages/common",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",
@@ -1408,6 +1408,7 @@
"@theme-ui/core": "^0.14.7",
"@trpc/client": "10.38.3",
"@trpc/react-query": "10.38.3",
"@zip.js/zip.js": "^2.7.32",
"allotment": "^1.19.0",
"axios": "^1.3.4",
"clipboard-polyfill": "4.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "2.6.13",
"version": "2.6.15",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -16,7 +16,7 @@
"@emotion/react": "11.11.1",
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.1",
"@notesnook-importer/core": "^1.7.1",
"@notesnook-importer/core": "^2.0.0",
"@notesnook/common": "file:../../packages/common",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",
@@ -36,6 +36,7 @@
"@theme-ui/core": "^0.14.7",
"@trpc/client": "10.38.3",
"@trpc/react-query": "10.38.3",
"@zip.js/zip.js": "^2.7.32",
"allotment": "^1.19.0",
"axios": "^1.3.4",
"clipboard-polyfill": "4.0.0",

View File

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

View File

@@ -17,21 +17,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { db } from "./db";
import { TaskManager } from "./task-manager";
import { zip } from "../utils/zip";
import { saveAs } from "file-saver";
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 { sanitizeFilename } from "@notesnook/common";
import Vault from "./vault";
const FORMAT_TO_EXT = {
pdf: "pdf",
md: "md",
txt: "txt",
html: "html",
"md-frontmatter": "md"
} as const;
import { db } from "./db";
import Note from "@notesnook/core/dist/models/note";
import { sanitizeFilename } from "@notesnook/common";
export async function exportToPDF(
title: string,
@@ -85,76 +79,91 @@ export async function exportNotes(
title: "Exporting notes",
subtitle: "Please wait while your notes are exported.",
action: async (report) => {
let vaultUnlocked = false;
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."
);
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);
}
const files = [];
let index = 0;
for (const noteId of noteIds) {
const note = db.notes?.note(noteId);
if (!note) continue;
if (!vaultUnlocked && note.data.locked) continue;
report({
current: ++index,
total: noteIds.length,
text: `Exporting "${note.title}"...`
});
const rawContent = await db.content?.raw(note.data.contentId);
const content = note.data.locked
? await db.vault?.decryptContent(rawContent)
: rawContent;
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}`
);
});
if (typeof exported !== "string") {
showToast("error", `Failed to export note "${note.title}"`);
continue;
}
if (format === "pdf") {
return await exportToPDF(note.title, exported);
}
files.push({
filename: note.title,
content: exported,
date: note.dateEdited
});
}
if (!files.length) return false;
if (files.length === 1) {
saveAs(
new Blob([Buffer.from(files[0].content, "utf-8")]),
`${sanitizeFilename(files[0].filename)}.${FORMAT_TO_EXT[format]}`
);
} else {
const zipped = await zip(files, FORMAT_TO_EXT[format]);
saveAs(new Blob([zipped.buffer]), "notes.zip");
}
return true;
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
};
}

View File

@@ -36,9 +36,9 @@ import { TaskManager } from "./task-manager";
import { EVENTS } from "@notesnook/core/dist/common";
import { getFormattedDate } from "@notesnook/common";
import { createWritableStream } from "./desktop-bridge";
import { ZipStream } from "../utils/streams/zip-stream";
import { createZipStream } from "../utils/streams/zip-stream";
import { FeatureKeys } from "../dialogs/feature-dialog";
import { Entry, Reader } from "../utils/zip-reader";
import { ZipEntry, createUnzipIterator } from "../utils/streams/unzip-stream";
export const CREATE_BUTTON_MAP = {
notes: {
@@ -113,7 +113,7 @@ export async function createBackup() {
controller.close();
}
})
.pipeThrough(new ZipStream())
.pipeThrough(createZipStream())
.pipeTo(writeStream);
}
});
@@ -166,12 +166,12 @@ export async function restoreBackupFile(backupFile: File) {
type: "modal",
action: async (report) => {
let cachedPassword: string | undefined = undefined;
const { read, totalFiles } = await Reader(backupFile);
const entries: Entry[] = [];
// const { read, totalFiles } = await Reader(backupFile);
const entries: ZipEntry[] = [];
let filesProcessed = 0;
let isValid = false;
for await (const entry of read()) {
for await (const entry of createUnzipIterator(backupFile)) {
if (entry.name === ".nnbackup") {
isValid = true;
continue;
@@ -200,7 +200,6 @@ export async function restoreBackupFile(backupFile: File) {
}
report({
total: totalFiles,
text: `Processed ${entry.name}`,
current: filesProcessed++
});

View File

@@ -35,20 +35,11 @@ type Item = {
};
async function moveNotesToTrash(notes: Item[], confirm = true) {
const item = notes[0];
if (confirm && !(await showMultiDeleteConfirmation(notes.length))) return;
if (notes.length === 1) {
if (
item.locked &&
!(await Vault.unlockNote(item.id, "unlock_and_delete_note"))
)
return;
item.locked = false;
}
if (notes.some((n) => n.locked) && !(await Vault.unlockVault())) return;
const items = notes.map((item) => {
if (item.locked || db.monographs?.isPublished(item.id)) return 0;
if (db.monographs?.isPublished(item.id)) return 0;
return item.id;
});

View File

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

View File

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

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

View File

@@ -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,
@@ -51,6 +51,7 @@ function Toolbar() {
const sessionId = useStore((store) => store.session.id);
const isDeleted = useStore((store) => store.session.isDeleted);
const isLocked = useStore((store) => store.session.locked);
const readonly = useStore((store) => store.session.readonly);
const [isFullscreen, setIsFullscreen] = useState(false);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const toggleFocusMode = useAppStore((store) => store.toggleFocusMode);
@@ -62,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();
@@ -91,6 +93,10 @@ function Toolbar() {
};
}, [isTitleVisible]);
useEffect(() => {
if (onScrollTitleRef.current) onScrollTitleRef.current.value = title;
}, [title]);
const tools = useMemo(
() => [
{
@@ -215,6 +221,7 @@ function Toolbar() {
}}
/>
<AnimatedInput
ref={onScrollTitleRef}
variant="clean"
ml={[2, 2, 0]}
initial={{
@@ -227,6 +234,7 @@ function Toolbar() {
}}
transition={{ duration: 0.5 }}
defaultValue={title}
readOnly={readonly}
onChange={(e) => {
AppEventManager.publish(AppEvents.changeNoteTitle, {
title: e.target.value,

View File

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

View File

@@ -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,
@@ -539,7 +560,7 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
icon: Trash.path,
isDisabled:
items.length === 1
? db.monographs?.isPublished(note.id) || note.locked
? db.monographs?.isPublished(note.id)
: items.some((item) => !db.notes?.note(item.id).synced()),
onClick: () => Multiselect.moveNotesToTrash(items, items.length > 1),
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)

View File

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

View File

@@ -59,7 +59,9 @@ function TrashItem(props: TrashItemProps) {
menuItems={menuItems}
onClick={() => {
if (item.itemType === "note")
hashNavigate(`/notes/${item.id}/edit`, { replace: true });
!item.locked
? hashNavigate(`/notes/${item.id}/edit`, { replace: true })
: showToast("error", "Locked notes cannot be previewed in trash.");
}}
/>
);

View File

@@ -91,7 +91,19 @@ const features: Record<FeatureKeys, Feature> = {
]
: [
{
title: "Check/uncheck all tasks in a task list"
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: {

View File

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

View File

@@ -149,13 +149,9 @@ export function Importer() {
setErrors((errors) => [...errors, message.error]);
break;
case "progress": {
const { count, filesRead, totalFiles } = message;
const { count } = message;
if (notesCounter.current)
notesCounter.current.innerText = `${count}`;
if (importProgress.current)
importProgress.current.style.width = `${
(filesRead / totalFiles) * 100
}%`;
break;
}
}

View File

@@ -196,40 +196,27 @@ export function SubscriptionStatus() {
Request a refund
</Button>
</>
) : isBasic ? (
<>
<Button
variant="accent"
onClick={async () => {
showBuyDialog();
}}
>
Upgrade to Pro
</Button>
<Button
variant="secondary"
onClick={activateTrial}
sx={{ bg: "background" }}
>
{isActivatingTrial ? (
<Loading size={16} />
) : (
"Try free for 14 days"
)}
</Button>
</>
) : isTrial ? (
<>
<Button
variant="accent"
onClick={async () => {
showBuyDialog();
}}
>
Upgrade to Pro
</Button>
</>
) : null}
{!isPro && (
<>
<Button variant="accent" onClick={() => showBuyDialog()}>
{isProCancelled ? "Resubscribe" : "Upgrade to Pro"}
</Button>
{isBasic && (
<Button
variant="secondary"
onClick={activateTrial}
sx={{ bg: "background" }}
>
{isActivatingTrial ? (
<Loading size={16} />
) : (
"Try free for 14 days"
)}
</Button>
)}
</>
)}
</Flex>
</Flex>
{isBasic ? <Features /> : null}

View File

@@ -75,16 +75,17 @@ export const AboutSettings: SettingsGroup[] = [
{
key: "source-code",
title: "Source code",
description: "All the source code for Notesnook is available & open for everyone on GitHub.",
description:
"All the source code for Notesnook is available & open for everyone on GitHub.",
components: [
{
type: "button",
action: () => {
window.open(
"https://github.com/streetwriters/notesnook",
"_blank"
);
},
window.open(
"https://github.com/streetwriters/notesnook",
"_blank"
);
},
title: "View source code",
variant: "secondary"
}
@@ -234,6 +235,23 @@ export const LegalSettings: SettingsGroup[] = [
variant: "secondary"
}
]
},
{
key: "license",
title: "License",
description: "GNU GENERAL PUBLIC LICENSE Version 3",
components: [
{
type: "button",
action: () =>
void window.open(
"https://github.com/streetwriters/notesnook/blob/master/LICENSE",
"_blank"
),
title: "Read License",
variant: "secondary"
}
]
}
]
}

View File

@@ -184,7 +184,7 @@ class AppStore extends BaseStore {
toggleRealtimeSync = () => {
const { isRealtimeSyncEnabled } = this.get();
Config.set("realtimeSyncEnabled", !isRealtimeSyncEnabled);
Config.set("isRealtimeSyncEnabled", !isRealtimeSyncEnabled);
this.set(
(state) => (state.isRealtimeSyncEnabled = !state.isRealtimeSyncEnabled)
);

View File

@@ -24,7 +24,7 @@ import { store as editorStore } from "./editor-store";
import { checkAttachment } from "../common/attachments";
import { showToast } from "../utils/toast";
import { AttachmentStream } from "../utils/streams/attachment-stream";
import { ZipStream } from "../utils/streams/zip-stream";
import { createZipStream } from "../utils/streams/zip-stream";
import { createWriteStream } from "../utils/stream-saver";
let abortController = undefined;
@@ -67,7 +67,7 @@ class AttachmentStore extends BaseStore {
}
);
await attachmentStream
.pipeThrough(new ZipStream())
.pipeThrough(createZipStream(abortController.signal))
.pipeTo(
await createWriteStream("attachments.zip", {
signal: abortController.signal

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -27,8 +27,8 @@ import {
ATTACHMENTS_DIRECTORY_NAME,
NOTE_DATA_FILENAME
} from "@notesnook-importer/core/dist/src/utils/note-stream";
import { Reader, Entry } from "./zip-reader";
import { path } from "@notesnook-importer/core/dist/src/utils/path";
import { ZipEntry, createUnzipIterator } from "./streams/unzip-stream";
export async function* importFiles(zipFiles: File[]) {
for (const zip of zipFiles) {
@@ -36,9 +36,8 @@ export async function* importFiles(zipFiles: File[]) {
let filesRead = 0;
const attachments: Record<string, any> = {};
const { read, totalFiles } = await Reader(zip);
for await (const entry of read()) {
for await (const entry of createUnzipIterator(zip)) {
++filesRead;
const isAttachment = entry.name.includes(
@@ -60,7 +59,6 @@ export async function* importFiles(zipFiles: File[]) {
yield {
type: "progress" as const,
count,
totalFiles,
filesRead
};
}
@@ -68,7 +66,7 @@ export async function* importFiles(zipFiles: File[]) {
}
async function processAttachment(
entry: Entry,
entry: ZipEntry,
attachments: Record<string, any>
) {
const name = path.basename(entry.name);
@@ -90,7 +88,7 @@ async function processAttachment(
attachments[name] = { ...cipherData, key };
}
async function processNote(entry: Entry, attachments: Record<string, any>) {
async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
const note = await fileToJson<Note>(entry);
for (const attachment of note.attachments || []) {
const cipherData = attachments[attachment.hash];
@@ -128,7 +126,7 @@ async function processNote(entry: Entry, attachments: Record<string, any>) {
}
}
async function fileToJson<T>(file: Entry) {
async function fileToJson<T>(file: ZipEntry) {
const text = await file.text();
return JSON.parse(text) as T;
}

View File

@@ -23,27 +23,36 @@ import {
logManager
} from "@notesnook/core/dist/logger";
import { LogMessage } from "@notesnook/logger";
import FileSaver from "file-saver";
import { DatabasePersistence, NNStorage } from "../interfaces/storage";
import { zip } from "./zip";
import { ZipFile, createZipStream } from "./streams/zip-stream";
import { createWriteStream } from "./stream-saver";
let logger: typeof _logger;
async function initalizeLogger(persistence: DatabasePersistence = "db") {
initalize(await NNStorage.createInstance("Logs", persistence));
initalize(await NNStorage.createInstance("Logs", persistence), false);
logger = _logger.scope("notesnook-web");
}
async function downloadLogs() {
if (!logManager) return;
const allLogs = await logManager.get();
const files = allLogs.map((log) => ({
filename: log.key,
content: (log.logs as LogMessage[])
.map((line) => JSON.stringify(line))
.join("\n")
}));
const archive = await zip(files, "log");
FileSaver.saveAs(new Blob([archive.buffer]), "notesnook-logs.zip");
const textEncoder = new TextEncoder();
await new ReadableStream<ZipFile>({
pull(controller) {
for (const log of allLogs) {
controller.enqueue({
path: log.key,
data: textEncoder.encode(
(log.logs as LogMessage[])
.map((line) => JSON.stringify(line))
.join("\n")
)
});
}
}
})
.pipeThrough(createZipStream())
.pipeTo(await createWriteStream("notesnook-logs.zip"));
}
async function clearLogs() {

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { db } from "../../common/db";
import { lazify } from "../lazify";
import { makeUniqueFilename } from "./utils";
import { ZipFile } from "./zip-stream";
export const METADATA_FILENAME = "metadata.json";
@@ -84,17 +85,3 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
});
}
}
function makeUniqueFilename(
filePath: string,
counters: Record<string, number>
) {
filePath = filePath.toLowerCase();
counters[filePath] = (counters[filePath] || 0) + 1;
if (counters[filePath] === 1) return filePath;
const parts = filePath.split(".");
return `${parts.slice(0, -1).join(".")}-${counters[filePath]}.${
parts[parts.length - 1]
}`;
}

View File

@@ -0,0 +1,96 @@
/*
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 Note from "@notesnook/core/dist/models/note";
import { db } from "../../common/db";
import { exportNote } from "../../common/export";
import { makeUniqueFilename } from "./utils";
import { ZipFile } from "./zip-stream";
export class ExportStream extends TransformStream<Note, ZipFile> {
constructor(
format: "pdf" | "md" | "txt" | "html" | "md-frontmatter",
signal?: AbortSignal
) {
const counters: Record<string, number> = {};
super({
async transform(note, controller) {
try {
if (signal?.aborted) {
controller.terminate();
return;
}
if (!note || format === "pdf") return;
const result = await exportNote(note, format);
if (!result) return;
const { filename, content } = result;
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)
});
});
} catch (e) {
controller.error(e);
}
}
});
}
}

View File

@@ -0,0 +1,101 @@
/*
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 { Deflate as FflateDeflate, Inflate as FflateInflate } from "fflate";
function initShimAsyncCodec(library, options = {}, registerDataHandler) {
return {
Deflate: createCodecClass(
library.Deflate,
options.deflate,
registerDataHandler
),
Inflate: createCodecClass(
library.Inflate,
options.inflate,
registerDataHandler
)
};
}
function objectHasOwn(object, propertyName) {
// eslint-disable-next-line no-prototype-builtins
return typeof Object.hasOwn === "function"
? Object.hasOwn(object, propertyName)
: // eslint-disable-next-line no-prototype-builtins
object.hasOwnProperty(propertyName);
}
function createCodecClass(
constructor,
constructorOptions,
registerDataHandler
) {
return class {
constructor(options) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const codecAdapter = this;
const onData = (data) => {
if (codecAdapter.pendingData) {
const previousPendingData = codecAdapter.pendingData;
codecAdapter.pendingData = new Uint8Array(
previousPendingData.length + data.length
);
const { pendingData } = codecAdapter;
pendingData.set(previousPendingData, 0);
pendingData.set(data, previousPendingData.length);
} else {
codecAdapter.pendingData = new Uint8Array(data);
}
};
if (objectHasOwn(options, "level") && options.level === undefined) {
delete options.level;
}
codecAdapter.codec = new constructor(
Object.assign({}, constructorOptions, options)
);
registerDataHandler(codecAdapter.codec, onData);
}
append(data) {
this.codec.push(data);
return getResponse(this);
}
flush() {
this.codec.push(new Uint8Array(), true);
return getResponse(this);
}
};
function getResponse(codec) {
if (codec.pendingData) {
const output = codec.pendingData;
codec.pendingData = null;
return output;
} else {
return new Uint8Array();
}
}
}
const { Deflate, Inflate } = initShimAsyncCodec(
{ Deflate: FflateDeflate, Inflate: FflateInflate },
undefined,
(codec, onData) => (codec.ondata = onData)
);
export { Deflate, Inflate };

View File

@@ -0,0 +1,77 @@
/*
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 { Deflate, Inflate } from "./fflate-shim";
import {
Entry,
TextWriter,
Uint8ArrayWriter,
ZipReader,
configure
} from "@zip.js/zip.js";
configure({ Deflate, Inflate });
export async function* createUnzipIterator(file: File) {
const reader = new ZipReader(file.stream());
for await (const entry of reader.getEntriesGenerator()) {
yield new ZipEntry(entry);
}
await reader.close();
}
export class ZipEntry extends Blob {
private ts = new TransformStream();
constructor(public readonly entry: Entry) {
super();
}
stream(): ReadableStream<Uint8Array> {
this.entry.getData?.(this.ts.writable);
return this.ts.readable;
}
async text(): Promise<string> {
const writer = new TextWriter("utf-8");
await this.entry.getData?.(writer);
return await writer.getData();
}
get size() {
return this.entry.uncompressedSize;
}
get name() {
return this.entry.filename;
}
async arrayBuffer(): Promise<ArrayBuffer> {
const writer = new Uint8ArrayWriter();
await this.entry.getData?.(writer);
return (await writer.getData()).buffer;
}
slice(
_start?: number | undefined,
_end?: number | undefined,
_contentType?: string | undefined
): Blob {
throw new Error("Slice is not supported.");
}
}

View File

@@ -0,0 +1,35 @@
/*
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 { path } from "@notesnook-importer/core/dist/src/utils/path";
export function makeUniqueFilename(
filePath: string,
counters: Record<string, number>
) {
const matchablePath = filePath.toLowerCase();
const count = (counters[matchablePath] = (counters[matchablePath] || 0) + 1);
if (count === 1) return filePath;
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);
}

View File

@@ -17,32 +17,62 @@ 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 { Zip, ZipDeflate } from "fflate";
import { Deflate, Inflate } from "./fflate-shim";
import {
Uint8ArrayReader,
TextReader,
ZipWriter,
configure
} from "@zip.js/zip.js";
export type ZipFile = { path: string; data: Uint8Array };
export class ZipStream extends TransformStream<ZipFile, Uint8Array> {
constructor() {
const zipper = new Zip();
super({
start(controller) {
zipper.ondata = (err, data) => {
if (err) {
controller.error(err);
} else controller.enqueue(data);
};
},
transform(chunk) {
const fileStream = new ZipDeflate(chunk.path, {
level: 5,
mem: 8
configure({ Deflate, Inflate });
export type ZipFile = {
path: string;
data: string | Uint8Array;
mtime?: Date;
ctime?: Date;
};
export function createZipStream(signal?: AbortSignal) {
const written = new Set<string>();
const ts = new TransformStream<Uint8Array, Uint8Array>();
const writer = new ZipWriter<Uint8Array>(ts.writable, {
zip64: true,
signal
});
const entryWriter = new WritableStream<ZipFile>({
start() {},
async write(chunk, c) {
// zip.js doesn't support overwriting files.
if (written.has(chunk.path)) return;
await writer
.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);
c.error(e);
});
zipper.add(fileStream);
fileStream.push(chunk.data, true);
},
flush() {
zipper.end();
zipper.terminate();
}
});
}
written.add(chunk.path);
},
async close() {
await writer.close();
await ts.writable.close();
},
async abort(reason) {
await ts.writable.abort(reason);
await ts.readable.cancel(reason);
}
});
return { writable: entryWriter, readable: ts.readable };
}

View File

@@ -1,418 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-underscore-dangle */
/**
* Conflux
* Read (and build) zip files with whatwg streams in the browser.
*
* @author Transcend Inc. <https://transcend.io>
* @license MIT
*/
import { Inflate } from "fflate";
const ERR_BAD_FORMAT = "File format is not recognized.";
const ZIP_COMMENT_MAX = 65536;
const EOCDR_MIN = 22;
const EOCDR_MAX = EOCDR_MIN + ZIP_COMMENT_MAX;
const MAX_VALUE_32BITS = 0xffffffff;
const decoder = new TextDecoder();
const uint16e = (b: Uint8Array, n: number) => b[n] | (b[n + 1] << 8);
export class Entry {
private dataView: DataView;
private _fileLike: File;
private _extraFields: Record<string, DataView>;
constructor(dataView: DataView, fileLike: File) {
if (dataView.getUint32(0) !== 0x504b0102) {
throw new Error("ERR_BAD_FORMAT");
}
const dv = dataView;
this.dataView = dv;
this._fileLike = fileLike;
this._extraFields = {};
for (let i = 46 + this.filenameLength; i < dv.byteLength; ) {
const id = dv.getUint16(i, true);
const len = dv.getUint16(i + 2, true);
const start = dv.byteOffset + i + 4;
this._extraFields[id] = new DataView(dv.buffer.slice(start, start + len));
i += len + 4;
}
}
get webkitRelativePath(): string {
return this.name;
}
get type(): string {
return "application/octet-stream";
}
get versionMadeBy() {
return this.dataView.getUint16(4, true);
}
get versionNeeded() {
return this.dataView.getUint16(6, true);
}
get bitFlag() {
return this.dataView.getUint16(8, true);
}
get encrypted() {
return (this.bitFlag & 0x0001) === 0x0001;
}
get compressionMethod() {
return this.dataView.getUint16(10, true);
}
get crc32() {
return this.dataView.getUint32(16, true);
}
get compressedSize() {
return this.dataView.getUint32(20, true);
}
get filenameLength() {
return this.dataView.getUint16(28, true);
}
get extraFieldLength() {
return this.dataView.getUint16(30, true);
}
get commentLength() {
return this.dataView.getUint16(32, true);
}
get diskNumberStart() {
return this.dataView.getUint16(34, true);
}
get internalFileAttributes() {
return this.dataView.getUint16(36, true);
}
get externalFileAttributes() {
return this.dataView.getUint32(38, true);
}
get directory() {
return !!(this.dataView.getUint8(38) & 16);
}
get offset() {
return this.dataView.getUint32(42, true);
}
get zip64() {
return this.dataView.getUint32(24, true) === MAX_VALUE_32BITS;
}
get comment() {
const dv = this.dataView;
const uint8 = new Uint8Array(
dv.buffer,
dv.byteOffset + this.filenameLength + this.extraFieldLength + 46,
this.commentLength
);
return decoder.decode(uint8);
}
// File like IDL methods
get lastModifiedDate() {
const t = this.dataView.getUint32(12, true);
return new Date(
// Date.UTC(
((t >> 25) & 0x7f) + 1980, // year
((t >> 21) & 0x0f) - 1, // month
(t >> 16) & 0x1f, // day
(t >> 11) & 0x1f, // hour
(t >> 5) & 0x3f, // minute
(t & 0x1f) << 1
// ),
);
}
get lastModified() {
return +this.lastModifiedDate;
}
get name() {
if (!this.bitFlag && this._extraFields && this._extraFields[0x7075]) {
return decoder.decode(this._extraFields[0x7075].buffer.slice(5));
}
const dv = this.dataView;
const uint8 = new Uint8Array(
dv.buffer,
dv.byteOffset + 46,
this.filenameLength
);
return decoder.decode(uint8);
}
get size() {
const size = this.dataView.getUint32(24, true);
return size === MAX_VALUE_32BITS ? this._extraFields[1].getUint8(0) : size;
}
stream() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const crc = new Crc32();
let inflator: Inflate | undefined;
const onEnd = (ctrl: ReadableStreamController<Uint8Array>) =>
crc.get() === self.crc32
? ctrl.close()
: ctrl.error(new Error("The crc32 checksum don't match"));
return new ReadableStream<Uint8Array>({
async start(ctrl) {
// Need to read local header to get fileName + extraField length
// Since they are not always the same length as in central dir...
const ab = await self._fileLike
.slice(self.offset + 26, self.offset + 30)
.arrayBuffer();
const bytes = new Uint8Array(ab);
const localFileOffset = uint16e(bytes, 0) + uint16e(bytes, 2) + 30;
const start = self.offset + localFileOffset;
const end = start + self.compressedSize;
(this as any).reader = self._fileLike
.slice(start, end)
.stream()
.getReader();
if (self.compressionMethod) {
inflator = new Inflate();
inflator.ondata = (chunk, final) => {
crc.append(chunk);
ctrl.enqueue(chunk);
if (final) onEnd(ctrl);
};
}
},
async pull(ctrl) {
const v = await (this as any).reader.read();
if (inflator && !v.done) {
inflator.push(v.value);
} else if (v.done) {
onEnd(ctrl);
} else {
ctrl.enqueue(v.value);
crc.append(v.value);
}
}
});
}
arrayBuffer() {
return new Response(this.stream()).arrayBuffer().catch((e) => {
throw new Error(`Failed to read Entry\n${e}`);
});
}
text() {
return new Response(this.stream()).text().catch((e) => {
throw new Error(`Failed to read Entry\n${e}`);
});
}
file() {
return new Response(this.stream())
.blob()
.then(
(blob) =>
new File([blob], this.name, { lastModified: this.lastModified })
)
.catch((e) => {
throw new Error(`Failed to read Entry\n${e}`);
});
}
}
/**
* Get a BigInt 64 from a DataView
*
* @param {DataView} view a dataview
* @param {number} position the position
* @param {boolean} littleEndian whether this uses littleEndian encoding
* @returns BigInt
*/
function getBigInt64(view: DataView, position: number, littleEndian = false) {
if ("getBigInt64" in DataView.prototype) {
return view.getBigInt64(position, littleEndian);
}
let value = BigInt(0);
const isNegative =
(view.getUint8(position + (littleEndian ? 7 : 0)) & 0x80) > 0;
let carrying = true;
for (let i = 0; i < 8; i++) {
let byte = view.getUint8(position + (littleEndian ? i : 7 - i));
if (isNegative) {
if (carrying) {
if (byte !== 0x00) {
byte = ~(byte - 1) & 0xff;
carrying = false;
}
} else {
byte = ~byte & 0xff;
}
}
value = value + BigInt(byte) + BigInt(256) ** BigInt(i);
}
if (isNegative) {
value = -value;
}
return value;
}
export async function Reader(file: File) {
// Seek EOCDR - "End of central directory record" is the last part of a zip archive, and is at least 22 bytes long.
// Zip file comment is the last part of EOCDR and has max length of 64KB,
// so we only have to search the last 64K + 22 bytes of a archive for EOCDR signature (0x06054b50).
if (file.size < EOCDR_MIN) throw new Error(ERR_BAD_FORMAT);
// seek last length bytes of file for EOCDR
async function doSeek(length: number) {
const ab = await file.slice(file.size - length).arrayBuffer();
const bytes = new Uint8Array(ab);
for (let i = bytes.length - EOCDR_MIN; i >= 0; i--) {
if (
bytes[i] === 0x50 &&
bytes[i + 1] === 0x4b &&
bytes[i + 2] === 0x05 &&
bytes[i + 3] === 0x06
) {
return new DataView(bytes.buffer, i, EOCDR_MIN);
}
}
return null;
}
// In most cases, the EOCDR is EOCDR_MIN bytes long
let dv =
(await doSeek(EOCDR_MIN)) || (await doSeek(Math.min(EOCDR_MAX, file.size)));
if (!dv) throw new Error(ERR_BAD_FORMAT);
let fileslength = dv.getUint16(8, true);
let centralDirSize = dv.getUint32(12, true);
let centralDirOffset = dv.getUint32(16, true);
// const fileCommentLength = dv.getUint16(20, true);
const isZip64 = centralDirOffset === MAX_VALUE_32BITS;
if (isZip64) {
const l = -dv.byteLength - 20;
dv = new DataView(await file.slice(l, -dv.byteLength).arrayBuffer());
// const signature = dv.getUint32(0, true) // 4 bytes
// const diskWithZip64CentralDirStart = dv.getUint32(4, true) // 4 bytes
const relativeOffsetEndOfZip64CentralDir = Number(getBigInt64(dv, 8, true)); // 8 bytes
// const numberOfDisks = dv.getUint32(16, true) // 4 bytes
const zip64centralBlob = file.slice(relativeOffsetEndOfZip64CentralDir, l);
dv = new DataView(await zip64centralBlob.arrayBuffer());
// const zip64EndOfCentralSize = dv.getBigInt64(4, true)
// const diskNumber = dv.getUint32(16, true)
// const diskWithCentralDirStart = dv.getUint32(20, true)
// const centralDirRecordsOnThisDisk = dv.getBigInt64(24, true)
fileslength = Number(getBigInt64(dv, 32, true));
centralDirSize = Number(getBigInt64(dv, 40, true));
centralDirOffset = Number(getBigInt64(dv, 48, true));
}
if (centralDirOffset < 0 || centralDirOffset >= file.size) {
throw new Error(ERR_BAD_FORMAT);
}
const start = centralDirOffset;
const end = centralDirOffset + centralDirSize;
const blob = file.slice(start, end);
const bytes = new Uint8Array(await blob.arrayBuffer());
async function* read() {
for (let i = 0, index = 0; i < fileslength; i++) {
const size =
uint16e(bytes, index + 28) + // filenameLength
uint16e(bytes, index + 30) + // extraFieldLength
uint16e(bytes, index + 32) + // commentLength
46;
if (index + size > bytes.length) {
throw new Error("Invalid ZIP file.");
}
yield new Entry(new DataView(bytes.buffer, index, size), file);
index += size;
}
}
return { read, totalFiles: fileslength };
}
/* eslint-disable no-param-reassign */
class Crc32 {
crc: number;
constructor() {
this.crc = -1;
}
append(data: Uint8Array) {
let crc = this.crc | 0;
const { table } = this;
for (let offset = 0, len = data.length | 0; offset < len; offset++) {
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xff];
}
this.crc = crc;
}
get() {
return (this.crc ^ -1) >>> 0;
}
private table = ((table: number[], i: number, j: number, t: number) => {
for (i = 0; i < 256; i++) {
t = i;
for (j = 0; j < 8; j++) {
t = t & 1 ? (t >>> 1) ^ 0xedb88320 : t >>> 1;
}
table[i] = t;
}
return table;
})([], 0, 0, 0);
}

View File

@@ -1,48 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { AsyncZippable, zip as zipAsync } from "fflate";
import { sanitizeFilename } from "@notesnook/common";
const textEncoder = new TextEncoder();
type File = { filename: string; content: string; date: number };
async function zip(files: File[], format: string): Promise<Uint8Array> {
const obj: AsyncZippable = Object.create(null);
files.forEach((file) => {
const name = sanitizeFilename(file.filename, { replacement: "-" });
let counter = 0;
while (obj[makeFilename(name, format, counter)]) ++counter;
obj[makeFilename(name, format, counter)] = [
textEncoder.encode(file.content),
{ mtime: file.date }
];
});
return new Promise((resolve, reject) =>
zipAsync(obj, (err, data) => (err ? reject(err) : resolve(data)))
);
}
export { zip };
function makeFilename(filename: string, extension: string, counter: number) {
let final = filename;
if (counter) final += `-${counter}`;
final += `.${extension}`;
return final;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -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)
![](/static/skiff-importer/1.png)
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.
![](/static/skiff-importer/2.png)
## 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".
![](/static/skiff-importer/3.png)
3. Drop your Skiff.zip file, or click anywhere inside the box to browse and select your Skiff.zip file. Then click "Start processing".
![](/static/skiff-importer/4.png)
4. Once the Importer finishes processing your files, download the .zip file.
![](/static/import-ready.png)
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.
![](/static/import-zip-app.png)
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

View File

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

View 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!

View 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!

View File

@@ -31,7 +31,8 @@
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@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.1-alpha.2",
"@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",

View File

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

View File

@@ -12,7 +12,8 @@
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@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.1-alpha.2",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.1-alpha.2.tgz",
"integrity": "sha512-GkIDQlcvJVVMgp1J5H5upiRr2hZQJAJK6gnDzOjvJAflKO4gV96xCq4RFakwahLLhL7uQ8Yx10qtmrtBuJMsEw==",
"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.1-alpha.2",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.1-alpha.2.tgz",
"integrity": "sha512-GkIDQlcvJVVMgp1J5H5upiRr2hZQJAJK6gnDzOjvJAflKO4gV96xCq4RFakwahLLhL7uQ8Yx10qtmrtBuJMsEw=="
"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",

View File

@@ -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.1-alpha.2",
"@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",

View File

@@ -160,7 +160,6 @@ class Database {
this.notes = await Notes.new(this, "notes");
await this.trash.init();
await this.relations.cleanup();
this.monographs.init().catch(console.error);
}

View File

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

View File

@@ -61,6 +61,10 @@ export default class Vault {
});
}
get unlocked() {
return !!this._vaultPassword;
}
/**
* Creates a new vault
* @param {string} password The password
@@ -210,7 +214,7 @@ export default class Vault {
return await this._lockNote(note, this._password);
}
async exists(vaultKey) {
async exists(vaultKey = undefined) {
if (!vaultKey) vaultKey = await this._getKey();
return vaultKey && vaultKey.cipher && vaultKey.iv;
}

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ 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 { logger } from "../logger";
import { makeId } from "../utils/id";
import Collection from "./collection";
@@ -189,39 +188,6 @@ export default class Relations extends Collection {
}
return items;
}
async cleanup() {
const relationsLogger = logger.scope("Relations");
const relations = this._collection.getItems();
for (const relation of relations) {
const references = [relation.to, relation.from];
for (let reference of references) {
let exists = false;
switch (reference.type) {
case "reminder":
exists = this._db.reminders.exists(reference.id);
break;
case "note":
exists =
this._db.notes.exists(reference.id) ||
this._db.trash.exists(reference.id);
break;
case "notebook":
exists =
this._db.notebooks.exists(reference.id) ||
this._db.trash.exists(reference.id);
break;
}
if (!exists) {
relationsLogger.debug(
`Removing relation during cleanup`,
relation.id
);
await this.remove(relation.id);
}
}
}
}
}
/**

View File

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

Some files were not shown because too many files have changed in this diff Show More