mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 10:39:07 +02:00
Compare commits
2 Commits
fix-loadin
...
fix-image-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2b46cad6b | ||
|
|
c6ccf3e32f |
@@ -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) {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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!
|
||||
@@ -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)";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.26",
|
||||
"version": "3.0.25",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
94
apps/web/src/app-effects.mobile.ts
Normal file
94
apps/web/src/app-effects.mobile.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -38,8 +38,5 @@ export const AppEvents = {
|
||||
|
||||
changeNoteTitle: "changeNoteTitle",
|
||||
|
||||
revealItemInList: "list:revealItem",
|
||||
|
||||
toggleSideMenu: "app:openSideMenu",
|
||||
toggleEditor: "app:toggleEditor"
|
||||
revealItemInList: "list:revealItem"
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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?.(
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export async function startApp() {
|
||||
console.error(e);
|
||||
root.render(
|
||||
<>
|
||||
<TitleBar isUnderlay={false} />
|
||||
<TitleBar />
|
||||
<ErrorComponent
|
||||
error={e}
|
||||
resetErrorBoundary={() => window.location.reload()}
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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!
|
||||
@@ -269,7 +269,6 @@ const Tiptap = ({
|
||||
? []
|
||||
: getTableOfContents(containerRef.current);
|
||||
},
|
||||
scrollTop: () => containerRef.current?.scrollTop || 0,
|
||||
scrollTo: (top) => {
|
||||
containerRef.current?.scrollTo({ top, behavior: "auto" });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
769
packages/editor/package-lock.json
generated
769
packages/editor/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
522
packages/editor/patches/@tiptap+core+2.6.6.patch
Normal file
522
packages/editor/patches/@tiptap+core+2.6.6.patch
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>"`;
|
||||
|
||||
@@ -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
@@ -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`,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user