mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 02:58:34 +02:00
Compare commits
10 Commits
ai
...
3.0.9-andr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a45b66d449 | ||
|
|
dd67b1a803 | ||
|
|
00568ae839 | ||
|
|
2b2ea5717c | ||
|
|
5672de8565 | ||
|
|
1c70ad8cec | ||
|
|
0c344a2146 | ||
|
|
38f69b514c | ||
|
|
b502f6a08f | ||
|
|
d68c3269d3 |
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { app, dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { AutoLaunch } from "../utils/autolaunch";
|
||||
import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
@@ -152,6 +152,10 @@ export const osIntegrationRouter = t.router({
|
||||
await rm(input);
|
||||
}),
|
||||
|
||||
restart: t.procedure.query(() => {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
}),
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
@@ -191,7 +195,10 @@ export const osIntegrationRouter = t.router({
|
||||
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
|
||||
if (windowControlsIconColor) {
|
||||
config.windowControlsIconColor = windowControlsIconColor;
|
||||
if (process.platform === "win32")
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
!config.desktopSettings.nativeTitlebar
|
||||
)
|
||||
globalThis.window?.setTitleBarOverlay({
|
||||
symbolColor: windowControlsIconColor
|
||||
});
|
||||
|
||||
@@ -84,17 +84,21 @@ async function createWindow() {
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
}),
|
||||
|
||||
titleBarStyle: "hidden",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
color: "#00000000",
|
||||
symbolColor: config.windowControlsIconColor
|
||||
},
|
||||
trafficLightPosition: {
|
||||
x: 16,
|
||||
y: 12
|
||||
},
|
||||
...(config.desktopSettings.nativeTitlebar
|
||||
? {}
|
||||
: {
|
||||
titleBarStyle: "hidden",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
color: "#00000000",
|
||||
symbolColor: config.windowControlsIconColor
|
||||
},
|
||||
trafficLightPosition: {
|
||||
x: 16,
|
||||
y: 12
|
||||
}
|
||||
}),
|
||||
|
||||
webPreferences: {
|
||||
zoomFactor: config.zoomFactor,
|
||||
|
||||
@@ -25,7 +25,8 @@ export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
startMinimized: z.boolean().optional(),
|
||||
minimizeToSystemTray: z.boolean().optional(),
|
||||
closeToSystemTray: z.boolean().optional()
|
||||
closeToSystemTray: z.boolean().optional(),
|
||||
nativeTitlebar: z.boolean().optional()
|
||||
});
|
||||
|
||||
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
|
||||
@@ -35,7 +36,8 @@ export const config = {
|
||||
autoStart: false,
|
||||
startMinimized: false,
|
||||
minimizeToSystemTray: false,
|
||||
closeToSystemTray: false
|
||||
closeToSystemTray: false,
|
||||
nativeTitlebar: false
|
||||
},
|
||||
privacyMode: false,
|
||||
isSpellCheckerEnabled: true,
|
||||
|
||||
@@ -355,6 +355,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
|
||||
AttachmentDialog.present = (note?: Note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog note={note} />
|
||||
component: () => <AttachmentDialog note={note} />,
|
||||
keyboardHandlerDisabled: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,12 +17,16 @@ 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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Sync from "../../services/sync";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
@@ -34,11 +38,6 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { useLogin } from "./use-login";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import Sync from "../../services/sync";
|
||||
import { Notice } from "../ui/notice";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -184,7 +183,11 @@ export const Login = ({ changeMode }) => {
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -243,6 +246,7 @@ export const Login = ({ changeMode }) => {
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
|
||||
@@ -37,6 +37,7 @@ import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
|
||||
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -143,7 +144,10 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
@@ -214,6 +218,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
code.current = value;
|
||||
//onNext();
|
||||
}}
|
||||
onSubmitEditing={onNext}
|
||||
caretHidden
|
||||
inputStyle={{
|
||||
fontSize: SIZE.lg,
|
||||
@@ -225,6 +230,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
keyboardType={
|
||||
currentMethod.method === "recoveryCode" ? "default" : "numeric"
|
||||
}
|
||||
enablesReturnKeyAutomatically
|
||||
containerStyle={{
|
||||
height: 60,
|
||||
borderWidth: 0,
|
||||
@@ -297,7 +303,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ export const Card = ({ color }: { color?: string }) => {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 0
|
||||
paddingHorizontal: 0,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -91,7 +92,6 @@ export const Card = ({ color }: { color?: string }) => {
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
flexShrink: 1,
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
DimensionValue,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle
|
||||
ViewStyle,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
@@ -92,6 +93,9 @@ export const Button = ({
|
||||
});
|
||||
const textColor = buttonType?.text ? buttonType.text : text;
|
||||
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const growFactor = 1 + (fontScale - 1) / 10;
|
||||
|
||||
const Component = bold ? Heading : Paragraph;
|
||||
|
||||
return (
|
||||
@@ -117,8 +121,11 @@ export const Button = ({
|
||||
customOpacity={buttonType?.opacity}
|
||||
customAlpha={buttonType?.alpha}
|
||||
style={{
|
||||
height: height,
|
||||
width: (width as DimensionValue) || undefined,
|
||||
height: typeof height === "number" ? height * growFactor : height,
|
||||
width:
|
||||
typeof width === "number"
|
||||
? width * growFactor
|
||||
: (width as DimensionValue) || undefined,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 5,
|
||||
alignSelf: "center",
|
||||
|
||||
@@ -21,11 +21,12 @@ import { VariantsWithStaticColors, useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useCallback } from "react";
|
||||
import {
|
||||
ColorValue,
|
||||
PressableStateCallbackType,
|
||||
Pressable as RNPressable,
|
||||
PressableProps as RNPressableProps,
|
||||
PressableStateCallbackType,
|
||||
View,
|
||||
ViewStyle
|
||||
ViewStyle,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import {
|
||||
RGB_Linear_Shade,
|
||||
@@ -256,6 +257,8 @@ export const Pressable = ({
|
||||
? 1
|
||||
: colorOpacity;
|
||||
const alpha = customAlpha ? customAlpha : isDark ? 0.03 : -0.03;
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const growFactor = 1 + (fontScale - 1) / 8;
|
||||
|
||||
const getStyle = useCallback(
|
||||
({ pressed }: PressableStateCallbackType): ViewStyle | ViewStyle[] => [
|
||||
@@ -276,7 +279,13 @@ export const Pressable = ({
|
||||
: borderColor || "transparent",
|
||||
borderWidth: borderWidth
|
||||
},
|
||||
style
|
||||
style,
|
||||
{
|
||||
height:
|
||||
typeof style.height === "number"
|
||||
? style.height * growFactor
|
||||
: style.height
|
||||
}
|
||||
],
|
||||
[
|
||||
alpha,
|
||||
@@ -288,7 +297,8 @@ export const Pressable = ({
|
||||
borderSelectedColor,
|
||||
borderColor,
|
||||
borderWidth,
|
||||
style
|
||||
style,
|
||||
growFactor
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3016
|
||||
versionCode 3017
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
- Bring back undo/redo buttons in editor
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -1015,7 +1015,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2106;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1089,7 +1089,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.9;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1120,7 +1120,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2106;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1194,7 +1194,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.9;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1353,7 +1353,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2106;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1365,7 +1365,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.9;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1396,7 +1396,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2106;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1409,7 +1409,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.9;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1439,7 +1439,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2106;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1513,7 +1513,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.9;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1544,7 +1544,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2106;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1619,7 +1619,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.9;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.8",
|
||||
"version": "3.0.9",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
3
apps/web/.gitignore
vendored
3
apps/web/.gitignore
vendored
@@ -30,5 +30,4 @@ dist
|
||||
public/workbox
|
||||
scripts/secrets
|
||||
test-results
|
||||
.swc
|
||||
public/models
|
||||
.swc
|
||||
@@ -1,128 +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 { describe, bench } from "vitest";
|
||||
import { pipeline } from "@xenova/transformers";
|
||||
import path from "path";
|
||||
|
||||
const text = `I am generally not in favor of huge feature packed releases instead preferring small releases without one or two features at most. v3 is a special case in that the sheer amount of changes required to migrate from our old key value database to SQLite forced us to release all of these updates together.
|
||||
Migrating to SQLite
|
||||
Initially, as all plans go, our only goal was to migrate to SQLite as quickly and with as few changes as possible. Unfortunately, that plan died in its infancy. Our old legacy database was not designed to handle tens of thousands of notes or gigabytes of data — something we quickly realized after some users with over 100K notes approached us with significant performance issues. While it "worked", it was neither efficient nor particularly safe.
|
||||
In August 2023, we began slowly migrating the Notesnook Core to SQLite. While migrating we quickly realized we'd need TypeScript if this was ever going to work because Kysely (the underlying query builder we used) heavily relied on types being there. So from the very first day we were doing 2 things: migrating the database to SQLite, and migrating the codebase to TypeScript.
|
||||
I won't bore you with too many technical details but we had a few primary goals behind all this upheaval:
|
||||
The clients should perform predictably regardless of how much data you have.
|
||||
The clients should use as less RAM as possible to work in extremely constrained enviroments (e.g. iOS limits all Share Extensions to around 50 MB before crashing).
|
||||
With v3 we have acheived both of these goals.
|
||||
Fixing the Sync
|
||||
Sync has notoriously been one of the least stable parts of Notesnook. From the beginning, we opted for a decentralized syncing system where each client could independently sync items without depending on a central server. This had a few drawbacks:
|
||||
Since each client was independent, it was a nightmare to debug why something wasn't syncing.
|
||||
In order to be efficient, we detected changed items based on their modified timestamp, but this forced us to juggle with the various inconsistencies of time on different devices.
|
||||
Building everything on top of timestamps meant resumability during sync could never be stable.
|
||||
In short, our old sync logic heavily depended on time to be the exact same (down to the last millisecond) for things to work. As you can imagine, this didn't work out that well.
|
||||
While migrating everything to SQLite, we greatly simplified the sync logic opting for a centralized system this time. This allowed us to completely get rid of all the time juggling resolving all the edge cases in our previous logic.
|
||||
As a result, in v3 you'll notice a bug free sync that "just works" regardless of where you are, how many devices you are on, the time difference between them etc.
|
||||
Features, Features
|
||||
In every new release, it is always a struggle to decide which features to add and which features to leave behind for later. v3 was no different because ultimately each feature you add pushes the deadline further and further away. Even then, v3 is one of our biggest releases ever.
|
||||
Note linking
|
||||
The crown jewel of v3! Yes, finally, you can link 2 notes together and even link directly to a specific block inside a note.
|
||||
We have also added controls to quickly see which notes you have linked to, and which notes link to a particular note.
|
||||
To keep things simple, a note link is no different than a normal hyperlink except that clicking on it will take you to the note instead of an external page. This also means that when you export your notes, your note links are automatically converted into hyperlinks that directly open the linked Markdown or HTML files of the notes. This is something no other app does instead keeping the links in their proprietary format making them essentially useless.
|
||||
Tabs
|
||||
The only reason we added tabs was to allow quick navigation to/from a note on clicking a note link. You can, of course, use tabs without ever linking a note.
|
||||
Notesnook is one of the few note taking apps (besides Obsidian & OneNote) that has a full fledged tab experience. With v3, we are slowly upgrading Notesnook to become a power user's tool while keeping things just as simple as before.
|
||||
Nested notebooks
|
||||
|
||||
At rest encryption
|
||||
Thanks to SQLite, at rest encryption is now a thing on all platforms. The encryption key is randomly generated on database creation without requiring any user intervention. In other words, it "just works" keeping your notes secure even if your device is compromised. Which brings me to...
|
||||
App lock
|
||||
App lock is an enhancement on at rest encryption. When you enable app lock, it further encrypts your database encryption key with your app lock pin (or security key) so it's not just an "overlay". If you forget your app lock pin, the only way into the app is to reset & clear the database and start over.
|
||||
At Notesnook, we take your security & privacy very seriously.
|
||||
Export attachments with notes
|
||||
Exports got a lot of love in v3 with things like automatic downloading & linking of attachments, organization of note files based on your notebook structure, and resolution of internal note links to actual HTML/MD files.
|
||||
User profile
|
||||
Custom colors for note organization
|
||||
Fixed colors were nice but not nicer than custom colors.
|
||||
Customizable side bar
|
||||
You can now drag/drop and reorganize how things in your side bar looks including hiding things that you don't use. It can be as simple or as complex as you want.
|
||||
Callouts
|
||||
|
||||
Beta release channels
|
||||
|
||||
|
||||
Nested notebooks/topics
|
||||
Note linking
|
||||
Tabs
|
||||
Table of Contents
|
||||
Customizable/sortable sidebar
|
||||
User profile
|
||||
Custom colors
|
||||
Yearly reminders
|
||||
Callouts in editor
|
||||
Migration to SQLite
|
||||
New sync system
|
||||
At rest encryption
|
||||
app lock
|
||||
export attachments with notes
|
||||
.ccccccccccxccccccc`;
|
||||
|
||||
describe("benchmarking embeddings", async () => {
|
||||
const models = [
|
||||
["Xenova/jina-embeddings-v2-small-en", 87],
|
||||
["Snowflake/snowflake-arctic-embed-s", 41],
|
||||
["Snowflake/snowflake-arctic-embed-m", 20],
|
||||
["Snowflake/snowflake-arctic-embed-xs", 62],
|
||||
// "Alibaba-NLP/gte-base-en-v1.5",
|
||||
// "andersonbcdefg/bge-small-4096"
|
||||
["Xenova/GIST-small-Embedding-v0", 57],
|
||||
["Xenova/GIST-all-MiniLM-L6-v2", 88],
|
||||
// "Xenova/all-MiniLM-L12-v2" // 96
|
||||
["Xenova/NoInstruct-small-Embedding-v0", 40],
|
||||
["TaylorAI/bge-micro-v2", 100],
|
||||
// "Snowflake/snowflake-arctic-embed-m-long" // 19
|
||||
// "Xenova/e5-small-v2" // 67
|
||||
// "nomic-ai/nomic-embed-text-v1.5",
|
||||
// "Xenova/bert-base-uncased",
|
||||
["TaylorAI/gte-tiny", 88],
|
||||
["Xenova/gte-small", 63]
|
||||
];
|
||||
for (const provider of ["cpu"]) {
|
||||
for (const [model, rank] of models) {
|
||||
const extractor = await pipeline("feature-extraction", model, {
|
||||
// progress_callback: console.log,
|
||||
device: provider,
|
||||
dtype: "q8",
|
||||
cache_dir: path.join(__dirname, "..", "..", "public", "models")
|
||||
});
|
||||
console.log(model, extractor.tokenizer.model_max_length);
|
||||
extractor.tokenizer.model_max_length = 512;
|
||||
bench(`${rank}. ${model} (${provider})`, async () => {
|
||||
const output = await extractor(text, {
|
||||
pooling: extractor.tokenizer._tokenizer_config.cls_token
|
||||
? "cls"
|
||||
: "mean"
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// console.time("extraction");
|
||||
// const output = await extractor(text);
|
||||
// console.timeEnd("extraction");
|
||||
// console.log(output.data.length);
|
||||
2310
apps/web/package-lock.json
generated
2310
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,6 @@
|
||||
"@theme-ui/core": "^0.16.1",
|
||||
"@trpc/client": "10.38.3",
|
||||
"@trpc/react-query": "10.38.3",
|
||||
"@xenova/transformers": "github:xenova/transformers.js#v3",
|
||||
"@zip.js/zip.js": "^2.7.32",
|
||||
"async-mutex": "^0.4.0",
|
||||
"axios": "^1.3.4",
|
||||
@@ -61,7 +60,6 @@
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"mac-scrollbar": "^0.13.5",
|
||||
"marked": "^4.1.0",
|
||||
"onnxruntime-node": "^1.18.0",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
"phone": "^3.1.14",
|
||||
"platform": "^1.3.6",
|
||||
@@ -132,9 +130,6 @@
|
||||
"workbox-routing": "^7.0.0",
|
||||
"workbox-strategies": "^7.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"onnxruntime-node": "^1.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env PLATFORM=web vite",
|
||||
"start:desktop": "cross-env PLATFORM=desktop vite",
|
||||
|
||||
@@ -44,272 +44,11 @@ import { User } from "@notesnook/core";
|
||||
import { LegacyBackupFile } from "@notesnook/core";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { formatDate } from "@notesnook/core/dist/utils/date";
|
||||
import {
|
||||
pipeline,
|
||||
AutoModel,
|
||||
env,
|
||||
Tensor,
|
||||
mean_pooling,
|
||||
FeatureExtractionPipeline
|
||||
} from "@xenova/transformers";
|
||||
import { toChunks } from "@notesnook/core/dist/utils/array";
|
||||
|
||||
env.allowLocalModels = true;
|
||||
console.log(env);
|
||||
const text = `I am generally not in favor of huge feature packed releases instead preferring small releases without one or two features at most. v3 is a special case in that the sheer amount of changes required to migrate from our old key value database to SQLite forced us to release all of these updates together.
|
||||
Migrating to SQLite
|
||||
Initially, as all plans go, our only goal was to migrate to SQLite as quickly and with as few changes as possible. Unfortunately, that plan died in its infancy. Our old legacy database was not designed to handle tens of thousands of notes or gigabytes of data — something we quickly realized after some users with over 100K notes approached us with significant performance issues. While it "worked", it was neither efficient nor particularly safe.
|
||||
In August 2023, we began slowly migrating the Notesnook Core to SQLite. While migrating we quickly realized we'd need TypeScript if this was ever going to work because Kysely (the underlying query builder we used) heavily relied on types being there. So from the very first day we were doing 2 things: migrating the database to SQLite, and migrating the codebase to TypeScript.
|
||||
I won't bore you with too many technical details but we had a few primary goals behind all this upheaval:
|
||||
The clients should perform predictably regardless of how much data you have.
|
||||
The clients should use as less RAM as possible to work in extremely constrained enviroments (e.g. iOS limits all Share Extensions to around 50 MB before crashing).
|
||||
With v3 we have acheived both of these goals.
|
||||
Fixing the Sync
|
||||
Sync has notoriously been one of the least stable parts of Notesnook. From the beginning, we opted for a decentralized syncing system where each client could independently sync items without depending on a central server. This had a few drawbacks:
|
||||
Since each client was independent, it was a nightmare to debug why something wasn't syncing.
|
||||
In order to be efficient, we detected changed items based on their modified timestamp, but this forced us to juggle with the various inconsistencies of time on different devices.
|
||||
Building everything on top of timestamps meant resumability during sync could never be stable.
|
||||
In short, our old sync logic heavily depended on time to be the exact same (down to the last millisecond) for things to work. As you can imagine, this didn't work out that well.
|
||||
While migrating everything to SQLite, we greatly simplified the sync logic opting for a centralized system this time. This allowed us to completely get rid of all the time juggling resolving all the edge cases in our previous logic.
|
||||
As a result, in v3 you'll notice a bug free sync that "just works" regardless of where you are, how many devices you are on, the time difference between them etc.
|
||||
Features, Features
|
||||
In every new release, it is always a struggle to decide which features to add and which features to leave behind for later. v3 was no different because ultimately each feature you add pushes the deadline further and further away. Even then, v3 is one of our biggest releases ever.
|
||||
Note linking
|
||||
The crown jewel of v3! Yes, finally, you can link 2 notes together and even link directly to a specific block inside a note.
|
||||
We have also added controls to quickly see which notes you have linked to, and which notes link to a particular note.
|
||||
To keep things simple, a note link is no different than a normal hyperlink except that clicking on it will take you to the note instead of an external page. This also means that when you export your notes, your note links are automatically converted into hyperlinks that directly open the linked Markdown or HTML files of the notes. This is something no other app does instead keeping the links in their proprietary format making them essentially useless.
|
||||
Tabs
|
||||
The only reason we added tabs was to allow quick navigation to/from a note on clicking a note link. You can, of course, use tabs without ever linking a note.
|
||||
Notesnook is one of the few note taking apps (besides Obsidian & OneNote) that has a full fledged tab experience. With v3, we are slowly upgrading Notesnook to become a power user's tool while keeping things just as simple as before.
|
||||
Nested notebooks
|
||||
|
||||
At rest encryption
|
||||
Thanks to SQLite, at rest encryption is now a thing on all platforms. The encryption key is randomly generated on database creation without requiring any user intervention. In other words, it "just works" keeping your notes secure even if your device is compromised. Which brings me to...
|
||||
App lock
|
||||
App lock is an enhancement on at rest encryption. When you enable app lock, it further encrypts your database encryption key with your app lock pin (or security key) so it's not just an "overlay". If you forget your app lock pin, the only way into the app is to reset & clear the database and start over.
|
||||
At Notesnook, we take your security & privacy very seriously.
|
||||
Export attachments with notes
|
||||
Exports got a lot of love in v3 with things like automatic downloading & linking of attachments, organization of note files based on your notebook structure, and resolution of internal note links to actual HTML/MD files.
|
||||
User profile
|
||||
Custom colors for note organization
|
||||
Fixed colors were nice but not nicer than custom colors.
|
||||
Customizable side bar
|
||||
You can now drag/drop and reorganize how things in your side bar looks including hiding things that you don't use. It can be as simple or as complex as you want.
|
||||
Callouts
|
||||
|
||||
Beta release channels
|
||||
|
||||
|
||||
Nested notebooks/topics
|
||||
Note linking
|
||||
Tabs
|
||||
Table of Contents
|
||||
Customizable/sortable sidebar
|
||||
User profile
|
||||
Custom colors
|
||||
Yearly reminders
|
||||
Callouts in editor
|
||||
Migration to SQLite
|
||||
New sync system
|
||||
At rest encryption
|
||||
app lock
|
||||
export attachments with notes.`;
|
||||
|
||||
const average = (array: number[]) =>
|
||||
array.reduce((a, b) => a + b) / array.length;
|
||||
|
||||
function createTensorWithBatchSize(opt: [number, number]) {
|
||||
const size = opt.reduce((n, i) => n * i, 1);
|
||||
return new Tensor("int64", new BigInt64Array(size).fill(BigInt(1)), opt);
|
||||
}
|
||||
|
||||
function createBatch(batchSize: number, sequenceLength: number) {
|
||||
const tensor = createTensorWithBatchSize([batchSize, sequenceLength]);
|
||||
return {
|
||||
input_ids: tensor,
|
||||
attention_mask: tensor
|
||||
};
|
||||
}
|
||||
|
||||
function chunkify(tokens: string[], chunkSize: number, overlap: number) {
|
||||
const chunks = [];
|
||||
const totalParts = Math.ceil(tokens.length / chunkSize);
|
||||
for (let i = 0; i < totalParts; ++i) {
|
||||
const start = i === 0 ? 0 : i * chunkSize - i * overlap;
|
||||
const end = start + chunkSize;
|
||||
chunks.push(tokens.slice(start, end));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Helper function for padding values of an object, which are each arrays.
|
||||
* NOTE: No additional checks are made here for validity of arguments.
|
||||
* @param {Record<string, any[]>} item The input object.
|
||||
* @param {number} length The length to pad to.
|
||||
* @param {(key: string) => any} value_fn Determine the value to fill the array, based on its key.
|
||||
* @param {string} side Which side to pad the array.
|
||||
* @private
|
||||
*/
|
||||
function padHelper(
|
||||
item: Record<string, any[]>,
|
||||
length: number,
|
||||
value_fn: (key: string) => any,
|
||||
side: string
|
||||
) {
|
||||
for (const key of Object.keys(item)) {
|
||||
const diff = length - item[key].length;
|
||||
const value = value_fn(key);
|
||||
|
||||
const padData = new Array(diff).fill(value);
|
||||
item[key] =
|
||||
side === "right"
|
||||
? [...item[key], ...padData]
|
||||
: [...padData, ...item[key]];
|
||||
}
|
||||
}
|
||||
|
||||
function tokenizeInput(
|
||||
pipeline: FeatureExtractionPipeline,
|
||||
options: { max_length: number }
|
||||
) {
|
||||
const { tokenizer } = pipeline;
|
||||
const { max_length } = options;
|
||||
|
||||
const encoded = tokenizer._encode_text(text) ?? [];
|
||||
const encodedTokens = chunkify(encoded, max_length - 12, 20).map((chunk) => {
|
||||
const { tokens, token_type_ids } = tokenizer.post_processor._call(
|
||||
chunk,
|
||||
[],
|
||||
{ add_special_tokens: true }
|
||||
);
|
||||
const input_ids = tokenizer.model.convert_tokens_to_ids(tokens);
|
||||
|
||||
return {
|
||||
input_ids,
|
||||
attention_mask: new Array(input_ids.length).fill(1),
|
||||
token_type_ids: token_type_ids || []
|
||||
};
|
||||
});
|
||||
|
||||
for (let i = 0; i < encodedTokens.length; ++i) {
|
||||
if (encodedTokens[i].input_ids.length === max_length) {
|
||||
continue;
|
||||
} else if (encodedTokens[i].input_ids.length > max_length) {
|
||||
throw new Error("Truncation is not allowed.");
|
||||
} else {
|
||||
// t.length < max_length
|
||||
// possibly pad
|
||||
padHelper(
|
||||
encodedTokens[i],
|
||||
max_length,
|
||||
(key) => (key === "input_ids" ? tokenizer.pad_token_id : 0),
|
||||
tokenizer.padding_side
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dims = [encodedTokens.length, encodedTokens[0].input_ids.length];
|
||||
const input: Record<string, Tensor> = {};
|
||||
for (const key of Object.keys(encodedTokens[0])) {
|
||||
input[key] = new Tensor(
|
||||
"int64",
|
||||
BigInt64Array.from(encodedTokens.flatMap((x) => x[key]).map(BigInt)),
|
||||
dims
|
||||
);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
async function generateEmbeddings(
|
||||
pipeline: FeatureExtractionPipeline,
|
||||
input: Record<string, Tensor>
|
||||
) {
|
||||
const { tokenizer, model } = pipeline;
|
||||
|
||||
const outputs = await model._call(input);
|
||||
|
||||
let result =
|
||||
outputs.last_hidden_state ?? outputs.logits ?? outputs.token_embeddings;
|
||||
const pooling: "mean" | "cls" = tokenizer._tokenizer_config.cls_token
|
||||
? "cls"
|
||||
: "mean";
|
||||
if (pooling === "mean") {
|
||||
result = mean_pooling(result, input.attention_mask);
|
||||
} else if (pooling === "cls") {
|
||||
result = result.slice(null, 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
title: "Add a note",
|
||||
onClick: async () => {
|
||||
const models = [
|
||||
// ["Xenova/jina-embeddings-v2-small-en", 87]
|
||||
["Snowflake/snowflake-arctic-embed-s", 41],
|
||||
// ["Snowflake/snowflake-arctic-embed-m", 20],
|
||||
["Snowflake/snowflake-arctic-embed-xs", 62],
|
||||
// ["Xenova/GIST-small-Embedding-v0", 57],
|
||||
["Xenova/GIST-all-MiniLM-L6-v2", 88],
|
||||
// ["Xenova/NoInstruct-small-Embedding-v0", 40],
|
||||
["TaylorAI/bge-micro-v2", 100]
|
||||
// ["TaylorAI/gte-tiny", 88],
|
||||
// ["Xenova/gte-small", 63]
|
||||
] as const;
|
||||
|
||||
// env.backends.onnx.wasm.proxy = false;
|
||||
// env.backends.onnx.webgpu.profiling = {
|
||||
// mode: "default"
|
||||
// };
|
||||
// env.webgpu.profiling = {
|
||||
// mode: "default"
|
||||
// };
|
||||
console.log(env);
|
||||
for (const provider of ["webgpu"] as const) {
|
||||
if (provider === "webgpu") env.backends.onnx.wasm.proxy = false;
|
||||
else env.backends.onnx.wasm.proxy = true;
|
||||
|
||||
for (const [modelId, rank] of models) {
|
||||
console.log("loading model", modelId);
|
||||
|
||||
const extractor = await pipeline("feature-extraction", modelId, {
|
||||
// progress_callback: console.log,
|
||||
device: provider,
|
||||
dtype: "fp32",
|
||||
cache_dir: "/models",
|
||||
local_files_only: true
|
||||
// session_options: {
|
||||
// // enableGraphCapture: true
|
||||
// }
|
||||
});
|
||||
|
||||
const input = tokenizeInput(extractor, { max_length: 512 });
|
||||
|
||||
// console.time(`[warming up] ${rank}. ${modelId} (${provider})`);
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
console.time(`${rank}. ${modelId} (${provider})`);
|
||||
await generateEmbeddings(extractor, input);
|
||||
console.timeEnd(`${rank}. ${modelId} (${provider})`);
|
||||
}
|
||||
// console.timeEnd(`[warming up] ${rank}. ${modelId} (${provider})`);
|
||||
|
||||
// const timings: number[] = [];
|
||||
// for (let i = 0; i < 10; ++i) {
|
||||
// const now = performance.now();
|
||||
// await generateEmbeddings(extractor, input);
|
||||
// timings.push(performance.now() - now);
|
||||
// }
|
||||
// console.log(
|
||||
// `[bench] ${rank}. ${modelId} (${provider})`,
|
||||
// `avg: ${average(timings)}ms`,
|
||||
// `min: ${Math.min(...timings)}ms`,
|
||||
// `max: ${Math.max(...timings)}ms`
|
||||
// );
|
||||
}
|
||||
}
|
||||
} // useEditorStore.getState().newSession()
|
||||
onClick: () => useEditorStore.getState().newSession()
|
||||
},
|
||||
notebooks: {
|
||||
title: "Create a notebook",
|
||||
|
||||
@@ -34,9 +34,11 @@ import {
|
||||
Publish,
|
||||
Published,
|
||||
Readonly,
|
||||
Redo,
|
||||
Search,
|
||||
TableOfContents,
|
||||
Trash,
|
||||
Undo,
|
||||
Unlock
|
||||
} from "../icons";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
@@ -79,8 +81,8 @@ export function EditorActionBar() {
|
||||
const activeSession = useEditorStore((store) =>
|
||||
store.activeSessionId ? store.getSession(store.activeSessionId) : undefined
|
||||
);
|
||||
const editor = useEditorManager((store) =>
|
||||
activeSession?.id ? store.editors[activeSession?.id]?.editor : undefined
|
||||
const editorManager = useEditorManager((store) =>
|
||||
activeSession?.id ? store.editors[activeSession?.id] : undefined
|
||||
);
|
||||
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
|
||||
const monographs = useMonographStore((store) => store.monographs);
|
||||
@@ -88,6 +90,18 @@ export function EditorActionBar() {
|
||||
activeSession && db.monographs.isPublished(activeSession.id);
|
||||
|
||||
const tools = [
|
||||
{
|
||||
title: "Undo",
|
||||
icon: Undo,
|
||||
enabled: editorManager?.canUndo,
|
||||
onClick: () => editorManager?.editor?.undo()
|
||||
},
|
||||
{
|
||||
title: "Redo",
|
||||
icon: Redo,
|
||||
enabled: editorManager?.canRedo,
|
||||
onClick: () => editorManager?.editor?.redo()
|
||||
},
|
||||
{
|
||||
title: isNotePublished ? "Published" : "Publish",
|
||||
icon: isNotePublished ? Published : Publish,
|
||||
@@ -154,7 +168,7 @@ export function EditorActionBar() {
|
||||
activeSession.type !== "locked" &&
|
||||
activeSession.type !== "diff" &&
|
||||
activeSession.type !== "conflicted",
|
||||
onClick: editor?.startSearch
|
||||
onClick: editorManager?.editor?.startSearch
|
||||
},
|
||||
{
|
||||
title: "Properties",
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function TabsView() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{IS_DESKTOP_APP ? (
|
||||
{!hasNativeTitlebar ? (
|
||||
ReactDOM.createPortal(
|
||||
<EditorActionBar />,
|
||||
document.getElementById("titlebar-portal-container")!
|
||||
@@ -811,7 +811,7 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
|
||||
throw new Error("note with this id does not exist.");
|
||||
|
||||
useEditorStore.getState().addSession({
|
||||
type: "default",
|
||||
type: session.note.readonly ? "readonly" : "default",
|
||||
locked: true,
|
||||
id: session.id,
|
||||
note: session.note,
|
||||
|
||||
@@ -340,9 +340,9 @@ export class Lightbox extends React.Component<LightboxProps> {
|
||||
overflow: "hidden",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
height: IS_DESKTOP_APP ? TITLE_BAR_HEIGHT : "auto",
|
||||
height: !hasNativeTitlebar ? TITLE_BAR_HEIGHT : "auto",
|
||||
pr:
|
||||
IS_DESKTOP_APP && getPlatform() !== "darwin"
|
||||
!hasNativeTitlebar && getPlatform() !== "darwin"
|
||||
? "calc(100vw - env(titlebar-area-width))"
|
||||
: 0
|
||||
}}
|
||||
|
||||
@@ -239,6 +239,10 @@ export function PdfPreview(props: PdfPreviewProps) {
|
||||
onZoom={(e) => {
|
||||
if (hash) setPDFConfig(hash, { scale: e.scale });
|
||||
}}
|
||||
transformGetDocumentParams={(options) => {
|
||||
(options as any).isEvalSupported = false;
|
||||
return options;
|
||||
}}
|
||||
// onDocumentAskPassword={(e) => {
|
||||
// e.verifyPassword("failed");
|
||||
// }}
|
||||
|
||||
@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { SettingsGroup } from "./types";
|
||||
import { useStore as useSettingStore } from "../../stores/setting-store";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { desktop } from "../../common/desktop-bridge";
|
||||
|
||||
export const DesktopIntegrationSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -126,6 +128,42 @@ export const DesktopIntegrationSettings: SettingsGroup[] = [
|
||||
})
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "use-native-titlebar",
|
||||
title: "Use native titlebar",
|
||||
description:
|
||||
"Use native OS titlebar instead of replacing it with a custom one. Requires app restart for changes to take effect.",
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe(
|
||||
(s) => s.desktopIntegrationSettings,
|
||||
listener
|
||||
),
|
||||
components: [
|
||||
{
|
||||
type: "toggle",
|
||||
isToggled: () =>
|
||||
!!useSettingStore.getState().desktopIntegrationSettings
|
||||
?.nativeTitlebar,
|
||||
toggle: () => {
|
||||
useSettingStore.getState().setDesktopIntegration({
|
||||
nativeTitlebar:
|
||||
!useSettingStore.getState().desktopIntegrationSettings
|
||||
?.nativeTitlebar
|
||||
});
|
||||
showToast(
|
||||
"success",
|
||||
"Restart the app for changes to take effect.",
|
||||
[
|
||||
{
|
||||
text: "Restart now",
|
||||
onClick: () => desktop?.integration.restart.query()
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1
apps/web/src/global.d.ts
vendored
1
apps/web/src/global.d.ts
vendored
@@ -32,6 +32,7 @@ declare global {
|
||||
var IS_BETA: boolean;
|
||||
var APP_TITLE: string;
|
||||
var IS_THEME_BUILDER: boolean;
|
||||
var hasNativeTitlebar: boolean;
|
||||
|
||||
interface AuthenticationExtensionsClientInputs {
|
||||
prf?: {
|
||||
|
||||
@@ -50,6 +50,9 @@ export function useWindowControls() {
|
||||
isMaximized,
|
||||
isFullscreen,
|
||||
hasNativeWindowControls:
|
||||
!IS_DESKTOP_APP || getPlatform() === "darwin" || getPlatform() === "win32"
|
||||
!IS_DESKTOP_APP ||
|
||||
hasNativeTitlebar ||
|
||||
getPlatform() === "darwin" ||
|
||||
getPlatform() === "win32"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { register } from "./utils/stream-saver/mitm";
|
||||
import { getServiceWorkerVersion } from "./utils/version";
|
||||
import { ErrorBoundary, ErrorComponent } from "./components/error-boundary";
|
||||
import { TitleBar } from "./components/title-bar";
|
||||
import { desktop } from "./common/desktop-bridge";
|
||||
|
||||
renderApp();
|
||||
|
||||
@@ -34,6 +35,12 @@ async function renderApp() {
|
||||
if (!rootElement) return;
|
||||
const root = createRoot(rootElement);
|
||||
|
||||
window.hasNativeTitlebar =
|
||||
!IS_DESKTOP_APP ||
|
||||
!!(await desktop?.integration.desktopIntegration
|
||||
.query()
|
||||
?.then((s) => s.nativeTitlebar));
|
||||
|
||||
try {
|
||||
const { component, props, path } = await init();
|
||||
|
||||
@@ -47,7 +54,7 @@ async function renderApp() {
|
||||
|
||||
root.render(
|
||||
<>
|
||||
{IS_DESKTOP_APP ? <TitleBar /> : null}
|
||||
{hasNativeTitlebar ? null : <TitleBar />}
|
||||
<ErrorBoundary>
|
||||
<BaseThemeProvider
|
||||
onRender={() => document.getElementById("splash")?.remove()}
|
||||
@@ -63,7 +70,7 @@ async function renderApp() {
|
||||
} catch (e) {
|
||||
root.render(
|
||||
<>
|
||||
{IS_DESKTOP_APP ? <TitleBar /> : null}
|
||||
{hasNativeTitlebar ? null : <TitleBar />}
|
||||
<ErrorComponent
|
||||
error={e}
|
||||
resetErrorBoundary={() => window.location.reload()}
|
||||
|
||||
@@ -123,8 +123,8 @@ const decoder = new TextDecoder();
|
||||
*/
|
||||
|
||||
class KeyStore extends BaseStore<KeyStore> {
|
||||
#secretStore: IKVStore;
|
||||
#metadataStore: IKVStore;
|
||||
#secretStore!: IKVStore;
|
||||
#metadataStore!: IKVStore;
|
||||
#keyId = "key";
|
||||
#wrappingKeyId = "wrappingKey";
|
||||
#key?: CryptoKey;
|
||||
@@ -134,25 +134,25 @@ class KeyStore extends BaseStore<KeyStore> {
|
||||
isLocked = false;
|
||||
|
||||
constructor(
|
||||
dbName: string,
|
||||
private readonly dbName: string,
|
||||
setState: SetState<KeyStore>,
|
||||
get: GetState<KeyStore>
|
||||
) {
|
||||
super(setState, get);
|
||||
|
||||
this.#metadataStore =
|
||||
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
|
||||
? new IndexedDBKVStore(`${dbName}-metadata`, "metadata")
|
||||
: new MemoryKVStore();
|
||||
this.#secretStore =
|
||||
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
|
||||
? new IndexedDBKVStore(`${dbName}-secrets`, "secrets")
|
||||
: new MemoryKVStore();
|
||||
}
|
||||
|
||||
activeCredentials = () => this.get().credentials.filter((c) => c.active);
|
||||
|
||||
init = async () => {
|
||||
this.#metadataStore =
|
||||
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
|
||||
? new IndexedDBKVStore(`${this.dbName}-metadata`, "metadata")
|
||||
: new MemoryKVStore();
|
||||
this.#secretStore =
|
||||
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
|
||||
? new IndexedDBKVStore(`${this.dbName}-secrets`, "secrets")
|
||||
: new MemoryKVStore();
|
||||
|
||||
const credentials = await this.getCredentials();
|
||||
const secrets = Object.fromEntries(
|
||||
await this.#secretStore.entries<EncryptedData>()
|
||||
|
||||
@@ -90,6 +90,7 @@ export type ReadonlyEditorSession = BaseEditorSession & {
|
||||
content?: NoteContent<false>;
|
||||
color?: string;
|
||||
tags?: Tag[];
|
||||
locked?: boolean;
|
||||
};
|
||||
|
||||
export type DeletedEditorSession = BaseEditorSession & {
|
||||
|
||||
@@ -89,6 +89,21 @@ async function processAttachment(
|
||||
attachments[name] = { ...cipherData, key };
|
||||
}
|
||||
|
||||
const colorMap: Record<string, string | undefined> = {
|
||||
default: undefined,
|
||||
teal: "#00897B",
|
||||
red: "#D32F2F",
|
||||
purple: "#7B1FA2",
|
||||
blue: "#1976D2",
|
||||
cerulean: "#03A9F4",
|
||||
pink: "#C2185B",
|
||||
brown: "#795548",
|
||||
gray: "#9E9E9E",
|
||||
green: "#388E3C",
|
||||
orange: "#FFA000",
|
||||
yellow: "#FFC107"
|
||||
};
|
||||
|
||||
async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
|
||||
const note = await fileToJson<Note>(entry);
|
||||
for (const attachment of note.attachments || []) {
|
||||
@@ -121,8 +136,49 @@ async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
|
||||
content: { type: "tiptap", data: note.content?.data },
|
||||
notebooks: []
|
||||
});
|
||||
|
||||
if (!noteId) return;
|
||||
|
||||
for (const tag of note.tags || []) {
|
||||
const tagId =
|
||||
(await db.tags.find(tag))?.id ||
|
||||
(await db.tags.add({
|
||||
title: tag
|
||||
}));
|
||||
|
||||
await db.relations.add(
|
||||
{
|
||||
id: tagId,
|
||||
type: "tag"
|
||||
},
|
||||
{
|
||||
id: noteId,
|
||||
type: "note"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const colorCode = note.color ? colorMap[note.color] : undefined;
|
||||
if (colorCode) {
|
||||
const colorId =
|
||||
(await db.colors.find(colorCode))?.id ||
|
||||
(await db.colors.add({
|
||||
colorCode: colorCode,
|
||||
title: note.color
|
||||
}));
|
||||
|
||||
await db.relations.add(
|
||||
{
|
||||
id: colorId,
|
||||
type: "color"
|
||||
},
|
||||
{
|
||||
id: noteId,
|
||||
type: "note"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (const nb of notebooks) {
|
||||
const notebookId = await importNotebook(nb).catch(() => undefined);
|
||||
if (!notebookId) continue;
|
||||
|
||||
4
fastlane/metadata/android/en-US/changelogs/15089.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/15089.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
- Bring back undo/redo buttons in editor
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -245,10 +245,10 @@ export class Notebooks implements ICollection {
|
||||
const query = tr
|
||||
.withRecursive(`subNotebooks(id)`, (eb) =>
|
||||
eb
|
||||
.selectFrom((eb) =>
|
||||
.selectFrom(() =>
|
||||
sql<{ id: string }>`(VALUES ${sql.join(
|
||||
ids.map((id) => eb.parens(sql`${id}`))
|
||||
)})`.as("notebookIds")
|
||||
ids.map((id) => sql.raw(`('${id}')`))
|
||||
)})`.as("roots")
|
||||
)
|
||||
.selectAll()
|
||||
.unionAll((eb) =>
|
||||
|
||||
@@ -39,33 +39,47 @@ export default class Trash {
|
||||
notebooks: [],
|
||||
notes: []
|
||||
};
|
||||
private userDeletedCache: {
|
||||
notes: string[];
|
||||
notebooks: string[];
|
||||
} = {
|
||||
notebooks: [],
|
||||
notes: []
|
||||
};
|
||||
constructor(private readonly db: Database) {}
|
||||
|
||||
async init() {
|
||||
await this.cleanup();
|
||||
await this.buildCache();
|
||||
await this.cleanup();
|
||||
}
|
||||
|
||||
async buildCache() {
|
||||
this.cache.notes = [];
|
||||
this.cache.notebooks = [];
|
||||
this.userDeletedCache.notes = [];
|
||||
this.userDeletedCache.notebooks = [];
|
||||
|
||||
const result = await this.db
|
||||
.sql()
|
||||
.selectFrom("notes")
|
||||
.where("type", "==", "trash")
|
||||
.select(["id", sql`'note'`.as("itemType")])
|
||||
.select(["id", sql`'note'`.as("itemType"), "deletedBy"])
|
||||
.unionAll((eb) =>
|
||||
eb
|
||||
.selectFrom("notebooks")
|
||||
.where("type", "==", "trash")
|
||||
.select(["id", sql`'notebook'`.as("itemType")])
|
||||
.select(["id", sql`'notebook'`.as("itemType"), "deletedBy"])
|
||||
)
|
||||
.execute();
|
||||
|
||||
for (const { id, itemType } of result) {
|
||||
if (itemType === "note") this.cache.notes.push(id);
|
||||
else if (itemType === "notebook") this.cache.notebooks.push(id);
|
||||
for (const { id, itemType, deletedBy } of result) {
|
||||
if (itemType === "note") {
|
||||
this.cache.notes.push(id);
|
||||
if (deletedBy === "user") this.userDeletedCache.notes.push(id);
|
||||
} else if (itemType === "notebook") {
|
||||
this.cache.notebooks.push(id);
|
||||
if (deletedBy === "user") this.userDeletedCache.notebooks.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +130,7 @@ export default class Trash {
|
||||
deletedBy
|
||||
});
|
||||
this.cache.notes.push(...ids);
|
||||
if (deletedBy === "user") this.userDeletedCache.notes.push(...ids);
|
||||
} else if (type === "notebook") {
|
||||
await this.db.notebooks.collection.update(ids, {
|
||||
type: "trash",
|
||||
@@ -124,24 +139,15 @@ export default class Trash {
|
||||
deletedBy
|
||||
});
|
||||
this.cache.notebooks.push(...ids);
|
||||
if (deletedBy === "user") this.userDeletedCache.notebooks.push(...ids);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(...ids: string[]) {
|
||||
if (ids.length <= 0) return;
|
||||
|
||||
const noteIds = [];
|
||||
const notebookIds = [];
|
||||
for (const id of ids) {
|
||||
const isNote = this.cache.notes.includes(id);
|
||||
if (isNote) {
|
||||
noteIds.push(id);
|
||||
this.cache.notes.splice(this.cache.notes.indexOf(id), 1);
|
||||
} else if (!isNote) {
|
||||
notebookIds.push(id);
|
||||
this.cache.notebooks.splice(this.cache.notebooks.indexOf(id), 1);
|
||||
}
|
||||
}
|
||||
const noteIds = ids.filter((id) => this.cache.notes.includes(id));
|
||||
const notebookIds = ids.filter((id) => this.cache.notebooks.includes(id));
|
||||
|
||||
await this._delete(noteIds, notebookIds);
|
||||
}
|
||||
@@ -153,6 +159,7 @@ export default class Trash {
|
||||
await this.db.noteHistory.clearSessions(...chunk);
|
||||
await this.db.notes.remove(...chunk);
|
||||
deleteItems(this.cache.notes, ...chunk);
|
||||
deleteItems(this.userDeletedCache.notes, ...chunk);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +169,7 @@ export default class Trash {
|
||||
await this.db.notebooks.remove(...chunk);
|
||||
await this.db.relations.unlinkOfType("notebook", chunk);
|
||||
deleteItems(this.cache.notebooks, ...chunk);
|
||||
deleteItems(this.userDeletedCache.notebooks, ...chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,18 +177,8 @@ export default class Trash {
|
||||
async restore(...ids: string[]) {
|
||||
if (ids.length <= 0) return;
|
||||
|
||||
const noteIds = [];
|
||||
const notebookIds = [];
|
||||
for (const id of ids) {
|
||||
const isNote = this.cache.notes.includes(id);
|
||||
if (isNote) {
|
||||
noteIds.push(id);
|
||||
// this.cache.notes.splice(this.cache.notes.indexOf(id), 1);
|
||||
} else if (!isNote) {
|
||||
notebookIds.push(id);
|
||||
// this.cache.notebooks.splice(this.cache.notebooks.indexOf(id), 1);
|
||||
}
|
||||
}
|
||||
const noteIds = ids.filter((id) => this.cache.notes.includes(id));
|
||||
const notebookIds = ids.filter((id) => this.cache.notebooks.includes(id));
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
await this.db.notes.collection.update(noteIds, {
|
||||
@@ -190,6 +188,7 @@ export default class Trash {
|
||||
deletedBy: null
|
||||
});
|
||||
deleteItems(this.cache.notes, ...noteIds);
|
||||
deleteItems(this.userDeletedCache.notes, ...noteIds);
|
||||
}
|
||||
|
||||
if (notebookIds.length > 0) {
|
||||
@@ -201,12 +200,14 @@ export default class Trash {
|
||||
deletedBy: null
|
||||
});
|
||||
deleteItems(this.cache.notebooks, ...ids);
|
||||
deleteItems(this.userDeletedCache.notebooks, ...ids);
|
||||
}
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this._delete(this.cache.notes, this.cache.notebooks);
|
||||
this.cache = { notebooks: [], notes: [] };
|
||||
this.userDeletedCache = { notebooks: [], notes: [] };
|
||||
}
|
||||
|
||||
// synced(id: string) {
|
||||
@@ -228,6 +229,7 @@ export default class Trash {
|
||||
ids: string[],
|
||||
deletedBy?: TrashItem["deletedBy"]
|
||||
) {
|
||||
if (ids.length <= 0) return [];
|
||||
return (await this.db
|
||||
.sql()
|
||||
.selectFrom("notes")
|
||||
@@ -242,6 +244,7 @@ export default class Trash {
|
||||
ids: string[],
|
||||
deletedBy?: TrashItem["deletedBy"]
|
||||
) {
|
||||
if (ids.length <= 0) return [];
|
||||
return (await this.db
|
||||
.sql()
|
||||
.selectFrom("notebooks")
|
||||
@@ -253,36 +256,31 @@ export default class Trash {
|
||||
}
|
||||
|
||||
async grouped(options: GroupOptions) {
|
||||
const ids = [...this.cache.notes, ...this.cache.notebooks];
|
||||
const ids = [
|
||||
...this.userDeletedCache.notes,
|
||||
...this.userDeletedCache.notebooks
|
||||
];
|
||||
const selector = getSortSelectors(options)[options.sortDirection];
|
||||
return new VirtualizedGrouping<TrashItem>(
|
||||
ids.length,
|
||||
this.db.options.batchSize,
|
||||
() => Promise.resolve(ids),
|
||||
async (start, end) => {
|
||||
const notesRange =
|
||||
end < this.cache.notes.length
|
||||
? [start, end]
|
||||
: [start, this.cache.notes.length];
|
||||
const notebooksRange =
|
||||
start >= this.cache.notes.length
|
||||
? [start, end]
|
||||
: [0, Math.min(this.cache.notebooks.length, end)];
|
||||
|
||||
const slicedIds = ids.slice(start, end);
|
||||
const noteIds = slicedIds.filter((id) =>
|
||||
this.userDeletedCache.notes.includes(id)
|
||||
);
|
||||
const notebookIds = slicedIds.filter((id) =>
|
||||
this.userDeletedCache.notebooks.includes(id)
|
||||
);
|
||||
const items = [
|
||||
...(await this.trashedNotes(
|
||||
this.cache.notes.slice(notesRange[0], notesRange[1]),
|
||||
"user"
|
||||
)),
|
||||
...(await this.trashedNotebooks(
|
||||
this.cache.notebooks.slice(notebooksRange[0], notebooksRange[1]),
|
||||
"user"
|
||||
))
|
||||
...(await this.trashedNotes(noteIds)),
|
||||
...(await this.trashedNotebooks(notebookIds))
|
||||
];
|
||||
items.sort(selector);
|
||||
|
||||
return {
|
||||
ids: ids.slice(start, end),
|
||||
ids: slicedIds,
|
||||
items
|
||||
};
|
||||
},
|
||||
@@ -318,32 +316,19 @@ export default class Trash {
|
||||
.selectAll()
|
||||
.unionAll((eb) =>
|
||||
eb
|
||||
.selectFrom(["relations", "subNotebooks", "notebooks"])
|
||||
.selectFrom(["relations", "subNotebooks"])
|
||||
.select("relations.toId as id")
|
||||
.where("toType", "==", "notebook")
|
||||
.where("fromType", "==", "notebook")
|
||||
.whereRef("fromId", "==", "subNotebooks.id")
|
||||
.where(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom("notebooks")
|
||||
.whereRef("notebooks.id", "==", "relations.toId")
|
||||
.where("notebooks.type", "==", "trash")
|
||||
.limit(1)
|
||||
.select("deletedBy"),
|
||||
"!=",
|
||||
"user"
|
||||
)
|
||||
.where("toId", "not in", this.userDeletedCache.notebooks)
|
||||
.$narrowType<{ id: string }>()
|
||||
)
|
||||
)
|
||||
.selectFrom("subNotebooks")
|
||||
.select("id")
|
||||
.where("id", "not in", notebookIds)
|
||||
.execute();
|
||||
|
||||
return deleteItems(
|
||||
ids.map((ref) => ref.id),
|
||||
...notebookIds
|
||||
);
|
||||
return ids.map((ref) => ref.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,33 +165,69 @@ function Header({
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
{!settings.premium && (
|
||||
<Button
|
||||
onPress={() => {
|
||||
post(EventTypes.pro);
|
||||
}}
|
||||
preventDefault={false}
|
||||
style={{
|
||||
borderWidth: 0,
|
||||
borderRadius: 100,
|
||||
color: "var(--nn_primary_icon)",
|
||||
marginRight: 10,
|
||||
width: 39,
|
||||
height: 39,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
<CrownIcon
|
||||
size={25 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
{tab.locked ? null : (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
editor?.commands.undo();
|
||||
}}
|
||||
color="orange"
|
||||
/>
|
||||
</Button>
|
||||
style={{
|
||||
borderWidth: 0,
|
||||
borderRadius: 100,
|
||||
color: "var(--nn_primary_icon)",
|
||||
marginRight: 10,
|
||||
width: 39,
|
||||
height: 39,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
<ArrowULeftTopIcon
|
||||
color={
|
||||
!hasUndo
|
||||
? "var(--nn_secondary_border)"
|
||||
: "var(--nn_primary_icon)"
|
||||
}
|
||||
size={25 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (tab.locked) return;
|
||||
editor?.commands.redo();
|
||||
}}
|
||||
style={{
|
||||
borderWidth: 0,
|
||||
borderRadius: 100,
|
||||
color: "var(--nn_primary_icon)",
|
||||
marginRight: 10,
|
||||
width: 39,
|
||||
height: 39,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
<ArrowURightTopIcon
|
||||
color={
|
||||
!hasRedo
|
||||
? "var(--nn_secondary_border)"
|
||||
: "var(--nn_primary_icon)"
|
||||
}
|
||||
size={25 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{settings.deviceMode !== "mobile" && !settings.fullscreen ? (
|
||||
@@ -304,7 +340,11 @@ function Header({
|
||||
<Button
|
||||
fwdRef={btnRef}
|
||||
onPress={() => {
|
||||
setOpen(!isOpen);
|
||||
if (tab.locked) {
|
||||
post(EventTypes.properties, undefined, tab.id, tab.noteId);
|
||||
} else {
|
||||
setOpen(!isOpen);
|
||||
}
|
||||
}}
|
||||
preventDefault={false}
|
||||
style={{
|
||||
@@ -320,13 +360,23 @@ function Header({
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
<DotsVerticalIcon
|
||||
size={25 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
color="var(--nn_primary_icon)"
|
||||
/>
|
||||
{tab.locked ? (
|
||||
<DotsHorizontalIcon
|
||||
size={25 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
color="var(--nn_primary_icon)"
|
||||
/>
|
||||
) : (
|
||||
<DotsVerticalIcon
|
||||
size={25 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
color="var(--nn_primary_icon)"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<ControlledMenu
|
||||
@@ -351,6 +401,9 @@ function Header({
|
||||
tab.noteId
|
||||
);
|
||||
break;
|
||||
case "search":
|
||||
editor?.commands.startSearch();
|
||||
break;
|
||||
case "properties":
|
||||
logger("info", "post properties...");
|
||||
post(EventTypes.properties, undefined, tab.id, tab.noteId);
|
||||
@@ -360,127 +413,66 @@ function Header({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
<MenuItem
|
||||
value="search"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%"
|
||||
gap: 10,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onPress={() => {
|
||||
editor?.commands.undo();
|
||||
}}
|
||||
<MagnifyIcon
|
||||
size={22 * settings.fontScale}
|
||||
color="var(--nn_primary_icon)"
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
borderWidth: 0,
|
||||
borderRadius: 100,
|
||||
color: "var(--nn_primary_icon)",
|
||||
marginRight: 10,
|
||||
width: 39,
|
||||
height: 39,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative"
|
||||
color: "var(--nn_primary_paragraph)"
|
||||
}}
|
||||
>
|
||||
<ArrowULeftTopIcon
|
||||
color={
|
||||
!hasUndo
|
||||
? "var(--nn_secondary_border)"
|
||||
: "var(--nn_primary_icon)"
|
||||
}
|
||||
size={25 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
editor?.commands.redo();
|
||||
}}
|
||||
style={{
|
||||
borderWidth: 0,
|
||||
borderRadius: 100,
|
||||
color: "var(--nn_primary_icon)",
|
||||
marginRight: 10,
|
||||
width: 39,
|
||||
height: 39,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
<ArrowURightTopIcon
|
||||
color={
|
||||
!hasRedo
|
||||
? "var(--nn_secondary_border)"
|
||||
: "var(--nn_primary_icon)"
|
||||
}
|
||||
size={25 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
editor?.commands.startSearch();
|
||||
}}
|
||||
style={{
|
||||
borderWidth: 0,
|
||||
borderRadius: 100,
|
||||
color: "var(--nn_primary_icon)",
|
||||
marginRight: 10,
|
||||
width: 39,
|
||||
height: 39,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
<MagnifyIcon
|
||||
size={28 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
color="var(--nn_primary_icon)"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
Search
|
||||
</span>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
value="toc"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 10
|
||||
gap: 10,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<TableOfContentsIcon
|
||||
size={22 * settings.fontScale}
|
||||
color="var(--nn_primary_icon)"
|
||||
/>
|
||||
Table of contents
|
||||
<span
|
||||
style={{
|
||||
color: "var(--nn_primary_paragraph)"
|
||||
}}
|
||||
>
|
||||
Table of contents
|
||||
</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="properties"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 10
|
||||
gap: 10,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<DotsHorizontalIcon
|
||||
size={22 * settings.fontScale}
|
||||
color="var(--nn_primary_icon)"
|
||||
/>
|
||||
Note Properties
|
||||
<span
|
||||
style={{
|
||||
color: "var(--nn_primary_paragraph)"
|
||||
}}
|
||||
>
|
||||
Properties
|
||||
</span>
|
||||
</MenuItem>
|
||||
</ControlledMenu>
|
||||
</div>
|
||||
|
||||
@@ -61,10 +61,12 @@ export function ImageComponent(
|
||||
threshold: 0.2,
|
||||
once: true
|
||||
});
|
||||
|
||||
const dom = editor.view.dom.parentElement || editor.view.dom;
|
||||
const size =
|
||||
editor.view.dom.clientWidth === 0
|
||||
? node.attrs
|
||||
: clampSize(node.attrs, editor.view.dom.clientWidth, aspectRatio);
|
||||
: clampSize(node.attrs, dom.clientWidth, aspectRatio);
|
||||
|
||||
const float = isMobile ? false : node.attrs.float;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user