Compare commits

..

2 Commits

Author SHA1 Message Date
Ammar Ahmed
b2b46cad6b mobile: fix build errors in editor-mobile 2024-12-30 14:35:31 +05:00
Ammar Ahmed
c6ccf3e32f mobile: fix image and file uploads 2024-12-30 14:35:10 +05:00
52 changed files with 3373 additions and 4094 deletions

View File

@@ -159,77 +159,79 @@ export function clearDatabaseKey() {
export async function getDatabaseKey(appLockPassword?: string) {
if (DB_KEY) return DB_KEY;
if (appLockPassword) {
const databaseKeyCipher: Cipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
try {
if (appLockPassword) {
const databaseKeyCipher: Cipher =
CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
if (hasKey) {
const credentials = await Keychain.getInternetCredentials(
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
if (hasKey) {
const credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = (credentials as Keychain.UserCredentials).password;
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = (credentials as Keychain.UserCredentials).password;
}
}
}
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key as string;
DB_KEY = derivedDatabaseKey.key as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
)) as Cipher;
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
if (!DB_KEY) {
throw new Error(
`Failed to get database key, ${await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
)}`
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
return DB_KEY;
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
)) as Cipher;
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
return DB_KEY;
} catch (e) {
ToastManager.error(e as Error, "Error getting database key");
console.log(e, "error");
DatabaseLogger.error(e);
return null;
}
}
export async function deriveCryptoKeyFallback(data: SerializedKey) {

View File

@@ -75,7 +75,7 @@ export const TrashIntervalPicker = createSettingsPicker({
? strings.never()
: item === 1
? strings.reminderRecurringMode.day()
: strings.days(item);
: item + " " + strings.days();
},
getItemKey: (item) => item.toString(),
options: [-1, 1, 7, 30, 365],

View File

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

View File

@@ -1,3 +1,5 @@
- Bug fixes and small improvements
- You can now share multiple files to Notesnook
- Fix file and image sharing not working
- Many other bug fixes and small improvements
Thank you for using Notesnook!

View File

@@ -1063,7 +1063,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2123;
CURRENT_PROJECT_VERSION = 2122;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1137,7 +1137,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.26;
MARKETING_VERSION = 3.0.25;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1168,7 +1168,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2123;
CURRENT_PROJECT_VERSION = 2122;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1242,7 +1242,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.26;
MARKETING_VERSION = 3.0.25;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1401,7 +1401,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2123;
CURRENT_PROJECT_VERSION = 2122;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1413,7 +1413,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.26;
MARKETING_VERSION = 3.0.25;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1444,7 +1444,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2123;
CURRENT_PROJECT_VERSION = 2122;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1457,7 +1457,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.26;
MARKETING_VERSION = 3.0.25;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1487,7 +1487,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2123;
CURRENT_PROJECT_VERSION = 2122;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1561,7 +1561,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.26;
MARKETING_VERSION = 3.0.25;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1592,7 +1592,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2123;
CURRENT_PROJECT_VERSION = 2122;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1667,7 +1667,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.26;
MARKETING_VERSION = 3.0.25;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.0.26",
"version": "3.0.25",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

@@ -370,57 +370,3 @@ test("when autosave is disabled, closing the note should save it", async ({
await expect(notes.editor.savedIcon).toBeVisible();
expect(await notes.editor.getContent("text")).toBe(content.trim());
});
test("control + alt + right arrow should go to next note", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note1 = await notes.createNote({
title: "Note 1",
content: "Note 1 content"
});
const note2 = await notes.createNote({
title: "Note 2",
content: "Note 2 content"
});
await note1?.openNote();
await note2?.openNote();
await page.keyboard.press("Control+Alt+ArrowRight");
expect(await notes.editor.getTitle()).toBe("Note 1");
expect(await notes.editor.getContent("text")).toBe("Note 1 content");
await page.keyboard.press("Control+Alt+ArrowRight");
expect(await notes.editor.getTitle()).toBe("Note 2");
expect(await notes.editor.getContent("text")).toBe("Note 2 content");
});
test("control + alt + left arrow should go to previous note", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note1 = await notes.createNote({
title: "Note 1",
content: "Note 1 content"
});
const note2 = await notes.createNote({
title: "Note 2",
content: "Note 2 content"
});
await note1?.openNote();
await note2?.openNote();
await page.keyboard.press("Control+Alt+ArrowLeft");
expect(await notes.editor.getTitle()).toBe("Note 1");
expect(await notes.editor.getContent("text")).toBe("Note 1 content");
await page.keyboard.press("Control+Alt+ArrowLeft");
expect(await notes.editor.getTitle()).toBe("Note 2");
expect(await notes.editor.getContent("text")).toBe("Note 2 content");
});

View File

@@ -242,23 +242,4 @@ export class EditorModel {
if ((await tabModel.getId()) === id) return tabModel;
}
}
async attachImage() {
await this.page
.context()
.grantPermissions(["clipboard-read", "clipboard-write"]);
await this.page.evaluate(async () => {
const resp = await fetch("https://dummyjson.com/image/150");
const blob = await resp.blob();
window.navigator.clipboard.write([
new ClipboardItem({
"image/png": new Blob([blob], { type: "image/png" })
})
]);
});
await this.page.keyboard.down("Control");
await this.page.keyboard.press("KeyV");
await this.page.keyboard.up("Control");
}
}

View File

@@ -139,15 +139,4 @@ export class SettingsViewModel {
await waitForDialog(this.page, "Restoring backup");
}
async selectImageCompression(option: { value: string; label: string }) {
const item = await this.navigation.findItem("Behaviour");
await item?.click();
const imageCompressionDropdown = this.page
.locator(getTestId("setting-image-compression"))
.locator("select");
await imageCompressionDropdown.selectOption(option);
}
}

View File

@@ -1,79 +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 { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { NOTE } from "./utils";
test("ask for image compression during image upload when 'Image Compression' setting is 'Ask every time'", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const settings = await app.goToSettings();
await settings.selectImageCompression({
value: "0",
label: "Ask every time"
});
await settings.close();
const notes = await app.goToNotes();
await notes.createNote(NOTE);
await notes.editor.attachImage();
await expect(page.getByText("Enable compression")).toBeVisible();
});
test("do not ask for image compression during image upload when 'Image Compression' setting is 'Enable (Recommended)'", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const settings = await app.goToSettings();
await settings.selectImageCompression({
value: "1",
label: "Enable (Recommended)"
});
await settings.close();
const notes = await app.goToNotes();
await notes.createNote(NOTE);
await notes.editor.attachImage();
await expect(page.getByText("Enable compression")).toBeHidden();
});
test("do not ask for image compression during image upload when 'Image Compression' setting is 'Disable'", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const settings = await app.goToSettings();
await settings.selectImageCompression({
value: "2",
label: "Disable"
});
await settings.close();
const notes = await app.goToNotes();
await notes.createNote(NOTE);
await notes.editor.attachImage();
await expect(page.getByText("Enable compression")).toBeHidden();
});

View File

