mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 19:19:34 +02:00
Compare commits
1 Commits
fix-back-n
...
fix-footer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fec64d2eb |
@@ -32,7 +32,6 @@ import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak } from "../utils";
|
||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||
import { rm } from "fs/promises";
|
||||
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
@@ -56,15 +55,6 @@ export const osIntegrationRouter = t.router({
|
||||
config.zoomFactor = factor;
|
||||
}),
|
||||
|
||||
customDns: t.procedure.query(() => config.customDns),
|
||||
setCustomDns: t.procedure
|
||||
.input(z.boolean())
|
||||
.mutation(({ input: customDns }) => {
|
||||
if (customDns) enableCustomDns();
|
||||
else disableCustomDns();
|
||||
config.customDns = customDns;
|
||||
}),
|
||||
|
||||
proxyRules: t.procedure.query(() => config.proxyRules),
|
||||
setProxyRules: t.procedure
|
||||
.input(z.string().optional())
|
||||
|
||||
@@ -35,7 +35,6 @@ import path from "path";
|
||||
import { bringToFront } from "./utils/bring-to-front";
|
||||
import { bridge } from "./api/bridge";
|
||||
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
||||
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
@@ -152,8 +151,14 @@ async function createWindow() {
|
||||
app.once("ready", async () => {
|
||||
console.info("App ready. Opening window.");
|
||||
|
||||
if (config.customDns) enableCustomDns();
|
||||
else disableCustomDns();
|
||||
app.configureHostResolver({
|
||||
secureDnsServers: [
|
||||
"https://mozilla.cloudflare-dns.com/dns-query",
|
||||
"https://dns.quad9.net/dns-query"
|
||||
],
|
||||
enableBuiltInResolver: true,
|
||||
secureDnsMode: "automatic"
|
||||
});
|
||||
|
||||
if (!isDevelopment()) registerProtocol();
|
||||
await createWindow();
|
||||
|
||||
@@ -43,7 +43,6 @@ export const config = {
|
||||
theme: nativeTheme.themeSource,
|
||||
automaticUpdates: true,
|
||||
proxyRules: "",
|
||||
customDns: true,
|
||||
|
||||
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
|
||||
windowControlsIconColor:
|
||||
|
||||
@@ -1,37 +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 { app } from "electron";
|
||||
|
||||
export function enableCustomDns() {
|
||||
app.configureHostResolver({
|
||||
secureDnsServers: [
|
||||
"https://mozilla.cloudflare-dns.com/dns-query",
|
||||
"https://dns.quad9.net/dns-query"
|
||||
],
|
||||
enableBuiltInResolver: true
|
||||
});
|
||||
}
|
||||
|
||||
export function disableCustomDns() {
|
||||
app.configureHostResolver({
|
||||
secureDnsServers: [],
|
||||
enableBuiltInResolver: true
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { View } from "react-native";
|
||||
import React from "react";
|
||||
export const Footer = () => {
|
||||
return <View style={{ height: 150 }} />;
|
||||
export const Footer = ({ height = 150 }) => {
|
||||
return <View style={{ height: height }} />;
|
||||
};
|
||||
|
||||
@@ -192,7 +192,9 @@ export default function List(props: ListProps) {
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
ListFooterComponent={<Footer />}
|
||||
ListFooterComponent={
|
||||
<Footer height={props.renderedInRoute === "Notebook" ? 300 : 150} />
|
||||
}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
{props.CustomLisHeader ? (
|
||||
|
||||
@@ -219,7 +219,7 @@ export default function LinkNote(props: {
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
minHeight: "100%",
|
||||
minHeight: 400,
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
>
|
||||
@@ -311,7 +311,6 @@ export default function LinkNote(props: {
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
windowSize={3}
|
||||
keyExtractor={(item) => item.id}
|
||||
data={nodes}
|
||||
@@ -325,7 +324,6 @@ export default function LinkNote(props: {
|
||||
onSelectNote={onSelectNote}
|
||||
/>
|
||||
)}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
|
||||
@@ -99,7 +99,6 @@ export default function Migrate() {
|
||||
const { error } = await BackupService.run(false, "local");
|
||||
if (error) {
|
||||
ToastManager.error(error, "Backup failed");
|
||||
reportError(error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,11 +21,15 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback } from "react";
|
||||
import { View } from "react-native";
|
||||
import { DraxProvider, DraxScrollView } from "react-native-drax";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { db } from "../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useMenuStore } from "../../stores/use-menu-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { SUBSCRIPTION_STATUS } from "../../utils/constants";
|
||||
import { eOpenPremiumDialog } from "../../utils/events";
|
||||
@@ -55,6 +59,26 @@ export const SideMenu = React.memo(
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const BottomItemsList = [
|
||||
{
|
||||
name: isDark ? "Day" : "Night",
|
||||
icon: "theme-light-dark",
|
||||
func: () => {
|
||||
useThemeStore.getState().setColorScheme();
|
||||
},
|
||||
switch: true,
|
||||
on: !!isDark,
|
||||
close: false
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
icon: "cog-outline",
|
||||
close: true,
|
||||
func: () => {
|
||||
Navigation.navigate("Settings");
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const pro = {
|
||||
name: "Notesnook Pro",
|
||||
@@ -97,7 +121,7 @@ export const SideMenu = React.memo(
|
||||
<PinnedSection />
|
||||
</>
|
||||
),
|
||||
[order, hiddensItems]
|
||||
[]
|
||||
);
|
||||
|
||||
return !isAppLoading && introCompleted ? (
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface TabsRef {
|
||||
lock: () => boolean;
|
||||
openDrawer: (animated?: boolean) => void;
|
||||
closeDrawer: (animated?: boolean) => void;
|
||||
page: () => number;
|
||||
page: number;
|
||||
setScrollEnabled: () => true;
|
||||
isDrawerOpen: () => boolean;
|
||||
node: RefObject<Animated.View>;
|
||||
@@ -239,7 +239,7 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
onDrawerStateChange(false);
|
||||
isDrawerOpen.value = false;
|
||||
},
|
||||
page: () => currentTab.value,
|
||||
page: currentTab.value,
|
||||
setScrollEnabled: () => true,
|
||||
node: node
|
||||
}),
|
||||
|
||||
@@ -65,7 +65,6 @@ import {
|
||||
eClearEditor,
|
||||
eCloseFullscreenEditor,
|
||||
eOnEnterEditor,
|
||||
eOnExitEditor,
|
||||
eOnLoadNote,
|
||||
eOpenFullscreenEditor,
|
||||
eUnlockNote
|
||||
@@ -512,7 +511,6 @@ const onChangeTab = async (event) => {
|
||||
editorState().movedAway = false;
|
||||
editorState().isFocused = true;
|
||||
activateKeepAwake();
|
||||
eSendEvent(eOnEnterEditor);
|
||||
|
||||
if (!useTabStore.getState().getCurrentNoteId()) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
@@ -524,13 +522,14 @@ const onChangeTab = async (event) => {
|
||||
) {
|
||||
eSendEvent(eUnlockNote);
|
||||
}
|
||||
eSendEvent(eOnEnterEditor);
|
||||
}
|
||||
} else {
|
||||
if (event.from === 2) {
|
||||
deactivateKeepAwake();
|
||||
editorState().movedAway = true;
|
||||
editorState().isFocused = false;
|
||||
eSendEvent(eOnExitEditor);
|
||||
eSendEvent(eClearEditor, "removeHandler");
|
||||
|
||||
// Lock all tabs with locked notes...
|
||||
for (const tab of useTabStore.getState().tabs) {
|
||||
|
||||
@@ -51,11 +51,7 @@ import { EditorProps, useEditorType } from "./tiptap/types";
|
||||
import { useEditor } from "./tiptap/use-editor";
|
||||
import { useEditorEvents } from "./tiptap/use-editor-events";
|
||||
import { syncTabs, useTabStore } from "./tiptap/use-tab-store";
|
||||
import {
|
||||
editorController,
|
||||
editorState,
|
||||
openInternalLink
|
||||
} from "./tiptap/utils";
|
||||
import { editorController, editorState } from "./tiptap/utils";
|
||||
|
||||
const style: ViewStyle = {
|
||||
height: "100%",
|
||||
@@ -65,10 +61,7 @@ const style: ViewStyle = {
|
||||
backgroundColor: "transparent"
|
||||
};
|
||||
const onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest) => {
|
||||
if (request.url.includes("nn://")) {
|
||||
openInternalLink(request.url);
|
||||
return false;
|
||||
} else if (request.url.includes("https")) {
|
||||
if (request.url.includes("https")) {
|
||||
if (Platform.OS === "ios" && !request.isTopFrame) return true;
|
||||
openLinkInBrowser(request.url);
|
||||
return false;
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/* eslint-disable no-case-declarations */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { parseInternalLink } from "@notesnook/core";
|
||||
import { ItemReference } from "@notesnook/core/dist/types";
|
||||
import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index";
|
||||
import { getDefaultPresets } from "@notesnook/editor/dist/toolbar/tool-definitions";
|
||||
@@ -26,6 +27,7 @@ import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
BackHandler,
|
||||
InteractionManager,
|
||||
Keyboard,
|
||||
KeyboardEventListener,
|
||||
NativeEventSubscription,
|
||||
@@ -35,7 +37,6 @@ import { WebViewMessageEvent } from "react-native-webview";
|
||||
import { DatabaseLogger, db } from "../../../common/database";
|
||||
import downloadAttachment from "../../../common/filesystem/download-attachment";
|
||||
import EditorTabs from "../../../components/sheets/editor-tabs";
|
||||
import { Issue } from "../../../components/sheets/github/issue";
|
||||
import LinkNote from "../../../components/sheets/link-note";
|
||||
import ManageTagsSheet from "../../../components/sheets/manage-tags";
|
||||
import { RelationsList } from "../../../components/sheets/relations-list";
|
||||
@@ -60,7 +61,6 @@ import {
|
||||
eCloseFullscreenEditor,
|
||||
eEditorTabFocused,
|
||||
eOnEnterEditor,
|
||||
eOnExitEditor,
|
||||
eOnLoadNote,
|
||||
eOpenFullscreenEditor,
|
||||
eOpenLoginDialog,
|
||||
@@ -75,8 +75,8 @@ import { useDragState } from "../../settings/editor/state";
|
||||
import { EventTypes } from "./editor-events";
|
||||
import { EditorMessage, EditorProps, useEditorType } from "./types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { EditorEvents, editorState, openInternalLink } from "./utils";
|
||||
|
||||
import { EditorEvents, editorState } from "./utils";
|
||||
import { Issue } from "../../../components/sheets/github/issue";
|
||||
|
||||
const publishNote = async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
@@ -270,20 +270,21 @@ export const useEditorEvents = (
|
||||
}, [editor, deviceMode, fullscreen]);
|
||||
|
||||
const onHardwareBackPress = useCallback(() => {
|
||||
console.log(tabBarRef.current?.page());
|
||||
if (tabBarRef.current?.page() === 2) {
|
||||
if (tabBarRef.current?.page === 2) {
|
||||
onBackPress();
|
||||
return true;
|
||||
}
|
||||
}, [onBackPress]);
|
||||
|
||||
const onEnterEditor = useCallback(async () => {
|
||||
if (!DDS.isTab) {
|
||||
handleBack.current = BackHandler.addEventListener(
|
||||
"hardwareBackPress",
|
||||
onHardwareBackPress
|
||||
);
|
||||
}
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
if (!DDS.isTab) {
|
||||
handleBack.current = BackHandler.addEventListener(
|
||||
"hardwareBackPress",
|
||||
onHardwareBackPress
|
||||
);
|
||||
}
|
||||
});
|
||||
}, [onHardwareBackPress]);
|
||||
|
||||
const onClearEditorSessionRequest = useCallback(
|
||||
@@ -330,14 +331,7 @@ export const useEditorEvents = (
|
||||
}, [fullscreen, onHardwareBackPress]);
|
||||
|
||||
useEffect(() => {
|
||||
const onExitEditor = () => {
|
||||
if (handleBack.current) {
|
||||
handleBack.current.remove();
|
||||
}
|
||||
};
|
||||
|
||||
eSubscribeEvent(eOnEnterEditor, onEnterEditor);
|
||||
eSubscribeEvent(eOnExitEditor, onExitEditor);
|
||||
eSubscribeEvent(
|
||||
eClearEditor + editor.editorId,
|
||||
onClearEditorSessionRequest
|
||||
@@ -345,7 +339,6 @@ export const useEditorEvents = (
|
||||
return () => {
|
||||
eUnSubscribeEvent(eClearEditor, onClearEditorSessionRequest);
|
||||
eUnSubscribeEvent(eOnEnterEditor, onEnterEditor);
|
||||
eUnSubscribeEvent(eOnExitEditor, onExitEditor);
|
||||
};
|
||||
}, [editor.editorId, onClearEditorSessionRequest, onEnterEditor]);
|
||||
|
||||
@@ -526,7 +519,27 @@ export const useEditorEvents = (
|
||||
break;
|
||||
case EventTypes.link:
|
||||
if (editorMessage.value.startsWith("nn://")) {
|
||||
openInternalLink(editorMessage.value);
|
||||
const data = parseInternalLink(editorMessage.value);
|
||||
if (!data?.id) break;
|
||||
if (
|
||||
data.id ===
|
||||
useTabStore
|
||||
.getState()
|
||||
.getNoteIdForTab(useTabStore.getState().currentTab)
|
||||
) {
|
||||
if (data.params?.blockId) {
|
||||
setTimeout(() => {
|
||||
if (!data.params?.blockId) return;
|
||||
editor.commands.scrollIntoViewById(data.params.blockId);
|
||||
}, 150);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(data?.id),
|
||||
blockId: data.params?.blockId
|
||||
});
|
||||
console.log(
|
||||
"Opening note from internal link:",
|
||||
editorMessage.value
|
||||
@@ -649,8 +662,10 @@ export const useEditorEvents = (
|
||||
.updateTab(useTabStore.getState().currentTab, {
|
||||
readonly: false
|
||||
});
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
ToastManager.show({
|
||||
heading: "Readonly mode disabled.",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -837,7 +837,7 @@ export const useEditor = (
|
||||
if (!noteId) {
|
||||
overlay(false);
|
||||
loadNote({ newNote: true });
|
||||
if (tabBarRef.current?.page() === 1) {
|
||||
if (tabBarRef.current?.page === 1) {
|
||||
state.current.currentlyEditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,15 +22,10 @@ import { TextInput } from "react-native";
|
||||
import WebView from "react-native-webview";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import { AppState, EditorState, useEditorType } from "./types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { parseInternalLink } from "@notesnook/core";
|
||||
import { eOnLoadNote } from "../../../utils/events";
|
||||
import { db } from "../../../common/database";
|
||||
export const textInput = createRef<TextInput>();
|
||||
export const editorController =
|
||||
createRef<useEditorType>() as MutableRefObject<useEditorType>;
|
||||
@@ -181,27 +176,3 @@ export function clearAppState() {
|
||||
appState = undefined;
|
||||
MMKV.removeItem("appState");
|
||||
}
|
||||
|
||||
export async function openInternalLink(url: string) {
|
||||
const data = parseInternalLink(url);
|
||||
if (!data?.id) return false;
|
||||
if (
|
||||
data.id ===
|
||||
useTabStore.getState().getNoteIdForTab(useTabStore.getState().currentTab)
|
||||
) {
|
||||
if (data.params?.blockId) {
|
||||
setTimeout(() => {
|
||||
if (!data.params?.blockId) return;
|
||||
editorController.current.commands.scrollIntoViewById(
|
||||
data.params.blockId
|
||||
);
|
||||
}, 150);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(data?.id),
|
||||
blockId: data.params?.blockId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import { eCloseSheet } from "../utils/events";
|
||||
import { sleep } from "../utils/time";
|
||||
import { ToastManager, eSendEvent, presentSheet } from "./event-manager";
|
||||
import SettingsService from "./settings";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
|
||||
const MS_DAY = 86400000;
|
||||
const MS_WEEK = MS_DAY * 7;
|
||||
@@ -192,10 +191,9 @@ async function run(progress = false, context) {
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
try {
|
||||
const user = await db.user.getUser();
|
||||
for await (const file of db.backup.export(
|
||||
"mobile",
|
||||
SettingsService.get().encryptedBackup && user
|
||||
SettingsService.get().encryptedBackup
|
||||
)) {
|
||||
console.log("Writing backup chunk of size...", file?.data?.length);
|
||||
await RNFetchBlob.fs.writeFile(
|
||||
|
||||
@@ -72,7 +72,7 @@ export const useMenuStore = create<MenuStore>((set, get) => ({
|
||||
section as SideBarSection
|
||||
);
|
||||
hiddenItems[section as SideBarHideableSection] =
|
||||
db.settings.getSideBarHiddenItems(section as SideBarHideableSection);
|
||||
db.settings.getSideBarHiddenItems("colors");
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -80,7 +80,6 @@ export const useMenuStore = create<MenuStore>((set, get) => ({
|
||||
JSON.stringify(get().hiddenItems || {}) !==
|
||||
JSON.stringify(hiddenItems || {})
|
||||
) {
|
||||
console.log(order, hiddenItems);
|
||||
set({
|
||||
order: order,
|
||||
hiddenItems: hiddenItems
|
||||
|
||||
@@ -173,4 +173,3 @@ export const eUnlockWithBiometrics = "618";
|
||||
export const eUnlockWithPassword = "619";
|
||||
export const eUpdateNoteInEditor = "620";
|
||||
export const eOnEnterEditor = "621";
|
||||
export const eOnExitEditor = "622";
|
||||
|
||||
@@ -142,13 +142,8 @@ function DesktopAppContents({
|
||||
}, [show]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocusMode) {
|
||||
const middlePaneSize = middlePane.current?.getSize() || 20;
|
||||
navPane.current?.collapse();
|
||||
// the middle pane has to be resized because collapsing the nav
|
||||
// pane increases the middle pane's size every time.
|
||||
middlePane.current?.resize(middlePaneSize);
|
||||
} else navPane.current?.expand();
|
||||
if (isFocusMode) navPane.current?.collapse();
|
||||
else navPane.current?.expand();
|
||||
}, [isFocusMode]);
|
||||
|
||||
return (
|
||||
@@ -164,10 +159,9 @@ function DesktopAppContents({
|
||||
ref={navPane}
|
||||
className="nav-pane"
|
||||
defaultSize={10}
|
||||
minSize={3.5}
|
||||
onResize={(size) => setIsNarrow(size <= 5)}
|
||||
minSize={3}
|
||||
onResize={(size) => setIsNarrow(size <= 3)}
|
||||
collapsible
|
||||
collapsedSize={3.5}
|
||||
>
|
||||
<NavigationMenu
|
||||
toggleNavigationContainer={(state) => {
|
||||
@@ -198,7 +192,7 @@ function DesktopAppContents({
|
||||
</ScopedThemeProvider>
|
||||
</Panel>
|
||||
<PanelResizeHandle className="panel-resize-handle" />
|
||||
<Panel className="editor-pane" defaultSize={70}>
|
||||
<Panel className="editor-pane">
|
||||
<Flex
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
||||
@@ -77,18 +77,18 @@ export async function introduceFeatures() {
|
||||
|
||||
export const DEFAULT_CONTEXT = { colors: [], tags: [], notebook: {} };
|
||||
|
||||
export async function createBackup(rescueMode = false) {
|
||||
export async function createBackup() {
|
||||
const { isLoggedIn } = useUserStore.getState();
|
||||
const { encryptBackups, toggleEncryptBackups } = useSettingStore.getState();
|
||||
if (!isLoggedIn && encryptBackups) toggleEncryptBackups();
|
||||
|
||||
const verified = rescueMode || encryptBackups || (await verifyAccount());
|
||||
const verified = encryptBackups || (await verifyAccount());
|
||||
if (!verified) {
|
||||
showToast("error", "Could not create a backup: user verification failed.");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptedBackups = !rescueMode && isLoggedIn && encryptBackups;
|
||||
const encryptedBackups = isLoggedIn && encryptBackups;
|
||||
|
||||
const filename = sanitizeFilename(
|
||||
`${formatDate(Date.now(), {
|
||||
@@ -139,9 +139,7 @@ export async function createBackup(rescueMode = false) {
|
||||
console.error(error);
|
||||
} else {
|
||||
showToast("success", `Backup saved at ${filePath}.`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function selectBackupFile() {
|
||||
|
||||
@@ -87,6 +87,7 @@ function saveContent(noteId: string, ignoreEdit: boolean, content: string) {
|
||||
const deferredSave = debounceWithId(saveContent, 100);
|
||||
|
||||
export default function TabsView() {
|
||||
const sessions = useEditorStore((store) => store.sessions);
|
||||
const documentPreview = useEditorStore((store) => store.documentPreview);
|
||||
const activeSessionId = useEditorStore((store) => store.activeSessionId);
|
||||
const arePropertiesVisible = useEditorStore(
|
||||
@@ -95,7 +96,6 @@ export default function TabsView() {
|
||||
const isTOCVisible = useEditorStore((store) => store.isTOCVisible);
|
||||
const [dropRef, overlayRef] = useDragOverlay();
|
||||
|
||||
const sessions = useEditorStore.getState().sessions;
|
||||
return (
|
||||
<>
|
||||
{IS_DESKTOP_APP ? (
|
||||
|
||||
@@ -46,7 +46,6 @@ import { Section } from "../properties";
|
||||
import { scrollIntoViewById } from "@notesnook/editor";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { useEditorManager } from "./manager";
|
||||
import { TITLE_BAR_HEIGHT } from "../title-bar";
|
||||
|
||||
type TableOfContentsProps = {
|
||||
sessionId: string;
|
||||
@@ -101,7 +100,7 @@ function TableOfContents(props: TableOfContentsProps) {
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: TITLE_BAR_HEIGHT,
|
||||
top: 0,
|
||||
zIndex: 999,
|
||||
height: "100%",
|
||||
width: "300px",
|
||||
|
||||
@@ -29,8 +29,6 @@ import {
|
||||
ZoomIn,
|
||||
ZoomOut
|
||||
} from "../icons";
|
||||
import { getPlatform } from "../../utils/platform";
|
||||
import { TITLE_BAR_HEIGHT } from "../title-bar";
|
||||
|
||||
const DEFAULT_ZOOM_STEP = 0.3;
|
||||
const DEFAULT_LARGE_ZOOM = 4;
|
||||
@@ -339,12 +337,7 @@ export class Lightbox extends React.Component<LightboxProps> {
|
||||
borderRadius: "0px 0px 0px 5px",
|
||||
overflow: "hidden",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
height: IS_DESKTOP_APP ? TITLE_BAR_HEIGHT : "auto",
|
||||
pr:
|
||||
IS_DESKTOP_APP && getPlatform() !== "darwin"
|
||||
? "calc(100vw - env(titlebar-area-width))"
|
||||
: 0
|
||||
justifyContent: "flex-end"
|
||||
}}
|
||||
>
|
||||
{tools.map((tool) => (
|
||||
@@ -356,7 +349,6 @@ export class Lightbox extends React.Component<LightboxProps> {
|
||||
bg="transparent"
|
||||
title={tool.title}
|
||||
sx={{
|
||||
height: "100%",
|
||||
borderRadius: 0,
|
||||
display: [
|
||||
tool.hideOnMobile ? "none" : "flex",
|
||||
|
||||
@@ -106,7 +106,7 @@ function NavigationItem(
|
||||
<Button
|
||||
data-test-id={`navigation-item`}
|
||||
sx={{
|
||||
px: isTablet ? 1 : 2,
|
||||
px: 2,
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: isTablet ? "center" : "flex-start",
|
||||
@@ -124,10 +124,7 @@ function NavigationItem(
|
||||
}}
|
||||
>
|
||||
{image ? (
|
||||
<Image
|
||||
src={image}
|
||||
sx={{ borderRadius: 50, size: 20, minWidth: 20, flexShrink: 0 }}
|
||||
/>
|
||||
<Image src={image} sx={{ borderRadius: 50, size: 20 }} />
|
||||
) : Icon ? (
|
||||
<Icon
|
||||
size={isTablet ? 16 : 15}
|
||||
|
||||
@@ -282,8 +282,7 @@ export default React.memo(Note, function (prevProps, nextProps) {
|
||||
prevProps.notebooks?.dateEdited === nextProps.notebooks?.dateEdited &&
|
||||
prevProps.tags?.dateEdited === nextProps.tags?.dateEdited &&
|
||||
prevProps.reminder?.dateModified === nextProps.reminder?.dateModified &&
|
||||
prevProps.attachments?.failed === nextProps.attachments?.failed &&
|
||||
prevProps.attachments?.total === nextProps.attachments?.total &&
|
||||
prevProps.attachments === nextProps.attachments &&
|
||||
prevProps.locked === nextProps.locked
|
||||
);
|
||||
});
|
||||
|
||||
@@ -61,7 +61,6 @@ import {
|
||||
} from "@notesnook/core";
|
||||
import { VirtualizedTable } from "../virtualized-table";
|
||||
import { TextSlice } from "@notesnook/core/dist/utils/content-block";
|
||||
import { TITLE_BAR_HEIGHT } from "../title-bar";
|
||||
|
||||
const tools = [
|
||||
{ key: "pin", property: "pinned", icon: Pin, label: "Pin" },
|
||||
@@ -130,7 +129,7 @@ function EditorProperties(props: EditorPropertiesProps) {
|
||||
sx={{
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
top: TITLE_BAR_HEIGHT,
|
||||
top: 0,
|
||||
right: 0,
|
||||
zIndex: 999,
|
||||
height: "100%",
|
||||
|
||||
@@ -56,7 +56,6 @@ function Toggle(props: ToggleProps) {
|
||||
<Switch
|
||||
sx={{ m: 0, bg: isOn ? "accent" : "icon-secondary" }}
|
||||
checked={isOn}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
} from "../icons";
|
||||
import { BaseThemeProvider } from "../theme-provider";
|
||||
|
||||
export const TITLE_BAR_HEIGHT = IS_DESKTOP_APP ? 37.8 : 0;
|
||||
export function TitleBar() {
|
||||
const { isMaximized, isFullscreen, hasNativeWindowControls } =
|
||||
useWindowControls();
|
||||
@@ -66,7 +65,7 @@ export function TitleBar() {
|
||||
scope="titleBar"
|
||||
sx={{
|
||||
background: "background",
|
||||
height: TITLE_BAR_HEIGHT,
|
||||
height: 37.8,
|
||||
display: "flex",
|
||||
borderBottom: "1px solid var(--border)",
|
||||
...(!isFullscreen && hasNativeWindowControls
|
||||
|
||||
@@ -185,16 +185,9 @@ export default function NoteLinkingDialog(props: NoteLinkingDialogProps) {
|
||||
autoFocus
|
||||
placeholder="Search for a note to link to..."
|
||||
sx={{ mx: 0 }}
|
||||
onChange={async (e) => {
|
||||
const query = e.target.value.trim();
|
||||
setNotes(
|
||||
query
|
||||
? await db.lookup.notes(e.target.value).sorted()
|
||||
: await db.notes.all.sorted(
|
||||
db.settings.getGroupOptions("home")
|
||||
)
|
||||
);
|
||||
}}
|
||||
onChange={async (e) =>
|
||||
setNotes(await db.lookup.notes(e.target.value).sorted())
|
||||
}
|
||||
/>
|
||||
{notes && (
|
||||
<ScrollContainer>
|
||||
|
||||
@@ -44,8 +44,7 @@ export const AppearanceSettings: SettingsGroup[] = [
|
||||
type: "input",
|
||||
inputType: "number",
|
||||
min: 0.5,
|
||||
max: 3.0,
|
||||
step: 0.1,
|
||||
max: 2.0,
|
||||
defaultValue: () => useSettingStore.getState().zoomFactor,
|
||||
onChange: (value) => useSettingStore.getState().setZoomFactor(value)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const AuthenticationSettings: SettingsGroup[] = [
|
||||
title: "Change password",
|
||||
variant: "secondary",
|
||||
action: async () => {
|
||||
if (!(await createBackup())) return;
|
||||
await createBackup();
|
||||
const result = await showPasswordDialog({
|
||||
title: "Change account password",
|
||||
message: `All your data will be re-encrypted and synced with the new password.
|
||||
|
||||
@@ -508,7 +508,6 @@ function SettingItem(props: { item: Setting }) {
|
||||
type={"number"}
|
||||
min={component.min}
|
||||
max={component.max}
|
||||
step={component.step}
|
||||
defaultValue={component.defaultValue()}
|
||||
sx={{ width: 80, mr: 1 }}
|
||||
onChange={debounce((e) => {
|
||||
|
||||
@@ -111,26 +111,6 @@ What data is collected & when?`,
|
||||
section: "privacy",
|
||||
header: "Advanced",
|
||||
settings: [
|
||||
{
|
||||
key: "custom-dns",
|
||||
title: "Use custom DNS",
|
||||
description: `Notesnook uses the following DNS providers:
|
||||
|
||||
1. Cloudflare DNS
|
||||
2. Quad9
|
||||
|
||||
This can sometimes bypass local ISP blockages on Notesnook traffic. Disable this if you want the app to use system's DNS settings.`,
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe((s) => s.customDns, listener),
|
||||
isHidden: () => !IS_DESKTOP_APP,
|
||||
components: [
|
||||
{
|
||||
type: "toggle",
|
||||
isToggled: () => useSettingStore.getState().customDns,
|
||||
toggle: () => useSettingStore.getState().toggleCustomDns()
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "custom-cors",
|
||||
title: "Custom CORS proxy",
|
||||
|
||||
@@ -134,7 +134,6 @@ export type NumberInputSettingComponent = BaseSettingComponent<"input"> & {
|
||||
inputType: "number";
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
defaultValue: () => number;
|
||||
onChange: (value: number) => void;
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ async function renderApp() {
|
||||
const { useKeyStore } = await import("./interfaces/key-store");
|
||||
await useKeyStore.getState().init();
|
||||
|
||||
// if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
|
||||
if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
|
||||
|
||||
const { default: Component } = await component();
|
||||
const { default: AppLock } = await import("./views/app-lock");
|
||||
|
||||
@@ -438,11 +438,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
if (index === -1) return;
|
||||
const session = state.sessions[index] as SessionTypeMap[T[number]];
|
||||
if (typeof partial === "function") partial(session);
|
||||
else {
|
||||
for (const key in partial) {
|
||||
session[key] = partial[key] as any;
|
||||
}
|
||||
}
|
||||
else state.sessions[index] = { ...session, ...partial };
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
|
||||
zoomFactor = 1.0;
|
||||
privacyMode = false;
|
||||
customDns = true;
|
||||
hideNoteTitle = Config.get("hideNoteTitle", false);
|
||||
telemetry = isTelemetryEnabled();
|
||||
dateFormat = "DD-MM-YYYY";
|
||||
@@ -68,7 +67,6 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
desktopIntegrationSettings:
|
||||
await desktop?.integration.desktopIntegration.query(),
|
||||
privacyMode: await desktop?.integration.privacyMode.query(),
|
||||
customDns: await desktop?.integration.customDns.query(),
|
||||
zoomFactor: await desktop?.integration.zoomFactor.query(),
|
||||
autoUpdates: await desktop?.updater.autoUpdates.query(),
|
||||
proxyRules: await desktop?.integration.proxyRules.query()
|
||||
@@ -179,12 +177,6 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
await desktop?.integration.setPrivacyMode.mutate({ enabled: !privacyMode });
|
||||
};
|
||||
|
||||
toggleCustomDns = async () => {
|
||||
const customDns = this.get().customDns;
|
||||
this.set({ customDns: !customDns });
|
||||
await desktop?.integration.setCustomDns.mutate(!customDns);
|
||||
};
|
||||
|
||||
toggleHideTitle = async () => {
|
||||
const { hideNoteTitle } = this.get();
|
||||
this.set({ hideNoteTitle: !hideNoteTitle });
|
||||
|
||||
@@ -460,7 +460,7 @@ function BackupData(props: BaseRecoveryComponentProps<"backup">) {
|
||||
"Please wait while we create a backup file for you to download."
|
||||
}}
|
||||
onSubmit={async () => {
|
||||
await createBackup(true);
|
||||
await createBackup();
|
||||
navigate("new");
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,176 +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 { expect, test } from "vitest";
|
||||
import { TEST_NOTE, databaseTest, noteTest } from "./utils";
|
||||
|
||||
test("updating deleted content should not throw", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
const id = await db.notes.add({
|
||||
title: "New note"
|
||||
});
|
||||
|
||||
const contentId = await db.content.add({
|
||||
data: "helloworld",
|
||||
noteId: id
|
||||
});
|
||||
|
||||
await db.content.remove(contentId!);
|
||||
|
||||
await expect(
|
||||
db.content.collection.update(
|
||||
[contentId!],
|
||||
{ synced: true },
|
||||
{ sendEvent: false }
|
||||
)
|
||||
).resolves.toBeFalsy();
|
||||
}));
|
||||
|
||||
test("updating content should not break full text search", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
const id = await db.notes.add({
|
||||
title: "New note"
|
||||
});
|
||||
|
||||
const contentId = await db.content.add({
|
||||
data: "hello world",
|
||||
noteId: id
|
||||
});
|
||||
|
||||
await db.content.add({
|
||||
id: contentId,
|
||||
data: "i am amazing",
|
||||
noteId: id
|
||||
});
|
||||
|
||||
expect(await db.lookup.notes("amazing").ids()).toContain(id);
|
||||
expect(await db.lookup.notes("hello world").ids()).not.toContain(id);
|
||||
}));
|
||||
|
||||
test("updating note title should not break full text search", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
const id = await db.notes.add({
|
||||
title: "New note"
|
||||
});
|
||||
await db.notes.add({
|
||||
id,
|
||||
title: "What an amazing note!"
|
||||
});
|
||||
expect(await db.lookup.notes("amazing").ids()).toContain(id);
|
||||
expect(await db.lookup.notes("new note").ids()).not.toContain(id);
|
||||
}));
|
||||
|
||||
test("updating deleted note should not throw", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
const id = await db.notes.add({
|
||||
title: "New note"
|
||||
});
|
||||
await db.notes.remove(id);
|
||||
|
||||
await expect(
|
||||
db.notes.collection.update([id], { synced: true }, { sendEvent: false })
|
||||
).resolves.toBeFalsy();
|
||||
}));
|
||||
|
||||
test("overwriting unlocked content with locked content should update search index", () =>
|
||||
noteTest({
|
||||
content: {
|
||||
data: "hello world",
|
||||
type: "tiptap"
|
||||
}
|
||||
}).then(async ({ db, id }) => {
|
||||
await db.content.collection.upsert({
|
||||
id: "something",
|
||||
locked: false,
|
||||
data: "What is this?",
|
||||
noteId: id,
|
||||
dateCreated: Date.now(),
|
||||
dateEdited: Date.now(),
|
||||
synced: false,
|
||||
dateModified: Date.now()
|
||||
});
|
||||
|
||||
await db.content.collection.put([
|
||||
{
|
||||
id: "something",
|
||||
locked: true,
|
||||
data: {
|
||||
alg: "as",
|
||||
cipher: "s",
|
||||
format: "base64",
|
||||
iv: "",
|
||||
length: 20,
|
||||
salt: ""
|
||||
},
|
||||
noteId: id,
|
||||
dateCreated: Date.now(),
|
||||
dateEdited: Date.now(),
|
||||
synced: false,
|
||||
dateModified: Date.now()
|
||||
}
|
||||
]);
|
||||
|
||||
expect(await db.lookup.notes("what is this").ids()).not.toContain(id);
|
||||
}));
|
||||
|
||||
test("overwriting content with deleted content should update search index", () =>
|
||||
noteTest({
|
||||
content: {
|
||||
data: "hello world",
|
||||
type: "tiptap"
|
||||
}
|
||||
}).then(async ({ db, id }) => {
|
||||
await db.content.collection.upsert({
|
||||
id: "something",
|
||||
locked: false,
|
||||
data: "What is this?",
|
||||
noteId: id,
|
||||
dateCreated: Date.now(),
|
||||
dateEdited: Date.now(),
|
||||
synced: false,
|
||||
dateModified: Date.now()
|
||||
});
|
||||
|
||||
await db.content.collection.put([
|
||||
{
|
||||
id: "something",
|
||||
deleted: true,
|
||||
synced: true,
|
||||
dateModified: Date.now()
|
||||
}
|
||||
]);
|
||||
|
||||
expect(await db.lookup.notes("what is this").ids()).not.toContain(id);
|
||||
}));
|
||||
|
||||
test("overwriting note with deleted note should update search index", () =>
|
||||
noteTest({
|
||||
title: "I am title"
|
||||
}).then(async ({ db, id }) => {
|
||||
await db.notes.collection.put([
|
||||
{
|
||||
id,
|
||||
deleted: true,
|
||||
synced: true,
|
||||
dateModified: Date.now()
|
||||
}
|
||||
]);
|
||||
|
||||
expect(await db.lookup.notes("title").ids()).not.toContain(id);
|
||||
}));
|
||||
@@ -24,7 +24,6 @@ import { DatabaseSchema, RawDatabaseSchema } from "../database";
|
||||
import { AnyColumnWithTable, Kysely, sql } from "kysely";
|
||||
import { FilteredSelector } from "../database/sql-collection";
|
||||
import { VirtualizedGrouping } from "../utils/virtualized-grouping";
|
||||
import { logger } from "../logger";
|
||||
|
||||
type SearchResults<T> = {
|
||||
sorted: (limit?: number) => Promise<VirtualizedGrouping<T>>;
|
||||
@@ -45,6 +44,8 @@ export default class Lookup {
|
||||
if (query.length < 3) return [];
|
||||
|
||||
const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>;
|
||||
query = query.replace(/"/, '""');
|
||||
|
||||
const excludedIds = this.db.trash.cache.notes;
|
||||
const results = await db
|
||||
.selectFrom((eb) =>
|
||||
@@ -56,7 +57,7 @@ export default class Lookup {
|
||||
.$if(excludedIds.length > 0, (eb) =>
|
||||
eb.where("id", "not in", excludedIds)
|
||||
)
|
||||
.where("title", "match", query)
|
||||
.where("title", "match", `"${query}"`)
|
||||
.select(["id", sql<number>`rank * 10`.as("rank")])
|
||||
.unionAll((eb) =>
|
||||
eb
|
||||
@@ -67,7 +68,7 @@ export default class Lookup {
|
||||
.$if(excludedIds.length > 0, (eb) =>
|
||||
eb.where("id", "not in", excludedIds)
|
||||
)
|
||||
.where("data", "match", query)
|
||||
.where("data", "match", `"${query}"`)
|
||||
.select(["noteId as id", "rank"])
|
||||
.$castTo<{ id: string; rank: number }>()
|
||||
)
|
||||
@@ -84,11 +85,7 @@ export default class Lookup {
|
||||
"in",
|
||||
(notes || this.db.notes.all).filter.select("id")
|
||||
)
|
||||
.execute()
|
||||
.catch((e) => {
|
||||
logger.error(e, `Error while searching`, { query });
|
||||
return [];
|
||||
});
|
||||
.execute();
|
||||
return results.map((r) => r.id);
|
||||
}, notes || this.db.notes.all);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
Kysely,
|
||||
SelectQueryBuilder,
|
||||
SqlBool,
|
||||
Transaction,
|
||||
sql
|
||||
} from "kysely";
|
||||
import { VirtualizedGrouping } from "../utils/virtualized-grouping";
|
||||
@@ -337,10 +338,8 @@ export class SQLCollection<
|
||||
}
|
||||
|
||||
export class FilteredSelector<T extends Item> {
|
||||
private _fields: (
|
||||
| AnyColumn<DatabaseSchema, keyof DatabaseSchema>
|
||||
| AnyColumnWithTable<DatabaseSchema, keyof DatabaseSchema>
|
||||
)[] = [];
|
||||
private _fields: AnyColumnWithTable<DatabaseSchema, keyof DatabaseSchema>[] =
|
||||
[];
|
||||
filter: SelectQueryBuilder<DatabaseSchema, keyof DatabaseSchema, unknown>;
|
||||
private _limit = 0;
|
||||
constructor(
|
||||
@@ -525,11 +524,6 @@ export class FilteredSelector<T extends Item> {
|
||||
|
||||
async *[Symbol.asyncIterator]() {
|
||||
let lastRow: any | null = null;
|
||||
const fields = this._fields.slice();
|
||||
if (!fields.find((f) => f.includes(".dateCreated")))
|
||||
fields.push("dateCreated");
|
||||
if (!fields.find((f) => f.includes(".id"))) fields.push("id");
|
||||
|
||||
while (true) {
|
||||
const rows = await this.filter
|
||||
.orderBy("dateCreated asc")
|
||||
@@ -542,8 +536,8 @@ export class FilteredSelector<T extends Item> {
|
||||
)
|
||||
)
|
||||
.limit(this.batchSize)
|
||||
.$if(fields.length === 0, (eb) => eb.selectAll())
|
||||
.$if(fields.length > 0, (eb) => eb.select(fields))
|
||||
.$if(this._fields.length === 0, (eb) => eb.selectAll())
|
||||
.$if(this._fields.length > 0, (eb) => eb.select(this._fields))
|
||||
.execute();
|
||||
if (rows.length === 0) break;
|
||||
for (const row of rows) {
|
||||
|
||||
@@ -31,8 +31,8 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
|
||||
.addEvent("insert")
|
||||
.when((eb) =>
|
||||
eb.and([
|
||||
eb("new.deleted", "is not", true),
|
||||
eb("new.locked", "is not", true),
|
||||
eb.or([eb("new.deleted", "is", null), eb("new.deleted", "==", false)]),
|
||||
eb.or([eb("new.locked", "is", null), eb("new.locked", "==", false)]),
|
||||
eb("new.data", "is not", null)
|
||||
])
|
||||
)
|
||||
@@ -71,13 +71,6 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
|
||||
.onTable("content", "main")
|
||||
.after()
|
||||
.addEvent("update")
|
||||
.when((eb) =>
|
||||
eb.and([
|
||||
eb("old.deleted", "is not", true),
|
||||
eb("old.noteId", "is not", null),
|
||||
eb("old.data", "is not", null)
|
||||
])
|
||||
)
|
||||
.addQuery((c) =>
|
||||
c.insertInto("content_fts").values({
|
||||
content_fts: sql.lit("delete"),
|
||||
@@ -142,13 +135,7 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
|
||||
.ifNotExists()
|
||||
.onTable("notes", "main")
|
||||
.after()
|
||||
.addEvent("update")
|
||||
.when((eb) =>
|
||||
eb.and([
|
||||
eb("old.deleted", "is not", true),
|
||||
eb("old.title", "is not", null)
|
||||
])
|
||||
)
|
||||
.addEvent("update", ["title"])
|
||||
.addQuery((c) =>
|
||||
c.insertInto("notes_fts").values({
|
||||
notes_fts: sql.lit("delete"),
|
||||
|
||||
@@ -331,10 +331,7 @@ function Header({
|
||||
|
||||
<ControlledMenu
|
||||
align="end"
|
||||
anchorPoint={{
|
||||
x: window.innerWidth - 10,
|
||||
y: 70
|
||||
}}
|
||||
anchorRef={btnRef}
|
||||
state={isOpen ? "open" : "closed"}
|
||||
menuClassName={menuClassName}
|
||||
onClose={() => {
|
||||
|
||||
@@ -19,7 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import { EditorState, Plugin, PluginKey, Transaction } from "prosemirror-state";
|
||||
import {
|
||||
EditorState,
|
||||
Plugin,
|
||||
PluginKey,
|
||||
TextSelection,
|
||||
Transaction
|
||||
} from "prosemirror-state";
|
||||
import { SearchSettings } from "../../toolbar/stores/search-store";
|
||||
|
||||
type DispatchFn = (tr: Transaction) => void;
|
||||
@@ -56,10 +62,7 @@ export type SearchStorage = {
|
||||
|
||||
interface TextNodesWithPosition {
|
||||
text: string;
|
||||
startPos: number;
|
||||
endPos: number;
|
||||
start: number;
|
||||
end: number;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
const updateView = (state: EditorState, dispatch: DispatchFn) => {
|
||||
@@ -68,19 +71,15 @@ const updateView = (state: EditorState, dispatch: DispatchFn) => {
|
||||
dispatch(state.tr);
|
||||
};
|
||||
|
||||
const regex = (s: string, settings: SearchSettings): RegExp | undefined => {
|
||||
const regex = (s: string, settings: SearchSettings): RegExp => {
|
||||
const { enableRegex, matchCase, matchWholeWord } = settings;
|
||||
const boundary = matchWholeWord ? "\\b" : "";
|
||||
try {
|
||||
return RegExp(
|
||||
boundary +
|
||||
(enableRegex ? s : s.replace(/[/\\^$*+?.()|[\]]/g, "\\$&")) +
|
||||
boundary,
|
||||
matchCase ? "gum" : "guim"
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return RegExp(
|
||||
boundary +
|
||||
(enableRegex ? s : s.replace(/[/\\^$*+?.()|[\]]/g, "\\$&")) +
|
||||
boundary,
|
||||
matchCase ? "gu" : "gui"
|
||||
);
|
||||
};
|
||||
|
||||
function searchDocument(
|
||||
@@ -99,61 +98,41 @@ function searchDocument(
|
||||
const doc = tr.doc;
|
||||
const results: Result[] = [];
|
||||
|
||||
let index = -1;
|
||||
const textNodesWithPosition: TextNodesWithPosition[] = [];
|
||||
let index = 0;
|
||||
let textNodesWithPosition: TextNodesWithPosition[] = [];
|
||||
|
||||
let cursor = 0;
|
||||
doc?.descendants((node, pos) => {
|
||||
if (node.isText) {
|
||||
if (textNodesWithPosition[index]) {
|
||||
textNodesWithPosition[index].text += node.text;
|
||||
textNodesWithPosition[index].end = cursor + (node.text?.length || 0);
|
||||
textNodesWithPosition[index] = {
|
||||
text: textNodesWithPosition[index].text + node.text,
|
||||
pos: textNodesWithPosition[index].pos
|
||||
};
|
||||
} else {
|
||||
textNodesWithPosition[index] = {
|
||||
text: node.text || "",
|
||||
startPos: pos,
|
||||
endPos: pos + node.nodeSize,
|
||||
start: cursor,
|
||||
end: cursor + (node.text?.length || 0)
|
||||
pos
|
||||
};
|
||||
}
|
||||
cursor += node.text?.length || 0;
|
||||
} else if (node.isBlock) {
|
||||
const lastNode = textNodesWithPosition[index];
|
||||
if (lastNode) {
|
||||
lastNode.text += "\n";
|
||||
lastNode.end++;
|
||||
lastNode.endPos = pos;
|
||||
cursor++;
|
||||
}
|
||||
index++;
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
});
|
||||
textNodesWithPosition = textNodesWithPosition.filter(Boolean);
|
||||
|
||||
const text = textNodesWithPosition.map((c) => c.text).join("");
|
||||
for (const match of text.matchAll(searchTerm)) {
|
||||
const start = match.index;
|
||||
const end = match.index + match[0].length;
|
||||
for (const { text, pos } of textNodesWithPosition) {
|
||||
const matches = text.matchAll(searchTerm);
|
||||
|
||||
// Gets all matching nodes that have either the start or end of the
|
||||
// search term in them. This adds support for multi line regex searches.
|
||||
const nodes = textNodesWithPosition.filter((node) => {
|
||||
const nodeStart = node.start;
|
||||
const nodeEnd = node.end;
|
||||
return (
|
||||
(start >= nodeStart && start < nodeEnd) ||
|
||||
(end >= nodeStart && end < nodeEnd)
|
||||
);
|
||||
});
|
||||
for (const m of matches) {
|
||||
if (m[0] === "") break;
|
||||
|
||||
if (!nodes.length) continue;
|
||||
const endNode = nodes[nodes.length - 1];
|
||||
const startNode = nodes[0];
|
||||
results.push({
|
||||
// reposition our RegExp match index relative to the actual node.
|
||||
from: start + (startNode.startPos - startNode.start),
|
||||
to: end + (endNode.endPos - endNode.end)
|
||||
});
|
||||
if (m.index !== undefined) {
|
||||
results.push({
|
||||
from: pos + m.index,
|
||||
to: pos + m.index + m[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { from: selectedFrom, to: selectedTo } = tr.selection;
|
||||
@@ -320,8 +299,24 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
|
||||
const { from, to } = results[index];
|
||||
|
||||
tr.insertText(term, from, to);
|
||||
|
||||
if (index + 1 < results.length) {
|
||||
const { from, to } = results[index + 1];
|
||||
const nextResult = (results[index + 1] = {
|
||||
from: tr.mapping.map(from),
|
||||
to: tr.mapping.map(to)
|
||||
});
|
||||
|
||||
commands.focus();
|
||||
tr.setSelection(
|
||||
new TextSelection(
|
||||
tr.doc.resolve(nextResult.from),
|
||||
tr.doc.resolve(nextResult.to)
|
||||
)
|
||||
);
|
||||
}
|
||||
dispatch(tr);
|
||||
commands.moveToNextResult();
|
||||
results.splice(index, 1);
|
||||
return true;
|
||||
},
|
||||
replaceAll:
|
||||
|
||||
@@ -50,7 +50,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
matchWholeWord
|
||||
});
|
||||
},
|
||||
[editor.commands, matchCase, enableRegex, matchWholeWord]
|
||||
[matchCase, enableRegex, matchWholeWord]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -94,7 +94,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
ref={searchInputRef}
|
||||
autoFocus
|
||||
placeholder="Find"
|
||||
sx={{ p: 0, fontFamily: "monospace" }}
|
||||
sx={{ p: 0 }}
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
search(e.target.value);
|
||||
@@ -102,8 +102,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.shiftKey) editor.commands.moveToPreviousResult();
|
||||
else editor.commands.moveToNextResult();
|
||||
editor.commands.moveToNextResult();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -137,10 +136,9 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
title="Match case"
|
||||
id="matchCase"
|
||||
icon="caseSensitive"
|
||||
onClick={() => {
|
||||
useEditorSearchStore.setState({ matchCase: !matchCase });
|
||||
search(useEditorSearchStore.getState().searchTerm);
|
||||
}}
|
||||
onClick={() =>
|
||||
useEditorSearchStore.setState({ matchCase: !matchCase })
|
||||
}
|
||||
iconSize={"medium"}
|
||||
/>
|
||||
<ToolButton
|
||||
@@ -151,12 +149,11 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
title="Match whole word"
|
||||
id="matchWholeWord"
|
||||
icon="wholeWord"
|
||||
onClick={() => {
|
||||
onClick={() =>
|
||||
useEditorSearchStore.setState({
|
||||
matchWholeWord: !matchWholeWord
|
||||
});
|
||||
search(useEditorSearchStore.getState().searchTerm);
|
||||
}}
|
||||
})
|
||||
}
|
||||
iconSize={"medium"}
|
||||
/>
|
||||
<ToolButton
|
||||
@@ -167,12 +164,11 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
title="Enable regex"
|
||||
id="enableRegex"
|
||||
icon="regex"
|
||||
onClick={() => {
|
||||
onClick={() =>
|
||||
useEditorSearchStore.setState({
|
||||
enableRegex: !enableRegex
|
||||
});
|
||||
search(useEditorSearchStore.getState().searchTerm);
|
||||
}}
|
||||
})
|
||||
}
|
||||
iconSize={"medium"}
|
||||
/>
|
||||
</>
|
||||
@@ -194,7 +190,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
</Flex>
|
||||
{isReplacing && (
|
||||
<Input
|
||||
sx={{ mt: 1, p: "7px", fontFamily: "monospace" }}
|
||||
sx={{ mt: 1, p: "7px" }}
|
||||
placeholder="Replace"
|
||||
value={replaceTerm}
|
||||
onChange={(e) =>
|
||||
@@ -255,11 +251,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
title="Replace"
|
||||
id="replace"
|
||||
icon="replaceOne"
|
||||
onClick={() =>
|
||||
editor.commands.replace(
|
||||
useEditorSearchStore.getState().replaceTerm
|
||||
)
|
||||
}
|
||||
onClick={() => editor.commands.replace(replaceTerm)}
|
||||
sx={{ mr: 0 }}
|
||||
iconSize={18}
|
||||
/>
|
||||
@@ -268,11 +260,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
title="Replace all"
|
||||
id="replaceAll"
|
||||
icon="replaceAll"
|
||||
onClick={() =>
|
||||
editor.commands.replaceAll(
|
||||
useEditorSearchStore.getState().replaceTerm
|
||||
)
|
||||
}
|
||||
onClick={() => editor.commands.replaceAll(replaceTerm)}
|
||||
sx={{ mr: 0 }}
|
||||
iconSize={18}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.ProseMirror p.is-editor-empty::before {
|
||||
.ProseMirror p.is-empty:first-child::before {
|
||||
color: var(--placeholder);
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
|
||||
Reference in New Issue
Block a user