mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
87 Commits
feat/at-re
...
fix-github
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e68dd129e | ||
|
|
2350d0e41d | ||
|
|
fbd5c8e3cd | ||
|
|
ee5c4077fe | ||
|
|
47bbfc618e | ||
|
|
b377329a8b | ||
|
|
ae8bcf3865 | ||
|
|
2f67611829 | ||
|
|
058c612f4f | ||
|
|
be8df90ccb | ||
|
|
aa730b5b93 | ||
|
|
34bbcf6387 | ||
|
|
33c3c78e2f | ||
|
|
bf144dcda4 | ||
|
|
284ebdb86f | ||
|
|
9dac4cf7f4 | ||
|
|
fb973a5514 | ||
|
|
f9226510f9 | ||
|
|
5c0a964699 | ||
|
|
41ffaab3dc | ||
|
|
764c721f1d | ||
|
|
3d267f91ef | ||
|
|
f10f28a7f8 | ||
|
|
8d04f89a18 | ||
|
|
9d1374dcd4 | ||
|
|
094a65e514 | ||
|
|
372da979e1 | ||
|
|
d3371326f2 | ||
|
|
57452bd0e6 | ||
|
|
993b8460bf | ||
|
|
081201aa42 | ||
|
|
270e6778d1 | ||
|
|
2074320583 | ||
|
|
cd90030616 | ||
|
|
807bd683b7 | ||
|
|
35929026a4 | ||
|
|
41eb94ee47 | ||
|
|
ba51e110ad | ||
|
|
ecad06d249 | ||
|
|
19a151f637 | ||
|
|
7c4a6f1e17 | ||
|
|
bc88e953d1 | ||
|
|
7d89ec676a | ||
|
|
c8f8bcca93 | ||
|
|
9fa904e849 | ||
|
|
05cc947cb4 | ||
|
|
a20a683b17 | ||
|
|
5fc43c931e | ||
|
|
0879c3b4d6 | ||
|
|
2eded71de3 | ||
|
|
9e30ee50b8 | ||
|
|
08621a5873 | ||
|
|
17f5bd28ba | ||
|
|
4558c0b2eb | ||
|
|
a442677e91 | ||
|
|
95666bee36 | ||
|
|
c4232ac95a | ||
|
|
0283037289 | ||
|
|
b6fd0df715 | ||
|
|
f53fb58024 | ||
|
|
21d452308d | ||
|
|
823608cca1 | ||
|
|
f3a0b998e4 | ||
|
|
a47a38b16b | ||
|
|
a184c53dd5 | ||
|
|
a3464255ec | ||
|
|
088c6670a9 | ||
|
|
27840b9bd6 | ||
|
|
ae819504f0 | ||
|
|
af792e394c | ||
|
|
76a4d5c2a5 | ||
|
|
4e23acd608 | ||
|
|
3c65dcdd8b | ||
|
|
efea6e3972 | ||
|
|
185cca7880 | ||
|
|
fbded93405 | ||
|
|
674881b873 | ||
|
|
248fb6dd2d | ||
|
|
3211e7a149 | ||
|
|
08496c699e | ||
|
|
060fa25814 | ||
|
|
a6b1effc77 | ||
|
|
5cf9c4808e | ||
|
|
d5a44067fd | ||
|
|
0de463526f | ||
|
|
5fd79875ef | ||
|
|
bdb19ad325 |
28
.github/workflows/android.publish.yml
vendored
28
.github/workflows/android.publish.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Publish @notesnook/mobile
|
||||
name: Publish @notesnook/android
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
@@ -28,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: |
|
||||
|
||||
30
.github/workflows/desktop.publish.yml
vendored
30
.github/workflows/desktop.publish.yml
vendored
@@ -18,6 +18,21 @@ on:
|
||||
required: true
|
||||
default: true
|
||||
description: "Publish on GitHub releases?"
|
||||
build-windows:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for Windows?"
|
||||
build-linux:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for Linux?"
|
||||
build-mac:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for macOS?"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -71,6 +86,7 @@ jobs:
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
if: inputs.build-mac
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
@@ -163,6 +179,7 @@ jobs:
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
needs: build
|
||||
if: inputs.build-linux
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
@@ -221,12 +238,22 @@ jobs:
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
if: inputs.build-windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET Core SDK
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
|
||||
- name: Install AzureCodeSigning
|
||||
run: Install-Module -Name AzureCodeSigning -RequiredVersion 0.3.0 -Force -Repository PSGallery
|
||||
shell: pwsh
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
@@ -247,6 +274,9 @@ jobs:
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
run: |
|
||||
if ($${{ inputs.publish-github }} -eq $true) {
|
||||
yarn electron-builder --win --publish always
|
||||
|
||||
67
.github/workflows/ios.publish.yml
vendored
Normal file
67
.github/workflows/ios.publish.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Publish @notesnook/ios
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-13
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Build packages
|
||||
run: npx nx run @notesnook/mobile:build
|
||||
|
||||
- name: Install Pods
|
||||
run: npm run prepare:ios
|
||||
|
||||
- name: Build iOS App
|
||||
uses: yukiarrr/ios-build-action@v1.11.2
|
||||
with:
|
||||
bundle-identifier: org.streetwriters.notesnook
|
||||
scheme: Notesnook
|
||||
configuration: "Release"
|
||||
export-options: apps/mobile/native/ios/ExportOptions.plist
|
||||
project-path: apps/mobile/native/ios/Notesnook.xcodeproj
|
||||
workspace-path: apps/mobile/native/ios/Notesnook.xcworkspace
|
||||
update-targets: |
|
||||
Notesnook
|
||||
Make Note
|
||||
NotesWidgetExtension
|
||||
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
|
||||
code-signing-identity: Apple Distribution
|
||||
team-id: ${{ secrets.APPLE_TEAM_ID }}
|
||||
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
|
||||
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
|
||||
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
|
||||
output-path: Notesnook.ipa
|
||||
mobileprovision-base64: |
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_APP }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
|
||||
|
||||
- name: 'Upload app to TestFlight'
|
||||
uses: apple-actions/upload-testflight-build@v1
|
||||
with:
|
||||
app-path: Notesnook.ipa
|
||||
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
api-key-id: ${{ secrets.API_KEY_ID }}
|
||||
api-private-key: ${{ secrets.API_KEY }}
|
||||
|
||||
- name: Upload to Github
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Notesnook.ipa
|
||||
path: Notesnook.ipa
|
||||
4
.github/workflows/web.beta.publish.yml
vendored
4
.github/workflows/web.beta.publish.yml
vendored
@@ -1,10 +1,11 @@
|
||||
name: Publish @notesnook/web Beta
|
||||
|
||||
on:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- '!v3-beta'
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
@@ -22,6 +23,7 @@ on:
|
||||
- synchronize
|
||||
branches:
|
||||
- master
|
||||
- '!v3-beta'
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
|
||||
40
.github/workflows/web.v3-beta.publish.yml
vendored
Normal file
40
.github/workflows/web.v3-beta.publish.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Publish @notesnook/web v3 Beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate beta build
|
||||
run: npm run build:beta:web
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
|
||||
id: pages
|
||||
with:
|
||||
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
cloudflare-project-name: notesnook-v3-app
|
||||
directory: ./apps/web/build
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,4 +9,5 @@ nx-cloud.env
|
||||
.env.local
|
||||
.nyc_output
|
||||
site
|
||||
node_modules.backup
|
||||
node_modules.backup
|
||||
.nx
|
||||
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.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": {
|
||||
|
||||
@@ -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
60
apps/desktop/sign.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { writeFileSync, rmSync, readFileSync } = require("fs");
|
||||
const { execSync } = require("child_process");
|
||||
const { relative, join } = require("path");
|
||||
|
||||
module.exports = async function (configuration) {
|
||||
const Endpoint = "https://weu.codesigning.azure.net";
|
||||
const CodeSigningAccountName = "Notesnook";
|
||||
const CertificateProfileName = "Notesnook";
|
||||
const FileDigest = configuration.hash.toUpperCase();
|
||||
const TimestampRfc3161 = "http://timestamp.acs.microsoft.com";
|
||||
const TimestampDigest = configuration.hash.toUpperCase();
|
||||
const Description = "The Notesnook app";
|
||||
const DescriptionUrl = "https://notesnook.com/";
|
||||
const FilesCatalog = createCatalog(configuration.path);
|
||||
|
||||
const command = `Invoke-AzureCodeSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
|
||||
|
||||
console.debug("Signing", configuration.path, "using command", command);
|
||||
|
||||
psexec(command);
|
||||
|
||||
console.debug("Signed", configuration.path);
|
||||
|
||||
deleteCatalog();
|
||||
};
|
||||
|
||||
function createCatalog(path) {
|
||||
writeFileSync("_catalog", relative(__dirname, path));
|
||||
return join(__dirname, "_catalog");
|
||||
}
|
||||
|
||||
function deleteCatalog() {
|
||||
rmSync("_catalog");
|
||||
}
|
||||
|
||||
function psexec(cmd) {
|
||||
return execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: "pwsh"
|
||||
});
|
||||
}
|
||||
@@ -17,13 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "@azure/core-asynciterator-polyfill";
|
||||
import SettingsService from "./services/settings";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
@@ -32,11 +31,16 @@ import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { ApplicationHolder } from "./navigation";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
import Notifications from "./services/notifications";
|
||||
import SettingsService from "./services/settings";
|
||||
import { TipManager } from "./services/tip-manager";
|
||||
import { useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
|
||||
SettingsService.checkOrientation();
|
||||
const App = () => {
|
||||
|
||||
@@ -34,7 +34,8 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir } from "./io";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -181,7 +182,9 @@ export default async function downloadAttachment(
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined
|
||||
groupId: undefined,
|
||||
base64: false,
|
||||
text: false
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
@@ -212,10 +215,17 @@ export default async function downloadAttachment(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (
|
||||
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
|
||||
)
|
||||
|
||||
if (!(await exists(attachment.metadata.hash))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.base64 || options.text) {
|
||||
return await db.attachments.read(
|
||||
attachment.metadata.hash,
|
||||
options.base64 ? "base64" : "text"
|
||||
);
|
||||
}
|
||||
|
||||
let filename = getFileNameWithExtension(
|
||||
attachment.metadata.filename,
|
||||
@@ -233,7 +243,8 @@ export default async function downloadAttachment(
|
||||
mime: attachment.metadata.type,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
};
|
||||
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
|
||||
@@ -36,9 +36,13 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
}
|
||||
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const isPng = /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
console.log("decrypting....");
|
||||
const isPng = !attachment.metadata.type
|
||||
? false
|
||||
: /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = !attachment.metadata.type
|
||||
? false
|
||||
: /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
|
||||
const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const contextId = attachment.metadata.hash;
|
||||
const [filename, setFilename] = useState(attachment.metadata.filename);
|
||||
@@ -64,7 +64,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
name: "Download",
|
||||
onPress: async () => {
|
||||
if (currentProgress) {
|
||||
await db.fs.cancel(attachment.metadata.hash, "download");
|
||||
await db.fs.cancel(attachment.metadata.hash);
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
}
|
||||
downloadAttachment(attachment.metadata.hash, false);
|
||||
@@ -117,7 +117,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
});
|
||||
}
|
||||
|
||||
setAttachments([...db.attachments.all]);
|
||||
setAttachments();
|
||||
setLoading({
|
||||
name: null
|
||||
});
|
||||
@@ -140,7 +140,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments([...db.attachments.all]);
|
||||
setAttachments();
|
||||
}
|
||||
},
|
||||
positiveText: "Rename"
|
||||
@@ -152,8 +152,8 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
name: "Delete",
|
||||
onPress: async () => {
|
||||
await db.attachments.remove(attachment.metadata.hash, false);
|
||||
setAttachments([...db.attachments.all]);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
setAttachments();
|
||||
close();
|
||||
},
|
||||
icon: "delete-outline"
|
||||
}
|
||||
@@ -362,8 +362,13 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
Actions.present = (attachment, set, context) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref) => (
|
||||
<Actions fwdRef={ref} setAttachments={set} attachment={attachment} />
|
||||
component: (ref, close) => (
|
||||
<Actions
|
||||
fwdRef={ref}
|
||||
setAttachments={set}
|
||||
close={close}
|
||||
attachment={attachment}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -132,7 +132,7 @@ export const AttachmentItem = ({
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption || !pressable) return;
|
||||
db.fs.cancel(attachment.metadata.hash, currentProgress.type);
|
||||
db.fs.cancel(attachment.metadata.hash);
|
||||
setCurrentProgress(null);
|
||||
}}
|
||||
style={{
|
||||
|
||||
@@ -68,7 +68,7 @@ export const AttachmentDialog = ({ note }) => {
|
||||
!attachmentSearchValue.current ||
|
||||
attachmentSearchValue.current === ""
|
||||
) {
|
||||
setAttachments([...attachments]);
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
}
|
||||
clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(() => {
|
||||
@@ -77,13 +77,16 @@ export const AttachmentDialog = ({ note }) => {
|
||||
attachmentSearchValue.current
|
||||
);
|
||||
if (results.length === 0) return;
|
||||
setAttachments(results);
|
||||
|
||||
setAttachments(filterAttachments(currentFilter, results));
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<AttachmentItem
|
||||
setAttachments={setAttachments}
|
||||
setAttachments={() => {
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
}}
|
||||
attachment={item}
|
||||
context="attachments-list"
|
||||
/>
|
||||
@@ -133,11 +136,12 @@ export const AttachmentDialog = ({ note }) => {
|
||||
}
|
||||
];
|
||||
|
||||
const filterAttachments = (type) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
isDocument;
|
||||
const filterAttachments = (type, _attachments) => {
|
||||
const attachments =
|
||||
_attachments || note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
switch (type) {
|
||||
case "all":
|
||||
return attachments;
|
||||
|
||||
@@ -101,6 +101,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
setVisible(false);
|
||||
setData(null);
|
||||
}}
|
||||
keyboardHandlerDisabled={data?.keyboardHandlerDisabled}
|
||||
bottomPadding={!data.noBottomPadding}
|
||||
enableGesturesInScrollView={
|
||||
typeof data.enableGesturesInScrollView === "undefined"
|
||||
|
||||
@@ -43,6 +43,7 @@ import Paragraph from "../../ui/typography/paragraph";
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
import { Dialog } from "../../dialog";
|
||||
|
||||
const ExportNotesSheet = ({ notes, update }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -157,6 +158,8 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Dialog context="export-notes" />
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
{!exporting && !complete ? (
|
||||
actions.map((item) => (
|
||||
@@ -346,7 +349,8 @@ ExportNotesSheet.present = (notes, allNotes) => {
|
||||
notes={allNotes ? db.notes.all : notes}
|
||||
update={update}
|
||||
/>
|
||||
)
|
||||
),
|
||||
keyboardHandlerDisabled: true
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
|
||||
const PublishNoteSheet = ({ note: item, update }) => {
|
||||
const PublishNoteSheet = ({ note: item }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const actionSheetRef = useRef();
|
||||
|
||||
@@ -48,8 +48,7 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
const [note, setNote] = useState(item);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const publishUrl =
|
||||
note &&
|
||||
`https://monograph.notesnook.com/${db?.monographs.monograph(note?.id)}`;
|
||||
note && `https://monogr.ph/${db?.monographs.monograph(note?.id)}`;
|
||||
const isPublished = note && db?.monographs.isPublished(note?.id);
|
||||
const pwdInput = useRef();
|
||||
const passwordValue = useRef();
|
||||
|
||||
@@ -37,11 +37,11 @@ const SheetWrapper = ({
|
||||
onOpen,
|
||||
closeOnTouchBackdrop = true,
|
||||
onHasReachedTop,
|
||||
keyboardMode,
|
||||
overlay,
|
||||
overlayOpacity = 0.3,
|
||||
enableGesturesInScrollView = false,
|
||||
bottomPadding = true
|
||||
bottomPadding = true,
|
||||
keyboardHandlerDisabled
|
||||
}) => {
|
||||
const localRef = useRef(null);
|
||||
const { colors } = useThemeColors("sheet");
|
||||
@@ -121,8 +121,9 @@ const SheetWrapper = ({
|
||||
initialOffsetFromBottom={1}
|
||||
onPositionChanged={onHasReachedTop}
|
||||
closeOnTouchBackdrop={closeOnTouchBackdrop}
|
||||
keyboardMode={keyboardMode}
|
||||
keyboardHandlerEnabled={sheetKeyboardHandler}
|
||||
keyboardHandlerEnabled={
|
||||
keyboardHandlerDisabled ? false : sheetKeyboardHandler
|
||||
}
|
||||
closeOnPressBack={closeOnTouchBackdrop}
|
||||
indicatorColor={colors.secondary.background}
|
||||
onOpen={_onOpen}
|
||||
|
||||
@@ -550,15 +550,6 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
}
|
||||
|
||||
async function exportNote() {
|
||||
if (item.locked) {
|
||||
ToastEvent.show({
|
||||
heading: "Note is locked",
|
||||
type: "error",
|
||||
message: "Locked notes cannot be exported",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
ExportNotesSheet.present([item]);
|
||||
}
|
||||
|
||||
|
||||
@@ -481,7 +481,7 @@ export const useAppEvents = () => {
|
||||
|
||||
subscribeToIAPListeners();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(error);
|
||||
DatabaseLogger.error(e);
|
||||
|
||||
ToastEvent.error(e, "An error occurred", "global");
|
||||
}
|
||||
@@ -623,15 +623,20 @@ export const useAppEvents = () => {
|
||||
}, [loading, onUserUpdated]);
|
||||
|
||||
const initializeDatabase = useCallback(async () => {
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
DatabaseLogger.info("Initializing database");
|
||||
await db.init();
|
||||
try {
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
DatabaseLogger.info("Initializing database");
|
||||
await db.init();
|
||||
}
|
||||
if (IsDatabaseMigrationRequired()) return;
|
||||
initialize();
|
||||
setLoading(false);
|
||||
Walkthrough.init();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
ToastEvent.error(e, "Error initializing database", "global");
|
||||
}
|
||||
if (IsDatabaseMigrationRequired()) return;
|
||||
initialize();
|
||||
setLoading(false);
|
||||
Walkthrough.init();
|
||||
}, [IsDatabaseMigrationRequired]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"url": "^0.11.0",
|
||||
"validator": "^13.5.2",
|
||||
"zustand": "^3.6.0",
|
||||
"katex": "0.16.2"
|
||||
"katex": "0.16.2",
|
||||
"@readme/data-urls": "3.0.0"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import { EditorProps, useEditorType } from "./tiptap/types";
|
||||
import { useEditor } from "./tiptap/use-editor";
|
||||
import { useEditorEvents } from "./tiptap/use-editor-events";
|
||||
import { editorController } from "./tiptap/utils";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
const style: ViewStyle = {
|
||||
height: "100%",
|
||||
@@ -93,35 +94,6 @@ const Editor = React.memo(
|
||||
get: () => editor
|
||||
}));
|
||||
|
||||
const onMediaDownloaded = useCallback(
|
||||
({
|
||||
hash,
|
||||
groupId,
|
||||
src,
|
||||
attachmentType
|
||||
}: {
|
||||
hash: string;
|
||||
groupId: string;
|
||||
src: string;
|
||||
attachmentType: string;
|
||||
}) => {
|
||||
if (groupId !== editor.note.current?.id) return;
|
||||
editorController.current.markImageLoaded(hash);
|
||||
if (attachmentType === "webclip") {
|
||||
editor.commands.updateWebclip({
|
||||
hash: hash,
|
||||
src: src
|
||||
});
|
||||
} else {
|
||||
editor.commands.updateImage({
|
||||
hash: hash,
|
||||
dataurl: src
|
||||
});
|
||||
}
|
||||
},
|
||||
[editor.commands, editor.note]
|
||||
);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
renderKey.current =
|
||||
renderKey.current === `editor-0` ? `editor-1` : `editor-0`;
|
||||
@@ -130,16 +102,11 @@ const Editor = React.memo(
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = [
|
||||
eSubscribeEvent("webview_reset", onError),
|
||||
EV.subscribe(EVENTS.mediaAttachmentDownloaded, onMediaDownloaded)
|
||||
];
|
||||
|
||||
const sub = [eSubscribeEvent("webview_reset", onError)];
|
||||
return () => {
|
||||
sub.forEach((s) => s.unsubscribe());
|
||||
EV.unsubscribe(EVENTS.mediaAttachmentDownloaded, onMediaDownloaded);
|
||||
};
|
||||
}, [onError, onMediaDownloaded]);
|
||||
}, [onError]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setImmediate(() => {
|
||||
@@ -210,6 +177,7 @@ export default Editor;
|
||||
const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
|
||||
const readonly = useEditorStore((state) => state.readonly);
|
||||
const keyboard = useKeyboard();
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const onPress = async () => {
|
||||
if (editor.note.current) {
|
||||
@@ -225,7 +193,7 @@ const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
|
||||
name="pencil-lock"
|
||||
type="grayBg"
|
||||
onPress={onPress}
|
||||
color="accent"
|
||||
color={colors.primary.accent}
|
||||
customStyle={{
|
||||
position: "absolute",
|
||||
bottom: 60,
|
||||
|
||||
@@ -17,10 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {
|
||||
Attachment,
|
||||
AttachmentProgress
|
||||
} from "@notesnook/editor/dist/extensions/attachment/index";
|
||||
import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index";
|
||||
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
|
||||
import { createRef, RefObject } from "react";
|
||||
import { Platform } from "react-native";
|
||||
@@ -203,11 +200,16 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
|
||||
);
|
||||
};
|
||||
|
||||
setAttachmentProgress = async (attachmentProgress: AttachmentProgress) => {
|
||||
setAttachmentProgress = async (attachmentProgress: Partial<Attachment>) => {
|
||||
await this.doAsync(
|
||||
`editor && editor.commands.setAttachmentProgress(${JSON.stringify(
|
||||
`editor && editor.commands.updateAttachment(${JSON.stringify(
|
||||
attachmentProgress
|
||||
)})`
|
||||
)}, {
|
||||
preventUpdate: true,
|
||||
query: (attachment) => {
|
||||
return attachment.hash === "${attachmentProgress.hash}";
|
||||
}
|
||||
})`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -36,5 +36,6 @@ export const EventTypes = {
|
||||
contentchange: "editor-event:content-change",
|
||||
reminders: "editor-event:reminders",
|
||||
previewAttachment: "editor-event:preview-attachment",
|
||||
copyToClipboard: "editor-events:copy-to-clipboard"
|
||||
copyToClipboard: "editor-events:copy-to-clipboard",
|
||||
getAttachmentData: "editor-events:get-attachment-data"
|
||||
};
|
||||
|
||||
@@ -97,6 +97,7 @@ export type SavePayload = {
|
||||
type?: Content["type"];
|
||||
sessionId?: string | null;
|
||||
sessionHistoryId?: number;
|
||||
ignoreEdit: boolean;
|
||||
};
|
||||
|
||||
export type AppState = {
|
||||
|
||||
@@ -66,6 +66,7 @@ import { EditorMessage, EditorProps, useEditorType } from "./types";
|
||||
import { EditorEvents, editorState } from "./utils";
|
||||
import { useNoteStore } from "../../../stores/use-notes-store";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import downloadAttachment from "../../../common/filesystem/download-attachment";
|
||||
|
||||
const publishNote = async (editor: useEditorType) => {
|
||||
const user = useUserStore.getState().user;
|
||||
@@ -127,6 +128,8 @@ const showActionsheet = async (editor: useEditorType) => {
|
||||
}
|
||||
};
|
||||
|
||||
type ContentMessage = { html: string; ignoreEdit: boolean };
|
||||
|
||||
export const useEditorEvents = (
|
||||
editor: useEditorType,
|
||||
{ readonly: editorPropReadonly, noHeader, noToolbar }: Partial<EditorProps>
|
||||
@@ -330,14 +333,16 @@ export const useEditorEvents = (
|
||||
if (editorMessage.type === EventTypes.content) {
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
content: editorMessage.value as string,
|
||||
content: (editorMessage.value as ContentMessage).html,
|
||||
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
|
||||
forSessionId: editorMessage.sessionId
|
||||
});
|
||||
} else if (editorMessage.type === EventTypes.title) {
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
title: editorMessage.value as string,
|
||||
forSessionId: editorMessage.sessionId
|
||||
forSessionId: editorMessage.sessionId,
|
||||
ignoreEdit: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,6 +421,44 @@ export const useEditorEvents = (
|
||||
break;
|
||||
}
|
||||
|
||||
case EventTypes.getAttachmentData: {
|
||||
const attachment = (editorMessage.value as any)
|
||||
.attachment as Attachment;
|
||||
|
||||
console.log(
|
||||
"Getting attachment data:",
|
||||
attachment.hash,
|
||||
attachment.type
|
||||
);
|
||||
downloadAttachment(attachment.hash, true, {
|
||||
base64: attachment.type === "image",
|
||||
text: attachment.type === "web-clip",
|
||||
silent: true,
|
||||
groupId: editor.note.current?.id,
|
||||
cache: true
|
||||
} as any)
|
||||
.then((data: any) => {
|
||||
console.log(
|
||||
"Got attachment data:",
|
||||
!!data,
|
||||
(editorMessage.value as any).resolverId
|
||||
);
|
||||
editor.postMessage(EditorEvents.attachmentData, {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
console.log("Error downloading attachment data");
|
||||
editor.postMessage(EditorEvents.attachmentData, {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data: undefined
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case EventTypes.pro:
|
||||
if (editor.state.current?.isFocused) {
|
||||
editor.state.current.isFocused = true;
|
||||
|
||||
@@ -78,7 +78,6 @@ export const useEditor = (
|
||||
const saveCount = useRef(0);
|
||||
const lastContentChangeTime = useRef<number>(0);
|
||||
const lock = useRef(false);
|
||||
const loadedImages = useRef<{ [name: string]: boolean }>({});
|
||||
const lockedSessionId = useRef<string>();
|
||||
const loadingState = useRef<string>();
|
||||
|
||||
@@ -129,12 +128,9 @@ export const useEditor = (
|
||||
|
||||
const reset = useCallback(
|
||||
async (resetState = true, resetContent = true) => {
|
||||
currentNote.current?.id &&
|
||||
db.fs?.cancel(currentNote.current.id, "download");
|
||||
currentNote.current?.id && db.fs?.cancel(currentNote.current.id);
|
||||
currentNote.current = null;
|
||||
loadedImages.current = {};
|
||||
currentContent.current = null;
|
||||
clearTimeout(timers.current["loading-images"]);
|
||||
sessionHistoryId.current = undefined;
|
||||
saveCount.current = 0;
|
||||
loadingState.current = undefined;
|
||||
@@ -166,7 +162,8 @@ export const useEditor = (
|
||||
data,
|
||||
type,
|
||||
sessionId: currentSessionId,
|
||||
sessionHistoryId: currentSessionHistoryId
|
||||
sessionHistoryId: currentSessionHistoryId,
|
||||
ignoreEdit
|
||||
}: SavePayload) => {
|
||||
if (
|
||||
readonly ||
|
||||
@@ -198,6 +195,12 @@ export const useEditor = (
|
||||
};
|
||||
|
||||
noteData.title = title;
|
||||
|
||||
if (ignoreEdit) {
|
||||
console.log("Ignoring edits...");
|
||||
noteData.dateEdited = note?.dateEdited;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
noteData.content = {
|
||||
data: data,
|
||||
@@ -287,58 +290,6 @@ export const useEditor = (
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getMediaToLoad = (previousContent?: string) => {
|
||||
if (!currentNote.current?.id) return [];
|
||||
|
||||
const previousAttachments =
|
||||
previousContent?.matchAll(/data-hash="(.+?)"/gm) || [];
|
||||
const attachments =
|
||||
currentContent.current?.data?.matchAll(/data-hash="(.+?)"/gm) || [];
|
||||
|
||||
const media: string[] = [];
|
||||
|
||||
const oldMatches = Array.from(previousAttachments).map((match) => match[1]);
|
||||
const matches = Array.from(attachments).map((match) => match[1]);
|
||||
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
const currentHash = matches[i];
|
||||
const oldHash = oldMatches[i];
|
||||
if (currentHash !== oldHash) {
|
||||
media.push(currentHash);
|
||||
loadedImages.current[currentHash] = false;
|
||||
}
|
||||
}
|
||||
return media;
|
||||
};
|
||||
|
||||
const markImageLoaded = (hash: string) => {
|
||||
const attachment = loadedImages.current[hash];
|
||||
if (typeof attachment === "boolean") {
|
||||
loadedImages.current[hash] = true;
|
||||
}
|
||||
};
|
||||
|
||||
const loadImages = useCallback((previousContent?: string) => {
|
||||
if (!currentNote.current?.id) return;
|
||||
const timerId = "loading-images";
|
||||
clearTimeout(timers.current[timerId]);
|
||||
timers.current[timerId] = setTimeout(() => {
|
||||
if (!currentNote.current?.id) return;
|
||||
if (currentNote.current?.content?.isPreview) {
|
||||
db.content?.downloadMedia(
|
||||
currentNote.current?.id,
|
||||
currentNote.current.content,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
const media = getMediaToLoad(previousContent);
|
||||
if (media.length > 0) {
|
||||
db.attachments?.downloadMedia(currentNote.current?.id, media);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const loadNote = useCallback(
|
||||
async (
|
||||
item: Omit<NoteType, "type"> & {
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -50,7 +50,8 @@ export const EditorEvents: { [name: string]: string } = {
|
||||
titleplaceholder: "native:titleplaceholder",
|
||||
logger: "native:logger",
|
||||
status: "native:status",
|
||||
keyboardShown: "native:keyboardShown"
|
||||
keyboardShown: "native:keyboardShown",
|
||||
attachmentData: "native:attachment-data"
|
||||
};
|
||||
|
||||
export function randId(prefix: string) {
|
||||
|
||||
@@ -115,7 +115,7 @@ export const EditorWrapper = ({ width }) => {
|
||||
style={{ height: 1, padding: 0, width: 1, position: "absolute" }}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<ProgressBar />
|
||||
{/* <ProgressBar /> */}
|
||||
<Editor key="editor" withController={true} />
|
||||
</KeyboardAvoidingViewIOS>
|
||||
)}
|
||||
|
||||
@@ -69,10 +69,10 @@ export const TrashIntervalPicker = createSettingsPicker({
|
||||
db.settings.setTrashCleanupInterval(item);
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return item === -1 ? "Never" : item + " days";
|
||||
return item === -1 ? "Never" : item === 1 ? "Daily" : item + " days";
|
||||
},
|
||||
getItemKey: (item) => item.toString(),
|
||||
options: [-1, 7, 30, 365],
|
||||
options: [-1, 1, 7, 30, 365],
|
||||
compareValue: (current, item) => current === item,
|
||||
premium: true
|
||||
});
|
||||
|
||||
@@ -65,7 +65,11 @@ const PLACEHOLDER_DATA = (trashCleanupInterval = 7) => ({
|
||||
paragraph:
|
||||
trashCleanupInterval === -1
|
||||
? "Set automatic trash cleanup interval from Settings > Behaviour > Clean trash interval."
|
||||
: `Items in the trash will be permanently deleted after after ${trashCleanupInterval} days.`,
|
||||
: `Trash gets automatically cleaned up ${
|
||||
trashCleanupInterval === 1
|
||||
? "daily."
|
||||
: `after ${trashCleanupInterval} days.`
|
||||
}`,
|
||||
button: null,
|
||||
loading: "Loading trash items"
|
||||
});
|
||||
|
||||
@@ -18,17 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { decode, EntityLevel } from "entities";
|
||||
import { zipSync } from "fflate";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNHTMLtoPDF from "react-native-html-to-pdf-lite";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { zip } from "react-native-zip-archive";
|
||||
import { DatabaseLogger, db } from "../common/database/index";
|
||||
import Storage from "../common/database/storage";
|
||||
import { convertNoteToText } from "../utils/note-to-text";
|
||||
|
||||
import { sleep } from "../utils/time";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import BiometicService from "./biometrics";
|
||||
import { ToastEvent } from "./event-manager";
|
||||
|
||||
const MIMETypes = {
|
||||
txt: "text/plain",
|
||||
@@ -99,8 +101,8 @@ async function save(path, data, fileName, extension) {
|
||||
return uri || path;
|
||||
}
|
||||
|
||||
async function makeHtml(note) {
|
||||
let html = await db.notes.note(note.id).export("html");
|
||||
async function makeHtml(note, content) {
|
||||
let html = await db.notes.note(note.id).export("html", content);
|
||||
html = decode(html, {
|
||||
level: EntityLevel.HTML
|
||||
});
|
||||
@@ -111,23 +113,25 @@ async function makeHtml(note) {
|
||||
*
|
||||
* @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type
|
||||
*/
|
||||
async function exportAs(type, note, bulk) {
|
||||
async function exportAs(type, note, bulk, content) {
|
||||
let data;
|
||||
switch (type) {
|
||||
case "html":
|
||||
{
|
||||
data = await makeHtml(note);
|
||||
data = await makeHtml(note, content);
|
||||
}
|
||||
break;
|
||||
case "md":
|
||||
data = await db.notes.note(note.id).export("md");
|
||||
data = await db.notes.note(note.id).export("md", content);
|
||||
break;
|
||||
case "md-frontmatter":
|
||||
data = await db.notes.note(note.id).export("md-frontmatter");
|
||||
data = await db.notes
|
||||
.note(note.id)
|
||||
.export("md-frontmatter", content?.data);
|
||||
break;
|
||||
case "pdf":
|
||||
{
|
||||
let html = await makeHtml(note);
|
||||
let html = await makeHtml(note, content);
|
||||
let fileName = sanitizeFilename(note.title + Date.now(), {
|
||||
replacement: "_"
|
||||
});
|
||||
@@ -153,21 +157,84 @@ async function exportAs(type, note, bulk) {
|
||||
}
|
||||
break;
|
||||
case "txt":
|
||||
data = await convertNoteToText(note);
|
||||
{
|
||||
data = await db.notes?.note(note.id).export("txt", content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function unlockVault() {
|
||||
let biometry = await BiometicService.isBiometryAvailable();
|
||||
let fingerprint = await BiometicService.hasInternetCredentials("nn_vault");
|
||||
if (biometry && fingerprint) {
|
||||
let credentials = await BiometicService.getCredentials(
|
||||
"Unlock vault",
|
||||
"Unlock vault to export locked notes"
|
||||
);
|
||||
if (credentials) {
|
||||
return db.vault.unlock(credentials.password);
|
||||
}
|
||||
}
|
||||
useSettingStore.getState().setSheetKeyboardHandler(false);
|
||||
return new Promise((resolve) => {
|
||||
setImmediate(() => {
|
||||
presentDialog({
|
||||
context: "export-notes",
|
||||
input: true,
|
||||
secureTextEntry: true,
|
||||
positiveText: "Unlock",
|
||||
title: "Unlock vault",
|
||||
paragraph: "Some exported notes are locked, Unlock to export them",
|
||||
inputPlaceholder: "Enter password",
|
||||
positivePress: async (value) => {
|
||||
const unlocked = await db.vault.unlock(value);
|
||||
if (!unlocked) {
|
||||
ToastEvent.show({
|
||||
heading: "Invalid password",
|
||||
message: "Please enter a valid password",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
resolve(unlocked);
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
return true;
|
||||
},
|
||||
onClose: () => {
|
||||
resolve(false);
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type
|
||||
*/
|
||||
async function exportNote(note, type) {
|
||||
let content;
|
||||
|
||||
if (note.locked) {
|
||||
try {
|
||||
let unlocked = await unlockVault();
|
||||
if (!unlocked) return null;
|
||||
const unlockedNote = await db.vault.open(note.id);
|
||||
content = unlockedNote.content;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
let path = await getPath(FolderNames[type]);
|
||||
if (!path) return;
|
||||
let result = await exportAs(type, note);
|
||||
|
||||
let result = await exportAs(type, note, false, content);
|
||||
if (!result) return null;
|
||||
let fileName = sanitizeFilename(note.title + Date.now(), {
|
||||
replacement: "_"
|
||||
@@ -195,11 +262,6 @@ function copyFileAsync(source, dest) {
|
||||
});
|
||||
}
|
||||
|
||||
function zipsync(results) {
|
||||
let data = zipSync(results);
|
||||
return Buffer.from(data.buffer).toString("base64");
|
||||
}
|
||||
|
||||
function getUniqueFileName(fileName, results) {
|
||||
const chunks = fileName.split(".");
|
||||
const ext = chunks.pop();
|
||||
@@ -221,19 +283,108 @@ async function bulkExport(notes, type, callback) {
|
||||
let path = await getPath(FolderNames[type]);
|
||||
if (!path) return;
|
||||
|
||||
const exportCacheFolder =
|
||||
RNFetchBlob.fs.dirs.CacheDir + `/export_${Date.now()}`;
|
||||
|
||||
await RNFetchBlob.fs.mkdir(exportCacheFolder).catch((e) => console.log(e));
|
||||
|
||||
const mkdir = async (dir) => {
|
||||
const folder = `${exportCacheFolder}/${dir}`;
|
||||
if (!(await RNFetchBlob.fs.exists(folder))) {
|
||||
await RNFetchBlob.fs.mkdir(folder);
|
||||
}
|
||||
};
|
||||
|
||||
const writeFile = async (path, result) => {
|
||||
const cacheFilePath = exportCacheFolder + path;
|
||||
await RNFetchBlob.fs.writeFile(
|
||||
cacheFilePath,
|
||||
result,
|
||||
type === "pdf" ? "base64" : "utf8"
|
||||
);
|
||||
};
|
||||
|
||||
const results = {};
|
||||
for (var i = 0; i < notes.length; i++) {
|
||||
try {
|
||||
await sleep(1);
|
||||
let note = notes[i];
|
||||
if (note.locked) continue;
|
||||
let result = await exportAs(type, note);
|
||||
let content;
|
||||
if (note.locked) {
|
||||
try {
|
||||
let unlocked = !db.vault.unlocked ? await unlockVault() : true;
|
||||
if (!unlocked) {
|
||||
continue;
|
||||
}
|
||||
const unlockedNote = await db.vault.open(note.id);
|
||||
content = unlockedNote.content;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let result = await exportAs(type, note, true, content);
|
||||
let fileName = sanitizeFilename(note.title, {
|
||||
replacement: "_"
|
||||
});
|
||||
if (result) {
|
||||
results[getUniqueFileName(fileName + `.${type}`, results)] =
|
||||
Buffer.from(result, type === "pdf" ? "base64" : "utf-8");
|
||||
const notebooks = [
|
||||
...(db.relations
|
||||
?.to({ id: note.id, type: "note" }, "notebook")
|
||||
.map((notebook) => ({
|
||||
title: notebook.title
|
||||
})) || []),
|
||||
...(note.notebooks || []).map((ref) => {
|
||||
const notebook = db.notebooks?.notebook(ref.id);
|
||||
const topics = notebook?.topics.all || [];
|
||||
|
||||
return {
|
||||
title: notebook?.title,
|
||||
topics: ref.topics
|
||||
.map((topicId) => topics.find((topic) => topic.id === topicId))
|
||||
.filter(Boolean)
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
for (const notebook of notebooks) {
|
||||
results[notebook.title] = results[notebook.title] || {};
|
||||
await mkdir(notebook.title);
|
||||
|
||||
if (notebook.topics && notebook.topics.length) {
|
||||
for (const topic of notebook.topics) {
|
||||
results[notebook.title][topic.title] =
|
||||
results[notebook.title][topic.title] || {};
|
||||
|
||||
await mkdir(`${notebook.title}/${topic.title}`);
|
||||
const exportedNoteName = getUniqueFileName(
|
||||
fileName + `.${type}`,
|
||||
results[notebook.title][topic.title]
|
||||
);
|
||||
results[notebook.title][topic.title][exportedNoteName] = true;
|
||||
|
||||
writeFile(
|
||||
`/${notebook.title}/${topic.title}/${exportedNoteName}`,
|
||||
result
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const exportedNoteName = getUniqueFileName(
|
||||
fileName + `.${type}`,
|
||||
results[notebook.title]
|
||||
);
|
||||
results[notebook.title][exportedNoteName] = true;
|
||||
writeFile(`/${notebook.title}/${exportedNoteName}`, result);
|
||||
}
|
||||
}
|
||||
|
||||
if (!notebooks.length) {
|
||||
const exportedNoteName = getUniqueFileName(
|
||||
fileName + `.${type}`,
|
||||
results
|
||||
);
|
||||
results[exportedNoteName] = true;
|
||||
writeFile(`/${exportedNoteName}`, result);
|
||||
}
|
||||
}
|
||||
callback(`${i + 1}/${notes.length}`);
|
||||
} catch (e) {
|
||||
@@ -244,26 +395,24 @@ async function bulkExport(notes, type, callback) {
|
||||
|
||||
try {
|
||||
callback("zipping");
|
||||
await sleep(1);
|
||||
const zipped = zipsync(results);
|
||||
const zipOutputPath =
|
||||
Platform.OS === "ios"
|
||||
? path + `/${fileName}`
|
||||
: RNFetchBlob.fs.dirs.CacheDir + `/${fileName}`;
|
||||
await zip(exportCacheFolder, zipOutputPath);
|
||||
callback("saving to disk");
|
||||
await sleep(1);
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
RNFetchBlob.fs.writeFile(path + `/${fileName}`, zipped, "base64");
|
||||
} else {
|
||||
const templFile = RNFetchBlob.fs.dirs.CacheDir + `/${fileName}`;
|
||||
await RNFetchBlob.fs.writeFile(templFile, zipped, "base64");
|
||||
if (Platform.OS === "android") {
|
||||
const file = await ScopedStorage.createFile(
|
||||
path,
|
||||
fileName,
|
||||
"application/zip"
|
||||
);
|
||||
path = file.uri;
|
||||
await copyFileAsync("file://" + templFile, path);
|
||||
await RNFetchBlob.fs.unlink(templFile);
|
||||
await copyFileAsync("file://" + zipOutputPath, path);
|
||||
await RNFetchBlob.fs.unlink(zipOutputPath);
|
||||
callback();
|
||||
}
|
||||
RNFetchBlob.fs.unlink(exportCacheFolder);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
|
||||
if (!progress) return;
|
||||
editorController.current?.commands.setAttachmentProgress({
|
||||
hash: hash,
|
||||
progress: 100,
|
||||
type: progress[hash]?.type || "download"
|
||||
progress: 100
|
||||
});
|
||||
progress[hash] = null;
|
||||
set({ progress: { ...progress } });
|
||||
@@ -80,10 +79,11 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
|
||||
progress[hash] = { sent, total, hash, recieved, type };
|
||||
const progressPercentage =
|
||||
type === "upload" ? sent / total : recieved / total;
|
||||
|
||||
editorController.current?.commands.setAttachmentProgress({
|
||||
hash: hash,
|
||||
progress: Math.round(Math.max(progressPercentage * 100, 0)),
|
||||
type: type
|
||||
//@ts-ignore
|
||||
progress: Math.round(Math.max(progressPercentage * 100, 0))
|
||||
});
|
||||
set({ progress: { ...progress } });
|
||||
},
|
||||
|
||||
@@ -61,7 +61,10 @@ export const getGithubVersion = async () => {
|
||||
if (!res?.ok) return null;
|
||||
const data = (await res?.json()) as GithubRelease[];
|
||||
|
||||
const versions = data?.filter((tag) => tag.tag_name.endsWith("android"));
|
||||
const versions = data?.filter(
|
||||
(tag) =>
|
||||
tag.tag_name.endsWith("android") && !tag.tag_name.endsWith("beta-android")
|
||||
);
|
||||
const latestVersion = versions[0];
|
||||
const version = latestVersion.tag_name.replace("-android", "");
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,7 @@ import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { isImage } from "@notesnook/core/dist/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { db } from "../common/database";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import { IOS_APPGROUPID } from "./constants";
|
||||
|
||||
const santizeUri = (uri) => {
|
||||
@@ -57,6 +57,7 @@ export async function attachFile(uri, hash, type, filename, options) {
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (Platform.OS === "ios") RNFetchBlob.fs.unlink(uri).catch(console.log);
|
||||
DatabaseLogger.error(e, "Attach file error");
|
||||
console.log("attach file error: ", e);
|
||||
return false;
|
||||
}
|
||||
@@ -107,26 +108,31 @@ async function createNotes(bundle) {
|
||||
uri: uri,
|
||||
type: "cache"
|
||||
});
|
||||
await attachFile(uri, hash, file.type, file.name, {
|
||||
const attached = await attachFile(uri, hash, file.type, file.name, {
|
||||
type: "cache",
|
||||
id: id,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
});
|
||||
let content = ``;
|
||||
if (isImage(file.type)) {
|
||||
content = `<img data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" />`;
|
||||
} else {
|
||||
content = `<p><span data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" data-size="${file.size}" /></p>`;
|
||||
|
||||
if (attached) {
|
||||
if (isImage(file.type)) {
|
||||
content = `<img data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" />`;
|
||||
} else {
|
||||
content = `<p><span data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" data-size="${file.size}" /></p>`;
|
||||
}
|
||||
const rawContent = await db.content.raw(
|
||||
db.notes.note(id).data?.contentId
|
||||
);
|
||||
await db.notes.add({
|
||||
id: id,
|
||||
content: {
|
||||
type: "tiptap",
|
||||
data: rawContent?.data ? rawContent?.data + content : content
|
||||
},
|
||||
sessionId: sessionId
|
||||
});
|
||||
}
|
||||
const rawContent = await db.content.raw(db.notes.note(id).data?.contentId);
|
||||
await db.notes.add({
|
||||
id: id,
|
||||
content: {
|
||||
type: "tiptap",
|
||||
data: rawContent?.data ? rawContent?.data + content : content
|
||||
},
|
||||
sessionId: sessionId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 2073
|
||||
versionCode 2076
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowDisablePreview">true</item>
|
||||
<item name="android:editTextBackground">@drawable/edit_text</item>
|
||||
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:datePickerDialogTheme">@style/DialogDatePicker.Theme</item>
|
||||
</style>
|
||||
@@ -20,6 +20,7 @@
|
||||
<style name="BootTheme" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/bootsplash_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/bootsplash_logo_dark</item>
|
||||
<item name="android:navigationBarColor">@color/bootsplash_background</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
|
||||
@@ -31,6 +32,7 @@
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
<item name="android:editTextBackground">@drawable/edit_text</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="DialogDatePicker.Theme" parent="Theme.AppCompat.Light.Dialog">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowDisablePreview">true</item>
|
||||
<item name="android:editTextBackground">@drawable/edit_text</item>
|
||||
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:datePickerDialogTheme">@style/DialogDatePicker.Theme</item>
|
||||
</style>
|
||||
@@ -20,6 +20,7 @@
|
||||
<style name="BootTheme" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/bootsplash_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/bootsplash_logo</item>
|
||||
<item name="android:navigationBarColor">@color/bootsplash_background</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
|
||||
@@ -31,6 +32,7 @@
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
<item name="android:editTextBackground">@drawable/edit_text</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="DialogDatePicker.Theme" parent="Theme.AppCompat.Light.Dialog">
|
||||
|
||||
@@ -1,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!
|
||||
15
apps/mobile/native/ios/ExportOptions.plist
Normal file
15
apps/mobile/native/ios/ExportOptions.plist
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>org.streetwriters.notesnook</key>
|
||||
<string>Notesnook App Distribution 2024</string>
|
||||
<key>org.streetwriters.notesnook.notewidget</key>
|
||||
<string>Notesnook Widget Distribution 2024</string>
|
||||
<key>org.streetwriters.notesnook.share</key>
|
||||
<string>Notesnook Share Distribution 2024</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -14,13 +14,13 @@
|
||||
2D02E4BD1E0B4A84006451C7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
2D02E4BF1E0B4AB3006451C7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
2DCD954D1E0B4F2C00145EB5 /* NotesnookTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* NotesnookTests.m */; };
|
||||
6510626F27042891009661C3 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6552012F27019F7700A43C51 /* OpenSans-Regular.ttf */; };
|
||||
6510627627042895009661C3 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6552012E27019F6E00A43C51 /* OpenSans-SemiBold.ttf */; };
|
||||
6510E6D72877215700DACAA9 /* build.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 6510E6D62877215700DACAA9 /* build.bundle */; };
|
||||
6515C42F2580AA3000E83E39 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6515C42E2580AA2F00E83E39 /* StoreKit.framework */; };
|
||||
6517B7C12B6838EB0079FF37 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */; };
|
||||
6517B7C22B6838EB0079FF37 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */; };
|
||||
6517B7C32B6838EB0079FF37 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */; };
|
||||
6529A13E279BC4C70048D4A8 /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6529A13D279BC4C70048D4A8 /* BootSplash.storyboard */; };
|
||||
6593E4A3281C345400492C50 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6593E4A2281C345400492C50 /* AppDelegate.mm */; };
|
||||
659670B22A2754FD00C5D2AF /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 659670B12A2754FD00C5D2AF /* OpenSans-Bold.ttf */; };
|
||||
659BE46725E11A5100E05671 /* notesnook-text.png in Resources */ = {isa = PBXBuildFile; fileRef = 659BE46625E11A5100E05671 /* notesnook-text.png */; };
|
||||
65AA857925E6DDEC00772A01 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65AA857825E6DDEC00772A01 /* WidgetKit.framework */; };
|
||||
65AA857B25E6DDEC00772A01 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65AA857A25E6DDEC00772A01 /* SwiftUI.framework */; };
|
||||
@@ -110,12 +110,12 @@
|
||||
4C4AF691C324D95337F2FB0A /* libPods-Make Note.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Make Note.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6510E6D62877215700DACAA9 /* build.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = build.bundle; path = "../../../../packages/editor-mobile/build.bundle"; sourceTree = "<group>"; };
|
||||
6515C42E2580AA2F00E83E39 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Regular.ttf"; path = "../../../../packages/editor-mobile/public/fonts/OpenSans-Regular.ttf"; sourceTree = "<group>"; };
|
||||
6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-SemiBold.ttf"; path = "../../../../packages/editor-mobile/public/fonts/OpenSans-SemiBold.ttf"; sourceTree = "<group>"; };
|
||||
6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Bold.ttf"; path = "../../../../packages/editor-mobile/public/fonts/OpenSans-Bold.ttf"; sourceTree = "<group>"; };
|
||||
6529A13D279BC4C70048D4A8 /* BootSplash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = BootSplash.storyboard; path = Notesnook/BootSplash.storyboard; sourceTree = "<group>"; };
|
||||
6529D2B0257B4A2900B49BC3 /* NotesnookDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NotesnookDebug.entitlements; path = Notesnook/NotesnookDebug.entitlements; sourceTree = "<group>"; };
|
||||
6552012E27019F6E00A43C51 /* OpenSans-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-SemiBold.ttf"; path = "../android/app/src/main/assets/fonts/OpenSans-SemiBold.ttf"; sourceTree = "<group>"; };
|
||||
6552012F27019F7700A43C51 /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Regular.ttf"; path = "../android/app/src/main/assets/fonts/OpenSans-Regular.ttf"; sourceTree = "<group>"; };
|
||||
6593E4A2281C345400492C50 /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Notesnook/AppDelegate.mm; sourceTree = "<group>"; };
|
||||
659670B12A2754FD00C5D2AF /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Bold.ttf"; path = "../android/app/src/main/assets/fonts/OpenSans-Bold.ttf"; sourceTree = "<group>"; };
|
||||
659BE46625E11A5100E05671 /* notesnook-text.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "notesnook-text.png"; path = "Notesnook/Images.xcassets/notesnook-text.png"; sourceTree = "<group>"; };
|
||||
65A7F34F255689AD00699170 /* Notesnook.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Notesnook.entitlements; path = Notesnook/Notesnook.entitlements; sourceTree = "<group>"; };
|
||||
65AA857725E6DDEC00772A01 /* NotesWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotesWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -293,13 +293,13 @@
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */,
|
||||
6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */,
|
||||
6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */,
|
||||
65C400DD2A80B68D00AA3DF5 /* MaterialCommunityIcons.ttf */,
|
||||
65C149862A61151B005C40F1 /* extension.bundle */,
|
||||
6510E6D62877215700DACAA9 /* build.bundle */,
|
||||
65EC5B71272A7EE200FB3748 /* NotesWidgetExtensionDebug.entitlements */,
|
||||
6552012F27019F7700A43C51 /* OpenSans-Regular.ttf */,
|
||||
659670B12A2754FD00C5D2AF /* OpenSans-Bold.ttf */,
|
||||
6552012E27019F6E00A43C51 /* OpenSans-SemiBold.ttf */,
|
||||
65D145C429DC30470056FE7D /* MaterialCommunityIcons.ttf */,
|
||||
13B07FAE1A68108700A75B9A /* Notesnook */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
@@ -542,12 +542,12 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
65C400DE2A80B68D00AA3DF5 /* MaterialCommunityIcons.ttf in Resources */,
|
||||
6510626F27042891009661C3 /* OpenSans-Regular.ttf in Resources */,
|
||||
659670B22A2754FD00C5D2AF /* OpenSans-Bold.ttf in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
6517B7C32B6838EB0079FF37 /* OpenSans-Bold.ttf in Resources */,
|
||||
6517B7C22B6838EB0079FF37 /* OpenSans-SemiBold.ttf in Resources */,
|
||||
6529A13E279BC4C70048D4A8 /* BootSplash.storyboard in Resources */,
|
||||
6510E6D72877215700DACAA9 /* build.bundle in Resources */,
|
||||
6510627627042895009661C3 /* OpenSans-SemiBold.ttf in Resources */,
|
||||
6517B7C12B6838EB0079FF37 /* OpenSans-Regular.ttf in Resources */,
|
||||
659BE46725E11A5100E05671 /* notesnook-text.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -997,7 +997,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 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)";
|
||||
|
||||
@@ -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
33
apps/mobile/package-lock.json
generated
33
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.6.14",
|
||||
"version": "2.6.15",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -62,7 +62,8 @@
|
||||
"@microsoft/signalr": "^7.0.10",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
apps/theme-builder/package-lock.json
generated
5
apps/theme-builder/package-lock.json
generated
@@ -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",
|
||||
|
||||
1737
apps/web/package-lock.json
generated
1737
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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++
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -72,11 +72,12 @@ type DocumentPreview = {
|
||||
function onEditorChange(
|
||||
noteId: string | undefined,
|
||||
sessionId: number,
|
||||
content: string
|
||||
content: string,
|
||||
ignoreEdit: boolean
|
||||
) {
|
||||
if (!content) return;
|
||||
|
||||
editorstore.get().saveSessionContent(noteId, sessionId, {
|
||||
editorstore.get().saveSessionContent(noteId, sessionId, ignoreEdit, {
|
||||
type: "tiptap",
|
||||
data: content
|
||||
});
|
||||
@@ -135,26 +136,7 @@ export default function EditorManager({
|
||||
if (result) item.data = result.data;
|
||||
else EV.publish(EVENTS.vaultLocked);
|
||||
}
|
||||
const oldHashes = editorInstance.current.getMediaHashes();
|
||||
|
||||
editorInstance.current.updateContent(item.data as string);
|
||||
|
||||
const newHashes = editorInstance.current.getMediaHashes();
|
||||
const hashesToLoad = newHashes.filter(
|
||||
(hash, index) => hash !== oldHashes[index]
|
||||
);
|
||||
|
||||
if (appstore.get().isSyncing()) {
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.syncCompleted,
|
||||
async () => {
|
||||
await db.attachments?.downloadMedia(id, hashesToLoad);
|
||||
},
|
||||
true
|
||||
);
|
||||
} else {
|
||||
await db.attachments?.downloadMedia(id, hashesToLoad);
|
||||
}
|
||||
} else if (isNote) {
|
||||
if (!locked && item.locked) return EV.publish(EVENTS.vaultLocked);
|
||||
|
||||
@@ -180,18 +162,6 @@ export default function EditorManager({
|
||||
setTimestamp(Date.now());
|
||||
}, []);
|
||||
|
||||
const loadMedia = useCallback(async () => {
|
||||
if (previewSession.current) {
|
||||
await db.content?.downloadMedia(
|
||||
noteId,
|
||||
previewSession.current.content,
|
||||
true
|
||||
);
|
||||
} else if (noteId && editorstore.get().session.content) {
|
||||
await db.attachments?.downloadMedia(noteId);
|
||||
}
|
||||
}, [noteId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewSession) return;
|
||||
|
||||
@@ -237,6 +207,7 @@ export default function EditorManager({
|
||||
/>
|
||||
)}
|
||||
<Editor
|
||||
id={noteId}
|
||||
nonce={timestamp}
|
||||
content={() =>
|
||||
previewSession.current?.content?.data ||
|
||||
@@ -247,7 +218,6 @@ export default function EditorManager({
|
||||
options={{
|
||||
readonly: isReadonly || isPreviewSession,
|
||||
onRequestFocus: () => toggleProperties(false),
|
||||
onLoadMedia: loadMedia,
|
||||
focusMode: isFocusMode,
|
||||
isMobile: isMobile || isTablet
|
||||
}}
|
||||
@@ -351,9 +321,9 @@ type EditorOptions = {
|
||||
readonly?: boolean;
|
||||
focusMode?: boolean;
|
||||
onRequestFocus?: () => void;
|
||||
onLoadMedia?: () => void;
|
||||
};
|
||||
type EditorProps = {
|
||||
id: string | number;
|
||||
content: () => string | undefined;
|
||||
nonce?: number;
|
||||
options?: EditorOptions;
|
||||
@@ -361,8 +331,9 @@ type EditorProps = {
|
||||
onPreviewDocument?: (preview: DocumentPreview) => void;
|
||||
};
|
||||
export function Editor(props: EditorProps) {
|
||||
const { content, nonce, options, onContentChange, onPreviewDocument } = props;
|
||||
const { readonly, headless, onLoadMedia, isMobile } = options || {
|
||||
const { id, content, nonce, options, onContentChange, onPreviewDocument } =
|
||||
props;
|
||||
const { readonly, headless, isMobile } = options || {
|
||||
headless: false,
|
||||
readonly: false,
|
||||
focusMode: false,
|
||||
@@ -375,40 +346,16 @@ export function Editor(props: EditorProps) {
|
||||
useEffect(() => {
|
||||
const event = AppEventManager.subscribe(
|
||||
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
|
||||
({ hash, loaded, total, type }: AttachmentProgress) => {
|
||||
({ hash, loaded, total }: AttachmentProgress) => {
|
||||
editor.current?.sendAttachmentProgress(
|
||||
hash,
|
||||
type,
|
||||
Math.round((loaded / total) * 100)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const mediaAttachmentDownloadedEvent = EV.subscribe(
|
||||
EVENTS.mediaAttachmentDownloaded,
|
||||
({
|
||||
groupId,
|
||||
hash,
|
||||
attachmentType,
|
||||
src
|
||||
}: {
|
||||
groupId?: string;
|
||||
attachmentType: "image" | "webclip" | "generic";
|
||||
hash: string;
|
||||
src: string;
|
||||
}) => {
|
||||
if (groupId?.startsWith("monograph")) return;
|
||||
if (attachmentType === "image") {
|
||||
editor.current?.loadImage(hash, src);
|
||||
} else if (attachmentType === "webclip") {
|
||||
editor.current?.loadWebClip(hash, src);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
event.unsubscribe();
|
||||
mediaAttachmentDownloadedEvent.unsubscribe();
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
@@ -424,7 +371,6 @@ export function Editor(props: EditorProps) {
|
||||
corsHost: Config.get("corsProxy", "https://cors.notesnook.com")
|
||||
}}
|
||||
onLoad={() => {
|
||||
if (onLoadMedia) onLoadMedia();
|
||||
if (nonce && nonce > 0) setIsLoading(false);
|
||||
}}
|
||||
onContentChange={onContentChange}
|
||||
@@ -437,8 +383,11 @@ export function Editor(props: EditorProps) {
|
||||
const container = document.getElementById("dialogContainer");
|
||||
if (!(container instanceof HTMLElement)) return;
|
||||
|
||||
const dataurl =
|
||||
data.bloburl || (await downloadAttachment(hash, "base64"));
|
||||
const dataurl = await downloadAttachment(
|
||||
hash,
|
||||
"base64",
|
||||
id.toString()
|
||||
);
|
||||
if (!dataurl)
|
||||
return showToast("error", "This image cannot be previewed.");
|
||||
|
||||
@@ -455,7 +404,7 @@ export function Editor(props: EditorProps) {
|
||||
);
|
||||
} else if (attachment && onPreviewDocument) {
|
||||
onPreviewDocument({ hash });
|
||||
const blob = await downloadAttachment(hash, "blob");
|
||||
const blob = await downloadAttachment(hash, "blob", id.toString());
|
||||
if (!blob) return;
|
||||
onPreviewDocument({ url: URL.createObjectURL(blob), hash });
|
||||
}
|
||||
@@ -467,6 +416,13 @@ export function Editor(props: EditorProps) {
|
||||
editor.current?.attachFile(file);
|
||||
});
|
||||
}}
|
||||
onGetAttachmentData={(attachment) => {
|
||||
return downloadAttachment(
|
||||
attachment.hash,
|
||||
attachment.type === "web-clip" ? "text" : "base64",
|
||||
id.toString()
|
||||
);
|
||||
}}
|
||||
onAttachFile={async (file) => {
|
||||
const result = await attachFile(file);
|
||||
if (!result) return;
|
||||
@@ -580,7 +536,7 @@ function PreviewModeNotice(props: PreviewModeNoticeProps) {
|
||||
async (cancelled: boolean) => {
|
||||
const { id, sessionId } = editorstore.get().session;
|
||||
if (!cancelled) {
|
||||
await editorstore.saveSessionContent(id, sessionId, content);
|
||||
await editorstore.saveSessionContent(id, sessionId, false, content);
|
||||
}
|
||||
onDiscard();
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ import { TaskManager } from "../../common/task-manager";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { showFilePicker } from "../../utils/file-picker";
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
|
||||
const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
|
||||
const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
|
||||
@@ -80,33 +81,54 @@ export async function reuploadAttachment(
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {File} selectedFile
|
||||
* @param {File} file
|
||||
* @returns
|
||||
*/
|
||||
async function pickFile(selectedFile: File, options?: AddAttachmentOptions) {
|
||||
async function pickFile(
|
||||
file: File,
|
||||
options?: AddAttachmentOptions
|
||||
): Promise<Attachment | undefined> {
|
||||
try {
|
||||
if (selectedFile.size > FILE_SIZE_LIMIT)
|
||||
if (file.size > FILE_SIZE_LIMIT)
|
||||
throw new Error("File too big. You cannot add files over 500 MB.");
|
||||
if (!selectedFile) return;
|
||||
if (!file) return;
|
||||
|
||||
return await addAttachment(selectedFile, undefined, options);
|
||||
const hash = await addAttachment(file, options);
|
||||
return {
|
||||
type: "file",
|
||||
filename: file.name,
|
||||
hash,
|
||||
mime: file.type,
|
||||
size: file.size
|
||||
};
|
||||
} catch (e) {
|
||||
showToast("error", `${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {File} selectedImage
|
||||
* @param {File} file
|
||||
* @returns
|
||||
*/
|
||||
async function pickImage(selectedImage: File, options?: AddAttachmentOptions) {
|
||||
async function pickImage(
|
||||
file: File,
|
||||
options?: AddAttachmentOptions
|
||||
): Promise<Attachment | undefined> {
|
||||
try {
|
||||
if (selectedImage.size > IMAGE_SIZE_LIMIT)
|
||||
if (file.size > IMAGE_SIZE_LIMIT)
|
||||
throw new Error("Image too big. You cannot add images over 50 MB.");
|
||||
if (!selectedImage) return;
|
||||
if (!file) return;
|
||||
|
||||
const dataurl = await toDataURL(selectedImage);
|
||||
return await addAttachment(selectedImage, dataurl, options);
|
||||
const hash = await addAttachment(file, options);
|
||||
const dimensions = await getImageDimensions(file);
|
||||
return {
|
||||
type: "image",
|
||||
filename: file.name,
|
||||
hash,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
...dimensions
|
||||
};
|
||||
} catch (e) {
|
||||
showToast("error", (e as Error).message);
|
||||
}
|
||||
@@ -118,12 +140,6 @@ async function getEncryptionKey(): Promise<SerializedKey> {
|
||||
return key;
|
||||
}
|
||||
|
||||
async function toDataURL(file: File): Promise<string> {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const base64 = Buffer.from(buffer).toString("base64");
|
||||
return `data:${file.type};base64,${base64}`;
|
||||
}
|
||||
|
||||
export type AttachmentProgress = {
|
||||
hash: string;
|
||||
type: "encrypt" | "download" | "upload";
|
||||
@@ -131,15 +147,6 @@ export type AttachmentProgress = {
|
||||
loaded: number;
|
||||
};
|
||||
|
||||
export type Attachment = {
|
||||
hash: string;
|
||||
filename: string;
|
||||
mime: string;
|
||||
size: number;
|
||||
dataurl?: string;
|
||||
bloburl?: string;
|
||||
};
|
||||
|
||||
type AddAttachmentOptions = {
|
||||
expectedFileHash?: string;
|
||||
showProgress?: boolean;
|
||||
@@ -148,9 +155,8 @@ type AddAttachmentOptions = {
|
||||
|
||||
async function addAttachment(
|
||||
file: File,
|
||||
dataurl: string | undefined,
|
||||
options: AddAttachmentOptions = {}
|
||||
): Promise<Attachment> {
|
||||
): Promise<string> {
|
||||
const { default: FS } = await import("../../interfaces/fs");
|
||||
const { expectedFileHash, showProgress = true } = options;
|
||||
let forceWrite = options.forceWrite;
|
||||
@@ -165,7 +171,7 @@ async function addAttachment(
|
||||
`Please select the same file for reuploading. Expected hash ${expectedFileHash} but got ${hash}.`
|
||||
);
|
||||
|
||||
const exists = db.attachments?.exists(hash);
|
||||
const exists = db.attachments?.attachment(hash);
|
||||
if (!forceWrite && exists) {
|
||||
forceWrite = (await FS.getUploadedFileSize(hash)) <= 0;
|
||||
}
|
||||
@@ -181,19 +187,13 @@ async function addAttachment(
|
||||
...output,
|
||||
hash,
|
||||
hashType,
|
||||
filename: file.name,
|
||||
type: file.type,
|
||||
filename: exists?.metadata.filename || file.name,
|
||||
type: exists?.metadata.type || file.type,
|
||||
key
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hash: hash,
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
dataurl
|
||||
};
|
||||
return hash;
|
||||
};
|
||||
|
||||
const result = showProgress
|
||||
@@ -204,7 +204,10 @@ async function addAttachment(
|
||||
return result;
|
||||
}
|
||||
|
||||
function withProgress<T>(file: File, action: () => Promise<T>): Promise<T> {
|
||||
function withProgress<T>(
|
||||
file: File,
|
||||
action: () => Promise<T>
|
||||
): Promise<T | Error> {
|
||||
return TaskManager.startTask({
|
||||
type: "modal",
|
||||
title: "Encrypting attachment",
|
||||
@@ -226,3 +229,19 @@ function withProgress<T>(file: File, action: () => Promise<T>): Promise<T> {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getImageDimensions(file: File) {
|
||||
return new Promise<{ width: number; height: number } | undefined>(
|
||||
(resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const { naturalWidth: width, naturalHeight: height } = img;
|
||||
resolve({ width, height });
|
||||
};
|
||||
img.onerror = () => {
|
||||
resolve(undefined);
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@ import {
|
||||
getTotalWords,
|
||||
countWords,
|
||||
getFontById,
|
||||
TiptapOptions
|
||||
TiptapOptions,
|
||||
Attachment
|
||||
} from "@notesnook/editor";
|
||||
import { Box, Flex } from "@theme-ui/components";
|
||||
import { PropsWithChildren, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Attachment } from "./picker";
|
||||
import { IEditor } from "./types";
|
||||
import {
|
||||
useConfigureEditor,
|
||||
@@ -57,12 +57,12 @@ import { store as editorstore } from "../../stores/editor-store";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
|
||||
|
||||
type OnChangeHandler = (
|
||||
id: string | undefined,
|
||||
sessionId: number,
|
||||
content: string
|
||||
content: string,
|
||||
ignoreEdit: boolean
|
||||
) => void;
|
||||
type TipTapProps = {
|
||||
editorContainer: HTMLElement;
|
||||
@@ -72,6 +72,7 @@ type TipTapProps = {
|
||||
onInsertAttachment?: (type: AttachmentType) => void;
|
||||
onDownloadAttachment?: (attachment: Attachment) => void;
|
||||
onPreviewAttachment?: (attachment: Attachment) => void;
|
||||
onGetAttachmentData?: (attachment: Attachment) => Promise<string | undefined>;
|
||||
onAttachFile?: (file: File) => void;
|
||||
onFocus?: () => void;
|
||||
content?: () => string | undefined;
|
||||
@@ -92,6 +93,7 @@ function save(
|
||||
editor: Editor,
|
||||
content: Fragment,
|
||||
preventSave: boolean,
|
||||
ignoreEdit: boolean,
|
||||
onChange?: OnChangeHandler
|
||||
) {
|
||||
configureEditor({
|
||||
@@ -105,7 +107,7 @@ function save(
|
||||
|
||||
if (preventSave) return;
|
||||
const html = getHTMLFromFragment(content, editor.schema);
|
||||
onChange?.(noteId, sessionId, html);
|
||||
onChange?.(noteId, sessionId, html, ignoreEdit);
|
||||
}
|
||||
|
||||
const deferredSave = debounceWithId(save, SAVE_INTERVAL);
|
||||
@@ -117,6 +119,7 @@ function TipTap(props: TipTapProps) {
|
||||
onInsertAttachment,
|
||||
onDownloadAttachment,
|
||||
onPreviewAttachment,
|
||||
onGetAttachmentData,
|
||||
onAttachFile,
|
||||
onContentChange,
|
||||
onFocus = () => {},
|
||||
@@ -210,7 +213,8 @@ function TipTap(props: TipTapProps) {
|
||||
onUpdate: ({ editor, transaction }) => {
|
||||
onContentChange?.();
|
||||
|
||||
const preventSave = transaction?.getMeta("preventSave") as boolean;
|
||||
const preventSave = transaction.getMeta("preventSave") as boolean;
|
||||
const ignoreEdit = transaction.getMeta("ignoreEdit") as boolean;
|
||||
const { id, sessionId } = editorstore.get().session;
|
||||
const content = editor.state.doc.content;
|
||||
deferredSave(
|
||||
@@ -220,6 +224,7 @@ function TipTap(props: TipTapProps) {
|
||||
editor as Editor,
|
||||
content,
|
||||
preventSave || !editor.isEditable,
|
||||
ignoreEdit,
|
||||
onChange
|
||||
);
|
||||
},
|
||||
@@ -282,7 +287,8 @@ function TipTap(props: TipTapProps) {
|
||||
onOpenLink: (url) => {
|
||||
window.open(url, "_blank");
|
||||
return true;
|
||||
}
|
||||
},
|
||||
getAttachmentData: onGetAttachmentData
|
||||
};
|
||||
}, [readonly, nonce, doubleSpacedLines, dateFormat, timeFormat]);
|
||||
|
||||
@@ -420,14 +426,6 @@ function toIEditor(editor: Editor): IEditor {
|
||||
}),
|
||||
undo: () => editor.current?.commands.undo(),
|
||||
redo: () => editor.current?.commands.redo(),
|
||||
getMediaHashes: () => {
|
||||
if (!editor.current) return [];
|
||||
const hashes: string[] = [];
|
||||
editor.current.state.doc.descendants((n) => {
|
||||
if (typeof n.attrs.hash === "string") hashes.push(n.attrs.hash);
|
||||
});
|
||||
return hashes;
|
||||
},
|
||||
updateContent: (content) => {
|
||||
const { from, to } = editor.state.selection;
|
||||
editor.current
|
||||
@@ -443,27 +441,17 @@ function toIEditor(editor: Editor): IEditor {
|
||||
})
|
||||
.run();
|
||||
},
|
||||
attachFile: (file: Attachment) => {
|
||||
if (file.dataurl) {
|
||||
editor.current?.commands.insertImage({
|
||||
...file,
|
||||
bloburl: toBlobURL(file.dataurl, file.hash)
|
||||
});
|
||||
} else editor.current?.commands.insertAttachment(file);
|
||||
},
|
||||
loadWebClip: (hash, src) =>
|
||||
editor.current?.commands.updateWebClip({ hash }, { src }),
|
||||
loadImage: (hash, dataurl) =>
|
||||
editor.current?.commands.updateImage(
|
||||
{ hash },
|
||||
{ hash, bloburl: toBlobURL(dataurl, hash), preventUpdate: true }
|
||||
),
|
||||
sendAttachmentProgress: (hash, type, progress) =>
|
||||
editor.current?.commands.setAttachmentProgress({
|
||||
hash,
|
||||
type,
|
||||
progress
|
||||
})
|
||||
attachFile: (file: Attachment) =>
|
||||
file.type === "image"
|
||||
? editor.current?.commands.insertImage(file)
|
||||
: editor.current?.commands.insertAttachment(file),
|
||||
sendAttachmentProgress: (hash, progress) =>
|
||||
editor.current?.commands.updateAttachment(
|
||||
{
|
||||
progress
|
||||
},
|
||||
{ query: (a) => a.hash === hash, preventUpdate: true }
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import {
|
||||
Published,
|
||||
@@ -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,
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Attachment } from "./picker";
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
|
||||
export type NoteStatistics = {
|
||||
words: {
|
||||
@@ -33,14 +33,7 @@ export interface IEditor {
|
||||
}) => void;
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
getMediaHashes: () => string[];
|
||||
updateContent: (content: string) => void;
|
||||
attachFile: (file: Attachment) => void;
|
||||
loadWebClip: (hash: string, html: string) => void;
|
||||
loadImage: (hash: string, src: string) => void;
|
||||
sendAttachmentProgress: (
|
||||
hash: string,
|
||||
type: "download" | "upload" | "encrypt",
|
||||
progress: number
|
||||
) => void;
|
||||
sendAttachmentProgress: (hash: string, progress: number) => void;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ import { hashNavigate, navigate } from "../../navigation";
|
||||
import { showPublishView } from "../publish-view";
|
||||
import IconTag from "../icon-tag";
|
||||
import { COLORS } from "../../common/constants";
|
||||
import { exportNotes } from "../../common/export";
|
||||
import { exportNote, exportNotes, exportToPDF } from "../../common/export";
|
||||
import { Multiselect } from "../../common/multi-select";
|
||||
import { store as selectionStore } from "../../stores/selection-store";
|
||||
import {
|
||||
@@ -86,7 +86,7 @@ import {
|
||||
ReferencesWithDateEdited
|
||||
} from "../list-container/types";
|
||||
import { SchemeColors } from "@notesnook/theme";
|
||||
import Vault from "../../common/vault";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
type NoteProps = {
|
||||
tags: Item[];
|
||||
@@ -369,10 +369,9 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
|
||||
onClick: async () => {
|
||||
const { unlock, lock } = store.get();
|
||||
if (!note.locked) {
|
||||
await lock(note.id);
|
||||
showToast("success", "Note locked successfully!");
|
||||
} else {
|
||||
await unlock(note.id);
|
||||
if (await lock(note.id))
|
||||
showToast("success", "Note locked successfully!");
|
||||
} else if (await unlock(note.id)) {
|
||||
showToast("success", "Note unlocked successfully!");
|
||||
}
|
||||
},
|
||||
@@ -422,7 +421,12 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
|
||||
isDisabled: !isSynced,
|
||||
icon: Print.path,
|
||||
onClick: async () => {
|
||||
await exportNotes("pdf", [note.id]);
|
||||
const item = db.notes?.note(note);
|
||||
if (!item) return;
|
||||
|
||||
const result = await exportNote(item, "pdf");
|
||||
if (!result) return;
|
||||
await exportToPDF(note.title, result.content);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -456,7 +460,24 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
|
||||
// ? "Multiple notes cannot be exported as PDF."
|
||||
// : false,
|
||||
isPro: format.type !== "txt",
|
||||
onClick: () => exportNotes(format.type, ids)
|
||||
onClick: async () => {
|
||||
if (ids.length === 1) {
|
||||
const item = db.notes?.note(note);
|
||||
if (!item) return;
|
||||
|
||||
const result = await exportNote(item, format.type);
|
||||
if (!result) return;
|
||||
if (format.type === "pdf")
|
||||
return exportToPDF(note.title, result.content);
|
||||
|
||||
return FileSaver.saveAs(
|
||||
new Blob([new TextEncoder().encode(result.content)]),
|
||||
result.filename
|
||||
);
|
||||
}
|
||||
|
||||
await exportNotes(format.type, ids);
|
||||
}
|
||||
}))
|
||||
},
|
||||
multiSelect: true,
|
||||
@@ -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)
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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.");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -306,8 +306,17 @@ class EditorStore extends BaseStore {
|
||||
return this.saveSession(noteId, { [name]: value });
|
||||
};
|
||||
|
||||
saveSessionContent = (noteId, sessionId, content) => {
|
||||
return this.saveSession(noteId, { sessionId, content });
|
||||
/**
|
||||
*
|
||||
* @param {*} noteId
|
||||
* @param {*} sessionId
|
||||
* @param {boolean} ignoreEdit if this is set to true, we save the content but do not update the dateEdited. Useful for metadata only changes in content.
|
||||
* @param {*} content
|
||||
* @returns
|
||||
*/
|
||||
saveSessionContent = (noteId, sessionId, ignoreEdit, content) => {
|
||||
const dateEdited = ignoreEdit ? this.get().session.dateEdited : undefined;
|
||||
return this.saveSession(noteId, { sessionId, content, dateEdited });
|
||||
};
|
||||
|
||||
setTag = (tag) => {
|
||||
|
||||
@@ -123,10 +123,11 @@ class NoteStore extends BaseStore {
|
||||
};
|
||||
|
||||
lock = async (id) => {
|
||||
await Vault.lockNote(id);
|
||||
if (!(await Vault.lockNote(id))) return false;
|
||||
this.refreshItem(id);
|
||||
if (editorStore.get().session.id === id)
|
||||
await editorStore.openSession(id, true);
|
||||
return true;
|
||||
};
|
||||
|
||||
readonly = async (id) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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]
|
||||
}`;
|
||||
}
|
||||
|
||||
96
apps/web/src/utils/streams/export-stream.ts
Normal file
96
apps/web/src/utils/streams/export-stream.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
101
apps/web/src/utils/streams/fflate-shim.js
Normal file
101
apps/web/src/utils/streams/fflate-shim.js
Normal 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 };
|
||||
77
apps/web/src/utils/streams/unzip-stream.ts
Normal file
77
apps/web/src/utils/streams/unzip-stream.ts
Normal 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.");
|
||||
}
|
||||
}
|
||||
35
apps/web/src/utils/streams/utils.ts
Normal file
35
apps/web/src/utils/streams/utils.ts
Normal 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);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
BIN
docs/help/contents/_include/static/skiff-importer/1.png
Normal file
BIN
docs/help/contents/_include/static/skiff-importer/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/help/contents/_include/static/skiff-importer/2.png
Normal file
BIN
docs/help/contents/_include/static/skiff-importer/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/help/contents/_include/static/skiff-importer/3.png
Normal file
BIN
docs/help/contents/_include/static/skiff-importer/3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
docs/help/contents/_include/static/skiff-importer/4.png
Normal file
BIN
docs/help/contents/_include/static/skiff-importer/4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Skiff Pages
|
||||
---
|
||||
|
||||
# How do I import notes from Skiff Pages?
|
||||
|
||||
The following steps will help you quickly import your notes from Skiff Pages into Notesnook.
|
||||
|
||||
## Exporting your Skiff Pages
|
||||
|
||||
1. Open the [Skiff Pages](https://app.skiff.com) app
|
||||
2. Open Settings > Export or just go directly to [https://app.skiff.com/dashboard/?settingTab=export](https://app.skiff.com/dashboard/?settingTab=export)
|
||||

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

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

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

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

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

|
||||
6. Once importing completes you should see all your notes in Notesnook. If you face any issues during importing, feel free to [report them on GitHub](https://github.com/streetwriters/notesnook-importer).
|
||||
|
||||
## Supported formats
|
||||
|
||||
- [x] Images
|
||||
- [x] Code blocks
|
||||
- [ ] Math blocks (Skiff Pages doesn't mark them properly so there's no way to detect them.)
|
||||
- [x] Tables
|
||||
- [x] Rich text (bold, italic, headings, lists etc.)
|
||||
- [x] Task lists
|
||||
@@ -38,6 +38,7 @@ navigation:
|
||||
- path: importing-notes/import-notes-from-simplenote.md
|
||||
- path: importing-notes/import-notes-from-standardnotes.md
|
||||
- path: importing-notes/import-notes-from-zoho-notebook.md
|
||||
- path: importing-notes/import-notes-from-skiff-pages.md
|
||||
- path: importing-notes/import-notes-from-obsidian.md
|
||||
- path: importing-notes/import-notes-from-html-files.md
|
||||
- path: importing-notes/import-notes-from-markdown-files.md
|
||||
|
||||
10
fastlane/metadata/android/en-US/changelogs/10374.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/10374.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
- Support for exporting locked notes
|
||||
- Maintain directory structure in exports based on Notebooks/Topics
|
||||
- Match color of navigation bar in sheets with app theme
|
||||
- Improved image loading and show progress when loading images
|
||||
- Support for setting trash interval to daily
|
||||
- Fix android app UI issues with RTL languages
|
||||
- Fix pasting links in editor
|
||||
- Other Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
10
fastlane/metadata/android/en-US/changelogs/10379.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/10379.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
- Support for exporting locked notes
|
||||
- Maintain directory structure in exports based on Notebooks/Topics
|
||||
- Match color of navigation bar in sheets with app theme
|
||||
- Improved image loading and show progress when loading images
|
||||
- Support for setting trash interval to daily
|
||||
- Fix android app UI issues with RTL languages
|
||||
- Fix pasting links in editor
|
||||
- Other Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
10
packages/common/package-lock.json
generated
10
packages/common/package-lock.json
generated
@@ -31,7 +31,8 @@
|
||||
"@microsoft/signalr": "^7.0.10",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.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",
|
||||
|
||||
@@ -75,7 +75,7 @@ function sanitize(input: string, replacement: string) {
|
||||
.replace(windowsReservedRe, replacement)
|
||||
.replace(windowsTrailingRe, replacement);
|
||||
|
||||
return sanitized.slice(0, 254).toLowerCase();
|
||||
return sanitized.slice(0, 254);
|
||||
}
|
||||
|
||||
export function sanitizeFilename(
|
||||
|
||||
109
packages/core/package-lock.json
generated
109
packages/core/package-lock.json
generated
@@ -12,7 +12,8 @@
|
||||
"@microsoft/signalr": "^7.0.10",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
|
||||
"@notesnook/logger": "file:../logger",
|
||||
"@streetwriters/showdown": "^3.0.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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ class Settings {
|
||||
}
|
||||
/**
|
||||
* Setting to -1 means never clear trash.
|
||||
* @param {7 | 30 | 365 | -1} time
|
||||
* @param {1 | 7 | 30 | 365 | -1} time
|
||||
*/
|
||||
async setTrashCleanupInterval(time) {
|
||||
this._settings.trashCleanupInterval = time;
|
||||
|
||||
@@ -61,6 +61,10 @@ export default class Vault {
|
||||
});
|
||||
}
|
||||
|
||||
get unlocked() {
|
||||
return !!this._vaultPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new vault
|
||||
* @param {string} password The password
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@ export default class Content extends Collection {
|
||||
}
|
||||
|
||||
async raw(id) {
|
||||
if (!id) return;
|
||||
|
||||
const content = await this._collection.getItem(id);
|
||||
if (!content) return;
|
||||
return content;
|
||||
@@ -118,6 +120,7 @@ export default class Content extends Collection {
|
||||
async downloadMedia(groupId, contentItem, notify = true) {
|
||||
if (!contentItem) return contentItem;
|
||||
const content = getContentFromData(contentItem.type, contentItem.data);
|
||||
if (!content) console.log(contentItem);
|
||||
contentItem.data = await content.insertMedia(async (hashes) => {
|
||||
const attachments = hashes.map((h) => this._db.attachments.attachment(h));
|
||||
await this._db.fs.queueDownloads(
|
||||
|
||||
@@ -129,7 +129,8 @@ export default class Notes extends Collection {
|
||||
readonly: !!note.readonly,
|
||||
|
||||
dateCreated: note.dateCreated,
|
||||
dateEdited: note.dateEdited || note.dateCreated || Date.now(),
|
||||
dateEdited:
|
||||
noteArg.dateEdited || note.dateEdited || note.dateCreated || Date.now(),
|
||||
dateModified: note.dateModified
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user