@@ -0,0 +1,94 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect } from "react";
import { useStore } from "./stores/app-store";
import useSlider from "./hooks/use-slider";
import useMobile from "./hooks/use-mobile";
import useTablet from "./hooks/use-tablet";
type MobileAppEffectsProps = {
sliderId: string;
overlayId: string;
setShow: (show: boolean) => void;
};
export default function MobileAppEffects({
sliderId,
overlayId,
setShow
}: MobileAppEffectsProps) {
const isMobile = useMobile();
const isTablet = useTablet();
const toggleSideMenu = useStore((store) => store.toggleSideMenu);
const setIsEditorOpen = useStore((store) => store.setIsEditorOpen);
const isEditorOpen = useStore((store) => store.isEditorOpen);
const isSideMenuOpen = useStore((store) => store.isSideMenuOpen);
const isFocusMode = useStore((store) => store.isFocusMode);
const [slideToIndex] = useSlider(sliderId, {
onSliding: (_e, { position }) => {
if (!isMobile) return;
const offset = 70;
const width = 300;
const percent = offset - (position / width) * offset;
const overlay = document.getElementById("overlay");
if (!overlay) return;
if (percent > 0) {
overlay.style.opacity = `${percent}%`;
overlay.style.pointerEvents = "all";
} else {
overlay.style.opacity = "0%";
overlay.style.pointerEvents = "none";
}
},
onChange: (e, { slide }) => {
toggleSideMenu(slide?.index === 0 ? true : false);
setIsEditorOpen(slide?.index === 3 ? true : false);
}
});
useEffect(() => {
if (!isMobile) return;
slideToIndex(isSideMenuOpen ? 1 : 2);
}, [isMobile, slideToIndex, isSideMenuOpen]);
useEffect(() => {
if (!isMobile) return;
slideToIndex(isEditorOpen ? 3 : 2);
}, [isMobile, slideToIndex, isEditorOpen]);
useEffect(() => {
toggleSideMenu(!isMobile);
if (!isMobile && !isTablet && !isFocusMode) setShow(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMobile, isTablet, isFocusMode, toggleSideMenu]);
useEffect(() => {
if (!overlayId) return;
const overlay = document.getElementById(overlayId);
if (!overlay) return;
overlay.onclick = () => toggleSideMenu(false);
return () => {
overlay.onclick = null;
};
}, [overlayId, toggleSideMenu]);
return null;
}

View File

@@ -5,16 +5,12 @@
.tabsScroll,
.titlebarLogo,
.theme-scope-titleBar,
.route-container-header {
.theme-scope-titleBar {
-webkit-app-region: drag;
}
.theme-scope-titleBar button,
.tabsScroll .tab,
.editor-action-bar button,
.route-container-header button,
.search-container {
.tabsScroll .tab {
-webkit-app-region: no-drag;
}

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useState, Suspense, useEffect, useRef } from "react";
import React, { useState, Suspense, useEffect, useRef } from "react";
import { Box, Flex } from "@theme-ui/components";
import { ScopedThemeProvider } from "./components/theme-provider";
import useMobile from "./hooks/use-mobile";
@@ -39,21 +39,16 @@ import AppEffects from "./app-effects";
import HashRouter from "./components/hash-router";
import { useWindowFocus } from "./hooks/use-window-focus";
import { Global } from "@emotion/react";
import { isMac } from "./utils/platform";
import useSlider from "./hooks/use-slider";
import { AppEventManager, AppEvents } from "./common/app-events";
import { TITLE_BAR_HEIGHT } from "./components/title-bar";
import { getFontSizes } from "@notesnook/theme/theme/font/fontsize.js";
import { useWindowControls } from "./hooks/use-window-controls";
new WebExtensionRelay();
const MobileAppEffects = React.lazy(() => import("./app-effects.mobile"));
function App() {
const isMobile = useMobile();
const [show, setShow] = useState(true);
const isFocusMode = useStore((store) => store.isFocusMode);
const { isFocused } = useWindowFocus();
const { isFullscreen } = useWindowControls();
console.timeEnd("loading app");
return (
@@ -70,44 +65,17 @@ function App() {
`}
/>
)}
{IS_DESKTOP_APP && isMac() && !isFullscreen ? (
<Global
// These styles to make sure the app content doesn't overlap with the traffic lights.
styles={`
.nav-pane,
.mobile-nav-pane {
margin-top: env(titlebar-area-height) !important;
}
.nav-pane.collapsed + .list-pane .route-container-header,
.nav-pane.collapsed + .list-pane.collapsed + .editor-pane .editor-action-bar,
.nav-pane.collapsed + .editor-pane .editor-action-bar {
padding-left: 25px;
}
.editor-pane:first-of-type .editor-action-bar,
.mobile-editor-pane.pane-active .editor-action-bar,
.mobile-list-pane.pane-active .route-container-header {
padding-left: 80px;
}
.route-container-header, .editor-action-bar {
transition: padding-left 0.4s ease-out;
}
.editor-action-bar {
border-bottom: none;
}
.route-container-header .routeHeader {
font-size: ${getFontSizes().title};
}
.global-split-pane .react-split__sash {
height: calc(100% - ${TITLE_BAR_HEIGHT}px);
}
`}
/>
) : null}
<Suspense fallback={<div style={{ display: "none" }} />}>
<div id="menu-wrapper">
<GlobalMenuWrapper />
</div>
{isMobile && (
<MobileAppEffects
sliderId="slider"
overlayId="overlay"
setShow={setShow}
/>
)}
</Suspense>
<AppEffects setShow={setShow} />
@@ -172,23 +140,16 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
}}
>
<SplitPane
className="global-split-pane"
ref={navPane}
autoSaveId="global-panel-group"
direction="vertical"
initialSizes={[180, 380]}
onChange={(sizes) => {
setIsNarrow(sizes[0] <= 70);
}}
>
{isFocusMode ? null : (
<Pane
id="nav-pane"
initialSize={180}
className={`nav-pane`}
minSize={50}
snapSize={120}
maxSize={300}
>
{!isFocusMode ? (
<Pane minSize={50} snapSize={120} maxSize={300}>
<NavigationMenu
toggleNavigationContainer={(state) => {
setShow(state || !show);
@@ -196,15 +157,12 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
isTablet={isNarrow}
/>
</Pane>
)}
{!isFocusMode && show ? (
) : null}
{!isFocusMode && show && (
<Pane
id="list-pane"
initialSize={380}
style={{ flex: 1, display: "flex" }}
snapSize={200}
maxSize={500}
className="list-pane"
>
<ScopedThemeProvider
className="listMenu"
@@ -220,11 +178,9 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
<CachedRouter />
</ScopedThemeProvider>
</Pane>
) : null}
)}
<Pane
id="editor-pane"
className="editor-pane"
style={{
flex: 1,
display: "flex",
@@ -243,46 +199,8 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
}
function MobileAppContents() {
const { ref, slideToIndex } = useSlider({
onSliding: (_e, { position }) => {
const offset = 70;
const width = 300;
const percent = offset - (position / width) * offset;
const overlay = document.getElementById("overlay");
if (!overlay) return;
if (percent > 0) {
overlay.style.opacity = `${percent}%`;
overlay.style.pointerEvents = "all";
} else {
overlay.style.opacity = "0%";
overlay.style.pointerEvents = "none";
}
},
onChange: (e, { slide, lastSlide }) => {
slide.node.classList.add("pane-active");
lastSlide?.node.classList.remove("pane-active");
}
});
useEffect(() => {
const toggleSideMenuEvent = AppEventManager.subscribe(
AppEvents.toggleSideMenu,
(state) => slideToIndex(state ? 0 : 1)
);
const toggleEditorEvent = AppEventManager.subscribe(
AppEvents.toggleEditor,
(state) => slideToIndex(state ? 2 : 1)
);
return () => {
toggleSideMenuEvent.unsubscribe();
toggleEditorEvent.unsubscribe();
};
}, [slideToIndex]);
return (
<FlexScrollContainer
scrollRef={ref}
id="slider"
suppressScrollX
style={{
@@ -299,18 +217,17 @@ function MobileAppContents() {
}}
>
<Flex
className="mobile-nav-pane"
sx={{
scrollSnapAlign: "start",
scrollSnapStop: "always",
width: 300,
width: [300, 60],
flexShrink: 0
}}
>
<NavigationMenu toggleNavigationContainer={() => { }} isTablet={false} />
</Flex>
<Flex
className="mobile-list-pane"
className="listMenu"
variant="columnFill"
sx={{
position: "relative",
@@ -323,7 +240,6 @@ function MobileAppContents() {
<CachedRouter />
<Box
id="overlay"
onClick={() => slideToIndex(1)}
sx={{
position: "absolute",
width: "100%",
@@ -339,7 +255,6 @@ function MobileAppContents() {
/>
</Flex>
<Flex
className="mobile-editor-pane"
sx={{
scrollSnapAlign: "start",
scrollSnapStop: "always",

View File

@@ -38,8 +38,5 @@ export const AppEvents = {
changeNoteTitle: "changeNoteTitle",
revealItemInList: "list:revealItem",
toggleSideMenu: "app:openSideMenu",
toggleEditor: "app:toggleEditor"
revealItemInList: "list:revealItem"
};

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Button, Flex, Text } from "@theme-ui/components";
import { useEffect, useRef, useState } from "react";
import { useState } from "react";
import {
ArrowLeft,
Cross,
@@ -79,15 +79,11 @@ import { showPublishView } from "../publish-view";
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
import useMobile from "../../hooks/use-mobile";
import { strings } from "@notesnook/intl";
import { TITLE_BAR_HEIGHT, getWindowControls } from "../title-bar";
import useTablet from "../../hooks/use-tablet";
import { isMac } from "../../utils/platform";
export function EditorActionBar() {
const { isMaximized, isFullscreen, hasNativeWindowControls } =
useWindowControls();
const editorMargins = useEditorStore((store) => store.editorMargins);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const { isFullscreen } = useWindowControls();
const activeSession = useEditorStore((store) =>
store.activeSessionId ? store.getSession(store.activeSessionId) : undefined
);
@@ -99,7 +95,7 @@ export function EditorActionBar() {
const isNotePublished =
activeSession && db.monographs.isPublished(activeSession.id);
const isMobile = useMobile();
const isTablet = useTablet();
const setIsEditorOpen = useAppStore((store) => store.setIsEditorOpen);
const tools = [
{
@@ -199,14 +195,7 @@ export function EditorActionBar() {
activeSession.type !== "conflicted" &&
!isFocusMode,
onClick: () => useEditorStore.getState().toggleProperties()
},
...getWindowControls(
hasNativeWindowControls,
isFullscreen,
isMaximized,
isTablet,
isMobile
)
}
];
return (
@@ -221,9 +210,7 @@ export function EditorActionBar() {
borderRadius: 0,
flexShrink: 0
}}
onClick={() =>
AppEventManager.publish(AppEvents.toggleEditor, false)
}
onClick={() => setIsEditorOpen(false)}
>
<ArrowLeft size={18} />
</Button>
@@ -231,46 +218,35 @@ export function EditorActionBar() {
) : (
<TabStrip />
)}
<Flex
sx={{
alignItems: "center",
justifyContent: "center",
mr:
hasNativeWindowControls && !isMac() && !isMobile && !isTablet
? `calc(100vw - env(titlebar-area-width))`
: 0
}}
>
{tools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
variant={tool.title === "Close" ? "error" : "secondary"}
title={tool.title}
key={tool.title}
sx={{
height: "100%",
alignItems: "center",
bg: "transparent",
display: [
"hideOnMobile" in tool && tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
],
borderRadius: 0,
flexShrink: 0,
"&:hover svg path": {
fill:
tool.title === "Close"
? "var(--accentForeground-error) !important"
: "var(--icon)"
}
}}
onClick={tool.onClick}
>
<tool.icon size={18} />
</Button>
))}
</Flex>
{tools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
variant={tool.title === "Close" ? "error" : "secondary"}
title={tool.title}
key={tool.title}
sx={{
height: "100%",
alignItems: "center",
bg: "transparent",
display: [
tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
],
borderRadius: 0,
flexShrink: 0,
"&:hover svg path": {
fill:
tool.title === "Close"
? "var(--accentForeground-error) !important"
: "var(--icon)"
}
}}
onClick={tool.onClick}
>
<tool.icon size={18} />
</Button>
))}
</>
);
}
@@ -283,7 +259,7 @@ function TabStrip() {
<ScrollContainer
className="tabsScroll"
suppressScrollY
style={{ flex: 1, height: TITLE_BAR_HEIGHT }}
style={{ flex: 1 }}
trackStyle={() => ({
backgroundColor: "transparent",
"--ms-track-size": "6px"
@@ -474,31 +450,19 @@ function Tab(props: TabProps) {
: Note;
const { attributes, listeners, setNodeRef, transform, transition, active } =
useSortable({ id });
const activeTabRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (activeTabRef.current && isActive) {
const tab = activeTabRef.current;
tab.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest"
});
}
}, [isActive]);
return (
<Flex
ref={(el) => {
setNodeRef(el);
activeTabRef.current = el;
}}
ref={setNodeRef}
className="tab"
data-test-id={`tab-${id}`}
sx={{
height: "100%",
cursor: "pointer",
px: 2,
":first-of-type": {
borderLeft: "1px solid var(--border)"
},
borderRight: "1px solid var(--border)",
transform: CSS.Transform.toString(transform),

View File

@@ -72,7 +72,6 @@ import { NoteLinkingDialog } from "../../dialogs/note-linking-dialog";
import { strings } from "@notesnook/intl";
import { onPageVisibilityChanged } from "../../utils/page-visibility";
import { Pane, SplitPane } from "../split-pane";
import { TITLE_BAR_HEIGHT } from "../title-bar";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
@@ -125,56 +124,33 @@ export default function TabsView() {
const isTOCVisible = useEditorStore((store) => store.isTOCVisible);
const [dropRef, overlayRef] = useDragOverlay();
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (
(event.ctrlKey || event.metaKey) &&
event.altKey &&
event.key === "ArrowRight"
) {
event.preventDefault();
useEditorStore.getState().openNextSession();
}
if (
(event.ctrlKey || event.metaKey) &&
event.altKey &&
event.key === "ArrowLeft"
) {
event.preventDefault();
useEditorStore.getState().openPreviousSession();
}
};
document.body.addEventListener("keydown", onKeyDown);
return () => {
document.body.removeEventListener("keydown", onKeyDown);
};
}, []);
return (
<>
<Flex
className="editor-action-bar"
sx={{
zIndex: 2,
height: TITLE_BAR_HEIGHT,
borderBottom: "1px solid var(--border)"
}}
>
<EditorActionBar />
</Flex>
{!hasNativeTitlebar ? (
<EditorActionBarPortal />
) : (
<Flex sx={{ px: 1 }}>
<EditorActionBar />
</Flex>
)}
<ScopedThemeProvider
scope="editor"
ref={dropRef}
sx={{
bg: "background",
pt: 1,
flex: 1,
overflow: "hidden",
display: "flex",
flexDirection: "column"
}}
>
<SplitPane direction="vertical" autoSaveId={"editor-panels"}>
<SplitPane
direction="vertical"
initialSizes={documentPreview ? [Infinity, 435] : [Infinity]}
autoSaveId={"editor-panels"}
>
<Pane id="editor-panel" className="editor-pane">
{sessions.map((session) => (
<Freeze key={session.id} freeze={session.id !== activeSessionId}>
@@ -190,7 +166,7 @@ export default function TabsView() {
</Pane>
{documentPreview ? (
<Pane id="pdf-preview-panel" initialSize={435} minSize={435}>
<Pane id="pdf-preview-panel" minSize={435}>
<ScopedThemeProvider
scope="editorSidebar"
id="editorSidebar"
@@ -227,7 +203,7 @@ export default function TabsView() {
) : null}
{isTOCVisible && activeSessionId ? (
<Pane id="table-of-contents-pane" initialSize={300} minSize={300}>
<Pane minSize={300}>
<TableOfContents sessionId={activeSessionId} />
</Pane>
) : null}
@@ -906,3 +882,9 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
</div>
);
}
function EditorActionBarPortal() {
const container = document.getElementById("titlebar-portal-container");
if (!container) return null;
return ReactDOM.createPortal(<EditorActionBar />, container);
}

View File

@@ -33,9 +33,6 @@ import {
hashStream,
writeEncryptedFile
} from "../../interfaces/fs";
import Config from "../../utils/config";
import { compressImage, FileWithURI } from "../../utils/image-compressor";
import { ImageCompressionOptions } from "../../stores/setting-store";
const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
@@ -61,43 +58,12 @@ export async function attachFiles(files: File[]) {
}
let images = files.filter((f) => f.type.startsWith("image/"));
const imageCompressionConfig = Config.get<ImageCompressionOptions>(
"imageCompression",
ImageCompressionOptions.ASK_EVERY_TIME
);
switch (imageCompressionConfig) {
case ImageCompressionOptions.ENABLE: {
let compressedImages: FileWithURI[] = [];
for (const image of images) {
const compressed = await compressImage(image, {
maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7),
width: (naturalWidth) => naturalWidth,
height: (_, naturalHeight) => naturalHeight,
resize: "contain",
quality: 0.7
});
compressedImages.push(
new FileWithURI([compressed], image.name, {
lastModified: image.lastModified,
type: image.type
})
);
}
images = compressedImages;
break;
}
case ImageCompressionOptions.DISABLE:
break;
default:
images =
images.length > 0
? (await ImagePickerDialog.show({
images
})) || []
: [];
}
images =
images.length > 0
? (await ImagePickerDialog.show({
images
})) || []
: [];
const documents = files.filter((f) => !f.type.startsWith("image/"));
const attachments: Attachment[] = [];
for (const file of [...images, ...documents]) {

View File

@@ -166,7 +166,7 @@ function TipTap(props: TipTapProps) {
const tiptapOptions = useMemo<Partial<TiptapOptions>>(() => {
return {
editorProps: {
handleKeyDown(_, event) {
handleKeyDown(view, event) {
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault();
onChange?.(

View File

@@ -24,13 +24,6 @@ import { ThemeUIStyleObject } from "@theme-ui/css";
import { PasswordVisible, PasswordInvisible, Icon } from "../icons";
import { useStore as useThemeStore } from "../../stores/theme-store";
type Action = {
testId?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
icon?: Icon;
component?: JSX.Element;
};
export type FieldProps = InputProps & {
label?: string;
helpText?: string;
@@ -42,9 +35,13 @@ export type FieldProps = InputProps & {
label?: ThemeUIStyleObject;
helpText?: ThemeUIStyleObject;
};
action?: Action;
rightActions?: Action[];
leftActions?: Action[];
action?: {
testId?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
icon?: Icon;
component?: JSX.Element;
};
};
function Field(props: FieldProps) {
@@ -64,8 +61,6 @@ function Field(props: FieldProps) {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const colorScheme = useThemeStore((state) => state.colorScheme);
const [isValid, setIsValid] = useState(true);
const rightActions = props.rightActions || (action ? [action] : []);
const leftActions = props.leftActions || [];
return (
<Flex
@@ -73,7 +68,6 @@ function Field(props: FieldProps) {
m: "2px",
mr: "2px",
opacity: disabled ? 0.7 : 1,
gap: 1,
...sx,
flexDirection: "column"
}}
@@ -104,7 +98,7 @@ function Field(props: FieldProps) {
)}
</Label>
<Flex sx={{ position: "relative", flex: 1 }}>
<Flex mt={1} sx={{ position: "relative" }}>
<Input
{...inputProps}
variant={isValid ? inputProps.variant || "input" : "error"}
@@ -145,89 +139,32 @@ function Field(props: FieldProps) {
{isPasswordVisible ? <PasswordVisible /> : <PasswordInvisible />}
</Flex>
)}
{rightActions.length > 0 ? (
<Flex
{action && (
<Button
type="button"
variant={"secondary"}
data-test-id={action.testId}
onClick={action.onClick}
sx={{
bg: "transparent",
position: "absolute",
margin: 0,
top: 0,
left: 0,
right: "2px",
height: "100%",
borderTopLeftRadius: "default",
borderBottomLeftRadius: "default",
overflow: "hidden"
px: 1,
borderRadius: "default",
":hover": { bg: "border" }
}}
disabled={action.disabled}
>
{leftActions.map((action) => (
<Button
key={action.testId}
type="button"
variant={"secondary"}
data-test-id={action.testId}
onClick={action.onClick}
sx={{
p: 0,
px: 1,
height: "100%",
bg: "transparent",
borderRadius: 0,
margin: 0,
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
disabled={action.disabled}
>
{action.component ? (
action.component
) : action.icon ? (
<action.icon size={20} />
) : null}
</Button>
))}
</Flex>
) : null}
{rightActions.length > 0 ? (
<Flex
sx={{
position: "absolute",
top: 0,
right: 0,
height: "100%",
borderTopRightRadius: "default",
borderBottomRightRadius: "default",
overflow: "hidden"
}}
>
{rightActions.map((action) => (
<Button
key={action.testId}
type="button"
variant={"secondary"}
data-test-id={action.testId}
onClick={action.onClick}
sx={{
p: 0,
px: 1,
height: "100%",
bg: "transparent",
borderRadius: 0,
margin: 0,
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
disabled={action.disabled}
>
{action.component ? (
action.component
) : action.icon ? (
<action.icon size={20} />
) : null}
</Button>
))}
</Flex>
) : null}
{action.component ? (
action.component
) : action.icon ? (
<action.icon size={20} />
) : null}
</Button>
)}
</Flex>
</Flex>
);

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Button, Flex, FlexProps, Image, Text } from "@theme-ui/components";
import { useStore as useAppStore } from "../../stores/app-store";
import { Menu } from "../../hooks/use-menu";
import useMobile from "../../hooks/use-mobile";
import { PropsWithChildren } from "react";
@@ -26,7 +27,6 @@ import { SchemeColors, createButtonVariant } from "@notesnook/theme";
import { MenuItem } from "@notesnook/ui";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { AppEventManager, AppEvents } from "../../common/app-events";
type NavigationItemProps = {
icon?: Icon;
@@ -66,6 +66,7 @@ function NavigationItem(
containerRef,
...restProps
} = props;
const toggleSideMenu = useAppStore((store) => store.toggleSideMenu);
const isMobile = useMobile();
return (
@@ -119,7 +120,7 @@ function NavigationItem(
Menu.openMenu(menuItems);
}}
onClick={() => {
AppEventManager.publish(AppEvents.toggleSideMenu, false);
if (isMobile) toggleSideMenu(false);
if (onClick) onClick();
}}
>

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { PropsWithChildren } from "react";
import { Button, Flex, Text } from "@theme-ui/components";
import { Flex, Text } from "@theme-ui/components";
import { ArrowLeft, Menu, Search, Plus, Close } from "../icons";
import { useStore } from "../../stores/app-store";
import { useStore as useSearchStore } from "../../stores/search-store";
@@ -26,8 +26,6 @@ import useMobile from "../../hooks/use-mobile";
import { debounce, usePromise } from "@notesnook/common";
import Field from "../field";
import { strings } from "@notesnook/intl";
import { TITLE_BAR_HEIGHT } from "../title-bar";
import { AppEventManager, AppEvents } from "../../common/app-events";
export type RouteContainerButtons = {
search?: {
@@ -66,6 +64,7 @@ function Header(props: RouteContainerProps) {
() => (typeof props.title === "string" ? props.title : props.title?.()),
[props.title]
);
const toggleSideMenu = useStore((store) => store.toggleSideMenu);
const isMobile = useMobile();
const isSearching = useSearchStore((store) => store.isSearching);
const query = useSearchStore((store) => store.query);
@@ -73,24 +72,16 @@ function Header(props: RouteContainerProps) {
if (isSearching)
return (
<Flex
sx={{
alignItems: "center",
justifyContent: "center",
height: TITLE_BAR_HEIGHT,
zIndex: 2,
px: 1
}}
className="route-container-header search-container"
sx={{ alignItems: "center", justifyContent: "center", mx: 1, my: 1 }}
>
<Field
data-test-id="search-input"
autoFocus
id="search"
name="search"
variant="borderless"
type="text"
sx={{ m: 0, flex: 1, gap: 0 }}
styles={{ input: { p: "5px", m: 0 } }}
sx={{ m: 0, flex: 1 }}
styles={{ input: { p: "7px" } }}
defaultValue={query}
placeholder={strings.typeAKeyword()}
onChange={debounce(
@@ -120,12 +111,11 @@ function Header(props: RouteContainerProps) {
return (
<Flex
className="route-container-header"
mx={2}
sx={{
px: 1,
alignItems: "center",
justifyContent: "space-between",
height: TITLE_BAR_HEIGHT,
zIndex: 2
height: 42.8
}}
>
<Flex
@@ -133,80 +123,65 @@ function Header(props: RouteContainerProps) {
sx={{
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
gap: 1
overflow: "hidden"
}}
>
{buttons?.back ? (
<Button
<ArrowLeft
size={24}
{...buttons.back}
sx={{ flexShrink: 0, mr: 2, cursor: "pointer" }}
data-test-id="go-back"
sx={{ p: 0, flexShrink: 0 }}
>
<ArrowLeft size={20} />
</Button>
/>
) : (
<Button
onClick={() =>
AppEventManager.publish(AppEvents.toggleSideMenu, true)
}
sx={{ p: 0, flexShrink: 0 }}
>
<Menu
sx={{
display: ["block", "none", "none"],
size: 23
}}
size={24}
/>
</Button>
<Menu
onClick={() => toggleSideMenu(true)}
sx={{
flexShrink: 0,
ml: 0,
mr: 4,
mt: 1,
display: ["block", "none", "none"]
}}
size={30}
/>
)}
{titlePromise.status === "fulfilled" && titlePromise.value && (
<Text
className="routeHeader"
variant="heading"
data-test-id="routeHeader"
color="heading"
>
<Text variant="heading" data-test-id="routeHeader" color="heading">
{titlePromise.value}
</Text>
)}
</Flex>
<Flex sx={{ flexShrink: 0, gap: 2 }}>
<Flex sx={{ flexShrink: 0 }}>
{buttons?.search && (
<Button
<Search
data-test-id={"open-search"}
size={24}
title={buttons.search.title}
onClick={() =>
useSearchStore.setState({ isSearching: true, searchType: type })
}
data-test-id={"open-search"}
sx={{ p: 0 }}
>
<Search
size={24}
sx={{
size: 24
}}
/>
</Button>
sx={{
size: 24,
cursor: "pointer"
}}
/>
)}
{!isMobile && buttons?.create && (
<Button
{...buttons.create}
<Plus
data-test-id={`${type}-action-button`}
sx={{ p: 0 }}
>
<Plus
color="accentForeground"
size={18}
sx={{
height: 24,
width: 24,
bg: "accent",
borderRadius: 100
}}
/>
</Button>
color="accentForeground"
size={18}
sx={{
bg: "accent",
ml: 2,
borderRadius: 100,
size: 28,
cursor: "pointer",
":hover": { boxShadow: "0px 0px 5px 0px var(--accent)" }
}}
{...buttons.create}
/>
)}
</Flex>
</Flex>

View File

@@ -46,16 +46,6 @@ import { IAxis, ISplitProps, IPaneConfigs } from "./types";
import Config from "../../utils/config";
export { Pane };
type PaneOptions = {
min: number;
max: number;
snap: number;
size: number;
nextSize?: number;
initialSize: number;
collapsed: boolean;
};
export type SplitPaneImperativeHandle = {
collapse: (index: number) => void;
expand: (index: number) => void;
@@ -66,10 +56,10 @@ export const SplitPane = React.forwardRef<
>(function SplitPane(
{
children,
initialSizes,
allowResize = true,
direction = "vertical",
className: wrapClassName,
sashStyle,
sashRender = (_, active) => (
<div
className={classNames(
@@ -89,72 +79,43 @@ export const SplitPane = React.forwardRef<
) {
const axis = useRef<IAxis>({ x: 0, y: 0 });
const wrapper = useRef<HTMLDivElement>(null);
const sizes = useRef<number[]>([]);
const collapsed = useRef<boolean[]>(
Config.get(`csp:${autoSaveId}:collapsed`, [])
);
const sashPosSizes = useRef<number[]>([]);
const panes = useRef<(HTMLDivElement | null)[]>([]);
const sashes = useRef<(HTMLDivElement | null)[]>([]);
const paneSizes = useRef<PaneOptions[]>([]);
const paneLimitSizes = useRef<{ min: number; max: number; snap: number }[]>(
[]
);
const wrapSize = useRef(0);
const childrenLength = childrenToArray(children).length;
const autoSaveKey = autoSaveId ? `csp:${autoSaveId}` : undefined;
const { sizeName, splitPos, splitAxis } = useMemo(
() =>
({
sizeName: direction === "vertical" ? "width" : "height",
splitPos: direction === "vertical" ? "left" : "top",
splitAxis: direction === "vertical" ? "x" : "y"
} as const),
({
sizeName: direction === "vertical" ? "width" : "height",
splitPos: direction === "vertical" ? "left" : "top",
splitAxis: direction === "vertical" ? "x" : "y"
} as const),
[direction]
);
const updatePaneLimitSizes = useCallback(
(children: React.ReactNode) => {
paneSizes.current =
childrenToArray(children).map((childNode) => {
const limits: PaneOptions = {
min: 0,
max: Infinity,
snap: 0,
size: Infinity,
initialSize: Infinity,
collapsed: false
};
if (React.isValidElement(childNode) && childNode.type === Pane) {
const { minSize, maxSize, snapSize, initialSize, id, collapsed } =
childNode.props as IPaneConfigs;
limits.min = assertsSize(minSize, wrapSize.current, 0);
limits.max = assertsSize(maxSize, wrapSize.current);
limits.snap = assertsSize(snapSize, wrapSize.current, 0);
limits.initialSize = assertsSize(initialSize, wrapSize.current);
Object.defineProperty(limits, "collapsed", {
get() {
return Config.get(`${autoSaveKey}-${id}:collapsed`, collapsed);
},
set(v) {
if (v == null) Config.remove(`${autoSaveKey}-${id}:collapsed`);
else Config.set(`${autoSaveKey}-${id}:collapsed`, v);
}
});
Object.defineProperty(limits, "size", {
get() {
return Config.get(
`${autoSaveKey}-${id}`,
assertsSize(initialSize, wrapSize.current)
);
},
set(v) {
if (v === null || v === undefined || v === Infinity)
Config.remove(`${autoSaveKey}-${id}`);
else Config.set(`${autoSaveKey}-${id}`, v);
}
});
}
return limits;
}) || [];
},
[autoSaveKey]
);
const updatePaneLimitSizes = useCallback((children: React.ReactNode) => {
paneLimitSizes.current =
childrenToArray(children).map((childNode) => {
const limits = { min: 0, max: Infinity, snap: 0 };
if (React.isValidElement(childNode) && childNode.type === Pane) {
const { minSize, maxSize, snapSize } =
childNode.props as IPaneConfigs;
limits.min = assertsSize(minSize, wrapSize.current, 0);
limits.max = assertsSize(maxSize, wrapSize.current);
limits.snap = assertsSize(snapSize, wrapSize.current, 0);
}
return limits;
}) || [];
}, []);
useLayoutEffect(() => {
if (wrapSize.current === 0) {
@@ -163,17 +124,32 @@ export const SplitPane = React.forwardRef<
}
if (wrapSize.current === 0) return;
updatePaneLimitSizes(children);
setSizes(paneSizes.current, wrapSize.current, false);
}, [children, childrenLength]);
setSizes(
sizes.current.length === childrenLength
? sizes.current
: Config.get(`csp:${autoSaveId}`, initialSizes),
wrapSize.current,
false
);
}, [initialSizes, children, childrenLength]);
const setSizes = useCallback(
function setSizes(
paneLimits: PaneOptions[],
paneSizes: (number | string)[],
wrapSize: number,
notify = true
) {
const normalized = normalizeSizes(children, paneLimits, wrapSize);
const normalized = normalizeSizes(
children,
paneSizes.map((size, i) =>
collapsed.current[i] ? paneLimitSizes.current[i].min : size
),
initialSizes,
wrapSize
);
sashPosSizes.current = normalized.reduce(
(a, b) => [...a, a[a.length - 1] + b],
[0]
@@ -181,15 +157,11 @@ export const SplitPane = React.forwardRef<
for (let i = 0; i < panes.current.length; ++i) {
const pane = panes.current[i];
if (!pane) continue;
const size = normalized[i];
const sashPos = sashPosSizes.current[i];
const limits = paneSizes.current[i];
if (!pane) continue;
pane.style[sizeName] = `${size}px`;
pane.style[splitPos] = `${sashPos}px`;
if (limits.collapsed || size === limits.min)
pane.classList.add("collapsed");
else pane.classList.remove("collapsed");
}
for (let i = 0; i < sashes.current.length; ++i) {
@@ -200,20 +172,35 @@ export const SplitPane = React.forwardRef<
}
}
paneSizes.current.forEach((limits, index) => {
limits.size = normalized[index];
});
sizes.current = normalizeSizes(
children,
paneSizes,
initialSizes,
wrapSize
);
if (autoSaveId) {
Config.set(`csp:${autoSaveId}`, sizes.current);
Config.set(`csp:${autoSaveId}:collapsed`, collapsed.current);
}
if (notify) onChange(normalized);
},
[children, onChange, sizeName, splitPos, resizerSize]
[
children,
initialSizes,
onChange,
autoSaveId,
sizeName,
splitPos,
resizerSize
]
);
useEffect(() => {
if (!wrapper.current) return;
const resizeObserver = new ResizeObserver((entries) => {
if (!paneSizes.current.length) return;
if (!sizes.current.length) return;
const [entry] = entries;
const newSize = entry.contentRect ? entry.contentRect[sizeName] : 0;
@@ -242,7 +229,7 @@ export const SplitPane = React.forwardRef<
// }
wrapSize.current = newSize;
setSizes(paneSizes.current, wrapSize.current);
setSizes(sizes.current, wrapSize.current);
});
resizeObserver.observe(wrapper.current);
return () => {
@@ -255,12 +242,12 @@ export const SplitPane = React.forwardRef<
() => {
return {
collapse: (index: number) => {
paneSizes.current[index].collapsed = true;
setSizes(paneSizes.current, wrapSize.current);
collapsed.current[index] = true;
setSizes(sizes.current, wrapSize.current);
},
expand: (index: number) => {
paneSizes.current[index].collapsed = false;
setSizes(paneSizes.current, wrapSize.current);
collapsed.current[index] = false;
setSizes(sizes.current, wrapSize.current);
}
};
},
@@ -295,17 +282,20 @@ export const SplitPane = React.forwardRef<
let distanceX = curAxis[splitAxis] - axis.current[splitAxis];
axis.current = { x: e.pageX, y: e.pageY };
const currentPane = paneSizes.current[i];
const nextPane = paneSizes.current[i + 1];
const currentSize = sizes.current[i];
const currentPaneLimits = paneLimitSizes.current[i];
const nextPaneLimits = paneLimitSizes.current[i + 1];
const rightBorder = sashPosSizes.current[i + 2];
if (currentPane.size + distanceX >= rightBorder)
distanceX = rightBorder - currentPane.size;
if (currentSize + distanceX >= rightBorder)
distanceX = rightBorder - currentSize;
const nextSizes = [...sizes.current];
// if current pane size is out of limit, adjust the previous pane
if (
currentPane.size + distanceX >= currentPane.max ||
currentPane.size + distanceX <= currentPane.min
currentSize + distanceX >= currentPaneLimits.max ||
currentSize + distanceX <= currentPaneLimits.min
) {
if (i > 0) {
// reset axis
@@ -315,32 +305,27 @@ export const SplitPane = React.forwardRef<
return;
}
currentPane.nextSize =
(currentPane.nextSize || currentPane.size) + distanceX;
nextSizes[i] += distanceX;
// keep the next pane size in the min-max range
nextPane.nextSize = Math.min(
nextPane.max,
Math.max(nextPane.min, (nextPane.nextSize || nextPane.size) - distanceX)
nextSizes[i + 1] = Math.min(
nextPaneLimits.max,
Math.max(nextPaneLimits.min, nextSizes[i + 1] - distanceX)
);
// snapping logic
if (currentPane.snap > 0) {
if (distanceX < 0 && currentPane.nextSize <= currentPane.snap / 2) {
currentPane.nextSize = currentPane.min;
} else if (currentPane.nextSize < currentPane.snap) {
if (currentPaneLimits.snap > 0) {
if (distanceX < 0 && nextSizes[i] <= currentPaneLimits.snap / 2) {
nextSizes[i] = currentPaneLimits.min;
} else if (nextSizes[i] < currentPaneLimits.snap) {
// reset axis
axis.current[splitAxis] += -distanceX;
return;
}
}
nextPane.size = nextPane.nextSize;
currentPane.size = currentPane.nextSize;
nextPane.nextSize = undefined;
currentPane.nextSize = undefined;
setSizes(paneSizes.current, wrapSize.current);
setSizes(nextSizes, wrapSize.current);
},
[paneSizes, setSizes, splitAxis]
[paneLimitSizes, setSizes, splitAxis]
);
return (
@@ -354,7 +339,9 @@ export const SplitPane = React.forwardRef<
ref={wrapper}
{...others}
>
{childrenToArray(children).map((childNode, childIndex) => {
{React.Children.map(children, (childNode, childIndex) => {
if (!childNode) return null;
const isPane = React.isValidElement(childNode)
? childNode.type === Pane
: false;
@@ -363,7 +350,6 @@ export const SplitPane = React.forwardRef<
return (
<Pane
id={paneProps.id}
key={childIndex}
paneRef={(e) => (panes.current[childIndex] = e)}
className={classNames(paneClassName, paneProps.className)}
@@ -384,17 +370,18 @@ export const SplitPane = React.forwardRef<
: sashHorizontalClassName
)}
style={{
[sizeName]: resizerSize,
...sashStyle
[sizeName]: resizerSize
}}
render={sashRender.bind(null, index)}
onDragStart={dragStart}
onDragging={(e) => onDragging(e, index)}
onDragEnd={dragEnd}
onDoubleClick={() => {
paneSizes.current[index].size =
paneSizes.current[index].initialSize;
setSizes(paneSizes.current, wrapSize.current);
sizes.current[index] = assertsSize(
initialSizes[index],
wrapSize.current
);
setSizes(sizes.current, wrapSize.current);
}}
/>
))}
@@ -403,19 +390,20 @@ export const SplitPane = React.forwardRef<
});
function childrenToArray(children: React.ReactNode) {
return React.Children.toArray(children);
return React.Children.toArray(children).filter(Boolean);
}
function normalizeSizes(
children: React.ReactNode,
panes: PaneOptions[],
currentSizes: (string | number)[],
initialSizes: (string | number)[],
wrapSize: number
): number[] {
let count = 0;
let curSum = 0;
const res = childrenToArray(children).map((_, index) => {
const initialSize = panes[index].initialSize;
const size = panes[index].collapsed ? panes[index].min : panes[index].size;
const initialSize = assertsSize(initialSizes[index], wrapSize);
const size = assertsSize(currentSizes[index], wrapSize);
initialSize === Infinity ? count++ : (curSum += size);
return size;
});
@@ -423,7 +411,7 @@ function normalizeSizes(
if (count > 0 || curSum > wrapSize) {
const average = (wrapSize - curSum) / count;
return res.map((size, index) => {
const initialSize = panes[index].initialSize;
const initialSize = assertsSize(initialSizes[index], wrapSize);
return initialSize === Infinity ? average : size;
});
}

View File

@@ -14,7 +14,7 @@
.react-split__sash {
height: 100%;
position: absolute;
bottom: 0;
top: 0;
transition: background-color 0.1s;
width: 100%;
z-index: 2;

View File

@@ -38,15 +38,34 @@ export interface ICacheSizes {
export interface ISplitProps extends HTMLElementProps {
autoSaveId?: string;
/**
* Should allowed to resized
*
* default is true
*/
allowResize?: boolean;
/**
* How to split the space
*
* default is vertical
*/
direction: "vertical" | "horizontal";
/**
* Only support controlled mode, so it's required
*/
initialSizes: (string | number)[];
sashRender?: (index: number, active: boolean) => React.ReactNode;
onChange?: (sizes: number[]) => void;
onDragStart?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onDragEnd?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
className?: string;
sashClassName?: string;
sashStyle?: React.CSSProperties;
// performanceMode?: boolean;
/**
* Specify the size fo resizer
*
* defualt size is 4px
*/
sashSize?: number;
}
@@ -68,11 +87,9 @@ export interface ISashContentProps {
}
export interface IPaneConfigs {
id: string;
id?: string;
paneRef?: React.LegacyRef<HTMLDivElement>;
maxSize?: number | string;
minSize?: number | string;
snapSize?: number | string;
initialSize?: number | string;
collapsed?: boolean;
}

View File

@@ -17,30 +17,25 @@ 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 { useWindowControls } from "../../hooks/use-window-controls";
import { isMac } from "../../utils/platform";
import { BaseThemeProvider } from "../theme-provider";
import { strings } from "@notesnook/intl";
import { Button } from "@theme-ui/components";
import { desktop } from "../../common/desktop-bridge";
import { useWindowControls } from "../../hooks/use-window-controls";
import { getPlatform } from "../../utils/platform";
import {
WindowClose,
WindowMaximize,
WindowMinimize,
WindowRestore
} from "../icons";
import { Button, Flex } from "@theme-ui/components";
import useMobile from "../../hooks/use-mobile";
import useTablet from "../../hooks/use-tablet";
import { BaseThemeProvider } from "../theme-provider";
import { strings } from "@notesnook/intl";
export function getWindowControls(
hasNativeWindowControls: boolean,
isFullscreen?: boolean,
isMaximized?: boolean,
isTablet?: boolean,
isMobile?: boolean
) {
if (isMobile || isTablet) return [];
return [
export const TITLE_BAR_HEIGHT = IS_DESKTOP_APP ? 37.8 : 0;
export function TitleBar() {
const { isMaximized, isFullscreen, hasNativeWindowControls } =
useWindowControls();
const tools = [
{
title: strings.minimize(),
icon: WindowMinimize,
@@ -66,21 +61,7 @@ export function getWindowControls(
onClick: () => window.close()
}
];
}
export const TITLE_BAR_HEIGHT = 37;
export function TitleBar({ isUnderlay = isMac() }: { isUnderlay?: boolean }) {
const { isFullscreen, hasNativeWindowControls, isMaximized } =
useWindowControls();
const isTablet = useTablet();
const isMobile = useMobile();
if ((!isMac() && !isMobile && !isTablet) || (isFullscreen && isMac()))
return null;
const tools = getWindowControls(
hasNativeWindowControls,
isFullscreen,
isMaximized
);
return (
<BaseThemeProvider
scope="titleBar"
@@ -91,21 +72,17 @@ export function TitleBar({ isUnderlay = isMac() }: { isUnderlay?: boolean }) {
minHeight: TITLE_BAR_HEIGHT,
maxHeight: TITLE_BAR_HEIGHT,
display: "flex",
justifyContent: "space-between",
flexShrink: 0,
width: "100%",
zIndex: 1,
borderBottom: "1px solid var(--border)",
...(isUnderlay
? {
position: "absolute",
top: 0
}
: {})
...(!isFullscreen && hasNativeWindowControls
? getPlatform() === "darwin"
? { pl: "calc(100vw - env(titlebar-area-width))" }
: { pr: "calc(100vw - env(titlebar-area-width))" }
: { pr: 0 })
}}
injectCssVars
>
{tools.filter((t) => !t.hidden).length > 0 ? (
{getPlatform() !== "darwin" || isFullscreen ? (
<svg
className="titlebarLogo"
style={{
@@ -119,34 +96,40 @@ export function TitleBar({ isUnderlay = isMac() }: { isUnderlay?: boolean }) {
<use href="#themed-logo" />
</svg>
) : null}
<Flex sx={{ alignItems: "center" }}>
{tools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
variant={tool.title === "Close" ? "error" : "secondary"}
title={tool.title}
key={tool.title}
sx={{
height: "100%",
alignItems: "center",
bg: "transparent",
display: tool.hidden ? "none" : "flex",
borderRadius: 0,
flexShrink: 0,
"&:hover svg path": {
fill:
tool.title === "Close"
? "var(--accentForeground-error) !important"
: "var(--icon)"
}
}}
onClick={tool.onClick}
>
<tool.icon size={18} />
</Button>
))}
</Flex>
<div
id="titlebar-portal-container"
style={{
flex: 1,
display: "flex",
overflow: "hidden"
}}
/>
{tools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
variant={tool.title === "Close" ? "error" : "secondary"}
title={tool.title}
key={tool.title}
sx={{
height: "100%",
alignItems: "center",
bg: "transparent",
display: tool.hidden ? "none" : "flex",
borderRadius: 0,
flexShrink: 0,
"&:hover svg path": {
fill:
tool.title === "Close"
? "var(--accentForeground-error) !important"
: "var(--icon)"
}
}}
onClick={tool.onClick}
>
<tool.icon size={18} />
</Button>
))}
</BaseThemeProvider>
);
}

View File

@@ -22,13 +22,24 @@ import Dialog from "../components/dialog";
import { ScrollContainer } from "@notesnook/ui";
import { Flex, Image, Label, Text } from "@theme-ui/components";
import { formatBytes } from "@notesnook/common";
import { compressImage, FileWithURI } from "../utils/image-compressor";
import { compressImage } from "../utils/image-compressor";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
export type ImagePickerDialogProps = BaseDialogProps<false | File[]> & {
images: File[];
};
class FileWithURI extends File {
uri: string;
constructor(
fileBits: BlobPart[],
fileName: string,
options?: FilePropertyBag
) {
super(fileBits, fileName, options);
this.uri = URL.createObjectURL(this);
}
}
export const ImagePickerDialog = DialogManager.register(
function ImagePickerDialog(props: ImagePickerDialogProps) {

View File

@@ -19,10 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { DATE_FORMATS } from "@notesnook/core";
import { SettingsGroup } from "./types";
import {
ImageCompressionOptions,
useStore as useSettingStore
} from "../../stores/setting-store";
import { useStore as useSettingStore } from "../../stores/setting-store";
import dayjs from "dayjs";
import { isUserPremium } from "../../hooks/use-is-user-premium";
import { TimeFormat } from "@notesnook/core";
@@ -58,37 +55,6 @@ export const BehaviourSettings: SettingsGroup[] = [
]
}
]
},
{
key: "image-compression",
title: strings.imageCompression(),
description: strings.imageCompressionDesc(),
keywords: ["compress images", "image quality"],
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.imageCompression, listener),
components: [
{
type: "dropdown",
onSelectionChanged: (value) =>
useSettingStore.getState().setImageCompression(parseInt(value)),
selectedOption: () =>
useSettingStore.getState().imageCompression.toString(),
options: [
{
value: ImageCompressionOptions.ASK_EVERY_TIME.toString(),
title: strings.askEveryTime()
},
{
value: ImageCompressionOptions.ENABLE.toString(),
title: strings.enableRecommended()
},
{
value: ImageCompressionOptions.DISABLE.toString(),
title: strings.disable()
}
]
}
]
}
]
},

View File

@@ -26,24 +26,27 @@ type Slide = {
width: number;
};
export default function useSlider({
onSliding,
onChange
}: {
onSliding?: (
e: Event,
options: {
lastSlide: Slide | null;
lastPosition: number;
position: number;
}
) => void;
onChange?: (
e: Event,
options: { position: number; slide: Slide; lastSlide: Slide | null }
) => void;
} = {}) {
const ref = useRef<HTMLDivElement>(null);
export default function useSlider(
sliderId: string,
{
onSliding,
onChange
}: {
onSliding?: (
e: Event,
options: {
lastSlide: Slide | null;
lastPosition: number;
position: number;
}
) => void;
onChange?: (
e: Event,
options: { position: number; slide: Slide; lastSlide: Slide | null }
) => void;
} = {}
) {
const ref = useRef(document.getElementById(sliderId));
const slides: Slide[] = useMemo(() => [], []);
useEffect(() => {
@@ -54,11 +57,7 @@ export default function useSlider({
let last = 0;
if (!slides.length) {
for (const node of slider.childNodes) {
if (
!(node instanceof HTMLElement) ||
node.classList.contains("ms-track-box")
)
continue;
if (!(node instanceof HTMLElement)) continue;
slides.push({
index: slides.length,
node,
@@ -103,5 +102,5 @@ export default function useSlider({
[ref, slides]
);
return { ref, slideToIndex };
return [slideToIndex];
}

View File

@@ -81,7 +81,7 @@ export async function startApp() {
console.error(e);
root.render(
<>
<TitleBar isUnderlay={false} />
<TitleBar />
<ErrorComponent
error={e}
resetErrorBoundary={() => window.location.reload()}

View File

@@ -61,7 +61,10 @@ let syncTimeout = 0;
let pendingSync: SyncOptions | undefined = undefined;
class AppStore extends BaseStore<AppStore> {
// default state
isSideMenuOpen = false;
isFocusMode = false;
isEditorOpen = false;
isVaultCreated = false;
isAutoSyncEnabled = Config.get("autoSyncEnabled", true);
isSyncEnabled = Config.get("syncEnabled", true);
@@ -196,6 +199,17 @@ class AppStore extends BaseStore<AppStore> {
);
};
toggleSideMenu = (toggleState: boolean) => {
console.log("toggling side menu");
this.set(
(state) => (state.isSideMenuOpen = toggleState ?? !state.isSideMenuOpen)
);
};
setIsEditorOpen = (toggleState: boolean) => {
this.set((state) => (state.isEditorOpen = toggleState));
};
setIsVaultCreated = (toggleState: boolean) => {
this.set((state) => (state.isVaultCreated = toggleState));
};

View File

@@ -518,7 +518,7 @@ class EditorStore extends BaseStore<EditorStore> {
} else setDocumentTitle();
this.set({ activeSessionId: id });
AppEventManager.publish(AppEvents.toggleEditor, true);
appStore.setIsEditorOpen(!!id);
if (id) {
const { history } = this.get();
@@ -717,32 +717,6 @@ class EditorStore extends BaseStore<EditorStore> {
}
};
openNextSession = () => {
const { sessions, activeSessionId } = this.get();
if (sessions.length === 0 || sessions.length === 1) return;
const index = sessions.findIndex((s) => s.id === activeSessionId);
if (index === -1) return;
if (index === sessions.length - 1) {
return this.openSession(sessions[0].id);
}
return this.openSession(sessions[index + 1].id);
};
openPreviousSession = () => {
const { sessions, activeSessionId } = this.get();
if (sessions.length === 0 || sessions.length === 1) return;
const index = sessions.findIndex((s) => s.id === activeSessionId);
if (index === -1) return;
if (index === 0) {
return this.openSession(sessions[sessions.length - 1].id);
}
return this.openSession(sessions[index - 1].id);
};
addSession = (session: EditorSession, activate = true) => {
let oldSessionId: string | null = null;

View File

@@ -35,13 +35,6 @@ export const HostIds = [
"MONOGRAPH_HOST"
] as const;
export type HostId = (typeof HostIds)[number];
export enum ImageCompressionOptions {
ASK_EVERY_TIME,
ENABLE,
DISABLE
}
class SettingStore extends BaseStore<SettingStore> {
encryptBackups = Config.get("encryptBackups", false);
backupReminderOffset = Config.get("backupReminderOffset", 0);
@@ -67,10 +60,6 @@ class SettingStore extends BaseStore<SettingStore> {
trashCleanupInterval: TrashCleanupInterval = 7;
homepage = Config.get("homepage", 0);
imageCompression = Config.get(
"imageCompression",
ImageCompressionOptions.ASK_EVERY_TIME
);
desktopIntegrationSettings?: DesktopIntegration;
autoUpdates = true;
isFlatpak = false;
@@ -136,11 +125,6 @@ class SettingStore extends BaseStore<SettingStore> {
Config.set("homepage", homepage);
};
setImageCompression = (imageCompression: ImageCompressionOptions) => {
this.set({ imageCompression });
Config.set("imageCompression", imageCompression);
};
setDesktopIntegration = async (settings: DesktopIntegration) => {
const { desktopIntegrationSettings } = this.get();

View File

@@ -17,18 +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/>.
*/
export class FileWithURI extends File {
uri: string;
constructor(
fileBits: BlobPart[],
fileName: string,
options?: FilePropertyBag
) {
super(fileBits, fileName, options);
this.uri = URL.createObjectURL(this);
}
}
type DeriveDimension = (naturalWidth: number, naturalHeight: number) => number;
interface CompressorOptions {

View File

@@ -95,6 +95,7 @@ function Notebook(props: NotebookProps) {
<SplitPane
ref={pane}
direction="horizontal"
initialSizes={[Infinity, 250]}
autoSaveId={`notebook-panel-sizes:${rootId}`}
onChange={([_, subnotebooksPane]) => {
setIsCollapsed((isCollapsed) => {
@@ -104,7 +105,7 @@ function Notebook(props: NotebookProps) {
});
}}
>
<Pane id="notes-pane" style={{ display: "flex" }}>
<Pane style={{ display: "flex" }}>
<Notes
header={
<NotebookHeader
@@ -115,7 +116,7 @@ function Notebook(props: NotebookProps) {
}
/>
</Pane>
<Pane id="subnotebooks-pane" initialSize={250} minSize={30}>
<Pane minSize={30}>
<SubNotebooks
isCollapsed={isCollapsed}
rootId={rootId}

View File

@@ -1,5 +0,0 @@
- You can now share multiple files to Notesnook
- Fix file and image sharing not working
- Many other bug fixes and small improvements
Thank you for using Notesnook!

View File

@@ -269,7 +269,6 @@ const Tiptap = ({
? []
: getTableOfContents(containerRef.current);
},
scrollTop: () => containerRef.current?.scrollTop || 0,
scrollTo: (top) => {
containerRef.current?.scrollTo({ top, behavior: "auto" });
}

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Editor, scrollIntoViewById } from "@notesnook/editor";
import { strings } from "@notesnook/intl";
import {
ThemeDefinition,
useThemeColors,
@@ -42,6 +41,7 @@ import {
import { injectCss, transform } from "../utils/css";
import { pendingSaveRequests } from "../utils/pending-saves";
import { useTabContext, useTabStore } from "./useTabStore";
import { strings } from "@notesnook/intl";
type Attachment = {
hash: string;
@@ -126,13 +126,11 @@ export type EditorController = {
export function useEditorController({
update,
getTableOfContents,
scrollTo,
scrollTop
scrollTo
}: {
update: () => void;
getTableOfContents: () => any[];
scrollTo: (top: number) => void;
scrollTop: () => number;
}): EditorController {
const passwordInputRef = useRef<HTMLInputElement | null>(null);
const tab = useTabContext();
@@ -344,28 +342,30 @@ export function useEditorController({
updateTabOnFocus.current = true;
} else {
if (!editor) break;
const noteState = tabRef.current?.noteId
? useTabStore.getState().noteState[tabRef.current?.noteId]
: null;
const top = scrollTop() || noteState?.top || 0;
editor?.commands.setContent(htmlContentRef.current, false, {
preserveWhitespace: true
});
if (noteState && editor.isFocused) {
if (noteState) {
editor.commands.setTextSelection({
from: noteState.from,
to: noteState.to
});
}
scrollTo?.(top || 0);
scrollTo?.(noteState?.top || 0);
countWords(0);
}
break;
}
case "native:html":
if (htmlContentRef.current === value) break;
htmlContentRef.current = value;
logger("info", "LOADING NOTE HTML");
if (!editor) break;

File diff suppressed because it is too large Load Diff

View File

@@ -35,33 +35,33 @@
"@notesnook/theme": "file:../theme",
"@notesnook/ui": "file:../ui",
"@social-embed/lib": "^0.1.0-next.7",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-blockquote": "2.10.4",
"@tiptap/extension-bullet-list": "2.10.4",
"@tiptap/extension-character-count": "2.10.4",
"@tiptap/extension-code": "2.10.4",
"@tiptap/extension-color": "2.10.4",
"@tiptap/extension-font-family": "2.10.4",
"@tiptap/extension-heading": "2.10.4",
"@tiptap/extension-history": "2.10.4",
"@tiptap/extension-horizontal-rule": "2.10.4",
"@tiptap/extension-list-item": "2.10.4",
"@tiptap/extension-list-keymap": "2.10.4",
"@tiptap/extension-ordered-list": "2.10.4",
"@tiptap/extension-placeholder": "2.10.4",
"@tiptap/extension-subscript": "2.10.4",
"@tiptap/extension-superscript": "2.10.4",
"@tiptap/extension-table": "2.10.4",
"@tiptap/extension-table-cell": "2.10.4",
"@tiptap/extension-table-header": "2.10.4",
"@tiptap/extension-table-row": "2.10.4",
"@tiptap/extension-task-item": "2.10.4",
"@tiptap/extension-task-list": "2.10.4",
"@tiptap/extension-text-align": "2.10.4",
"@tiptap/extension-text-style": "2.10.4",
"@tiptap/extension-underline": "2.10.4",
"@tiptap/pm": "2.10.4",
"@tiptap/starter-kit": "2.10.4",
"@tiptap/core": "2.6.6",
"@tiptap/extension-blockquote": "^2.6.6",
"@tiptap/extension-bullet-list": "^2.6.6",
"@tiptap/extension-character-count": "2.6.6",
"@tiptap/extension-code": "^2.6.6",
"@tiptap/extension-color": "2.6.6",
"@tiptap/extension-font-family": "2.6.6",
"@tiptap/extension-heading": "^2.6.6",
"@tiptap/extension-history": "2.6.6",
"@tiptap/extension-horizontal-rule": "2.6.6",
"@tiptap/extension-list-item": "^2.6.6",
"@tiptap/extension-list-keymap": "2.6.6",
"@tiptap/extension-ordered-list": "^2.6.6",
"@tiptap/extension-placeholder": "2.6.6",
"@tiptap/extension-subscript": "2.6.6",
"@tiptap/extension-superscript": "2.6.6",
"@tiptap/extension-table": "2.6.6",
"@tiptap/extension-table-cell": "2.6.6",
"@tiptap/extension-table-header": "2.6.6",
"@tiptap/extension-table-row": "2.6.6",
"@tiptap/extension-task-item": "2.6.6",
"@tiptap/extension-task-list": "2.6.6",
"@tiptap/extension-text-align": "2.6.6",
"@tiptap/extension-text-style": "2.6.6",
"@tiptap/extension-underline": "2.6.6",
"@tiptap/pm": "2.6.6",
"@tiptap/starter-kit": "2.6.6",
"alfaaz": "^1.1.0",
"async-mutex": "^0.5.0",
"clipboard-polyfill": "4.1.0",
@@ -73,7 +73,7 @@
"nanoid": "^5.0.7",
"prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-view": "1.37.1",
"prosemirror-view": "1.34.2",
"re-resizable": "^6.9.18",
"react-colorful": "^5.6.1",
"redent": "^4.0.0",
@@ -128,4 +128,4 @@
"url": "git://github.com/streetwriters/notesnook.git",
"directory": "packages/editor"
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,54 @@
diff --git a/node_modules/@tiptap/extension-list-keymap/dist/index.cjs b/node_modules/@tiptap/extension-list-keymap/dist/index.cjs
index 0f8ae36..adeee92 100644
--- a/node_modules/@tiptap/extension-list-keymap/dist/index.cjs
+++ b/node_modules/@tiptap/extension-list-keymap/dist/index.cjs
@@ -82,7 +82,9 @@ const handleBackspace = (editor, name, parentListTypes) => {
// the previous item is a list (orderedList or bulletList)
// move the cursor into the list and delete the current item
if (!core.isNodeActive(editor.state, name) && hasListBefore(editor.state, name, parentListTypes)) {
- const { $anchor } = editor.state.selection;
+ const { $anchor, empty } = editor.state.selection;
+ if (!empty) return false;
+
const $listPos = editor.state.doc.resolve($anchor.before() - 1);
const listDescendants = [];
$listPos.node().descendants((node, pos) => {
@@ -111,6 +113,11 @@ const handleBackspace = (editor, name, parentListTypes) => {
if (!listItemPos) {
return false;
}
+ // if the current position is not at the start of the list item
+ // then join backward i.e. join within the list item
+ if (listItemPos.$pos.parentOffset !== 0) {
+ return editor.commands.joinBackward();
+ }
const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2);
const prevNode = $prev.node(listItemPos.depth);
const previousListItemHasSubList = listItemHasSubList(name, editor.state, prevNode);
diff --git a/node_modules/@tiptap/extension-list-keymap/dist/index.js b/node_modules/@tiptap/extension-list-keymap/dist/index.js
index f7ab1e4..6ea03d5 100644
--- a/node_modules/@tiptap/extension-list-keymap/dist/index.js
+++ b/node_modules/@tiptap/extension-list-keymap/dist/index.js
@@ -78,7 +78,9 @@ const handleBackspace = (editor, name, parentListTypes) => {
// the previous item is a list (orderedList or bulletList)
// move the cursor into the list and delete the current item
if (!isNodeActive(editor.state, name) && hasListBefore(editor.state, name, parentListTypes)) {
- const { $anchor } = editor.state.selection;
+ const { $anchor, empty } = editor.state.selection;
+ if (!empty) return false;
+
const $listPos = editor.state.doc.resolve($anchor.before() - 1);
const listDescendants = [];
$listPos.node().descendants((node, pos) => {
@@ -107,6 +109,11 @@ const handleBackspace = (editor, name, parentListTypes) => {
if (!listItemPos) {
return false;
}
+ // if the current position is not at the start of the list item
+ // then join backward i.e. join within the list item
+ if (listItemPos.$pos.parentOffset !== 0) {
+ return editor.commands.joinBackward();
+ }
const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2);
const prevNode = $prev.node(listItemPos.depth);
const previousListItemHasSubList = listItemHasSubList(name, editor.state, prevNode);

View File

@@ -1,8 +1,8 @@
diff --git a/node_modules/prosemirror-view/dist/index.cjs b/node_modules/prosemirror-view/dist/index.cjs
index 5903e89..6a85c3d 100644
index 8ea57c7..aeda01d 100644
--- a/node_modules/prosemirror-view/dist/index.cjs
+++ b/node_modules/prosemirror-view/dist/index.cjs
@@ -3482,7 +3482,7 @@ editHandlers.drop = function (view, _event) {
@@ -3456,7 +3456,7 @@ editHandlers.drop = function (view, _event) {
});
tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)));
}
@@ -12,10 +12,10 @@ index 5903e89..6a85c3d 100644
};
handlers.focus = function (view) {
diff --git a/node_modules/prosemirror-view/dist/index.js b/node_modules/prosemirror-view/dist/index.js
index d0c1f9d..f5b3f53 100644
index 9583dc3..991bf0a 100644
--- a/node_modules/prosemirror-view/dist/index.js
+++ b/node_modules/prosemirror-view/dist/index.js
@@ -3754,7 +3754,7 @@ editHandlers.drop = (view, _event) => {
@@ -3731,7 +3731,7 @@ editHandlers.drop = (view, _event) => {
tr.mapping.maps[tr.mapping.maps.length - 1].forEach((_from, _to, _newFrom, newTo) => end = newTo);
tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)));
}

File diff suppressed because one or more lines are too long

View File

@@ -60,7 +60,6 @@ export function ImageComponent(
});
const dom = editor.view.dom.parentElement || editor.view.dom;
const size =
editor.view.dom.clientWidth === 0
? node.attrs
@@ -267,7 +266,7 @@ export function ImageComponent(
title={title}
sx={{
animation: bloburl || src ? "0.2s ease-in 0s 1 fadeIn" : "none",
objectFit: "scale-down",
objectFit: "contain",
width: editor.isEditable ? "100%" : size.width,
height: editor.isEditable ? "100%" : size.height,
border: selected

View File

@@ -1,5 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`paste text > with markdown link 1`] = `"<div><div contenteditable="true" role="textbox" translate="no" class="tiptap ProseMirror" tabindex="0"><p><a target="_blank" rel="noopener noreferrer nofollow" href="example.com">test</a></p></div></div>"`;
exports[`paste text > with markdown link 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><p><a target="_blank" rel="noopener noreferrer nofollow" href="example.com">test</a></p></div></div>"`;
exports[`paste text > with multiple markdown links 1`] = `"<div><div contenteditable="true" role="textbox" translate="no" class="tiptap ProseMirror" tabindex="0"><p><a target="_blank" rel="noopener noreferrer nofollow" href="example.com">test</a> some text <a target="_blank" rel="noopener noreferrer nofollow" href="example2.com">test2</a></p></div></div>"`;
exports[`paste text > with multiple markdown links 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><p><a target="_blank" rel="noopener noreferrer nofollow" href="example.com">test</a> some text <a target="_blank" rel="noopener noreferrer nofollow" href="example2.com">test2</a></p></div></div>"`;

View File

@@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`hitting backspace at the start of first list item 1`] = `"<div><div contenteditable="true" role="textbox" translate="no" class="tiptap ProseMirror" tabindex="0"><p>item1</p><ul><li><p>item2</p></li></ul></div></div>"`;
exports[`hitting backspace at the start of first list item 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><p>item1</p><ul><li><p>item2</p></li></ul></div></div>"`;
exports[`hitting backspace at the start of the second (or next) list item 1`] = `"<div><div contenteditable="true" role="textbox" translate="no" class="tiptap ProseMirror" tabindex="0"><ul><li><p>item1item2</p></li></ul></div></div>"`;
exports[`hitting backspace at the start of the second (or next) list item 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><ul><li><p>item1item2</p></li></ul></div></div>"`;
exports[`hitting backspace at the start of the second (or next) paragraph inside the list item 1`] = `"<div><div contenteditable="true" role="textbox" translate="no" class="tiptap ProseMirror" tabindex="0"><ul><li><p>item 1item 2</p></li></ul></div></div>"`;
exports[`hitting backspace at the start of the second (or next) paragraph inside the list item 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><ul><li><p>item 1item 2</p></li></ul></div></div>"`;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1111,10 +1111,6 @@ $headline$: Use starting line of the note as title.`,
behaviorDesc: () => t`Change how the app behaves in different situations`,
homepage: () => t`Homepage`,
homepageDesc: () => t`Default screen to open on app launch`,
imageCompression: () => t`Image Compression`,
imageCompressionDesc: () => t`Compress images before uploading`,
askEveryTime: () => t`Ask every time`,
enableRecommended: () => t`Enable (Recommended)`,
dateFormat: () => t`Date format`,
dateFormatDesc: () => t`Choose how dates are displayed in the app`,
timeFormat: () => t`Time format`,

View File

@@ -45,26 +45,6 @@ const defaultVariant: ThemeUIStyleObject = {
}
};
const borderless: ThemeUIStyleObject = {
variant: "forms.input",
outline: "none",
boxShadow: "none",
":-webkit-autofill": {
WebkitTextFillColor: "var(--paragraph)",
caretColor: "var(--paragraph)",
fontSize: "inherit"
},
":focus": {
bg: "var(--background-secondary)"
},
":hover:not(:focus)": {
outline: "var(--background-secondary)"
},
"::placeholder": {
color: "placeholder"
}
};
const clean: ThemeUIStyleObject = {
variant: "forms.input",
outline: "none",
@@ -96,7 +76,6 @@ const radio: ThemeUIStyleObject = {
export const inputVariants = {
input: defaultVariant,
borderless,
error,
clean,
radio