From 6ac09c5478524ecb912e2cdf818a009ef1847c56 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 27 Aug 2026 08:47:53 +0500 Subject: [PATCH] editor: drop block virtualization and fold paging into one extension --- .../mobile/app/screens/editor/tiptap/types.ts | 2 +- .../app/screens/settings/components.tsx | 2 - .../app/screens/settings/picker/pickers.tsx | 28 - .../app/screens/settings/settings-data.tsx | 3 +- apps/mobile/app/stores/use-setting-store.ts | 4 +- apps/web/src/components/editor/tiptap.tsx | 3 +- .../src/dialogs/settings/editor-settings.ts | 17 +- apps/web/src/stores/setting-store.ts | 12 +- .../editor-mobile/src/components/editor.tsx | 4 +- packages/editor-mobile/src/utils/index.ts | 2 +- .../paging/__tests__/decorations.test.ts | 142 +++++ .../__tests__/height-map.test.ts | 2 +- .../__tests__/paged-virtualization.test.ts | 12 +- .../__tests__/scroll-anchor.test.ts | 6 +- .../__tests__/viewport-window.test.ts | 239 ++++---- .../{virtualization => paging}/anchor.ts | 19 +- .../{virtualization => paging}/height-map.ts | 8 - .../editor/src/extensions/paging/index.ts | 62 ++- .../editor/src/extensions/paging/page-view.ts | 110 ++++ packages/editor/src/extensions/paging/page.ts | 25 +- .../editor/src/extensions/paging/parser.ts | 2 - .../src/extensions/paging/serializer.ts | 27 +- .../editor/src/extensions/paging/split.ts | 35 +- .../viewport-plugin.ts | 122 ++--- .../__tests__/decoration-cache.test.ts | 118 ---- .../__tests__/decoration-updates.test.ts | 182 ------- .../__tests__/first-view.test.ts | 112 ---- .../src/extensions/virtualization/index.ts | 131 ----- .../extensions/virtualization/node-views.ts | 267 --------- packages/editor/src/index.ts | 23 +- packages/intl/locale/en.po | 511 +++++++++--------- packages/intl/locale/pseudo-LOCALE.po | 511 +++++++++--------- packages/intl/src/strings.ts | 3 - 33 files changed, 1036 insertions(+), 1710 deletions(-) create mode 100644 packages/editor/src/extensions/paging/__tests__/decorations.test.ts rename packages/editor/src/extensions/{virtualization => paging}/__tests__/height-map.test.ts (99%) rename packages/editor/src/extensions/{virtualization => paging}/__tests__/scroll-anchor.test.ts (96%) rename packages/editor/src/extensions/{virtualization => paging}/__tests__/viewport-window.test.ts (70%) rename packages/editor/src/extensions/{virtualization => paging}/anchor.ts (86%) rename packages/editor/src/extensions/{virtualization => paging}/height-map.ts (94%) create mode 100644 packages/editor/src/extensions/paging/page-view.ts rename packages/editor/src/extensions/{virtualization => paging}/viewport-plugin.ts (78%) delete mode 100644 packages/editor/src/extensions/virtualization/__tests__/decoration-cache.test.ts delete mode 100644 packages/editor/src/extensions/virtualization/__tests__/decoration-updates.test.ts delete mode 100644 packages/editor/src/extensions/virtualization/__tests__/first-view.test.ts delete mode 100644 packages/editor/src/extensions/virtualization/index.ts delete mode 100644 packages/editor/src/extensions/virtualization/node-views.ts diff --git a/apps/mobile/app/screens/editor/tiptap/types.ts b/apps/mobile/app/screens/editor/tiptap/types.ts index 52a7a482d..584eee8f3 100644 --- a/apps/mobile/app/screens/editor/tiptap/types.ts +++ b/apps/mobile/app/screens/editor/tiptap/types.ts @@ -58,7 +58,7 @@ export type Settings = { timeFormat: string; fontScale: number; markdownShortcuts: boolean; - virtualization?: "off" | "blocks" | "pages"; + virtualization?: boolean; features: Record; loggedIn: boolean; defaultLineHeight: number; diff --git a/apps/mobile/app/screens/settings/components.tsx b/apps/mobile/app/screens/settings/components.tsx index c5e48e818..006205569 100644 --- a/apps/mobile/app/screens/settings/components.tsx +++ b/apps/mobile/app/screens/settings/components.tsx @@ -34,7 +34,6 @@ import { DateFormatPicker, DayFormatPicker, WeekFormatPicker, - EditorVirtualizationPicker, FontPicker, HomePicker, ImageCompressionPicker, @@ -65,7 +64,6 @@ export const components: { [name: string]: ReactElement } = { licenses: , "trash-interval-selector": , "font-selector": , - "editor-virtualization-selector": , "title-format": , "date-format-selector": , "time-format-selector": , diff --git a/apps/mobile/app/screens/settings/picker/pickers.tsx b/apps/mobile/app/screens/settings/picker/pickers.tsx index ced17a963..8dace1d24 100644 --- a/apps/mobile/app/screens/settings/picker/pickers.tsx +++ b/apps/mobile/app/screens/settings/picker/pickers.tsx @@ -95,34 +95,6 @@ export const HomePicker = createSettingsPicker({ isOptionAvailable: async () => true }); -type VirtualizationMode = NonNullable; - -const VIRTUALIZATION_MODES: VirtualizationMode[] = ["off", "blocks", "pages"]; - -export const EditorVirtualizationPicker = createSettingsPicker< - VirtualizationMode, - VirtualizationMode ->({ - getValue: () => - useSettingStore.getState().settings.editorVirtualization || "off", - updateValue: async (item) => { - SettingsService.set({ editorVirtualization: item }); - }, - formatValue: (item) => { - const mode = ( - typeof item === "object" ? "off" : item - ) as VirtualizationMode; - if (mode === "blocks") return strings.editorVirtualizationBlocks(); - if (mode === "pages") return strings.editorVirtualizationPages(); - return strings.editorVirtualizationOff(); - }, - getItemKey: (item) => item, - options: VIRTUALIZATION_MODES, - compareValue: (current, item) => current === item, - isFeatureAvailable: async () => true, - isOptionAvailable: async () => true -}); - export const SidebarTabPicker = createSettingsPicker({ getValue: () => useSettingStore.getState().settings.defaultSidebarTab, updateValue: async (item) => { diff --git a/apps/mobile/app/screens/settings/settings-data.tsx b/apps/mobile/app/screens/settings/settings-data.tsx index 1a97f8e21..f29e6342c 100644 --- a/apps/mobile/app/screens/settings/settings-data.tsx +++ b/apps/mobile/app/screens/settings/settings-data.tsx @@ -1027,8 +1027,7 @@ export const settingsGroups: SettingSection[] = [ id: "editor-virtualization", name: strings.editorVirtualization(), description: strings.editorVirtualizationDesc(), - type: "component", - component: "editor-virtualization-selector", + type: "switch", property: "editorVirtualization", icon: "page-next-outline" }, diff --git a/apps/mobile/app/stores/use-setting-store.ts b/apps/mobile/app/stores/use-setting-store.ts index a983826a1..32a86bc15 100644 --- a/apps/mobile/app/stores/use-setting-store.ts +++ b/apps/mobile/app/stores/use-setting-store.ts @@ -78,7 +78,7 @@ export type Settings = { sessionExpired: boolean; version: string | null; doubleSpacedLines?: boolean; - editorVirtualization?: "off" | "blocks" | "pages"; + editorVirtualization?: boolean; disableAutoSync?: boolean; disableSync?: boolean; reminderNotifications?: boolean; @@ -197,7 +197,7 @@ export const defaultSettings: SettingStore["settings"] = { sessionExpired: false, version: null, doubleSpacedLines: true, - editorVirtualization: "off", + editorVirtualization: false, reminderNotifications: true, defaultSnoozeTime: "5", corsProxy: "https://cors.notesnook.com", diff --git a/apps/web/src/components/editor/tiptap.tsx b/apps/web/src/components/editor/tiptap.tsx index 7eeafd7c4..d04806b84 100644 --- a/apps/web/src/components/editor/tiptap.tsx +++ b/apps/web/src/components/editor/tiptap.tsx @@ -43,7 +43,6 @@ import { restoreScrollAnchor, serializeDocumentHTML, toFlatPosition, - type VirtualizationMode, type Selection } from "@notesnook/editor"; import { installProfilerGlobals, setProfiledEditor } from "./profiling"; @@ -122,7 +121,7 @@ type TipTapProps = { dayFormat: DayFormat; markdownShortcuts: boolean; fontLigatures: boolean; - virtualization: VirtualizationMode; + virtualization: boolean; }; function countCharacters(text: string) { diff --git a/apps/web/src/dialogs/settings/editor-settings.ts b/apps/web/src/dialogs/settings/editor-settings.ts index 9e8628858..22b2e56d1 100644 --- a/apps/web/src/dialogs/settings/editor-settings.ts +++ b/apps/web/src/dialogs/settings/editor-settings.ts @@ -18,7 +18,6 @@ along with this program. If not, see . */ import { SettingsGroup } from "./types"; -import { VirtualizationMode } from "@notesnook/editor"; import { editorConfig, onEditorConfigChange, @@ -166,18 +165,10 @@ export const EditorSettings: SettingsGroup[] = [ useSettingStore.subscribe((c) => c.editorVirtualization, listener), components: [ { - type: "dropdown", - options: [ - { value: "off", title: strings.editorVirtualizationOff() }, - { value: "blocks", title: strings.editorVirtualizationBlocks() }, - { value: "pages", title: strings.editorVirtualizationPages() } - ], - selectedOption: () => - useSettingStore.getState().editorVirtualization, - onSelectionChanged: (value) => - useSettingStore - .getState() - .setEditorVirtualization(value as VirtualizationMode) + type: "toggle", + isToggled: () => useSettingStore.getState().editorVirtualization, + toggle: () => + useSettingStore.getState().toggleEditorVirtualization() } ] } diff --git a/apps/web/src/stores/setting-store.ts b/apps/web/src/stores/setting-store.ts index d2287d114..1f37eb3ad 100644 --- a/apps/web/src/stores/setting-store.ts +++ b/apps/web/src/stores/setting-store.ts @@ -25,7 +25,6 @@ import Config from "../utils/config"; import BaseStore from "./index"; import { TimeFormat, DayFormat, WeekFormat, EVENTS } from "@notesnook/core"; import { Profile, TrashCleanupInterval } from "@notesnook/core"; -import { VirtualizationMode, toVirtualizationMode } from "@notesnook/editor"; import { showToast } from "../utils/toast"; import { ConfirmDialog } from "../dialogs/confirm"; import * as openpgp from "openpgp"; @@ -58,9 +57,7 @@ class SettingStore extends BaseStore { doubleSpacedParagraphs = Config.get("doubleSpacedLines", true); markdownShortcuts = Config.get("markdownShortcuts", false); fontLigatures = Config.get("fontLigatures", false); - editorVirtualization: VirtualizationMode = toVirtualizationMode( - Config.get("editorVirtualization", "off") - ); + editorVirtualization = Config.get("editorVirtualization", false); notificationsSettings = Config.get("notifications", { reminder: true }); isFullOfflineMode = Config.get("fullOfflineMode", false); serverUrls: Partial> = Config.get("serverUrls", {}); @@ -254,9 +251,10 @@ class SettingStore extends BaseStore { Config.set("fontLigatures", !fontLigatures); }; - setEditorVirtualization = (mode: VirtualizationMode) => { - this.set((state) => (state.editorVirtualization = mode)); - Config.set("editorVirtualization", mode); + toggleEditorVirtualization = (toggleState?: boolean) => { + const next = toggleState ?? !this.get().editorVirtualization; + this.set((state) => (state.editorVirtualization = next)); + Config.set("editorVirtualization", next); }; togglePrivacyMode = async () => { diff --git a/packages/editor-mobile/src/components/editor.tsx b/packages/editor-mobile/src/components/editor.tsx index 764067be0..f95f60119 100644 --- a/packages/editor-mobile/src/components/editor.tsx +++ b/packages/editor-mobile/src/components/editor.tsx @@ -259,7 +259,7 @@ const Tiptap = ({ timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined, dayFormat: settings.dayFormat, enableInputRules: settings.markdownShortcuts, - virtualization: settings.virtualization || "off" + virtualization: !!settings.virtualization }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ @@ -963,7 +963,7 @@ const Tiptap = ({ // once and only rebuilds its view afterwards, so extension options are // frozen at construction — toggling paging only takes effect when this // component remounts. - key={tick + tab.id + "-editor-" + (settings.virtualization || "off")} + key={tick + tab.id + "-editor-" + !!settings.virtualization} options={tiptapOptions} settings={settings} onEditorUpdate={(editor) => { diff --git a/packages/editor-mobile/src/utils/index.ts b/packages/editor-mobile/src/utils/index.ts index 8a8408724..11a2659d2 100644 --- a/packages/editor-mobile/src/utils/index.ts +++ b/packages/editor-mobile/src/utils/index.ts @@ -52,7 +52,7 @@ export type Settings = { dateFormat: string; fontScale: number; markdownShortcuts: boolean; - virtualization?: "off" | "blocks" | "pages"; + virtualization?: boolean; features: Record; loggedIn: boolean; defaultLineHeight: number; diff --git a/packages/editor/src/extensions/paging/__tests__/decorations.test.ts b/packages/editor/src/extensions/paging/__tests__/decorations.test.ts new file mode 100644 index 000000000..6d5ab88f4 --- /dev/null +++ b/packages/editor/src/extensions/paging/__tests__/decorations.test.ts @@ -0,0 +1,142 @@ +/* +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 . +*/ + +import { afterEach, describe, expect, test } from "vitest"; +import { Editor, Node } from "@tiptap/core"; +import StarterKit from "@tiptap/starter-kit"; +import { DecorationSet } from "@tiptap/pm/view"; +import { Page, Paging, viewportKey } from "../index.js"; +import { BlockId } from "../../block-id/block-id.js"; +import { profiler } from "../../../utils/profiler.js"; + +const PagedDocument = Node.create({ + name: "doc", + topNode: true, + content: "(page | block)+" +}); + +const BLOCKS = 200; +const PAGE_SIZE = 50; + +function savedNoteHTML(n: number) { + let content = ""; + for (let i = 0; i < n; i++) + content += `

Paragraph number ${i}.

`; + return content; +} + +function createEditor() { + return new Editor({ + extensions: [ + StarterKit.configure({ document: false }), + PagedDocument, + Page, + BlockId, + Paging.configure({ + enabled: true, + pageSize: PAGE_SIZE, + thresholdBlocks: 10 + }) + ], + content: savedNoteHTML(BLOCKS) + }); +} + +function decorations(editor: Editor): DecorationSet { + return viewportKey.getState(editor.state)?.decorations ?? DecorationSet.empty; +} + +function counters() { + return profiler.report().counters; +} + +afterEach(() => { + profiler.disable(); + profiler.reset(); +}); + +describe("viewport decorations", () => { + test("a transaction that changes nothing relevant reuses the state", () => { + const editor = createEditor(); + profiler.enable(); + + const before = decorations(editor); + editor.view.dispatch(editor.state.tr.setMeta("unrelated", true)); + + expect(decorations(editor)).toBe(before); + expect(counters()["paging.decorationReuses"]).toBe(1); + expect(counters()["paging.decorationBuilds"]).toBeUndefined(); + editor.destroy(); + }); + + test("a text edit maps the decorations instead of rebuilding them", () => { + const editor = createEditor(); + profiler.enable(); + + editor.view.dispatch(editor.state.tr.insertText("x", 3)); + + expect(counters()["paging.decorationMaps"]).toBeGreaterThanOrEqual(1); + expect(counters()["paging.decorationBuilds"]).toBeUndefined(); + editor.destroy(); + }); + + test("adding a page rebuilds the decorations", () => { + const editor = createEditor(); + profiler.enable(); + + editor.view.dispatch( + editor.state.tr.replaceWith( + 0, + 0, + editor.state.schema.nodes.paragraph.create() + ) + ); + + expect(counters()["paging.decorationBuilds"]).toBeGreaterThanOrEqual(1); + editor.destroy(); + }); + + test("the caret's page stays rendered while typing at its end", () => { + const editor = createEditor(); + const firstPage = editor.state.doc.child(0); + editor.commands.setTextSelection(firstPage.nodeSize - 2); + profiler.enable(); + + for (let i = 0; i < 5; i++) + editor.view.dispatch( + editor.state.tr.insertText("a", editor.state.selection.from) + ); + + const page = editor.view.dom.children[0] as HTMLElement; + expect(page.hasAttribute("data-virtual-placeholder")).toBe(false); + editor.destroy(); + }); + + test("only pages are decorated", () => { + const editor = createEditor(); + + const spans = decorations(editor).find(0, editor.state.doc.content.size); + expect(spans.length).toBeGreaterThan(0); + for (const span of spans) { + const node = editor.state.doc.nodeAt(span.from); + expect(node?.type.name).toBe("page"); + } + editor.destroy(); + }); +}); diff --git a/packages/editor/src/extensions/virtualization/__tests__/height-map.test.ts b/packages/editor/src/extensions/paging/__tests__/height-map.test.ts similarity index 99% rename from packages/editor/src/extensions/virtualization/__tests__/height-map.test.ts rename to packages/editor/src/extensions/paging/__tests__/height-map.test.ts index 4de4e5c3c..da33d15be 100644 --- a/packages/editor/src/extensions/virtualization/__tests__/height-map.test.ts +++ b/packages/editor/src/extensions/paging/__tests__/height-map.test.ts @@ -22,7 +22,7 @@ import { Editor, Node } from "@tiptap/core"; import StarterKit from "@tiptap/starter-kit"; import { Node as ProsemirrorNode } from "@tiptap/pm/model"; import { HeightMap } from "../height-map.js"; -import { Page, Paging } from "../../paging/index.js"; +import { Page, Paging } from "../index.js"; import { BlockId } from "../../block-id/block-id.js"; import { ImageNode } from "../../image/index.js"; import { Table } from "../../table/index.js"; diff --git a/packages/editor/src/extensions/paging/__tests__/paged-virtualization.test.ts b/packages/editor/src/extensions/paging/__tests__/paged-virtualization.test.ts index d40d6d268..677425062 100644 --- a/packages/editor/src/extensions/paging/__tests__/paged-virtualization.test.ts +++ b/packages/editor/src/extensions/paging/__tests__/paged-virtualization.test.ts @@ -23,10 +23,7 @@ import StarterKit from "@tiptap/starter-kit"; import { DecorationSet } from "@tiptap/pm/view"; import { Page, Paging, countPages } from "../index.js"; import { BlockId } from "../../block-id/block-id.js"; -import { - Virtualization, - virtualizationKey -} from "../../virtualization/index.js"; +import { viewportKey } from "../index.js"; const PagedDocument = Node.create({ name: "doc", @@ -55,8 +52,7 @@ function createEditor() { enabled: true, pageSize: PAGE_SIZE, thresholdBlocks: 10 - }), - Virtualization.configure({ enabled: true, unit: "pages" }) + }) ], content: savedNoteHTML(BLOCKS) }); @@ -67,9 +63,7 @@ async function created() { } function decorations(editor: Editor): DecorationSet { - return ( - virtualizationKey.getState(editor.state)?.decorations ?? DecorationSet.empty - ); + return viewportKey.getState(editor.state)?.decorations ?? DecorationSet.empty; } describe("paged virtualization", () => { diff --git a/packages/editor/src/extensions/virtualization/__tests__/scroll-anchor.test.ts b/packages/editor/src/extensions/paging/__tests__/scroll-anchor.test.ts similarity index 96% rename from packages/editor/src/extensions/virtualization/__tests__/scroll-anchor.test.ts rename to packages/editor/src/extensions/paging/__tests__/scroll-anchor.test.ts index f06dd8909..d79550c4a 100644 --- a/packages/editor/src/extensions/virtualization/__tests__/scroll-anchor.test.ts +++ b/packages/editor/src/extensions/paging/__tests__/scroll-anchor.test.ts @@ -20,9 +20,8 @@ along with this program. If not, see . import { afterEach, describe, expect, test } from "vitest"; import { Editor, Node } from "@tiptap/core"; import StarterKit from "@tiptap/starter-kit"; -import { Page, Paging } from "../../paging/index.js"; +import { Page, Paging } from "../index.js"; import { BlockId } from "../../block-id/block-id.js"; -import { Virtualization } from "../index.js"; import { getScrollAnchor, restoreScrollAnchor } from "../anchor.js"; const PagedDocument = Node.create({ @@ -105,8 +104,7 @@ function createEditor(container?: HTMLElement) { enabled: true, pageSize: PAGE_SIZE, thresholdBlocks: 10 - }), - Virtualization.configure({ enabled: true, unit: "pages" }) + }) ], content: savedNoteHTML(BLOCKS) }); diff --git a/packages/editor/src/extensions/virtualization/__tests__/viewport-window.test.ts b/packages/editor/src/extensions/paging/__tests__/viewport-window.test.ts similarity index 70% rename from packages/editor/src/extensions/virtualization/__tests__/viewport-window.test.ts rename to packages/editor/src/extensions/paging/__tests__/viewport-window.test.ts index 2dc4ed707..a3f2c46fe 100644 --- a/packages/editor/src/extensions/virtualization/__tests__/viewport-window.test.ts +++ b/packages/editor/src/extensions/paging/__tests__/viewport-window.test.ts @@ -18,13 +18,19 @@ along with this program. If not, see . */ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { Editor } from "@tiptap/core"; +import { Editor, Node } from "@tiptap/core"; import StarterKit from "@tiptap/starter-kit"; -import { Virtualization, virtualizationKey } from "../index.js"; +import { Page, Paging, viewportKey } from "../index.js"; import { BlockId } from "../../block-id/block-id.js"; -const BLOCKS = 60; -const BLOCK_HEIGHT = 100; +const PagedDocument = Node.create({ + name: "doc", + topNode: true, + content: "(page | block)+" +}); + +const PAGES = 60; +const PAGE_HEIGHT = 100; function savedNoteHTML(n: number) { let content = ""; @@ -47,17 +53,16 @@ function rect(top: number, height: number) { } as DOMRect; } -/** happy-dom does no layout, so the block geometry is stubbed in. */ -function stubLayout(editor: Editor, blockHeight = BLOCK_HEIGHT) { +/** happy-dom does no layout, so the page geometry is stubbed in. */ +function stubLayout(editor: Editor, pageHeight = PAGE_HEIGHT) { const dom = editor.view.dom as HTMLElement; - const total = dom.children.length * blockHeight; - dom.getBoundingClientRect = () => rect(0, total); + dom.getBoundingClientRect = () => rect(0, dom.children.length * pageHeight); let top = 0; for (const child of Array.from(dom.children)) { const childTop = top; (child as HTMLElement).getBoundingClientRect = () => - rect(childTop, blockHeight); - top += blockHeight; + rect(childTop, pageHeight); + top += pageHeight; } } @@ -70,8 +75,28 @@ function frames(count = 2) { }); } -function visibleBlocks(editor: Editor) { - return [...(virtualizationKey.getState(editor.state)?.visible ?? [])].sort(); +/** The indexes of the pages the window currently considers visible. */ +function visiblePages(editor: Editor) { + const visible = viewportKey.getState(editor.state)?.visible ?? new Set(); + const indexes: number[] = []; + editor.state.doc.forEach((page, _offset, index) => { + if (visible.has(page.attrs.blockId)) indexes.push(index); + }); + return indexes; +} + +// one block per page keeps the geometry easy to reason about +function createEditor() { + return new Editor({ + extensions: [ + StarterKit.configure({ document: false }), + PagedDocument, + Page, + BlockId, + Paging.configure({ enabled: true, pageSize: 1, thresholdBlocks: 5 }) + ], + content: savedNoteHTML(PAGES) + }); } let observers: number; @@ -92,18 +117,88 @@ afterEach(() => { vi.restoreAllMocks(); }); -function createEditor() { - return new Editor({ - extensions: [ - StarterKit, - BlockId, - Virtualization.configure({ enabled: true, thresholdBlocks: 5 }) - ], - content: savedNoteHTML(BLOCKS) - }); -} - describe("viewport window", () => { + test("observes nothing: no IntersectionObserver is created", async () => { + const editor = createEditor(); + stubLayout(editor); + editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); + await frames(); + + expect(observers).toBe(0); + editor.destroy(); + }); + + test("tracks only the pages within the overscan band", async () => { + const editor = createEditor(); + stubLayout(editor); + editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); + await frames(); + + const visible = visiblePages(editor); + expect(visible.length).toBeGreaterThan(0); + expect(visible.length).toBeLessThan(PAGES); + // happy-dom reports a 768px viewport and the band reaches one viewport + // past it, so nothing beyond ~1536px may be tracked + expect(Math.max(...visible) * PAGE_HEIGHT).toBeLessThanOrEqual(1536); + editor.destroy(); + }); + + test("does not measure while the editor has no layout", async () => { + const editor = createEditor(); + editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); + await frames(); + + expect(visiblePages(editor)).toEqual([]); + editor.destroy(); + }); + + test("keeps pages that drift into the hysteresis band", async () => { + const editor = createEditor(); + stubLayout(editor); + editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); + await frames(); + const before = visiblePages(editor); + + const dom = editor.view.dom as HTMLElement; + let top = 384; + for (const child of Array.from(dom.children)) { + const childTop = top; + (child as HTMLElement).getBoundingClientRect = () => + rect(childTop, PAGE_HEIGHT); + top += PAGE_HEIGHT; + } + editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); + await frames(); + + const after = visiblePages(editor); + for (const index of before) expect(after).toContain(index); + editor.destroy(); + }); + + test("keeps the visible page still when a placeholder resizes", async () => { + const editor = createEditor(); + stubLayout(editor); + editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); + await frames(); + const container = editor.view.dom.parentElement; + const before = container?.scrollTop ?? 0; + + const dom = editor.view.dom as HTMLElement; + let top = 0; + for (const [index, child] of Array.from(dom.children).entries()) { + const height = index === 1 ? PAGE_HEIGHT * 10 : PAGE_HEIGHT; + const childTop = top; + (child as HTMLElement).getBoundingClientRect = () => + rect(childTop, height); + top += height; + } + editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); + await frames(); + + expect(container?.scrollTop ?? 0).toBeGreaterThanOrEqual(before); + editor.destroy(); + }); + test("re-measures when the editor is re-laid out", async () => { const observed: Element[] = []; const RealResizeObserver = globalThis.ResizeObserver; @@ -123,113 +218,21 @@ describe("viewport window", () => { stubLayout(editor); await frames(); - // zooming changes the layout without scrolling or editing, so a resize is - // the only signal that what is on screen has moved expect(observed).toContain(editor.view.dom); - expect(notify).toBeDefined(); const dom = editor.view.dom as HTMLElement; let top = 0; for (const child of Array.from(dom.children)) { const childTop = top; (child as HTMLElement).getBoundingClientRect = () => - rect(childTop, BLOCK_HEIGHT * 4); - top += BLOCK_HEIGHT * 4; + rect(childTop, PAGE_HEIGHT * 4); + top += PAGE_HEIGHT * 4; } notify?.(); await frames(); - expect(visibleBlocks(editor).length).toBeGreaterThan(0); + expect(visiblePages(editor).length).toBeGreaterThan(0); globalThis.ResizeObserver = RealResizeObserver; editor.destroy(); }); - - test("observes nothing: no IntersectionObserver is created", async () => { - const editor = createEditor(); - stubLayout(editor); - editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); - await frames(); - - expect(observers).toBe(0); - editor.destroy(); - }); - - test("tracks only the blocks within the overscan band", async () => { - const editor = createEditor(); - stubLayout(editor); - editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); - await frames(); - - const visible = visibleBlocks(editor); - expect(visible.length).toBeGreaterThan(0); - expect(visible.length).toBeLessThan(BLOCKS); - - // window.innerHeight is 768 in happy-dom and the add band is one viewport - // in each direction, so blocks past ~1536px must stay out. - const highest = Math.max( - ...visible.map((id) => Number(id.replace("blk", ""))) - ); - expect(highest * BLOCK_HEIGHT).toBeLessThanOrEqual(1536); - editor.destroy(); - }); - - test("does not measure while the editor has no layout", async () => { - const editor = createEditor(); - editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); - await frames(); - - expect(visibleBlocks(editor)).toEqual([]); - editor.destroy(); - }); - - test("keeps the visible unit still when a placeholder resizes", async () => { - const editor = createEditor(); - stubLayout(editor); - editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); - await frames(); - - const container = editor.view.dom.parentElement; - const before = container?.scrollTop ?? 0; - - // the second block renders and turns out to be far taller than its - // estimate: everything after it moves, so the scroll must follow - const dom = editor.view.dom as HTMLElement; - let top = 0; - for (const [index, child] of Array.from(dom.children).entries()) { - const height = index === 1 ? BLOCK_HEIGHT * 10 : BLOCK_HEIGHT; - const childTop = top; - (child as HTMLElement).getBoundingClientRect = () => - rect(childTop, height); - top += height; - } - editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); - await frames(); - - expect(container?.scrollTop ?? 0).toBeGreaterThanOrEqual(before); - editor.destroy(); - }); - - test("keeps blocks that drift into the hysteresis band", async () => { - const editor = createEditor(); - stubLayout(editor); - editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); - await frames(); - const before = visibleBlocks(editor); - - // Everything shifts down by half a viewport: blocks that leave the add - // band but stay inside the keep band must not be dropped. - const dom = editor.view.dom as HTMLElement; - let top = 384; - for (const child of Array.from(dom.children)) { - const childTop = top; - (child as HTMLElement).getBoundingClientRect = () => - rect(childTop, BLOCK_HEIGHT); - top += BLOCK_HEIGHT; - } - editor.view.dispatch(editor.state.tr.setMeta("nudge", true)); - await frames(); - - for (const id of before) expect(visibleBlocks(editor)).toContain(id); - editor.destroy(); - }); }); diff --git a/packages/editor/src/extensions/virtualization/anchor.ts b/packages/editor/src/extensions/paging/anchor.ts similarity index 86% rename from packages/editor/src/extensions/virtualization/anchor.ts rename to packages/editor/src/extensions/paging/anchor.ts index 9d86367f9..ee9c1d859 100644 --- a/packages/editor/src/extensions/virtualization/anchor.ts +++ b/packages/editor/src/extensions/paging/anchor.ts @@ -20,8 +20,8 @@ along with this program. If not, see . import { Node as ProsemirrorNode } from "@tiptap/pm/model"; import { EditorView } from "@tiptap/pm/view"; import { profiler } from "../../utils/profiler.js"; -import { isPage } from "../paging/split.js"; -import { findScrollParent, virtualizationKey } from "./viewport-plugin.js"; +import { isPage } from "./split.js"; +import { findScrollParent, viewportKey } from "./viewport-plugin.js"; export type ScrollAnchor = { /** The block that was at the top of the viewport. */ @@ -59,7 +59,6 @@ export function getScrollAnchor(view: EditorView): ScrollAnchor | undefined { : undefined; } - // Inside a rendered page, anchor on the exact block at the fold. for (const child of Array.from(element.children)) { const childRect = child.getBoundingClientRect(); if (childRect.bottom <= top) continue; @@ -67,8 +66,6 @@ export function getScrollAnchor(view: EditorView): ScrollAnchor | undefined { if (blockId) return { blockId, offset: Math.round(top - childRect.top) }; } - // A page that has not rendered has no blocks to inspect, but the document - // still knows which block it starts with. const blockId = node.firstChild?.attrs.blockId as string | undefined; return blockId ? { blockId, offset: Math.round(top - rect.top) } @@ -105,7 +102,7 @@ export function restoreScrollAnchor( view: EditorView, anchor: ScrollAnchor ): boolean { - const end = profiler.start("virtualization.restoreAnchor"); + const end = profiler.start("paging.restoreAnchor"); const { container, top } = containerOf(view); if (!container) { end(); @@ -115,18 +112,18 @@ export function restoreScrollAnchor( const target = findBlock(view.state.doc, anchor.blockId); if (!target.found) { end(); - profiler.count("virtualization.restoreAnchorMissed"); + profiler.count("paging.restoreAnchorMissed"); return false; } if (target.pageId) { - const visible = virtualizationKey.getState(view.state)?.visible; + const visible = viewportKey.getState(view.state)?.visible; if (!visible?.has(target.pageId)) { const next = new Set(visible ?? []); next.add(target.pageId); view.dispatch( view.state.tr - .setMeta(virtualizationKey, { visible: next }) + .setMeta(viewportKey, { visible: next }) .setMeta("preventUpdate", true) .setMeta("addToHistory", false) ); @@ -138,13 +135,13 @@ export function restoreScrollAnchor( ); if (!element) { end(); - profiler.count("virtualization.restoreAnchorMissed"); + profiler.count("paging.restoreAnchorMissed"); return false; } container.scrollTop += element.getBoundingClientRect().top - top - anchor.offset; end(); - profiler.count("virtualization.restoreAnchors"); + profiler.count("paging.restoreAnchors"); return true; } diff --git a/packages/editor/src/extensions/virtualization/height-map.ts b/packages/editor/src/extensions/paging/height-map.ts similarity index 94% rename from packages/editor/src/extensions/virtualization/height-map.ts rename to packages/editor/src/extensions/paging/height-map.ts index d42abf9e3..c07c2a05b 100644 --- a/packages/editor/src/extensions/virtualization/height-map.ts +++ b/packages/editor/src/extensions/paging/height-map.ts @@ -147,12 +147,8 @@ export class HeightMap { const stored = this.storedHeight(node); if (stored) return stored; - // Structure beats the type's fallback: a two-row table is not as tall as - // the average table, it is as tall as two rows. if (node.type.name === TABLE_TYPE) return this.table(node) || base; - // Lists, callouts, quotes, pages: a container is as tall as its contents, - // and its children carry their own structure down as far as it goes. if (this.holdsBlocks(node)) { let total = 0; node.forEach((child) => (total += this.heightFor(child))); @@ -168,8 +164,6 @@ export class HeightMap { * line model cannot see. */ private text(node: ProsemirrorNode, base: number): number { - // `content.size` is O(1) and proportional to how much text a node holds, - // unlike `textContent`, which would copy every character of every page. const content = node.content.size; if (!content) return base; @@ -248,8 +242,6 @@ export class HeightMap { this.estimates.delete(node); profiler.gauge("virtualization.heightMap.size", this.measured.size); - // Pages are containers; calibrating from them would average away the - // difference between the types they hold. if (node.type.name === PAGE_TYPE) return; const content = node.content.size; diff --git a/packages/editor/src/extensions/paging/index.ts b/packages/editor/src/extensions/paging/index.ts index a0841cdc8..da5bd11b8 100644 --- a/packages/editor/src/extensions/paging/index.ts +++ b/packages/editor/src/extensions/paging/index.ts @@ -19,9 +19,14 @@ along with this program. If not, see . import { Extension } from "@tiptap/core"; import { profiler } from "../../utils/profiler.js"; -import { installFlatteningSerializer } from "./serializer.js"; +import { HeightMap } from "./height-map.js"; import { installPagingParser } from "./parser.js"; +import { installFlatteningSerializer } from "./serializer.js"; import { DEFAULT_PAGE_SIZE, countPages, toPages } from "./split.js"; +import { viewportPlugin } from "./viewport-plugin.js"; + +/** Paging only engages for notes larger than this many top-level blocks. */ +const DEFAULT_THRESHOLD_BLOCKS = 300; export type PagingOptions = { enabled: boolean; @@ -29,6 +34,19 @@ export type PagingOptions = { thresholdBlocks: number; }; +export type PagingStorage = { + heights: HeightMap; +}; + +/** + * Renders only the pages near the viewport, keeping the rest of the note in the + * document as empty boxes of their estimated height. Pages are grouped as a + * note is parsed and stripped again on serialization, so nothing about how a + * note is stored changes. + * + * Browser find-in-page and printing only cover rendered pages, so this is + * opt-in and only engages past a size threshold. + */ export const Paging = Extension.create({ name: "paging", @@ -36,12 +54,14 @@ export const Paging = Extension.create({ return { enabled: false, pageSize: DEFAULT_PAGE_SIZE, - thresholdBlocks: 300 + thresholdBlocks: DEFAULT_THRESHOLD_BLOCKS }; }, - // Runs before the first view exists, so the document arrives already paged - // and is never rendered flat. + addStorage(): PagingStorage { + return { heights: new HeightMap() }; + }, + onBeforeCreate() { installFlatteningSerializer(this.editor.schema); if (!this.options.enabled) return; @@ -51,8 +71,6 @@ export const Paging = Extension.create({ }); }, - // Content that arrives as JSON bypasses the parser, so this stays as a - // fallback for documents the parser never saw. onCreate() { if (!this.options.enabled) return; const { editor } = this; @@ -60,22 +78,37 @@ export const Paging = Extension.create({ if (doc.childCount <= this.options.thresholdBlocks) return; if (countPages(doc) > 0) return; - const end = profiler.start("paging.split"); - const pages = toPages(doc, editor.schema, this.options.pageSize); editor.view.dispatch( editor.state.tr - .replaceWith(0, doc.content.size, pages) + .replaceWith( + 0, + doc.content.size, + toPages(doc, editor.schema, this.options.pageSize) + ) .setMeta("preventUpdate", true) .setMeta("addToHistory", false) .setMeta("ignoreEdit", true) ); - end(); - profiler.count("paging.splits"); profiler.gauge("paging.pages", countPages(editor.state.doc)); + }, + + addProseMirrorPlugins() { + if (!this.options.enabled) return []; + return [viewportPlugin(this.storage.heights)]; } }); -export { Page, PAGE_NODE } from "./page.js"; +export { HeightMap } from "./height-map.js"; +export { PAGE_NODE, Page } from "./page.js"; +export { + getScrollAnchor, + restoreScrollAnchor, + type ScrollAnchor +} from "./anchor.js"; +export { installPagingParser, uninstallPagingParser } from "./parser.js"; +export { fromFlatPosition, toFlatPosition } from "./positions.js"; +export { serializeDocumentHTML } from "./serialize.js"; +export { installFlatteningSerializer } from "./serializer.js"; export { DEFAULT_PAGE_SIZE, countPages, @@ -84,7 +117,4 @@ export { isPage, toPages } from "./split.js"; -export { installFlatteningSerializer } from "./serializer.js"; -export { installPagingParser, uninstallPagingParser } from "./parser.js"; -export { fromFlatPosition, toFlatPosition } from "./positions.js"; -export { serializeDocumentHTML } from "./serialize.js"; +export { viewportKey } from "./viewport-plugin.js"; diff --git a/packages/editor/src/extensions/paging/page-view.ts b/packages/editor/src/extensions/paging/page-view.ts new file mode 100644 index 000000000..1f0fe19b1 --- /dev/null +++ b/packages/editor/src/extensions/paging/page-view.ts @@ -0,0 +1,110 @@ +/* +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 . +*/ + +import { DOMSerializer, Node as ProsemirrorNode } from "@tiptap/pm/model"; +import { Decoration, NodeView } from "@tiptap/pm/view"; +import { profiler } from "../../utils/profiler.js"; +import { HeightMap } from "./height-map.js"; + +export function isMaterialized(decorations: readonly Decoration[]): boolean { + return decorations.some( + (d) => (d.spec as { materialize?: boolean })?.materialize + ); +} + +let template: HTMLDivElement | undefined; + +function placeholderElement(): HTMLDivElement { + if (!template) { + template = document.createElement("div"); + template.setAttribute("data-virtual-placeholder", "true"); + template.style.width = "100%"; + template.style.contain = "strict"; + } + return template.cloneNode(false) as HTMLDivElement; +} + +/** + * An empty box standing in for a page. ProseMirror keeps the page and its + * blocks in the document but renders nothing for them, so the browser lays out + * and paints one sized div instead of a few hundred elements. + */ +function placeholder(node: ProsemirrorNode, heights: HeightMap): NodeView { + profiler.count("paging.placeholderCreated"); + const dom = placeholderElement(); + const blockId = node.attrs.blockId as string | undefined; + if (blockId) dom.setAttribute("data-block-id", blockId); + dom.style.height = `${heights.heightFor(node)}px`; + + return { + dom, + contentDOM: null, + update(updated, decorations) { + if (updated.type !== node.type) return false; + if (isMaterialized(decorations)) { + profiler.count("paging.materialized"); + return false; + } + node = updated; + dom.style.height = `${heights.heightFor(updated)}px`; + return true; + }, + ignoreMutation() { + return true; + } + }; +} + +/** A page rendered normally, measured so its placeholder is the right size. */ +function rendered(node: ProsemirrorNode, heights: HeightMap): NodeView { + const spec = node.type.spec.toDOM?.(node); + if (!spec) return { dom: document.createElement("div") }; + + const { dom, contentDOM } = DOMSerializer.renderSpec(document, spec); + const measure = () => { + if (dom instanceof HTMLElement) heights.record(node, dom.offsetHeight); + }; + + return { + dom, + contentDOM, + update(updated, decorations) { + if (updated.type !== node.type) return false; + if (!isMaterialized(decorations)) { + profiler.count("paging.dematerialized"); + return false; + } + if (!node.sameMarkup(updated)) return false; + node = updated; + measure(); + return true; + }, + destroy: measure + }; +} + +export function createPageView( + node: ProsemirrorNode, + decorations: readonly Decoration[], + heights: HeightMap +): NodeView { + return isMaterialized(decorations) + ? rendered(node, heights) + : placeholder(node, heights); +} diff --git a/packages/editor/src/extensions/paging/page.ts b/packages/editor/src/extensions/paging/page.ts index 1b6629a7e..4c9425c7d 100644 --- a/packages/editor/src/extensions/paging/page.ts +++ b/packages/editor/src/extensions/paging/page.ts @@ -18,14 +18,19 @@ along with this program. If not, see . */ import { Node, mergeAttributes } from "@tiptap/core"; +import { HeightMap } from "./height-map.js"; +import { createPageView } from "./page-view.js"; export const PAGE_NODE = "page"; /** - * A grouping wrapper around a run of top-level blocks. Pages exist only in the - * editor's document: they are created when a note is opened and removed again - * on serialization, so stored content is unchanged and older clients are - * unaffected. There is deliberately no `parseHTML` rule for the same reason. + * A run of top-level blocks, rendered as one unit so an off-screen page costs a + * single empty box instead of a few hundred elements. + * + * Pages exist only in the editor's document: they are created when a note is + * opened and removed again on serialization, so stored content is unchanged and + * older clients are unaffected. There is deliberately no `parseHTML` rule for + * the same reason. */ export const Page = Node.create({ name: PAGE_NODE, @@ -33,11 +38,15 @@ export const Page = Node.create({ group: "page", selectable: false, - // The block id must survive onto the element: the viewport plugin tracks - // pages by `data-block-id`, and a page that renders without one is invisible - // to it -- it materializes, disappears from the window, and dematerializes - // again on the next frame. renderHTML({ HTMLAttributes }) { return ["div", mergeAttributes(HTMLAttributes, { "data-page": "true" }), 0]; + }, + + addNodeView() { + return ({ node, decorations }) => { + const heights = (this.editor.storage.paging as { heights: HeightMap }) + .heights; + return createPageView(node, decorations, heights); + }; } }); diff --git a/packages/editor/src/extensions/paging/parser.ts b/packages/editor/src/extensions/paging/parser.ts index 66a8bec8a..8e614dde8 100644 --- a/packages/editor/src/extensions/paging/parser.ts +++ b/packages/editor/src/extensions/paging/parser.ts @@ -54,8 +54,6 @@ export function installPagingParser( ): void { const cached = schema.cached as { domParser?: DOMParser }; if (!(cached.domParser instanceof PagingDOMParser)) { - // `fromSchema` seeds the default parser; reuse its rules so the paging - // parser behaves identically apart from the wrapping. const base = DOMParser.fromSchema(schema); cached.domParser = new PagingDOMParser(schema, base.rules); } diff --git a/packages/editor/src/extensions/paging/serializer.ts b/packages/editor/src/extensions/paging/serializer.ts index 991df0060..3892404bc 100644 --- a/packages/editor/src/extensions/paging/serializer.ts +++ b/packages/editor/src/extensions/paging/serializer.ts @@ -17,29 +17,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { - DOMSerializer, - Fragment, - Node as ProsemirrorNode, - Schema -} from "@tiptap/pm/model"; -import { PAGE_NODE } from "./page.js"; - -function withoutPages(fragment: Fragment): Fragment { - let paged = false; - fragment.forEach((node) => { - if (node.type.name === PAGE_NODE) paged = true; - }); - if (!paged) return fragment; - - const blocks: ProsemirrorNode[] = []; - fragment.forEach((node) => { - if (node.type.name === PAGE_NODE) - node.content.forEach((child) => blocks.push(child)); - else blocks.push(node); - }); - return Fragment.fromArray(blocks); -} +import { DOMSerializer, Fragment, Schema } from "@tiptap/pm/model"; +import { flattenPages } from "./split.js"; /** * Serializes pages as if they were not there, so stored HTML, the clipboard and @@ -53,7 +32,7 @@ class FlatteningDOMSerializer extends DOMSerializer { options?: { document?: Document }, target?: HTMLElement | DocumentFragment ) { - return super.serializeFragment(withoutPages(fragment), options, target); + return super.serializeFragment(flattenPages(fragment), options, target); } } diff --git a/packages/editor/src/extensions/paging/split.ts b/packages/editor/src/extensions/paging/split.ts index 03fe10da7..b00924887 100644 --- a/packages/editor/src/extensions/paging/split.ts +++ b/packages/editor/src/extensions/paging/split.ts @@ -44,9 +44,6 @@ export function toPages( const blocks = flattenBlocks(doc); if (!blocks.length) return doc.content; - // Pages are identified at creation rather than by the block id plugin: the - // viewport plugin needs an id the moment a page exists, and one assigned a - // transaction later would leave every page permanently materialized. const identify = "blockId" in (pageType.spec.attrs ?? {}); const pages: ProsemirrorNode[] = []; for (let i = 0; i < blocks.length; i += pageSize) @@ -60,24 +57,26 @@ export function toPages( return Fragment.fromArray(pages); } -/** The document's blocks with every page wrapper removed. */ -export function flattenBlocks(doc: ProsemirrorNode): ProsemirrorNode[] { - const blocks: ProsemirrorNode[] = []; - doc.forEach((node) => { - if (isPage(node)) node.forEach((child) => blocks.push(child)); - else blocks.push(node); - }); - return blocks; -} - -/** The document's content with page wrappers removed, for serialization. */ -export function flattenPages(doc: ProsemirrorNode): Fragment { +/** The same content with every page wrapper removed. */ +export function flattenPages(fragment: Fragment): Fragment { let paged = false; - doc.forEach((node) => { + fragment.forEach((node) => { if (isPage(node)) paged = true; }); - if (!paged) return doc.content; - return Fragment.fromArray(flattenBlocks(doc)); + if (!paged) return fragment; + + const blocks: ProsemirrorNode[] = []; + fragment.forEach((node) => { + if (isPage(node)) node.content.forEach((child) => blocks.push(child)); + else blocks.push(node); + }); + return Fragment.fromArray(blocks); +} + +export function flattenBlocks(doc: ProsemirrorNode): ProsemirrorNode[] { + const blocks: ProsemirrorNode[] = []; + flattenPages(doc.content).forEach((node) => blocks.push(node)); + return blocks; } export function countPages(doc: ProsemirrorNode): number { diff --git a/packages/editor/src/extensions/virtualization/viewport-plugin.ts b/packages/editor/src/extensions/paging/viewport-plugin.ts similarity index 78% rename from packages/editor/src/extensions/virtualization/viewport-plugin.ts rename to packages/editor/src/extensions/paging/viewport-plugin.ts index 948678a01..0f5f4a551 100644 --- a/packages/editor/src/extensions/virtualization/viewport-plugin.ts +++ b/packages/editor/src/extensions/paging/viewport-plugin.ts @@ -22,13 +22,11 @@ import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; import { Decoration, DecorationSet } from "@tiptap/pm/view"; import { profiler } from "../../utils/profiler.js"; import { HeightMap } from "./height-map.js"; -import { VirtualizationUnit, unitTypes } from "./node-views.js"; +import { PAGE_NODE } from "./page.js"; -export const virtualizationKey = new PluginKey( - "notesnook-virtualization" -); +export const viewportKey = new PluginKey("notesnook-paging"); -type VirtualizationState = { +type ViewportState = { visible: Set; selectionIndex: number; blockCount: number; @@ -43,8 +41,6 @@ const KEEP_OVERSCAN = 1.5; const MATERIALIZE_ATTRS = {}; const MATERIALIZE_SPEC = { materialize: true }; -type IsPageable = (typeName: string) => boolean; - export function findScrollParent(node: HTMLElement): HTMLElement | null { let current: HTMLElement | null = node.parentElement; while (current) { @@ -74,20 +70,20 @@ function shouldMaterialize( selectionIndex: number ): boolean { if (index === 0 || index === lastIndex) { - profiler.count("virtualization.materializedBy.edge"); + profiler.count("paging.materializedBy.edge"); return true; } if (Math.abs(index - selectionIndex) <= 1) { - profiler.count("virtualization.materializedBy.selection"); + profiler.count("paging.materializedBy.selection"); return true; } const blockId = node.attrs.blockId as string | undefined; if (!blockId) { - profiler.count("virtualization.materializedBy.missingBlockId"); + profiler.count("paging.materializedBy.missingBlockId"); return true; } if (visible.has(blockId)) { - profiler.count("virtualization.materializedBy.visible"); + profiler.count("paging.materializedBy.visible"); return true; } return false; @@ -106,17 +102,16 @@ function materializeDecoration(from: number, to: number): Decoration { function buildDecorations( doc: ProsemirrorNode, visible: Set, - selectionIndex: number, - isPageable: IsPageable + selectionIndex: number ): DecorationSet { - const end = profiler.start("virtualization.decorations"); + const end = profiler.start("paging.decorations"); const decorations: Decoration[] = []; const lastIndex = doc.childCount - 1; let index = -1; doc.forEach((node, offset) => { index++; - if (!isPageable(node.type.name)) return; + if (node.type.name !== PAGE_NODE) return; if (!shouldMaterialize(node, index, lastIndex, visible, selectionIndex)) return; decorations.push(materializeDecoration(offset, offset + node.nodeSize)); @@ -124,9 +119,9 @@ function buildDecorations( const set = DecorationSet.create(doc, decorations); end(); - profiler.count("virtualization.decorationBuilds"); - profiler.gauge("virtualization.materializedBlocks", decorations.length); - profiler.gauge("virtualization.blocksInDoc", doc.childCount); + profiler.count("paging.decorationBuilds"); + profiler.gauge("paging.materializedBlocks", decorations.length); + profiler.gauge("paging.blocksInDoc", doc.childCount); return set; } @@ -185,30 +180,23 @@ function hasMaterializeDecoration( */ function repairSelection( set: DecorationSet, - state: EditorState, - isPageable: IsPageable + state: EditorState ): DecorationSet { const missing: Decoration[] = []; for (const range of selectionRanges(state)) { - if (!isPageable(state.doc.child(range.index).type.name)) continue; + if (state.doc.child(range.index).type.name !== PAGE_NODE) continue; if (hasMaterializeDecoration(set, range)) continue; missing.push(materializeDecoration(range.from, range.to)); } if (!missing.length) return set; - profiler.count("virtualization.decorationRepairs", missing.length); + profiler.count("paging.decorationRepairs", missing.length); return set.add(state.doc, missing); } -export function virtualizationPlugin( - unit: VirtualizationUnit = "blocks", - heightMap?: HeightMap -): Plugin { - const types = unitTypes(unit); - const isPageable: IsPageable = (typeName) => types.includes(typeName); - - return new Plugin({ - key: virtualizationKey, +export function viewportPlugin(heights: HeightMap): Plugin { + return new Plugin({ + key: viewportKey, state: { init(_config, state) { const selectionIndex = state.selection.$from.index(0); @@ -219,13 +207,12 @@ export function virtualizationPlugin( decorations: buildDecorations( state.doc, EMPTY_VISIBLE, - selectionIndex, - isPageable + selectionIndex ) }; }, apply(tr, value, _oldState, newState) { - const meta = tr.getMeta(virtualizationKey) as + const meta = tr.getMeta(viewportKey) as | { visible: Set } | undefined; const visible = meta?.visible ?? value.visible; @@ -236,7 +223,7 @@ export function virtualizationPlugin( const structural = blockCount !== value.blockCount; if (!meta && !selectionMoved && !structural && !tr.docChanged) { - profiler.count("virtualization.decorationReuses"); + profiler.count("paging.decorationReuses"); return value; } @@ -245,33 +232,24 @@ export function virtualizationPlugin( visible, selectionIndex, blockCount, - decorations: buildDecorations( - tr.doc, - visible, - selectionIndex, - isPageable - ) + decorations: buildDecorations(tr.doc, visible, selectionIndex) }; } - // Text-only edit: the block structure is unchanged, so the existing - // decorations only need their positions mapped instead of a full - // O(blocks x decorations) rebuild. - const end = profiler.start("virtualization.decorationMap"); + const end = profiler.start("paging.decorationMap"); const mapped = repairSelection( value.decorations.map(tr.mapping, tr.doc), - newState, - isPageable + newState ); end(); - profiler.count("virtualization.decorationMaps"); + profiler.count("paging.decorationMaps"); return { visible, selectionIndex, blockCount, decorations: mapped }; } }, props: { decorations(state) { - return virtualizationKey.getState(state)?.decorations; + return viewportKey.getState(state)?.decorations; } }, view(editorView) { @@ -281,12 +259,9 @@ export function virtualizationPlugin( const measuredPages = new Set(); const ensureScrollParent = () => { - // Resolved lazily: at view-init the document may not overflow yet. const resolved = findScrollParent(editorView.dom); if (!resolved || resolved === scrollParent) return; scrollParent?.removeEventListener("scroll", schedule); - // Keep scroll anchoring on so a placeholder above the viewport growing - // to its real height does not shove the visible content. resolved.style.overflowAnchor = "auto"; resolved.addEventListener("scroll", schedule, { passive: true }); scrollParent = resolved; @@ -389,32 +364,32 @@ export function virtualizationPlugin( const delta = element.getBoundingClientRect().top - pin.top; if (!delta) return; scrollParent.scrollTop += delta; - profiler.record("virtualization.pinCorrection", Math.abs(delta)); + profiler.record("paging.pinCorrection", Math.abs(delta)); }; const flush = () => { frame = 0; - const end = profiler.start("virtualization.measure"); + const end = profiler.start("paging.measure"); ensureScrollParent(); const next = measure(); end(); - profiler.count("virtualization.measures"); + profiler.count("paging.measures"); if (!next) return; - const current = virtualizationKey.getState(editorView.state)?.visible; + const current = viewportKey.getState(editorView.state)?.visible; if (current && sameSet(current, next)) { - profiler.count("virtualization.measuresUnchanged"); + profiler.count("paging.measuresUnchanged"); return; } visible = next; - profiler.count("virtualization.visibilityFlushes"); - profiler.gauge("virtualization.visibleBlocks", next.size); + profiler.count("paging.visibilityFlushes"); + profiler.gauge("paging.visibleBlocks", next.size); const pin = pinnedUnit(); editorView.dispatch( editorView.state.tr - .setMeta(virtualizationKey, { visible: next }) + .setMeta(viewportKey, { visible: next }) .setMeta("preventUpdate", true) .setMeta("addToHistory", false) ); @@ -430,7 +405,7 @@ export function virtualizationPlugin( * place while they change. */ const resizePlaceholders = () => { - if (!heightMap?.needsRecalibration) return; + if (!heights.needsRecalibration) return; const pin = pinnedUnit(); const children = editorView.dom.children; const doc = editorView.state.doc; @@ -438,11 +413,11 @@ export function virtualizationPlugin( for (let i = 0; i < count; i++) { const element = children[i] as HTMLElement; if (!element.hasAttribute("data-virtual-placeholder")) continue; - element.style.height = `${heightMap.heightFor(doc.child(i))}px`; + element.style.height = `${heights.heightFor(doc.child(i))}px`; } - heightMap.markRecalibrated(); + heights.markRecalibrated(); restorePin(pin); - profiler.count("virtualization.placeholderResizes"); + profiler.count("paging.placeholderResizes"); }; /** @@ -450,13 +425,10 @@ export function virtualizationPlugin( * its placeholder is the right size when it scrolls away again. */ const recordRenderedHeights = () => { - if (!heightMap) return; - // Read the layout the note is actually rendered in, so estimates for - // everything still off screen match what the reader will see. const style = getComputedStyle(editorView.dom); const fontSize = parseFloat(style.fontSize); const lineHeight = parseFloat(style.lineHeight); - heightMap.setMetrics({ + heights.setMetrics({ width: editorView.dom.clientWidth, fontSize, lineHeight: Number.isFinite(lineHeight) ? lineHeight : fontSize * 1.5 @@ -468,12 +440,8 @@ export function virtualizationPlugin( const element = children[i] as HTMLElement; if (element.hasAttribute("data-virtual-placeholder")) continue; const node = doc.child(i); - heightMap.record(node, element.offsetHeight); + heights.record(node, element.offsetHeight); - // The blocks inside a rendered page are what the estimates are built - // from, and their ids outlive this session's page boundaries. Measure - // each page once; re-measuring on every flush would read layout for - // hundreds of elements for nothing. const pageId = node.attrs.blockId as string | undefined; if (node.type.name !== "page" || !pageId || measuredPages.has(pageId)) continue; @@ -481,11 +449,11 @@ export function virtualizationPlugin( const blocks = element.children; const blockCount = Math.min(blocks.length, node.childCount); for (let j = 0; j < blockCount; j++) - heightMap.record( + heights.record( node.child(j), (blocks[j] as HTMLElement).offsetHeight ); - profiler.count("virtualization.pagesMeasured"); + profiler.count("paging.pagesMeasured"); } }; @@ -496,9 +464,6 @@ export function virtualizationPlugin( window.addEventListener("resize", schedule, { passive: true }); - // Zooming or changing the font re-lays out the note without scrolling it - // and without touching the document, so nothing else would tell the - // window that what is on screen has moved. let layout: ResizeObserver | undefined; try { if (typeof ResizeObserver !== "undefined") { @@ -513,7 +478,6 @@ export function virtualizationPlugin( return { update() { - // Materializing changes block heights, which moves the window. schedule(); }, destroy() { diff --git a/packages/editor/src/extensions/virtualization/__tests__/decoration-cache.test.ts b/packages/editor/src/extensions/virtualization/__tests__/decoration-cache.test.ts deleted file mode 100644 index 59a61e101..000000000 --- a/packages/editor/src/extensions/virtualization/__tests__/decoration-cache.test.ts +++ /dev/null @@ -1,118 +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 . -*/ - -import { describe, expect, test } from "vitest"; -import { Editor } from "@tiptap/core"; -import StarterKit from "@tiptap/starter-kit"; -import { EditorState, Plugin } from "@tiptap/pm/state"; -import { DecorationSet } from "@tiptap/pm/view"; -import { Virtualization, virtualizationKey } from "../index.js"; -import { BlockId } from "../../block-id/block-id.js"; - -const BLOCKS = 60; - -function html(n: number) { - let content = ""; - for (let i = 0; i < n; i++) - content += `

Paragraph number ${i}.

`; - return content; -} - -function createEditor() { - return new Editor({ - extensions: [ - StarterKit, - BlockId, - Virtualization.configure({ enabled: true, thresholdBlocks: 10 }) - ], - content: html(BLOCKS) - }); -} - -function decorationsOf(editor: Editor, state: EditorState): DecorationSet { - const plugin = editor.state.plugins.find( - (p) => p.spec.key === virtualizationKey - ) as Plugin; - const decorations = plugin.props.decorations as ( - this: Plugin, - state: EditorState - ) => DecorationSet; - return decorations.call(plugin, state); -} - -describe("virtualization decoration cache", () => { - test("returns the identical set for an unchanged state", () => { - const editor = createEditor(); - - const first = decorationsOf(editor, editor.state); - const second = decorationsOf(editor, editor.state); - - expect(second).toBe(first); - editor.destroy(); - }); - - test("rebuilds when the document changes", () => { - const editor = createEditor(); - const before = decorationsOf(editor, editor.state); - - editor.view.dispatch(editor.state.tr.insertText("typed", 1)); - const after = decorationsOf(editor, editor.state); - - expect(after).not.toBe(before); - editor.destroy(); - }); - - test("rebuilds when the visible set changes", () => { - const editor = createEditor(); - const before = decorationsOf(editor, editor.state); - - editor.view.dispatch( - editor.state.tr - .setMeta(virtualizationKey, { visible: new Set(["blk30"]) }) - .setMeta("addToHistory", false) - ); - const after = decorationsOf(editor, editor.state); - - expect(after).not.toBe(before); - editor.destroy(); - }); - - test("rebuilds when the selection moves to another block", () => { - const editor = createEditor(); - const before = decorationsOf(editor, editor.state); - - const target = editor.state.doc.resolve(1).after(1) + 1; - editor.commands.setTextSelection(target); - const after = decorationsOf(editor, editor.state); - - expect(after).not.toBe(before); - editor.destroy(); - }); - - test("keeps the cached set when a transaction changes nothing relevant", () => { - const editor = createEditor(); - const before = decorationsOf(editor, editor.state); - - editor.view.dispatch(editor.state.tr.setMeta("unrelated", true)); - const after = decorationsOf(editor, editor.state); - - expect(after).toBe(before); - editor.destroy(); - }); -}); diff --git a/packages/editor/src/extensions/virtualization/__tests__/decoration-updates.test.ts b/packages/editor/src/extensions/virtualization/__tests__/decoration-updates.test.ts deleted file mode 100644 index 4857990e2..000000000 --- a/packages/editor/src/extensions/virtualization/__tests__/decoration-updates.test.ts +++ /dev/null @@ -1,182 +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 . -*/ - -import { afterEach, describe, expect, test } from "vitest"; -import { Editor } from "@tiptap/core"; -import StarterKit from "@tiptap/starter-kit"; -import { DecorationSet } from "@tiptap/pm/view"; -import { Virtualization, virtualizationKey } from "../index.js"; -import { BlockId } from "../../block-id/block-id.js"; -import { profiler } from "../../../utils/profiler.js"; - -const BLOCKS = 40; - -function savedNoteHTML(n: number, extra = "") { - let content = ""; - for (let i = 0; i < n; i++) - content += `

Paragraph number ${i}.

`; - return content + extra; -} - -function createEditor(content: string) { - return new Editor({ - extensions: [ - StarterKit, - BlockId, - Virtualization.configure({ enabled: true, thresholdBlocks: 5 }) - ], - content - }); -} - -function decorationsOf(editor: Editor): DecorationSet { - return ( - virtualizationKey.getState(editor.state)?.decorations ?? DecorationSet.empty - ); -} - -function counters() { - return profiler.report().counters; -} - -function blockRange(editor: Editor, index: number) { - let from = 0; - for (let i = 0; i < index; i++) from += editor.state.doc.child(i).nodeSize; - return { from, to: from + editor.state.doc.child(index).nodeSize }; -} - -function isMaterialized(editor: Editor, index: number) { - const { from, to } = blockRange(editor, index); - return decorationsOf(editor) - .find(from, to) - .some( - (d) => - d.from === from && - d.to === to && - (d.spec as { materialize?: boolean }).materialize - ); -} - -afterEach(() => { - profiler.disable(); - profiler.reset(); -}); - -describe("decoration updates", () => { - test("a text edit maps the decorations instead of rebuilding them", () => { - const editor = createEditor(savedNoteHTML(BLOCKS)); - profiler.enable(); - - editor.view.dispatch(editor.state.tr.insertText("x", 2)); - - expect(counters()["virtualization.decorationMaps"]).toBeGreaterThanOrEqual( - 1 - ); - expect(counters()["virtualization.decorationBuilds"]).toBeUndefined(); - editor.destroy(); - }); - - test("adding a block rebuilds the decorations", () => { - const editor = createEditor(savedNoteHTML(BLOCKS)); - profiler.enable(); - - editor.commands.insertContentAt(2, "

new block

"); - - expect( - counters()["virtualization.decorationBuilds"] - ).toBeGreaterThanOrEqual(1); - editor.destroy(); - }); - - test("a transaction that changes nothing relevant reuses the state", () => { - const editor = createEditor(savedNoteHTML(BLOCKS)); - profiler.enable(); - - editor.view.dispatch(editor.state.tr.setMeta("unrelated", true)); - - expect(counters()["virtualization.decorationReuses"]).toBe(1); - expect(counters()["virtualization.decorationBuilds"]).toBeUndefined(); - expect(counters()["virtualization.decorationMaps"]).toBeUndefined(); - editor.destroy(); - }); - - test("the caret's block stays materialized while typing at its end", () => { - const editor = createEditor(savedNoteHTML(BLOCKS)); - const endOfFirstBlock = editor.state.doc.child(0).nodeSize - 1; - editor.commands.setTextSelection(endOfFirstBlock); - profiler.enable(); - - for (let i = 0; i < 5; i++) - editor.view.dispatch( - editor.state.tr.insertText("a", editor.state.selection.from) - ); - - expect(isMaterialized(editor, 0)).toBe(true); - expect(counters()["virtualization.decorationBuilds"]).toBeUndefined(); - editor.destroy(); - }); - - test("mapped decorations keep covering the whole edited node", () => { - const editor = createEditor(savedNoteHTML(BLOCKS)); - editor.commands.setTextSelection(3); - profiler.enable(); - - editor.view.dispatch(editor.state.tr.insertText("inserted", 3)); - - const { from, to } = blockRange(editor, 0); - const decoration = decorationsOf(editor) - .find(from, to) - .find((d) => (d.spec as { materialize?: boolean }).materialize); - expect(decoration?.from).toBe(from); - expect(decoration?.to).toBe(to); - editor.destroy(); - }); - - test("non-pageable blocks get no decoration", () => { - const editor = createEditor( - savedNoteHTML(BLOCKS, "
not pageable
") - ); - - const lastIndex = editor.state.doc.childCount - 1; - expect(editor.state.doc.child(lastIndex).type.name).toBe("codeBlock"); - expect(isMaterialized(editor, lastIndex)).toBe(false); - editor.destroy(); - }); - - test("non-pageable blocks do not inflate the decoration set", () => { - let rules = ""; - for (let i = 0; i < 100; i++) rules += "
"; - const editor = createEditor(savedNoteHTML(BLOCKS, rules)); - - expect(editor.state.doc.childCount).toBe(BLOCKS + 100); - expect( - decorationsOf(editor).find(0, editor.state.doc.content.size).length - ).toBeLessThan(10); - editor.destroy(); - }); - - test("indexes top-level positions once per document version", () => { - profiler.enable(); - const editor = createEditor(savedNoteHTML(BLOCKS)); - - expect(editor.state.doc.childCount).toBe(BLOCKS); - expect(counters()["virtualization.topLevelIndexBuilds"]).toBe(1); - editor.destroy(); - }); -}); diff --git a/packages/editor/src/extensions/virtualization/__tests__/first-view.test.ts b/packages/editor/src/extensions/virtualization/__tests__/first-view.test.ts deleted file mode 100644 index 47afecc80..000000000 --- a/packages/editor/src/extensions/virtualization/__tests__/first-view.test.ts +++ /dev/null @@ -1,112 +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 . -*/ - -import { describe, expect, test } from "vitest"; -import { Editor } from "@tiptap/core"; -import StarterKit from "@tiptap/starter-kit"; -import { Virtualization } from "../index.js"; -import { BlockId } from "../../block-id/block-id.js"; - -const BLOCKS = 400; - -/** - * Mirrors a saved Notesnook note: BlockId renders `data-block-id` into the - * stored HTML, and paging keys off it. - */ -function savedNoteHTML(n: number) { - let html = ""; - for (let i = 0; i < n; i++) - html += `

Paragraph number ${i} with filler.

`; - return html; -} - -/** A legacy/imported/pasted note, whose HTML has no block ids yet. */ -function unidentifiedHTML(n: number) { - let html = ""; - for (let i = 0; i < n; i++) - html += `

Paragraph number ${i} with filler.

`; - return html; -} - -function makeEditor(enabled: boolean, content = savedNoteHTML(BLOCKS)) { - return new Editor({ - element: document.createElement("div"), - content, - extensions: [ - StarterKit, - // Paging keys off blockId: a block without one always materializes - // (see viewport-plugin), so this is required, not incidental. - BlockId, - Virtualization.configure({ enabled, thresholdBlocks: 50 }) - ] - }); -} - -function placeholderCount(editor: Editor) { - return editor.view.dom.querySelectorAll("[data-virtual-placeholder]").length; -} - -describe("virtualization: the first view", () => { - test("the view built by the Editor constructor is already virtualized", () => { - const editor = makeEditor(true); - - // Nothing has run except the constructor - no React effect, no manual - // createView(). If this passes, the document was never mounted in full. - expect(editor.state.doc.childCount).toBe(BLOCKS); - expect(placeholderCount(editor)).toBeGreaterThan(0); - - editor.destroy(); - }); - - test("recreating the view keeps virtualization installed", () => { - const editor = makeEditor(true); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - mirrors what useEditor does on every deps change - editor.createView(); - - expect(editor.state.doc.childCount).toBe(BLOCKS); - expect(placeholderCount(editor)).toBeGreaterThan(0); - - editor.destroy(); - }); - - test("nothing is virtualized when the extension is disabled", () => { - const editor = makeEditor(false); - - expect(editor.state.doc.childCount).toBe(BLOCKS); - expect(placeholderCount(editor)).toBe(0); - - editor.destroy(); - }); - - // KNOWN GAP, not desired behaviour. BlockId assigns ids from an - // appendTransaction, which does not run during construction, so a note whose - // stored HTML has no data-block-id renders in full on its first view. The fix - // is to assign block ids at parse/load time - see - // docs/editor-performance/05-per-transaction-work.md section 4.1. When that - // lands, this expectation should flip to toBeGreaterThan(0). - test("a note with no block ids is not yet paged on its first view", () => { - const editor = makeEditor(true, unidentifiedHTML(BLOCKS)); - - expect(editor.state.doc.childCount).toBe(BLOCKS); - expect(placeholderCount(editor)).toBe(0); - - editor.destroy(); - }); -}); diff --git a/packages/editor/src/extensions/virtualization/index.ts b/packages/editor/src/extensions/virtualization/index.ts deleted file mode 100644 index d5dc9c698..000000000 --- a/packages/editor/src/extensions/virtualization/index.ts +++ /dev/null @@ -1,131 +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 . -*/ - -import { Editor, Extension } from "@tiptap/core"; -import { HeightMap } from "./height-map.js"; -import { VirtualizationUnit, withVirtualization } from "./node-views.js"; -import { virtualizationPlugin } from "./viewport-plugin.js"; - -/** Paging only engages for notes larger than this many top-level blocks. */ -const DEFAULT_THRESHOLD_BLOCKS = 300; - -export type VirtualizationOptions = { - enabled: boolean; - thresholdBlocks: number; - unit: VirtualizationUnit; -}; - -export type VirtualizationStorage = { - enabled: boolean; - thresholdBlocks: number; - unit: VirtualizationUnit; - heightMap: HeightMap; -}; - -/** - * Renders only the top-level blocks near the viewport, keeping the rest in - * editor state as content-less placeholders. This is the "hidden paging" from - * docs/editor-performance — the only lever that reduces the browser layout/paint - * cost of a very large document. - * - * High risk (breaks browser find-in-page and printing without the companion - * work). Disabled by default; enable per-note above a size threshold. - */ -export const Virtualization = Extension.create({ - name: "virtualization", - - addOptions() { - return { - enabled: false, - thresholdBlocks: DEFAULT_THRESHOLD_BLOCKS, - unit: "blocks" - }; - }, - - addStorage(): VirtualizationStorage { - return { - enabled: this.options.enabled, - thresholdBlocks: this.options.thresholdBlocks, - unit: this.options.unit, - heightMap: new HeightMap() - }; - }, - - // Runs immediately before the Editor constructor creates its first view, so - // that view is already virtualized. Installing any later (e.g. from - // useEditor's effect) means the whole document gets mounted unvirtualized - // once, which is the exact cost virtualization exists to avoid. - onBeforeCreate() { - installVirtualization(this.editor); - }, - - addProseMirrorPlugins() { - if (!this.options.enabled) return []; - return [virtualizationPlugin(this.options.unit, this.storage.heightMap)]; - } -}); - -/** - * Wraps the editor's node views with the virtualization layer. - * - * Called from the extension's own `onBeforeCreate`, which fires before the - * Editor constructor's `createView()` — so the very first view is virtualized. - * - * A ProseMirror plugin cannot do this: prosemirror-view consults the view's own - * `nodeViews` prop before any plugin (buildNodeViews is first-wins), and Tiptap - * overwrites `editorProps.nodeViews` with `extensionManager.nodeViews` via - * setProps right after construction. So we decorate the getter at its source. - * - * The patch lives on the extensionManager instance, which outlives individual - * views, so later `createView()` calls pick it up without reinstalling. - */ -export function installVirtualization(editor: Editor): void { - const storage = editor.storage.virtualization as - | VirtualizationStorage - | undefined; - if (!storage?.enabled) return; - - const manager = editor.extensionManager as unknown as Record; - if (Object.prototype.hasOwnProperty.call(manager, "nodeViews")) return; - - const proto = Object.getPrototypeOf(editor.extensionManager); - const descriptor = Object.getOwnPropertyDescriptor(proto, "nodeViews"); - const originalGetter = descriptor?.get; - if (!originalGetter) return; - - Object.defineProperty(editor.extensionManager, "nodeViews", { - configurable: true, - get() { - return withVirtualization( - originalGetter.call(this), - storage.heightMap, - storage.thresholdBlocks, - storage.unit - ); - } - }); -} - -export { HeightMap } from "./height-map.js"; -export { virtualizationKey } from "./viewport-plugin.js"; -export { - getScrollAnchor, - restoreScrollAnchor, - type ScrollAnchor -} from "./anchor.js"; diff --git a/packages/editor/src/extensions/virtualization/node-views.ts b/packages/editor/src/extensions/virtualization/node-views.ts deleted file mode 100644 index 326732c00..000000000 --- a/packages/editor/src/extensions/virtualization/node-views.ts +++ /dev/null @@ -1,267 +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 . -*/ - -import { DOMSerializer, Node as ProsemirrorNode } from "@tiptap/pm/model"; -import { - Decoration, - DecorationSource, - EditorView, - NodeView, - NodeViewConstructor -} from "@tiptap/pm/view"; -import { profiler } from "../../utils/profiler.js"; -import { HeightMap } from "./height-map.js"; - -export type VirtualizationUnit = "blocks" | "pages"; - -/** A page is virtualized exactly like a block; it is just a much taller one. */ -export const PAGE_TYPES = ["page"]; - -export function unitTypes(unit: VirtualizationUnit): string[] { - return unit === "pages" ? PAGE_TYPES : TOP_LEVEL_BLOCK_TYPES; -} - -export const TOP_LEVEL_BLOCK_TYPES = [ - "paragraph", - "heading", - "blockquote", - "bulletList", - "orderedList", - "checkList", - "taskList", - "outlineList", - "codeblock", - "table", - "image", - "webclip", - "embed", - "mathBlock", - "callout" -]; - -function isMaterialized(decorations: readonly Decoration[]): boolean { - return decorations.some( - (d) => (d.spec as { materialize?: boolean })?.materialize - ); -} - -const topLevelOffsets = new WeakMap>(); - -function topLevelOffsetsOf(doc: ProsemirrorNode): Set { - const cached = topLevelOffsets.get(doc); - if (cached) return cached; - - const end = profiler.start("virtualization.topLevelIndex"); - const offsets = new Set(); - doc.forEach((_node, offset) => offsets.add(offset)); - end(); - profiler.count("virtualization.topLevelIndexBuilds"); - - topLevelOffsets.set(doc, offsets); - return offsets; -} - -function isTopLevel( - view: EditorView, - getPos: () => number | undefined -): boolean { - const pos = getPos(); - if (pos == null) return false; - return topLevelOffsetsOf(view.state.doc).has(pos); -} - -let placeholderTemplate: HTMLDivElement | undefined; - -function createPlaceholderElement(): HTMLDivElement { - if (!placeholderTemplate) { - placeholderTemplate = document.createElement("div"); - placeholderTemplate.setAttribute("data-virtual-placeholder", "true"); - placeholderTemplate.style.width = "100%"; - // an empty, explicitly sized box: tell the browser its (absent) contents - // can never affect layout elsewhere, so it stays out of layout cascades. - placeholderTemplate.style.contain = "strict"; - } - return placeholderTemplate.cloneNode(false) as HTMLDivElement; -} - -/** - * A content-less placeholder. ProseMirror keeps the node in state but renders - * nothing for its children, so the browser lays out and paints only an empty - * box of the node's estimated height. See prosemirror-view domFromPos / - * ignoreMutation handling of contentDOM-less node views. - */ -function createPlaceholder( - node: ProsemirrorNode, - getPos: () => number | undefined, - heightMap: HeightMap -): NodeView { - profiler.count("virtualization.nodeView.placeholderCreated"); - const dom = createPlaceholderElement(); - const blockId = node.attrs.blockId as string | undefined; - if (blockId) dom.setAttribute("data-block-id", blockId); - dom.style.height = `${heightMap.heightFor(node)}px`; - - return { - dom, - contentDOM: null, - update(updatedNode: ProsemirrorNode, decorations: readonly Decoration[]) { - if (updatedNode.type !== node.type) return false; - if (isMaterialized(decorations)) { - profiler.count("virtualization.materialized"); - return false; - } - node = updatedNode; - dom.style.height = `${heightMap.heightFor(updatedNode)}px`; - return true; - }, - ignoreMutation() { - return true; - } - }; -} - -/** - * Renders a block that has no custom node view (paragraph, heading, etc.) the - * same way ProseMirror would by default — via the schema's DOM spec — so it can - * be materialized/dematerialized on viewport entry like the custom ones. - */ -function createMaterializedDefault( - node: ProsemirrorNode, - heightMap: HeightMap -): NodeView { - const spec = node.type.spec.toDOM?.(node); - if (!spec) { - const dom = document.createElement("div"); - return { dom }; - } - const { dom, contentDOM } = DOMSerializer.renderSpec(document, spec); - - const record = () => { - if (dom instanceof HTMLElement) heightMap.record(node, dom.offsetHeight); - }; - - return { - dom, - contentDOM, - update(updatedNode: ProsemirrorNode, decorations: readonly Decoration[]) { - if (updatedNode.type !== node.type) return false; - if (!isMaterialized(decorations)) { - profiler.count("virtualization.dematerialized"); - return false; - } - if (!node.sameMarkup(updatedNode)) return false; - node = updatedNode; - record(); - return true; - }, - destroy() { - record(); - } - }; -} - -/** - * Wraps a custom node view so it de-materializes (returns false -> rebuild as a - * placeholder) when its materialize decoration disappears, and records its real - * height. The custom view keeps full ownership while materialized. - */ -function wrapCustom( - inner: NodeView, - node: ProsemirrorNode, - heightMap: HeightMap -): NodeView { - const originalUpdate = inner.update?.bind(inner); - const originalDestroy = inner.destroy?.bind(inner); - - const record = () => { - if (inner.dom instanceof HTMLElement) - heightMap.record(node, inner.dom.offsetHeight); - }; - - inner.update = ( - updatedNode: ProsemirrorNode, - decorations: readonly Decoration[], - innerDecorations: DecorationSource - ) => { - if (!isMaterialized(decorations)) { - profiler.count("virtualization.dematerialized"); - return false; - } - node = updatedNode; - record(); - return originalUpdate - ? originalUpdate(updatedNode, decorations, innerDecorations) - : updatedNode.type === node.type; - }; - - inner.destroy = () => { - record(); - originalDestroy?.(); - }; - - return inner; -} - -export function withVirtualization( - nodeViews: Record, - heightMap: HeightMap, - thresholdBlocks: number, - unit: VirtualizationUnit = "blocks" -): Record { - const wrapped: Record = { ...nodeViews }; - - for (const type of unitTypes(unit)) { - const inner = nodeViews[type]; - wrapped[type] = (node, view, getPos, decorations, innerDecorations) => { - const topLevel = isTopLevel(view, getPos as () => number | undefined); - const materialize = isMaterialized(decorations); - - // Pages only exist once a note is past the threshold, so their presence - // is the gate; blocks are counted directly. - const belowThreshold = - unit === "pages" ? false : view.state.doc.childCount <= thresholdBlocks; - - if (!topLevel || belowThreshold) { - profiler.count("virtualization.nodeView.unvirtualized"); - return inner - ? inner(node, view, getPos, decorations, innerDecorations) - : createMaterializedDefault(node, heightMap); - } - - if (materialize) { - profiler.count("virtualization.nodeView.materializedCreated"); - return inner - ? wrapCustom( - inner(node, view, getPos, decorations, innerDecorations), - node, - heightMap - ) - : createMaterializedDefault(node, heightMap); - } - - return createPlaceholder( - node, - getPos as () => number | undefined, - heightMap - ); - }; - } - - return wrapped; -} diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index ba4717216..d3ebf3cf9 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -83,7 +83,6 @@ import CheckList from "./extensions/check-list/index.js"; import CheckListItem from "./extensions/check-list-item/index.js"; import { Callout } from "./extensions/callout/index.js"; import BlockId from "./extensions/block-id/index.js"; -import { Virtualization } from "./extensions/virtualization/index.js"; import { EditorProfiler } from "./extensions/profiler/index.js"; import { Page, Paging } from "./extensions/paging/index.js"; import { useEditorSearchStore } from "./toolbar/stores/search-store.js"; @@ -141,7 +140,8 @@ export type TiptapOptions = EditorOptions & isMobile?: boolean; doubleSpacedLines?: boolean; enableFontLigatures?: boolean; - virtualization?: VirtualizationMode | boolean; + /** Render only the pages near the viewport. */ + virtualization?: boolean; /** How many top-level blocks make up a page when paging is on. */ pageSize?: number; } & { @@ -198,7 +198,6 @@ const useTiptap = ( }, [closeAllPopups]); const defaultOptions = useMemo>(() => { - const mode = toVirtualizationMode(virtualization); return { enableCoreExtensions: false, editorProps: { @@ -285,12 +284,8 @@ const useTiptap = ( BlockId, PagedDocument, Page, - Virtualization.configure({ - enabled: mode !== "off", - unit: mode === "pages" ? "pages" : "blocks" - }), Paging.configure({ - enabled: mode === "pages", + enabled: !!virtualization, ...(pageSize ? { pageSize } : {}) }), EditorProfiler, @@ -472,16 +467,6 @@ const PagedDocument = TiptapNode.create({ content: "(page | block)+" }); -export type VirtualizationMode = "off" | "blocks" | "pages"; - -export function toVirtualizationMode( - value: VirtualizationMode | boolean | undefined -): VirtualizationMode { - if (value === true) return "blocks"; - if (!value) return "off"; - return value; -} - function hasStyle(element: HTMLElement | string) { const style = (element as HTMLElement).getAttribute("style"); if (!style || style === "font-family: inherit;") return false; @@ -510,7 +495,7 @@ export { getScrollAnchor, restoreScrollAnchor, type ScrollAnchor -} from "./extensions/virtualization/index.js"; +} from "./extensions/paging/index.js"; export * from "./utils/downloader.js"; export { useTiptap, diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index 828a096ec..19346963e 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -69,7 +69,7 @@ msgid "{0} Highlights 🎉" msgstr "{0} Highlights 🎉" #. placeholder {0}: platform === "ios" ? "Apple" : "Google" -#: src/strings.ts:2585 +#: src/strings.ts:2582 msgid "{0} will remind you before your trial ends" msgstr "{0} will remind you before your trial ends" @@ -508,7 +508,7 @@ msgstr "{count, plural, one {Unpublish note} other {Unpublish # notes}}" msgid "{count, plural, one {Version deleted} other {# versions deleted}}" msgstr "{count, plural, one {Version deleted} other {# versions deleted}}" -#: src/strings.ts:2516 +#: src/strings.ts:2513 msgid "{count} characters" msgstr "{count} characters" @@ -516,7 +516,7 @@ msgstr "{count} characters" msgid "{days, plural, one {1 day} other {# days}}" msgstr "{days, plural, one {1 day} other {# days}}" -#: src/strings.ts:2576 +#: src/strings.ts:2573 msgid "{days} days free" msgstr "{days} days free" @@ -580,7 +580,7 @@ msgstr "{notes, plural, one {Export note} other {Export # notes}}" msgid "{percentage}% updating..." msgstr "{percentage}% updating..." -#: src/strings.ts:2560 +#: src/strings.ts:2557 msgid "{plan} plan" msgstr "{plan} plan" @@ -624,19 +624,19 @@ msgstr "{words, plural, other {# selected}}" msgid "#notesnook" msgstr "#notesnook" -#: src/strings.ts:2685 +#: src/strings.ts:2682 msgid "1 day" msgstr "1 day" -#: src/strings.ts:2687 +#: src/strings.ts:2684 msgid "1 month" msgstr "1 month" -#: src/strings.ts:2686 +#: src/strings.ts:2683 msgid "1 week" msgstr "1 week" -#: src/strings.ts:2688 +#: src/strings.ts:2685 msgid "1 year" msgstr "1 year" @@ -656,7 +656,7 @@ msgstr "2FA code is required()" msgid "2FA code sent via {method}" msgstr "2FA code sent via {method}" -#: src/strings.ts:2603 +#: src/strings.ts:2600 msgid "5 year plan (One time purchase)" msgstr "5 year plan (One time purchase)" @@ -732,7 +732,7 @@ msgstr "Add a tag" msgid "Add color" msgstr "Add color" -#: src/strings.ts:2673 +#: src/strings.ts:2670 msgid "Add key" msgstr "Add key" @@ -740,7 +740,7 @@ msgstr "Add key" msgid "Add notebook" msgstr "Add notebook" -#: src/strings.ts:2498 +#: src/strings.ts:2495 msgid "Add notes" msgstr "Add notes" @@ -772,11 +772,11 @@ msgstr "Add tags to multiple notes at once" msgid "Add to dictionary" msgstr "Add to dictionary" -#: src/strings.ts:2636 +#: src/strings.ts:2633 msgid "Add to home" msgstr "Add to home" -#: src/strings.ts:2496 +#: src/strings.ts:2493 msgid "Add to notebook" msgstr "Add to notebook" @@ -788,7 +788,7 @@ msgstr "Add your first note" msgid "Add your first notebook" msgstr "Add your first notebook" -#: src/strings.ts:2639 +#: src/strings.ts:2636 msgid "Adjust the line height of the editor" msgstr "Adjust the line height of the editor" @@ -812,7 +812,7 @@ msgstr "Align left" msgid "Align right" msgstr "Align right" -#: src/strings.ts:2819 +#: src/strings.ts:2816 msgid "Alignment" msgstr "Alignment" @@ -836,7 +836,7 @@ msgstr "All fields are required" msgid "All files" msgstr "All files" -#: src/strings.ts:2768 +#: src/strings.ts:2765 msgid "All items deleted" msgstr "All items deleted" @@ -888,7 +888,7 @@ msgstr "Amount" msgid "An error occurred while migrating your data. You can logout of your account and try to relogin. However this is not recommended as it may result in some data loss if your data was not synced." msgstr "An error occurred while migrating your data. You can logout of your account and try to relogin. However this is not recommended as it may result in some data loss if your data was not synced." -#: src/strings.ts:2597 +#: src/strings.ts:2594 msgid "and" msgstr "and" @@ -900,7 +900,7 @@ msgstr "and " msgid "and get a chance to win free promo codes." msgstr "and get a chance to win free promo codes." -#: src/strings.ts:2553 +#: src/strings.ts:2550 msgid "and much more." msgstr "and much more." @@ -908,27 +908,27 @@ msgstr "and much more." msgid "and we will manually confirm your account." msgstr "and we will manually confirm your account." -#: src/strings.ts:2614 +#: src/strings.ts:2611 msgid "ANNOUNCEMENT" msgstr "ANNOUNCEMENT" -#: src/strings.ts:2703 +#: src/strings.ts:2700 msgid "API key copied to clipboard" msgstr "API key copied to clipboard" -#: src/strings.ts:2681 +#: src/strings.ts:2678 msgid "API key created successfully" msgstr "API key created successfully" -#: src/strings.ts:2708 +#: src/strings.ts:2705 msgid "API key revoked" msgstr "API key revoked" -#: src/strings.ts:2674 +#: src/strings.ts:2671 msgid "API Keys" msgstr "API Keys" -#: src/strings.ts:2695 +#: src/strings.ts:2692 msgid "API Keys Limit Reached" msgstr "API Keys Limit Reached" @@ -979,7 +979,7 @@ msgid "Applying changes" msgstr "Applying changes" #: src/strings.ts:1532 -#: src/strings.ts:2503 +#: src/strings.ts:2500 msgid "Archive" msgstr "Archive" @@ -995,11 +995,11 @@ msgstr "Are you sure you want to clear all logs from {key}?" msgid "Are you sure you want to clear trash?" msgstr "Are you sure you want to clear trash?" -#: src/strings.ts:2767 +#: src/strings.ts:2764 msgid "Are you sure you want to delete all failed inbox items?" msgstr "Are you sure you want to delete all failed inbox items?" -#: src/strings.ts:2740 +#: src/strings.ts:2737 msgid "Are you sure you want to delete this attachment?" msgstr "Are you sure you want to delete this attachment?" @@ -1011,7 +1011,7 @@ msgstr "Are you sure you want to logout and clear all data stored on THIS DEVICE msgid "Are you sure you want to logout from this device? Any unsynced changes will be lost." msgstr "Are you sure you want to logout from this device? Any unsynced changes will be lost." -#: src/strings.ts:2778 +#: src/strings.ts:2775 msgid "Are you sure you want to open this file: {filePath}?" msgstr "Are you sure you want to open this file: {filePath}?" @@ -1023,7 +1023,7 @@ msgstr "Are you sure you want to remove your name?" msgid "Are you sure you want to remove your profile picture?" msgstr "Are you sure you want to remove your profile picture?" -#: src/strings.ts:2707 +#: src/strings.ts:2704 msgid "Are you sure you want to revoke the key \"{name}\"? All inbox actions using this key will stop working immediately." msgstr "Are you sure you want to revoke the key \"{name}\"? All inbox actions using this key will stop working immediately." @@ -1055,7 +1055,7 @@ msgstr "Attach image from URL" msgid "Attached files" msgstr "Attached files" -#: src/strings.ts:2769 +#: src/strings.ts:2766 msgid "Attaching files" msgstr "Attaching files" @@ -1068,7 +1068,7 @@ msgstr "attachment" msgid "Attachment" msgstr "Attachment" -#: src/strings.ts:2741 +#: src/strings.ts:2738 msgid "Attachment deleted" msgstr "Attachment deleted" @@ -1113,11 +1113,11 @@ msgstr "Audios" msgid "Auth server" msgstr "Auth server" -#: src/strings.ts:2701 +#: src/strings.ts:2698 msgid "Authenticate" msgstr "Authenticate" -#: src/strings.ts:2698 +#: src/strings.ts:2695 msgid "Authenticate to view API key" msgstr "Authenticate to view API key" @@ -1157,7 +1157,7 @@ msgstr "Auto save: off" msgid "Auto start on system startup" msgstr "Auto start on system startup" -#: src/strings.ts:2756 +#: src/strings.ts:2753 msgid "Auto-generate keys" msgstr "Auto-generate keys" @@ -1206,7 +1206,7 @@ msgstr "Available on iOS" msgid "Available on iOS & Android" msgstr "Available on iOS & Android" -#: src/strings.ts:2726 +#: src/strings.ts:2723 msgid "Back" msgstr "Back" @@ -1298,11 +1298,11 @@ msgstr "Behavior" msgid "Behaviour" msgstr "Behaviour" -#: src/strings.ts:2490 +#: src/strings.ts:2487 msgid "Believer plan" msgstr "Believer plan" -#: src/strings.ts:2600 +#: src/strings.ts:2597 msgid "Best value" msgstr "Best value" @@ -1314,11 +1314,11 @@ msgstr "Beta" msgid "Bi-directional note link" msgstr "Bi-directional note link" -#: src/strings.ts:2573 +#: src/strings.ts:2570 msgid "billed annually at {price}" msgstr "billed annually at {price}" -#: src/strings.ts:2574 +#: src/strings.ts:2571 msgid "billed monthly at {price}" msgstr "billed monthly at {price}" @@ -1346,10 +1346,6 @@ msgstr "Biometrics authentication failed. Please try again." msgid "Biometrics not enrolled" msgstr "Biometrics not enrolled" -#: src/strings.ts:2483 -msgid "Blocks" -msgstr "Blocks" - #: src/strings.ts:2269 msgid "Bold" msgstr "Bold" @@ -1370,7 +1366,7 @@ msgstr "Bullet list" msgid "By" msgstr "By" -#: src/strings.ts:2595 +#: src/strings.ts:2592 msgid "By joining you agree to our" msgstr "By joining you agree to our" @@ -1382,7 +1378,7 @@ msgstr "By signing up, you agree to our " msgid "Callout" msgstr "Callout" -#: src/strings.ts:2533 +#: src/strings.ts:2530 msgid "Can I cancel my free trial anytime?" msgstr "Can I cancel my free trial anytime?" @@ -1390,11 +1386,11 @@ msgstr "Can I cancel my free trial anytime?" msgid "Cancel" msgstr "Cancel" -#: src/strings.ts:2593 +#: src/strings.ts:2590 msgid "Cancel anytime, subscription auto-renews." msgstr "Cancel anytime, subscription auto-renews." -#: src/strings.ts:2555 +#: src/strings.ts:2552 msgid "Cancel anytime." msgstr "Cancel anytime." @@ -1414,7 +1410,7 @@ msgstr "Cancel subscription" msgid "Cancel upload" msgstr "Cancel upload" -#: src/strings.ts:2697 +#: src/strings.ts:2694 msgid "Cannot create more than 10 api keys at a time. Please revoke some existing keys before creating new ones." msgstr "Cannot create more than 10 api keys at a time. Please revoke some existing keys before creating new ones." @@ -1482,7 +1478,7 @@ msgstr "Change notification sound" msgid "Change password" msgstr "Change password" -#: src/strings.ts:2607 +#: src/strings.ts:2604 msgid "Change plan" msgstr "Change plan" @@ -1518,7 +1514,7 @@ msgstr "Change your primary two-factor authentication method" msgid "Changes from other devices won't be updated in the editor in real-time." msgstr "Changes from other devices won't be updated in the editor in real-time." -#: src/strings.ts:2717 +#: src/strings.ts:2714 msgid "Changing Inbox PGP keys will delete all your unsynced inbox items." msgstr "Changing Inbox PGP keys will delete all your unsynced inbox items." @@ -1526,7 +1522,7 @@ msgstr "Changing Inbox PGP keys will delete all your unsynced inbox items." msgid "Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection." msgstr "Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection." -#: src/strings.ts:2509 +#: src/strings.ts:2506 msgid "Characters" msgstr "Characters" @@ -1598,7 +1594,7 @@ msgstr "Choose from pre-built themes or create your own" msgid "Choose how dates are displayed in the app" msgstr "Choose how dates are displayed in the app" -#: src/strings.ts:2642 +#: src/strings.ts:2639 msgid "Choose how day is displayed in the app" msgstr "Choose how day is displayed in the app" @@ -1614,11 +1610,11 @@ msgstr "Choose how time is displayed in the app" msgid "Choose how you want to secure your notes locally." msgstr "Choose how you want to secure your notes locally." -#: src/strings.ts:2755 +#: src/strings.ts:2752 msgid "Choose how you want to set up your Inbox PGP keys:" msgstr "Choose how you want to set up your Inbox PGP keys:" -#: src/strings.ts:2647 +#: src/strings.ts:2644 msgid "Choose what day to display as the first day of the week" msgstr "Choose what day to display as the first day of the week" @@ -1666,7 +1662,7 @@ msgstr "Clear data & reset account" msgid "Clear default notebook" msgstr "Clear default notebook" -#: src/strings.ts:2809 +#: src/strings.ts:2806 msgid "Clear history" msgstr "Clear history" @@ -1720,7 +1716,7 @@ msgstr "" msgid "Clearing trash will permanently delete all the items in your trash. This action is IRREVERSIBLE." msgstr "Clearing trash will permanently delete all the items in your trash. This action is IRREVERSIBLE." -#: src/strings.ts:2629 +#: src/strings.ts:2626 msgid "Click here to directly claim the promotion." msgstr "Click here to directly claim the promotion." @@ -1740,11 +1736,11 @@ msgstr "Click to remove" msgid "Click to reset {title}" msgstr "Click to reset {title}" -#: src/strings.ts:2637 +#: src/strings.ts:2634 msgid "Click to save" msgstr "Click to save" -#: src/strings.ts:2633 +#: src/strings.ts:2630 msgid "Click to update" msgstr "Click to update" @@ -1752,7 +1748,7 @@ msgstr "Click to update" msgid "Close" msgstr "Close" -#: src/strings.ts:2770 +#: src/strings.ts:2767 msgid "Close ({seconds})" msgstr "Close ({seconds})" @@ -1784,7 +1780,7 @@ msgstr "Close to the left" msgid "Close to the right" msgstr "Close to the right" -#: src/strings.ts:2547 +#: src/strings.ts:2544 msgid "cloud storage space for storing images and files." msgstr "cloud storage space for storing images and files." @@ -1825,7 +1821,7 @@ msgstr "Color scheme" msgid "Color title" msgstr "Color title" -#: src/strings.ts:2802 +#: src/strings.ts:2799 msgid "Colornote password for {filename}" msgstr "Colornote password for {filename}" @@ -1849,7 +1845,7 @@ msgstr "Command palette" msgid "Community" msgstr "Community" -#: src/strings.ts:2567 +#: src/strings.ts:2564 msgid "Compare plans" msgstr "Compare plans" @@ -1869,11 +1865,11 @@ msgstr "Compress images before uploading" msgid "Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases." msgstr "Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases." -#: src/strings.ts:2771 +#: src/strings.ts:2768 msgid "Compressing" msgstr "Compressing" -#: src/strings.ts:2775 +#: src/strings.ts:2772 msgid "Compression failed" msgstr "Compression failed" @@ -1901,7 +1897,7 @@ msgstr "Confirm new password" msgid "Confirm password" msgstr "Confirm password" -#: src/strings.ts:2735 +#: src/strings.ts:2732 msgid "Confirm password required" msgstr "Confirm password required" @@ -1909,7 +1905,7 @@ msgstr "Confirm password required" msgid "Confirm pin" msgstr "Confirm pin" -#: src/strings.ts:2661 +#: src/strings.ts:2658 msgid "Confirmation email sent" msgstr "Confirmation email sent" @@ -1974,7 +1970,7 @@ msgstr "Copy link" msgid "Copy link text" msgstr "Copy link text" -#: src/strings.ts:2794 +#: src/strings.ts:2791 msgid "Copy logs" msgstr "Copy logs" @@ -2046,11 +2042,11 @@ msgstr "Create a tag to group related notes together." msgid "Create account" msgstr "Create account" -#: src/strings.ts:2676 +#: src/strings.ts:2673 msgid "Create API Key" msgstr "Create API Key" -#: src/strings.ts:2693 +#: src/strings.ts:2690 msgid "Create Key" msgstr "Create Key" @@ -2082,7 +2078,7 @@ msgstr "Create vault" msgid "Create your account" msgstr "Create your account" -#: src/strings.ts:2692 +#: src/strings.ts:2689 msgid "Create your first api key to get started." msgstr "Create your first api key to get started." @@ -2090,7 +2086,7 @@ msgstr "Create your first api key to get started." msgid "Created at" msgstr "Created at" -#: src/strings.ts:2712 +#: src/strings.ts:2709 msgid "Created on" msgstr "Created on" @@ -2099,11 +2095,11 @@ msgstr "Created on" msgid "Creating a{0} backup" msgstr "Creating a{0} backup" -#: src/strings.ts:2684 +#: src/strings.ts:2681 msgid "Creating..." msgstr "Creating..." -#: src/strings.ts:2786 +#: src/strings.ts:2783 msgid "Creation date cannot be after last edited date" msgstr "Creation date cannot be after last edited date" @@ -2128,7 +2124,7 @@ msgstr "Current note" msgid "Current password" msgstr "Current password" -#: src/strings.ts:2744 +#: src/strings.ts:2741 msgid "Current password required" msgstr "Current password required" @@ -2225,7 +2221,7 @@ msgstr "Date format" msgid "Date modified" msgstr "Date modified" -#: src/strings.ts:2759 +#: src/strings.ts:2756 msgid "Date synced" msgstr "Date synced" @@ -2237,7 +2233,7 @@ msgstr "Date uploaded" msgid "Day" msgstr "Day" -#: src/strings.ts:2641 +#: src/strings.ts:2638 msgid "Day format" msgstr "Day format" @@ -2294,7 +2290,7 @@ msgstr "Default notebook cleared" msgid "Default screen to open on app launch" msgstr "Default screen to open on app launch" -#: src/strings.ts:2500 +#: src/strings.ts:2497 msgid "Default sidebar tab" msgstr "Default sidebar tab" @@ -2314,15 +2310,15 @@ msgstr "Delete" msgid "Delete account" msgstr "Delete account" -#: src/strings.ts:2765 +#: src/strings.ts:2762 msgid "Delete all" msgstr "Delete all" -#: src/strings.ts:2811 +#: src/strings.ts:2808 msgid "Delete all version history for this note?" msgstr "Delete all version history for this note?" -#: src/strings.ts:2738 +#: src/strings.ts:2735 msgid "Delete attachment" msgstr "Delete attachment" @@ -2334,7 +2330,7 @@ msgstr "Delete collapsed section" msgid "Delete column" msgstr "Delete column" -#: src/strings.ts:2658 +#: src/strings.ts:2655 msgid "Delete data" msgstr "Delete data" @@ -2342,7 +2338,7 @@ msgstr "Delete data" msgid "Delete group" msgstr "Delete group" -#: src/strings.ts:2805 +#: src/strings.ts:2802 msgid "Delete item" msgstr "Delete item" @@ -2394,7 +2390,7 @@ msgstr "Desktop app" msgid "Desktop integration" msgstr "Desktop integration" -#: src/strings.ts:2758 +#: src/strings.ts:2755 msgid "Details" msgstr "Details" @@ -2414,7 +2410,7 @@ msgstr "Disable auto sync" msgid "Disable editor margins" msgstr "Disable editor margins" -#: src/strings.ts:2670 +#: src/strings.ts:2667 msgid "Disable Inbox API" msgstr "Disable Inbox API" @@ -2430,7 +2426,7 @@ msgstr "Disable sync" msgid "Disabled" msgstr "Disabled" -#: src/strings.ts:2672 +#: src/strings.ts:2669 msgid "Disabling will delete all your unsynced inbox items. Additionally, disabling will revoke all existing API keys, they will no longer work. Are you sure?" msgstr "Disabling will delete all your unsynced inbox items. Additionally, disabling will revoke all existing API keys, they will no longer work. Are you sure?" @@ -2579,7 +2575,7 @@ msgstr "Drop the files here" msgid "Drop your files here to attach" msgstr "Drop your files here to attach" -#: src/strings.ts:2577 +#: src/strings.ts:2574 msgid "Due {date}" msgstr "Due {date}" @@ -2587,7 +2583,7 @@ msgstr "Due {date}" msgid "Due date" msgstr "Due date" -#: src/strings.ts:2575 +#: src/strings.ts:2572 msgid "Due today" msgstr "Due today" @@ -2595,7 +2591,7 @@ msgstr "Due today" msgid "Duplicate" msgstr "Duplicate" -#: src/strings.ts:2678 +#: src/strings.ts:2675 msgid "e.g., Todo integration" msgstr "e.g., Todo integration" @@ -2611,7 +2607,7 @@ msgstr "Easy access" msgid "Edit" msgstr "Edit" -#: src/strings.ts:2648 +#: src/strings.ts:2645 msgid "Edit creation date" msgstr "Edit creation date" @@ -2624,7 +2620,7 @@ msgstr "Edit internal link" msgid "Edit link" msgstr "Edit link" -#: src/strings.ts:2493 +#: src/strings.ts:2490 msgid "Edit profile" msgstr "Edit profile" @@ -2645,7 +2641,7 @@ msgstr "Editor" msgid "Editor paging (experimental)" msgstr "Editor paging (experimental)" -#: src/strings.ts:2604 +#: src/strings.ts:2601 msgid "Education plan" msgstr "Education plan" @@ -2705,7 +2701,7 @@ msgstr "Enable app lock" msgid "Enable editor margins" msgstr "Enable editor margins" -#: src/strings.ts:2665 +#: src/strings.ts:2662 msgid "Enable Inbox API" msgstr "Enable Inbox API" @@ -2725,7 +2721,7 @@ msgstr "Enable spell checker" msgid "Enable two-factor authentication to add an extra layer of security to your account." msgstr "Enable two-factor authentication to add an extra layer of security to your account." -#: src/strings.ts:2666 +#: src/strings.ts:2663 msgid "Enable/Disable Inbox API" msgstr "Enable/Disable Inbox API" @@ -2745,7 +2741,7 @@ msgstr "Encrypted backup" msgid "Encrypted, private, secure." msgstr "Encrypted, private, secure." -#: src/strings.ts:2772 +#: src/strings.ts:2769 msgid "Encrypting" msgstr "Encrypting" @@ -2845,7 +2841,7 @@ msgstr "Enter the gift code to redeem your subscription." msgid "Enter the recovery code to continue logging in" msgstr "Enter the recovery code to continue logging in" -#: src/strings.ts:2640 +#: src/strings.ts:2637 msgid "Enter title" msgstr "Enter title" @@ -2857,11 +2853,11 @@ msgstr "Enter verification code sent to your new email" msgid "Enter your new email" msgstr "Enter your new email" -#: src/strings.ts:2799 +#: src/strings.ts:2796 msgid "Enter your PGP private key" msgstr "Enter your PGP private key" -#: src/strings.ts:2798 +#: src/strings.ts:2795 msgid "Enter your PGP public key" msgstr "Enter your PGP public key" @@ -2905,7 +2901,7 @@ msgstr "Errors" msgid "Errors in {count} attachments" msgstr "Errors in {count} attachments" -#: src/strings.ts:2489 +#: src/strings.ts:2486 msgid "Essential plan" msgstr "Essential plan" @@ -2941,7 +2937,7 @@ msgstr "Exit fullscreen" msgid "Expand" msgstr "Expand" -#: src/strings.ts:2485 +#: src/strings.ts:2482 msgid "Expand sidebar" msgstr "Expand sidebar" @@ -2949,39 +2945,39 @@ msgstr "Expand sidebar" msgid "Experience the next level of private note taking\"" msgstr "Experience the next level of private note taking\"" -#: src/strings.ts:2714 +#: src/strings.ts:2711 msgid "Expired" msgstr "Expired" -#: src/strings.ts:2679 +#: src/strings.ts:2676 msgid "Expires in" msgstr "Expires in" -#: src/strings.ts:2715 +#: src/strings.ts:2712 msgid "Expires on" msgstr "Expires on" -#: src/strings.ts:2654 +#: src/strings.ts:2651 msgid "Expiry date" msgstr "Expiry date" -#: src/strings.ts:2783 +#: src/strings.ts:2780 msgid "Expiry date cannot be more than 1 year in the future" msgstr "Expiry date cannot be more than 1 year in the future" -#: src/strings.ts:2781 +#: src/strings.ts:2778 msgid "Expiry date must be in the future" msgstr "Expiry date must be in the future" -#: src/strings.ts:2800 +#: src/strings.ts:2797 msgid "Expiry date removed" msgstr "Expiry date removed" -#: src/strings.ts:2784 +#: src/strings.ts:2781 msgid "Expiry date set" msgstr "Expiry date set" -#: src/strings.ts:2558 +#: src/strings.ts:2555 msgid "Explore all plans" msgstr "Explore all plans" @@ -3006,7 +3002,7 @@ msgstr "Export all notes as pdf, markdown, html or text in a single zip file" msgid "Export as{0}" msgstr "Export as{0}" -#: src/strings.ts:2655 +#: src/strings.ts:2652 msgid "Export CSV" msgstr "Export CSV" @@ -3034,11 +3030,11 @@ msgstr "Faced an issue or have a suggestion? Click here to create a bug report" msgid "Failed" msgstr "Failed" -#: src/strings.ts:2760 +#: src/strings.ts:2757 msgid "Failed inbox items" msgstr "Failed inbox items" -#: src/strings.ts:2659 +#: src/strings.ts:2656 msgid "Failed to attach file" msgstr "Failed to attach file" @@ -3046,12 +3042,12 @@ msgstr "Failed to attach file" msgid "Failed to copy note" msgstr "Failed to copy note" -#: src/strings.ts:2704 +#: src/strings.ts:2701 msgid "Failed to copy to clipboard" msgstr "Failed to copy to clipboard" #. placeholder {0}: message ? `: ${message}` : "" -#: src/strings.ts:2683 +#: src/strings.ts:2680 msgid "Failed to create API key{0}" msgstr "Failed to create API key{0}" @@ -3075,7 +3071,7 @@ msgstr "Failed to download file" msgid "Failed to install theme." msgstr "Failed to install theme." -#: src/strings.ts:2690 +#: src/strings.ts:2687 msgid "Failed to load API keys. Please try again." msgstr "Failed to load API keys. Please try again." @@ -3095,7 +3091,7 @@ msgstr "Failed to register task" msgid "Failed to resolve download url" msgstr "Failed to resolve download url" -#: src/strings.ts:2709 +#: src/strings.ts:2706 msgid "Failed to revoke API key" msgstr "Failed to revoke API key" @@ -3131,7 +3127,7 @@ msgstr "Failed to zip files" msgid "Fallback method for 2FA enabled" msgstr "Fallback method for 2FA enabled" -#: src/strings.ts:2568 +#: src/strings.ts:2565 msgid "FAQs" msgstr "FAQs" @@ -3144,7 +3140,7 @@ msgstr "Favorite" msgid "Favorites" msgstr "Favorites" -#: src/strings.ts:2566 +#: src/strings.ts:2563 msgid "Featured on" msgstr "Featured on" @@ -3172,7 +3168,7 @@ msgstr "File length is 0. Please upload this file again from the attachment mana msgid "File length mismatch. Expected {expectedSize} but got {currentSize} bytes. Please upload this file again from the attachment manager." msgstr "File length mismatch. Expected {expectedSize} but got {currentSize} bytes. Please upload this file again from the attachment manager." -#: src/strings.ts:2780 +#: src/strings.ts:2777 msgid "File links cannot be opened in browsers. Please use the Notesnook desktop app." msgstr "File links cannot be opened in browsers. Please use the Notesnook desktop app." @@ -3180,7 +3176,7 @@ msgstr "File links cannot be opened in browsers. Please use the Notesnook deskto msgid "File mismatch" msgstr "File mismatch" -#: src/strings.ts:2774 +#: src/strings.ts:2771 msgid "File size limit exceeded. Please upgrade your plan." msgstr "File size limit exceeded. Please upgrade your plan." @@ -3200,7 +3196,7 @@ msgstr "Filter attachments by filename, type or hash" msgid "Filter languages" msgstr "Filter languages" -#: src/strings.ts:2626 +#: src/strings.ts:2623 msgid "Finish your purchase in the browser." msgstr "Finish your purchase in the browser." @@ -3244,7 +3240,7 @@ msgstr "Font ligatures" msgid "Font size" msgstr "Font size" -#: src/strings.ts:2540 +#: src/strings.ts:2537 msgid "For a monthly subscription, you can get a refund within 7 days of purchase. For a yearly subscription, we offer a full refund within 14 days of purchase. For a 5 year subscription, you can request a refund within 30 days of purchase." msgstr "For a monthly subscription, you can get a refund within 7 days of purchase. For a yearly subscription, we offer a full refund within 14 days of purchase. For a 5 year subscription, you can request a refund within 30 days of purchase." @@ -3256,7 +3252,7 @@ msgstr "For a more integrated user experience, try out Notesnook for {platform}" msgid "for help regarding how to use the Notesnook Importer." msgstr "for help regarding how to use the Notesnook Importer." -#: src/strings.ts:2549 +#: src/strings.ts:2546 msgid "for locking your notes as soon as app enters background" msgstr "for locking your notes as soon as app enters background" @@ -3294,11 +3290,11 @@ msgstr "" msgid "Forgot password?" msgstr "Forgot password?" -#: src/strings.ts:2583 +#: src/strings.ts:2580 msgid "Free {duration} day trial, cancel any time" msgstr "Free {duration} day trial, cancel any time" -#: src/strings.ts:2487 +#: src/strings.ts:2484 msgid "Free plan" msgstr "Free plan" @@ -3370,7 +3366,7 @@ msgstr "Get Pro" msgid "Get started" msgstr "Get started" -#: src/strings.ts:2546 +#: src/strings.ts:2543 msgid "Get this and so much more:" msgstr "Get this and so much more:" @@ -3386,7 +3382,7 @@ msgstr "Getting information" msgid "Getting recovery codes" msgstr "Getting recovery codes" -#: src/strings.ts:2737 +#: src/strings.ts:2734 msgid "Gift code required" msgstr "Gift code required" @@ -3394,7 +3390,7 @@ msgstr "Gift code required" msgid "GNU GENERAL PUBLIC LICENSE Version 3" msgstr "GNU GENERAL PUBLIC LICENSE Version 3" -#: src/strings.ts:2627 +#: src/strings.ts:2624 msgid "Go back" msgstr "Go back" @@ -3434,7 +3430,7 @@ msgstr "Go to previous page" msgid "Go to web app" msgstr "Go to web app" -#: src/strings.ts:2557 +#: src/strings.ts:2554 msgid "Google will remind you 2 days before your trial ends." msgstr "Google will remind you 2 days before your trial ends." @@ -3462,7 +3458,7 @@ msgstr "Hash copied" msgid "Having problems with sync?" msgstr "Having problems with sync?" -#: src/strings.ts:2572 +#: src/strings.ts:2569 msgid "hdImages" msgstr "hdImages" @@ -3534,7 +3530,7 @@ msgstr "How to fix it?" msgid "hr" msgstr "hr" -#: src/strings.ts:2517 +#: src/strings.ts:2514 msgid "I already have an account" msgstr "I already have an account" @@ -3658,7 +3654,7 @@ msgstr "Import & export" msgid "Import completed" msgstr "Import completed" -#: src/strings.ts:2656 +#: src/strings.ts:2653 msgid "Import CSV" msgstr "Import CSV" @@ -3666,15 +3662,15 @@ msgstr "Import CSV" msgid "import guide" msgstr "import guide" -#: src/strings.ts:2662 +#: src/strings.ts:2659 msgid "Inbox API" msgstr "Inbox API" -#: src/strings.ts:2722 +#: src/strings.ts:2719 msgid "Inbox keys saved" msgstr "Inbox keys saved" -#: src/strings.ts:2667 +#: src/strings.ts:2664 msgid "Inbox PGP Keys" msgstr "Inbox PGP Keys" @@ -3758,15 +3754,15 @@ msgstr "Invalid CORS proxy url" msgid "Invalid email" msgstr "Invalid email" -#: src/strings.ts:2702 +#: src/strings.ts:2699 msgid "Invalid password" msgstr "Invalid password" -#: src/strings.ts:2721 +#: src/strings.ts:2718 msgid "Invalid PGP key pair. Please check your keys and try again." msgstr "Invalid PGP key pair. Please check your keys and try again." -#: src/strings.ts:2728 +#: src/strings.ts:2725 msgid "Invalid recovery key. Make sure to input your account recovery key, not a 2FA recovery code." msgstr "Invalid recovery key. Make sure to input your account recovery key, not a 2FA recovery code." @@ -3799,7 +3795,7 @@ msgstr "item" msgid "Item" msgstr "Item" -#: src/strings.ts:2764 +#: src/strings.ts:2761 msgid "Item deleted" msgstr "Item deleted" @@ -3852,7 +3848,7 @@ msgstr "Keep" msgid "Keep open" msgstr "Keep open" -#: src/strings.ts:2806 +#: src/strings.ts:2803 msgid "Keep screen on" msgstr "Keep screen on" @@ -3860,7 +3856,7 @@ msgstr "Keep screen on" msgid "Keep your data safe" msgstr "Keep your data safe" -#: src/strings.ts:2677 +#: src/strings.ts:2674 msgid "Key name" msgstr "Key name" @@ -3872,7 +3868,7 @@ msgstr "Languages" msgid "Last edited at" msgstr "Last edited at" -#: src/strings.ts:2710 +#: src/strings.ts:2707 msgid "Last used on" msgstr "Last used on" @@ -3932,7 +3928,7 @@ msgstr "Light" msgid "Line {line}, Column {column}" msgstr "Line {line}, Column {column}" -#: src/strings.ts:2638 +#: src/strings.ts:2635 msgid "Line height" msgstr "Line height" @@ -3952,7 +3948,7 @@ msgstr "Link copied" msgid "Link notebooks" msgstr "Link notebooks" -#: src/strings.ts:2494 +#: src/strings.ts:2491 msgid "Link notes" msgstr "Link notes" @@ -4001,7 +3997,7 @@ msgstr "Loading" msgid "Loading {0}, please wait..." msgstr "Loading {0}, please wait..." -#: src/strings.ts:2689 +#: src/strings.ts:2686 msgid "Loading API keys..." msgstr "Loading API keys..." @@ -4061,7 +4057,7 @@ msgstr "Lock note" msgid "Lock the app with a password or pin" msgstr "Lock the app with a password or pin" -#: src/strings.ts:2723 +#: src/strings.ts:2720 msgid "Lock vault after" msgstr "Lock vault after" @@ -4105,7 +4101,7 @@ msgstr "Login failed" msgid "Login required" msgstr "Login required" -#: src/strings.ts:2745 +#: src/strings.ts:2742 msgid "Login required to restore attachments" msgstr "Login required to restore attachments" @@ -4117,7 +4113,7 @@ msgstr "Login successful" msgid "Login to encrypt and sync notes" msgstr "Login to encrypt and sync notes" -#: src/strings.ts:2631 +#: src/strings.ts:2628 msgid "Login to upload attachments. [Read more](https://notesnook.com/help/faqs/login-to-upload-attachments)" msgstr "Login to upload attachments. [Read more](https://notesnook.com/help/faqs/login-to-upload-attachments)" @@ -4213,7 +4209,7 @@ msgstr "Math & formulas" msgid "Maximize" msgstr "Maximize" -#: src/strings.ts:2789 +#: src/strings.ts:2786 msgid "Maximum reminder date is {maxDate}" msgstr "Maximum reminder date is {maxDate}" @@ -4363,7 +4359,7 @@ msgstr "Multi-layer encryption to most important notes" msgid "Name" msgstr "Name" -#: src/strings.ts:2743 +#: src/strings.ts:2740 msgid "Name is required." msgstr "Name is required." @@ -4383,7 +4379,7 @@ msgstr "Never" msgid "Never ask again" msgstr "Never ask again" -#: src/strings.ts:2713 +#: src/strings.ts:2710 msgid "Never expires" msgstr "Never expires" @@ -4395,7 +4391,7 @@ msgstr "Never hesitate to choose privacy" msgid "Never show again" msgstr "Never show again" -#: src/strings.ts:2711 +#: src/strings.ts:2708 msgid "Never used" msgstr "Never used" @@ -4507,7 +4503,7 @@ msgstr "No downloads in progress." msgid "No encryption key found" msgstr "No encryption key found" -#: src/strings.ts:2762 +#: src/strings.ts:2759 msgid "No failed inbox items" msgstr "No failed inbox items" @@ -4527,7 +4523,7 @@ msgstr "No links found" msgid "No note history available for this device." msgstr "No note history available for this device." -#: src/strings.ts:2511 +#: src/strings.ts:2508 msgid "No notebooks selected to move" msgstr "No notebooks selected to move" @@ -4535,7 +4531,7 @@ msgstr "No notebooks selected to move" msgid "No one can view this {type} except you." msgstr "No one can view this {type} except you." -#: src/strings.ts:2634 +#: src/strings.ts:2631 msgid "No password" msgstr "No password" @@ -4593,7 +4589,7 @@ msgstr "Note copied to clipboard" msgid "Note does not exist" msgstr "Note does not exist" -#: src/strings.ts:2787 +#: src/strings.ts:2784 msgid "Note duplicated" msgstr "Note duplicated" @@ -4634,7 +4630,7 @@ msgstr "notebook" msgid "Notebook" msgstr "Notebook" -#: src/strings.ts:2497 +#: src/strings.ts:2494 msgid "Notebook added" msgstr "Notebook added" @@ -4673,15 +4669,15 @@ msgstr "Notes exported as {path} successfully" msgid "notes imported" msgstr "notes imported" -#: src/strings.ts:2561 +#: src/strings.ts:2558 msgid "Notesnook" msgstr "Notesnook" -#: src/strings.ts:2619 +#: src/strings.ts:2616 msgid "Notesnook Circle" msgstr "Notesnook Circle" -#: src/strings.ts:2621 +#: src/strings.ts:2618 msgid "Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom." msgstr "Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom." @@ -4738,7 +4734,6 @@ msgid "of" msgstr "of" #: src/strings.ts:1604 -#: src/strings.ts:2482 msgid "Off" msgstr "Off" @@ -4746,11 +4741,11 @@ msgstr "Off" msgid "Offline" msgstr "Offline" -#: src/strings.ts:2816 +#: src/strings.ts:2813 msgid "Offline mode" msgstr "Offline mode" -#: src/strings.ts:2694 +#: src/strings.ts:2691 msgid "OK" msgstr "OK" @@ -4774,7 +4769,7 @@ msgstr "Oldest - newest" msgid "Once your password is changed, please make sure to save the new account recovery key" msgstr "Once your password is changed, please make sure to save the new account recovery key" -#: src/strings.ts:2579 +#: src/strings.ts:2576 msgid "One time purchase, no auto-renewal" msgstr "One time purchase, no auto-renewal" @@ -4842,7 +4837,7 @@ msgstr "Open source." msgid "Open the two-factor authentication (TOTP) app to view your authentication code." msgstr "Open the two-factor authentication (TOTP) app to view your authentication code." -#: src/strings.ts:2776 +#: src/strings.ts:2773 msgid "Opening local file" msgstr "Opening local file" @@ -4875,15 +4870,11 @@ msgstr "Other" msgid "Outline list" msgstr "Outline list" -#: src/strings.ts:2484 -msgid "Pages" -msgstr "Pages" - #: src/strings.ts:2361 msgid "Paragraph" msgstr "Paragraph" -#: src/strings.ts:2510 +#: src/strings.ts:2507 msgid "Paragraphs" msgstr "Paragraphs" @@ -4927,7 +4918,7 @@ msgstr "Password not entered" msgid "Password protection" msgstr "Password protection" -#: src/strings.ts:2734 +#: src/strings.ts:2731 msgid "Password required" msgstr "Password required" @@ -4959,7 +4950,7 @@ msgstr "Paste image URL here" msgid "Paste without formatting" msgstr "Paste without formatting" -#: src/strings.ts:2580 +#: src/strings.ts:2577 msgid "Pay once and use for 5 years" msgstr "Pay once and use for 5 years" @@ -4971,7 +4962,7 @@ msgstr "Payment method" msgid "PDF is password protected" msgstr "PDF is password protected" -#: src/strings.ts:2796 +#: src/strings.ts:2793 msgid "Permission required to save QR-Code to Gallery" msgstr "Permission required to save QR-Code to Gallery" @@ -5003,11 +4994,11 @@ msgstr "Pin notification" msgid "Pinned" msgstr "Pinned" -#: src/strings.ts:2601 +#: src/strings.ts:2598 msgid "Plan limits" msgstr "Plan limits" -#: src/strings.ts:2561 +#: src/strings.ts:2558 msgid "Plans" msgstr "Plans" @@ -5041,12 +5032,12 @@ msgstr "Please download a backup of your data as your account will be cleared be msgid "Please enable automatic backups to avoid losing important data." msgstr "Please enable automatic backups to avoid losing important data." -#: src/strings.ts:2680 +#: src/strings.ts:2677 msgid "Please enter a key name" msgstr "Please enter a key name" #: src/strings.ts:1514 -#: src/strings.ts:2736 +#: src/strings.ts:2733 msgid "Please enter a valid email address" msgstr "Please enter a valid email address" @@ -5082,7 +5073,7 @@ msgstr "Please enter the password to unlock this note" msgid "Please enter the password to view this version" msgstr "Please enter the password to view this version" -#: src/strings.ts:2700 +#: src/strings.ts:2697 msgid "Please enter your account password to view this API key." msgstr "Please enter your account password to view this API key." @@ -5110,7 +5101,7 @@ msgstr "Please fill all the fields to continue." msgid "Please grant notifications permission to add new reminders." msgstr "Please grant notifications permission to add new reminders." -#: src/strings.ts:2751 +#: src/strings.ts:2748 msgid "Please login to download attachments." msgstr "Please login to download attachments." @@ -5240,7 +5231,7 @@ msgstr "Pressing \"X\" will hide the app in your system tray." msgid "Prevent note title from appearing in tab/window title." msgstr "Prevent note title from appearing in tab/window title." -#: src/strings.ts:2808 +#: src/strings.ts:2805 msgid "Prevent the screen from turning off while the editor is focused." msgstr "Prevent the screen from turning off while the editor is focused." @@ -5284,7 +5275,7 @@ msgstr "Privacy for everyone" msgid "Privacy mode" msgstr "Privacy mode" -#: src/strings.ts:2596 +#: src/strings.ts:2593 msgid "privacy policy" msgstr "privacy policy" @@ -5300,15 +5291,15 @@ msgstr "Privacy Policy. " msgid "private analytics and bug reports." msgstr "private analytics and bug reports." -#: src/strings.ts:2733 +#: src/strings.ts:2730 msgid "Private key is passphrase-protected. Please provide the decrypted key or a key without a passphrase." msgstr "Private key is passphrase-protected. Please provide the decrypted key or a key without a passphrase." -#: src/strings.ts:2753 +#: src/strings.ts:2750 msgid "Private key required" msgstr "Private key required" -#: src/strings.ts:2719 +#: src/strings.ts:2716 msgid "Private Key:" msgstr "Private Key:" @@ -5324,7 +5315,7 @@ msgstr "privileged few" msgid "Pro" msgstr "Pro" -#: src/strings.ts:2488 +#: src/strings.ts:2485 msgid "Pro plan" msgstr "Pro plan" @@ -5356,7 +5347,7 @@ msgstr "Properties" msgid "Protect your notes" msgstr "Protect your notes" -#: src/strings.ts:2757 +#: src/strings.ts:2754 msgid "Provide your own keys" msgstr "Provide your own keys" @@ -5364,11 +5355,11 @@ msgstr "Provide your own keys" msgid "Proxy" msgstr "Proxy" -#: src/strings.ts:2752 +#: src/strings.ts:2749 msgid "Public key required" msgstr "Public key required" -#: src/strings.ts:2718 +#: src/strings.ts:2715 msgid "Public Key:" msgstr "Public Key:" @@ -5380,7 +5371,7 @@ msgstr "Publish" msgid "Publish note" msgstr "Publish note" -#: src/strings.ts:2635 +#: src/strings.ts:2632 msgid "Publish to the web" msgstr "Publish to the web" @@ -5404,7 +5395,7 @@ msgstr "Published note can only be viewed by someone with the password." msgid "Published note link will be automatically deleted once it is viewed by someone." msgstr "Published note link will be automatically deleted once it is viewed by someone." -#: src/strings.ts:2589 +#: src/strings.ts:2586 msgid "Purchase" msgstr "Purchase" @@ -5468,7 +5459,7 @@ msgstr "Read the terms of service" msgid "Reading backup file..." msgstr "Reading backup file..." -#: src/strings.ts:2563 +#: src/strings.ts:2560 msgid "Ready to take the next step on your private note taking journey?" msgstr "Ready to take the next step on your private note taking journey?" @@ -5500,7 +5491,7 @@ msgstr "Recipes" msgid "Recommended" msgstr "Recommended" -#: src/strings.ts:2565 +#: src/strings.ts:2562 msgid "Recommended by Privacy Guides" msgstr "Recommended by Privacy Guides" @@ -5544,7 +5535,7 @@ msgstr "Recovery successful!" msgid "Redeem" msgstr "Redeem" -#: src/strings.ts:2618 +#: src/strings.ts:2615 msgid "Redeem code" msgstr "Redeem code" @@ -5767,7 +5758,7 @@ msgstr "Reset" msgid "Reset account password" msgstr "Reset account password" -#: src/strings.ts:2502 +#: src/strings.ts:2499 msgid "Reset homepage" msgstr "Reset homepage" @@ -5863,7 +5854,7 @@ msgstr "Resubscribe from Playstore" msgid "Resubscribe to Pro" msgstr "Resubscribe to Pro" -#: src/strings.ts:2691 +#: src/strings.ts:2688 msgid "Retry" msgstr "Retry" @@ -5883,7 +5874,7 @@ msgstr "Revoke" msgid "Revoke biometric unlocking" msgstr "Revoke biometric unlocking" -#: src/strings.ts:2705 +#: src/strings.ts:2702 msgid "Revoke Inbox API Key - {name}" msgstr "Revoke Inbox API Key - {name}" @@ -6015,11 +6006,11 @@ msgstr "Scan the QR code with your authenticator app" msgid "School work" msgstr "School work" -#: src/strings.ts:2513 +#: src/strings.ts:2510 msgid "Scroll to bottom" msgstr "Scroll to bottom" -#: src/strings.ts:2512 +#: src/strings.ts:2509 msgid "Scroll to top" msgstr "Scroll to top" @@ -6140,7 +6131,7 @@ msgstr "Select" msgid "Select a backup file from your device to restore backup" msgstr "Select a backup file from your device to restore backup" -#: src/strings.ts:2507 +#: src/strings.ts:2504 msgid "Select a notebook to move this notebook into, or unselect to move it to the root level." msgstr "Select a notebook to move this notebook into, or unselect to move it to the root level." @@ -6200,7 +6191,7 @@ msgstr "Select notebooks" msgid "Select notebooks you want to add note(s) to." msgstr "Select notebooks you want to add note(s) to." -#: src/strings.ts:2495 +#: src/strings.ts:2492 msgid "Select notes to link to \"{title}\"" msgstr "Select notes to link to \"{title}\"" @@ -6212,7 +6203,7 @@ msgstr "Select nth day of the month to repeat the reminder." msgid "Select profile picture" msgstr "Select profile picture" -#: src/strings.ts:2501 +#: src/strings.ts:2498 msgid "Select the default sidebar tab" msgstr "Select the default sidebar tab" @@ -6316,7 +6307,7 @@ msgstr "Set as dark theme" msgid "Set as default" msgstr "Set as default" -#: src/strings.ts:2499 +#: src/strings.ts:2496 msgid "Set as homepage" msgstr "Set as homepage" @@ -6328,7 +6319,7 @@ msgstr "Set as light theme" msgid "Set automatic trash cleanup interval from Settings > Behaviour > Clean trash interval." msgstr "Set automatic trash cleanup interval from Settings > Behaviour > Clean trash interval." -#: src/strings.ts:2652 +#: src/strings.ts:2649 msgid "Set expiry" msgstr "Set expiry" @@ -6402,7 +6393,7 @@ msgstr "Setup app lock password" msgid "Setup app lock pin" msgstr "Setup app lock pin" -#: src/strings.ts:2797 +#: src/strings.ts:2794 msgid "Setup inbox keys" msgstr "Setup inbox keys" @@ -6442,7 +6433,7 @@ msgstr "Share note" msgid "Share Notesnook with friends!" msgstr "Share Notesnook with friends!" -#: src/strings.ts:2664 +#: src/strings.ts:2661 msgid "Share things to Notesnook from anywhere using the Inbox API" msgstr "Share things to Notesnook from anywhere using the Inbox API" @@ -6470,7 +6461,7 @@ msgstr "shortcuts" msgid "Shortcuts" msgstr "Shortcuts" -#: src/strings.ts:2763 +#: src/strings.ts:2760 msgid "Show" msgstr "Show" @@ -6534,7 +6525,7 @@ msgstr "Source code" msgid "Spaces" msgstr "Spaces" -#: src/strings.ts:2611 +#: src/strings.ts:2608 msgid "Special Offer" msgstr "Special Offer" @@ -6590,7 +6581,7 @@ msgstr "Start writing your note..." msgid "Status" msgstr "Status" -#: src/strings.ts:2491 +#: src/strings.ts:2488 msgid "Storage" msgstr "Storage" @@ -6614,11 +6605,11 @@ msgstr "Subgroup added" msgid "Submit" msgstr "Submit" -#: src/strings.ts:2590 +#: src/strings.ts:2587 msgid "Subscribe" msgstr "Subscribe" -#: src/strings.ts:2591 +#: src/strings.ts:2588 msgid "Subscribe and start free trial" msgstr "Subscribe and start free trial" @@ -6738,7 +6729,7 @@ msgstr "Table of contents" msgid "Table settings" msgstr "Table settings" -#: src/strings.ts:2552 +#: src/strings.ts:2549 msgid "tables, outlines, block level note linking" msgstr "tables, outlines, block level note linking" @@ -6877,7 +6868,7 @@ msgstr "Terms of service" msgid "Terms of Service " msgstr "Terms of Service " -#: src/strings.ts:2598 +#: src/strings.ts:2595 msgid "terms of use." msgstr "terms of use." @@ -6905,11 +6896,11 @@ msgstr "Thank you for choosing end-to-end encrypted note taking. Now you can syn msgid "Thank you for reporting!" msgstr "Thank you for reporting!" -#: src/strings.ts:2569 +#: src/strings.ts:2566 msgid "Thank you for subscribing" msgstr "Thank you for subscribing" -#: src/strings.ts:2606 +#: src/strings.ts:2603 msgid "Thank you for the purchase" msgstr "Thank you for the purchase" @@ -6929,7 +6920,7 @@ msgstr "Thank you. You are the proof that privacy always comes first." msgid "The {title} at {url} is not compatible with this client." msgstr "The {title} at {url} is not compatible with this client." -#: src/strings.ts:2651 +#: src/strings.ts:2648 msgid "The incoming note could not be unlocked with the provided password. Enter the correct password for the incoming note" msgstr "The incoming note could not be unlocked with the provided password. Enter the correct password for the incoming note" @@ -6937,11 +6928,11 @@ msgstr "The incoming note could not be unlocked with the provided password. Ente msgid "The information above will be publically available at" msgstr "The information above will be publically available at" -#: src/strings.ts:2625 +#: src/strings.ts:2622 msgid "The Notesnook Circle is exclusive to subscribers. Please consider subscribing to gain access to Notesnook Circle and enjoy additional benefits." msgstr "The Notesnook Circle is exclusive to subscribers. Please consider subscribing to gain access to Notesnook Circle and enjoy additional benefits." -#: src/strings.ts:2804 +#: src/strings.ts:2801 msgid "The password for decrypting the Colornote backup file." msgstr "The password for decrypting the Colornote backup file." @@ -7013,7 +7004,7 @@ msgstr "This error usually means the database file is either corrupt or it could msgid "This error usually means the search index is corrupted." msgstr "This error usually means the search index is corrupted." -#: src/strings.ts:2729 +#: src/strings.ts:2726 msgid "This feature is not available on this plan." msgstr "This feature is not available on this plan." @@ -7021,7 +7012,7 @@ msgstr "This feature is not available on this plan." msgid "This image cannot be previewed" msgstr "This image cannot be previewed" -#: src/strings.ts:2592 +#: src/strings.ts:2589 msgid "This is a one time purchase, no subscription." msgstr "This is a one time purchase, no subscription." @@ -7034,7 +7025,7 @@ msgstr "This may take a while" msgid "This must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co." msgstr "This must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co." -#: src/strings.ts:2657 +#: src/strings.ts:2654 msgid "This note is empty" msgstr "This note is empty" @@ -7095,7 +7086,7 @@ msgstr "Title" msgid "Title format" msgstr "Title format" -#: src/strings.ts:2742 +#: src/strings.ts:2739 msgid "Title is required" msgstr "Title is required" @@ -7148,7 +7139,7 @@ msgstr "Trash gets automatically cleaned up after {days} days" msgid "Trash gets automatically cleaned up daily" msgstr "Trash gets automatically cleaned up daily" -#: src/strings.ts:2559 +#: src/strings.ts:2556 msgid "Try {plan} for free" msgstr "Try {plan} for free" @@ -7160,7 +7151,7 @@ msgstr "Try compact mode to fit more items on screen" msgid "Try free for 14 days" msgstr "Try free for 14 days" -#: src/strings.ts:2545 +#: src/strings.ts:2542 msgid "Try it for free" msgstr "Try it for free" @@ -7216,7 +7207,7 @@ msgstr "Unable to resolve download url" msgid "Unable to send 2FA code" msgstr "Unable to send 2FA code" -#: src/strings.ts:2505 +#: src/strings.ts:2502 msgid "Unarchive" msgstr "Unarchive" @@ -7232,7 +7223,7 @@ msgstr "Undo" msgid "Unfavorite" msgstr "Unfavorite" -#: src/strings.ts:2602 +#: src/strings.ts:2599 msgid "Unlimited" msgstr "Unlimited" @@ -7248,7 +7239,7 @@ msgstr "Unlink notebook" msgid "Unlock" msgstr "Unlock" -#: src/strings.ts:2649 +#: src/strings.ts:2646 msgid "Unlock incoming note" msgstr "Unlock incoming note" @@ -7265,7 +7256,7 @@ msgstr "Unlock note" msgid "Unlock note to delete it" msgstr "Unlock note to delete it" -#: src/strings.ts:2660 +#: src/strings.ts:2657 msgid "Unlock note to merge conflicts" msgstr "Unlock note to merge conflicts" @@ -7317,7 +7308,7 @@ msgstr "Unpublish notes to delete them" msgid "Unregister" msgstr "Unregister" -#: src/strings.ts:2653 +#: src/strings.ts:2650 msgid "Unset expiry" msgstr "Unset expiry" @@ -7338,7 +7329,7 @@ msgstr "Update available" msgid "Update now" msgstr "Update now" -#: src/strings.ts:2519 +#: src/strings.ts:2516 msgid "Upgrade" msgstr "Upgrade" @@ -7346,11 +7337,11 @@ msgstr "Upgrade" msgid "Upgrade now" msgstr "Upgrade now" -#: src/strings.ts:2518 +#: src/strings.ts:2515 msgid "Upgrade plan" msgstr "Upgrade plan" -#: src/strings.ts:2544 +#: src/strings.ts:2541 msgid "Upgrade plan to {plan} to use this feature." msgstr "Upgrade plan to {plan} to use this feature." @@ -7366,7 +7357,7 @@ msgstr "Upgrade to Notesnook Pro to create more tags." msgid "Upgrade to Pro" msgstr "Upgrade to Pro" -#: src/strings.ts:2617 +#: src/strings.ts:2614 msgid "Upgrade to redeem" msgstr "Upgrade to redeem" @@ -7424,7 +7415,7 @@ msgstr "Use a data recovery key to reset your account password." msgid "Use account password" msgstr "Use account password" -#: src/strings.ts:2551 +#: src/strings.ts:2548 msgid "Use advanced note taking features like" msgstr "Use advanced note taking features like" @@ -7498,7 +7489,7 @@ msgstr "Use this if changes from other devices are not appearing on this device. msgid "Use this if changes made on this device are not appearing on other devices. This will overwrite the data on the server with the data from this device." msgstr "Use this if changes made on this device are not appearing on other devices. This will overwrite the data on the server with the data from this device." -#: src/strings.ts:2492 +#: src/strings.ts:2489 msgid "used" msgstr "used" @@ -7510,7 +7501,7 @@ msgstr "User verification failed" msgid "Using {instance} (v{version})" msgstr "Using {instance} (v{version})" -#: src/strings.ts:2818 +#: src/strings.ts:2815 msgid "Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly." msgstr "Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly." @@ -7522,7 +7513,7 @@ msgstr "Using official Notesnook instance" msgid "v{version} available" msgstr "v{version} available" -#: src/strings.ts:2731 +#: src/strings.ts:2728 msgid "Value must be between {min} and {max}" msgstr "Value must be between {min} and {max}" @@ -7582,7 +7573,7 @@ msgstr "Verifying your email" msgid "Version" msgstr "Version" -#: src/strings.ts:2810 +#: src/strings.ts:2807 msgid "Version history cleared" msgstr "Version history cleared" @@ -7598,11 +7589,11 @@ msgstr "Videos" msgid "View all linked notebooks" msgstr "View all linked notebooks" -#: src/strings.ts:2669 +#: src/strings.ts:2666 msgid "View and edit your inbox public/private key pair" msgstr "View and edit your inbox public/private key pair" -#: src/strings.ts:2675 +#: src/strings.ts:2672 msgid "View and manage inbox API keys" msgstr "View and manage inbox API keys" @@ -7610,7 +7601,7 @@ msgstr "View and manage inbox API keys" msgid "View and share debug logs" msgstr "View and share debug logs" -#: src/strings.ts:2761 +#: src/strings.ts:2758 msgid "View failed inbox items and error contexts" msgstr "View failed inbox items and error contexts" @@ -7630,7 +7621,7 @@ msgstr "View source code" msgid "View your recovery codes to recover your account in case you lose access to your two-factor authentication methods." msgstr "View your recovery codes to recover your account in case you lose access to your two-factor authentication methods." -#: src/strings.ts:2632 +#: src/strings.ts:2629 msgid "Views" msgstr "Views" @@ -7654,11 +7645,11 @@ msgstr "We are creating a backup of your data. Please wait..." msgid "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap." msgstr "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap." -#: src/strings.ts:2791 +#: src/strings.ts:2788 msgid "We couldn't load this theme. Please make sure the file is a valid JSON theme file." msgstr "We couldn't load this theme. Please make sure the file is a valid JSON theme file." -#: src/strings.ts:2793 +#: src/strings.ts:2790 msgid "We couldn't load this theme. The file appears to be incomplete or missing required theme properties." msgstr "We couldn't load this theme. The file appears to be incomplete or missing required theme properties." @@ -7666,7 +7657,7 @@ msgstr "We couldn't load this theme. The file appears to be incomplete or missin msgid "We have sent you an email confirmation link. Please check your email inbox. If you cannot find the email, check your spam folder." msgstr "We have sent you an email confirmation link. Please check your email inbox. If you cannot find the email, check your spam folder." -#: src/strings.ts:2530 +#: src/strings.ts:2527 msgid "We require credit card details to fight abuse and to make it seamless for you to upgrade. Your credit card is NOT charged until your free trial ends and your subscription starts. You will be notified via email of the upcoming charge before your trial ends." msgstr "We require credit card details to fight abuse and to make it seamless for you to upgrade. Your credit card is NOT charged until your free trial ends and your subscription starts. You will be notified via email of the upcoming charge before your trial ends." @@ -7682,7 +7673,7 @@ msgstr "We will send you occasional promotional offers & product updates on your msgid "We would love to know what you think!" msgstr "We would love to know what you think!" -#: src/strings.ts:2571 +#: src/strings.ts:2568 msgid "We’re setting up your plan right now. We’ll notify you as soon as everything is ready." msgstr "We’re setting up your plan right now. We’ll notify you as soon as everything is ready." @@ -7706,7 +7697,7 @@ msgstr "Wednesday" msgid "Week" msgstr "Week" -#: src/strings.ts:2645 +#: src/strings.ts:2642 msgid "Week format" msgstr "Week format" @@ -7723,7 +7714,7 @@ msgstr "Welcome back, {email}" msgid "Welcome back!" msgstr "Welcome back!" -#: src/strings.ts:2605 +#: src/strings.ts:2602 msgid "Welcome to Notesnook {plan}" msgstr "Welcome to Notesnook {plan}" @@ -7735,11 +7726,11 @@ msgstr "Welcome to Notesnook Pro" msgid "What do I do if I am not getting the email?" msgstr "What do I do if I am not getting the email?" -#: src/strings.ts:2522 +#: src/strings.ts:2519 msgid "What happens to my data if I switch plans?" msgstr "What happens to my data if I switch plans?" -#: src/strings.ts:2538 +#: src/strings.ts:2535 msgid "What is your refund policy?" msgstr "What is your refund policy?" @@ -7747,7 +7738,7 @@ msgstr "What is your refund policy?" msgid "What went wrong?" msgstr "What went wrong?" -#: src/strings.ts:2528 +#: src/strings.ts:2525 msgid "Why do you need my credit card details for a free trial?" msgstr "Why do you need my credit card details for a free trial?" @@ -7755,7 +7746,7 @@ msgstr "Why do you need my credit card details for a free trial?" msgid "Width" msgstr "Width" -#: src/strings.ts:2508 +#: src/strings.ts:2505 msgid "Words" msgstr "Words" @@ -7792,7 +7783,7 @@ msgstr "Yearly" msgid "Yes" msgstr "Yes" -#: src/strings.ts:2535 +#: src/strings.ts:2532 msgid "Yes, you can cancel your trial anytime. No questions asked." msgstr "Yes, you can cancel your trial anytime. No questions asked." @@ -7800,7 +7791,7 @@ msgstr "Yes, you can cancel your trial anytime. No questions asked." msgid "You also agree to receive marketing emails from us which you can opt-out of from app settings." msgstr "You also agree to receive marketing emails from us which you can opt-out of from app settings." -#: src/strings.ts:2610 +#: src/strings.ts:2607 msgid "You are already subscribed to this plan." msgstr "You are already subscribed to this plan." @@ -7832,7 +7823,7 @@ msgstr "You can also link a note to multiple Notebooks. Tap and hold any noteboo msgid "You can change the theme at any time from Settings or the side menu." msgstr "You can change the theme at any time from Settings or the side menu." -#: src/strings.ts:2613 +#: src/strings.ts:2610 msgid "You can change your subscription plan from the web app" msgstr "You can change your subscription plan from the web app" @@ -7912,7 +7903,7 @@ msgstr "You have been logged out from all other devices." msgid "You have been logged out." msgstr "You have been logged out." -#: src/strings.ts:2609 +#: src/strings.ts:2606 msgid "You have made a one time purchase. To change your plan please contact support." msgstr "You have made a one time purchase. To change your plan please contact support." @@ -7948,7 +7939,7 @@ msgstr "You have unsynced notes. Take a backup or sync your notes to avoid losin msgid "You must log out in order to change/reset server URLs." msgstr "You must log out in order to change/reset server URLs." -#: src/strings.ts:2747 +#: src/strings.ts:2744 msgid "" "You need to login to restore attachments from a backup file. [Read more](https://notesnook.com/help/faqs/login-to-restore-attachments-in-backup).\n" " \n" @@ -8023,7 +8014,7 @@ msgstr "Your account will be permanently deleted along with all your data, login msgid "Your archive" msgstr "Your archive" -#: src/strings.ts:2504 +#: src/strings.ts:2501 msgid "Your archive is empty" msgstr "Your archive is empty" @@ -8043,7 +8034,7 @@ msgstr "Your changes have been saved and will be reflected after the app has ref msgid "Your current 2FA method is {method}" msgstr "Your current 2FA method is {method}" -#: src/strings.ts:2616 +#: src/strings.ts:2613 msgid "Your current subscription does not allow changing plans" msgstr "Your current subscription does not allow changing plans" @@ -8055,7 +8046,7 @@ msgstr "Your data recovery key is basically a hashed version of your password (p msgid "Your data recovery key will be used to decrypt your data" msgstr "Your data recovery key will be used to decrypt your data" -#: src/strings.ts:2524 +#: src/strings.ts:2521 msgid "Your data remains 100% accessible regardless of what plan you are on. That includes your notes, notebooks, attachments, and anything else you might have created." msgstr "Your data remains 100% accessible regardless of what plan you are on. That includes your notes, notebooks, attachments, and anything else you might have created." @@ -8063,7 +8054,7 @@ msgstr "Your data remains 100% accessible regardless of what plan you are on. Th msgid "Your email has been confirmed." msgstr "Your email has been confirmed." -#: src/strings.ts:2515 +#: src/strings.ts:2512 msgid "Your email has been confirmed. You can now securely sync your encrypted notes across all devices." msgstr "Your email has been confirmed. You can now securely sync your encrypted notes across all devices." @@ -8091,7 +8082,7 @@ msgstr "Your free trial has started" msgid "Your free trial is ending soon" msgstr "Your free trial is ending soon" -#: src/strings.ts:2644 +#: src/strings.ts:2641 msgid "Your free trial is on-going. Your subscription will start on {trialExpiryDate}" msgstr "Your free trial is on-going. Your subscription will start on {trialExpiryDate}" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index b228330d6..5395f03f0 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -69,7 +69,7 @@ msgid "{0} Highlights 🎉" msgstr "" #. placeholder {0}: platform === "ios" ? "Apple" : "Google" -#: src/strings.ts:2585 +#: src/strings.ts:2582 msgid "{0} will remind you before your trial ends" msgstr "" @@ -508,7 +508,7 @@ msgstr "" msgid "{count, plural, one {Version deleted} other {# versions deleted}}" msgstr "" -#: src/strings.ts:2516 +#: src/strings.ts:2513 msgid "{count} characters" msgstr "" @@ -516,7 +516,7 @@ msgstr "" msgid "{days, plural, one {1 day} other {# days}}" msgstr "" -#: src/strings.ts:2576 +#: src/strings.ts:2573 msgid "{days} days free" msgstr "" @@ -580,7 +580,7 @@ msgstr "" msgid "{percentage}% updating..." msgstr "" -#: src/strings.ts:2560 +#: src/strings.ts:2557 msgid "{plan} plan" msgstr "" @@ -624,19 +624,19 @@ msgstr "" msgid "#notesnook" msgstr "" -#: src/strings.ts:2685 +#: src/strings.ts:2682 msgid "1 day" msgstr "" -#: src/strings.ts:2687 +#: src/strings.ts:2684 msgid "1 month" msgstr "" -#: src/strings.ts:2686 +#: src/strings.ts:2683 msgid "1 week" msgstr "" -#: src/strings.ts:2688 +#: src/strings.ts:2685 msgid "1 year" msgstr "" @@ -656,7 +656,7 @@ msgstr "" msgid "2FA code sent via {method}" msgstr "" -#: src/strings.ts:2603 +#: src/strings.ts:2600 msgid "5 year plan (One time purchase)" msgstr "" @@ -732,7 +732,7 @@ msgstr "" msgid "Add color" msgstr "" -#: src/strings.ts:2673 +#: src/strings.ts:2670 msgid "Add key" msgstr "" @@ -740,7 +740,7 @@ msgstr "" msgid "Add notebook" msgstr "" -#: src/strings.ts:2498 +#: src/strings.ts:2495 msgid "Add notes" msgstr "" @@ -772,11 +772,11 @@ msgstr "" msgid "Add to dictionary" msgstr "" -#: src/strings.ts:2636 +#: src/strings.ts:2633 msgid "Add to home" msgstr "" -#: src/strings.ts:2496 +#: src/strings.ts:2493 msgid "Add to notebook" msgstr "" @@ -788,7 +788,7 @@ msgstr "" msgid "Add your first notebook" msgstr "" -#: src/strings.ts:2639 +#: src/strings.ts:2636 msgid "Adjust the line height of the editor" msgstr "" @@ -812,7 +812,7 @@ msgstr "" msgid "Align right" msgstr "" -#: src/strings.ts:2819 +#: src/strings.ts:2816 msgid "Alignment" msgstr "" @@ -836,7 +836,7 @@ msgstr "" msgid "All files" msgstr "" -#: src/strings.ts:2768 +#: src/strings.ts:2765 msgid "All items deleted" msgstr "" @@ -888,7 +888,7 @@ msgstr "" msgid "An error occurred while migrating your data. You can logout of your account and try to relogin. However this is not recommended as it may result in some data loss if your data was not synced." msgstr "" -#: src/strings.ts:2597 +#: src/strings.ts:2594 msgid "and" msgstr "" @@ -900,7 +900,7 @@ msgstr "" msgid "and get a chance to win free promo codes." msgstr "" -#: src/strings.ts:2553 +#: src/strings.ts:2550 msgid "and much more." msgstr "" @@ -908,27 +908,27 @@ msgstr "" msgid "and we will manually confirm your account." msgstr "" -#: src/strings.ts:2614 +#: src/strings.ts:2611 msgid "ANNOUNCEMENT" msgstr "" -#: src/strings.ts:2703 +#: src/strings.ts:2700 msgid "API key copied to clipboard" msgstr "" -#: src/strings.ts:2681 +#: src/strings.ts:2678 msgid "API key created successfully" msgstr "" -#: src/strings.ts:2708 +#: src/strings.ts:2705 msgid "API key revoked" msgstr "" -#: src/strings.ts:2674 +#: src/strings.ts:2671 msgid "API Keys" msgstr "" -#: src/strings.ts:2695 +#: src/strings.ts:2692 msgid "API Keys Limit Reached" msgstr "" @@ -979,7 +979,7 @@ msgid "Applying changes" msgstr "" #: src/strings.ts:1532 -#: src/strings.ts:2503 +#: src/strings.ts:2500 msgid "Archive" msgstr "" @@ -995,11 +995,11 @@ msgstr "" msgid "Are you sure you want to clear trash?" msgstr "" -#: src/strings.ts:2767 +#: src/strings.ts:2764 msgid "Are you sure you want to delete all failed inbox items?" msgstr "" -#: src/strings.ts:2740 +#: src/strings.ts:2737 msgid "Are you sure you want to delete this attachment?" msgstr "" @@ -1011,7 +1011,7 @@ msgstr "" msgid "Are you sure you want to logout from this device? Any unsynced changes will be lost." msgstr "" -#: src/strings.ts:2778 +#: src/strings.ts:2775 msgid "Are you sure you want to open this file: {filePath}?" msgstr "" @@ -1023,7 +1023,7 @@ msgstr "" msgid "Are you sure you want to remove your profile picture?" msgstr "" -#: src/strings.ts:2707 +#: src/strings.ts:2704 msgid "Are you sure you want to revoke the key \"{name}\"? All inbox actions using this key will stop working immediately." msgstr "" @@ -1055,7 +1055,7 @@ msgstr "" msgid "Attached files" msgstr "" -#: src/strings.ts:2769 +#: src/strings.ts:2766 msgid "Attaching files" msgstr "" @@ -1068,7 +1068,7 @@ msgstr "" msgid "Attachment" msgstr "" -#: src/strings.ts:2741 +#: src/strings.ts:2738 msgid "Attachment deleted" msgstr "" @@ -1113,11 +1113,11 @@ msgstr "" msgid "Auth server" msgstr "" -#: src/strings.ts:2701 +#: src/strings.ts:2698 msgid "Authenticate" msgstr "" -#: src/strings.ts:2698 +#: src/strings.ts:2695 msgid "Authenticate to view API key" msgstr "" @@ -1157,7 +1157,7 @@ msgstr "" msgid "Auto start on system startup" msgstr "" -#: src/strings.ts:2756 +#: src/strings.ts:2753 msgid "Auto-generate keys" msgstr "" @@ -1206,7 +1206,7 @@ msgstr "" msgid "Available on iOS & Android" msgstr "" -#: src/strings.ts:2726 +#: src/strings.ts:2723 msgid "Back" msgstr "" @@ -1298,11 +1298,11 @@ msgstr "" msgid "Behaviour" msgstr "" -#: src/strings.ts:2490 +#: src/strings.ts:2487 msgid "Believer plan" msgstr "" -#: src/strings.ts:2600 +#: src/strings.ts:2597 msgid "Best value" msgstr "" @@ -1314,11 +1314,11 @@ msgstr "" msgid "Bi-directional note link" msgstr "" -#: src/strings.ts:2573 +#: src/strings.ts:2570 msgid "billed annually at {price}" msgstr "" -#: src/strings.ts:2574 +#: src/strings.ts:2571 msgid "billed monthly at {price}" msgstr "" @@ -1346,10 +1346,6 @@ msgstr "" msgid "Biometrics not enrolled" msgstr "" -#: src/strings.ts:2483 -msgid "Blocks" -msgstr "" - #: src/strings.ts:2269 msgid "Bold" msgstr "" @@ -1370,7 +1366,7 @@ msgstr "" msgid "By" msgstr "" -#: src/strings.ts:2595 +#: src/strings.ts:2592 msgid "By joining you agree to our" msgstr "" @@ -1382,7 +1378,7 @@ msgstr "" msgid "Callout" msgstr "" -#: src/strings.ts:2533 +#: src/strings.ts:2530 msgid "Can I cancel my free trial anytime?" msgstr "" @@ -1390,11 +1386,11 @@ msgstr "" msgid "Cancel" msgstr "" -#: src/strings.ts:2593 +#: src/strings.ts:2590 msgid "Cancel anytime, subscription auto-renews." msgstr "" -#: src/strings.ts:2555 +#: src/strings.ts:2552 msgid "Cancel anytime." msgstr "" @@ -1414,7 +1410,7 @@ msgstr "" msgid "Cancel upload" msgstr "" -#: src/strings.ts:2697 +#: src/strings.ts:2694 msgid "Cannot create more than 10 api keys at a time. Please revoke some existing keys before creating new ones." msgstr "" @@ -1482,7 +1478,7 @@ msgstr "" msgid "Change password" msgstr "" -#: src/strings.ts:2607 +#: src/strings.ts:2604 msgid "Change plan" msgstr "" @@ -1518,7 +1514,7 @@ msgstr "" msgid "Changes from other devices won't be updated in the editor in real-time." msgstr "" -#: src/strings.ts:2717 +#: src/strings.ts:2714 msgid "Changing Inbox PGP keys will delete all your unsynced inbox items." msgstr "" @@ -1526,7 +1522,7 @@ msgstr "" msgid "Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection." msgstr "" -#: src/strings.ts:2509 +#: src/strings.ts:2506 msgid "Characters" msgstr "" @@ -1598,7 +1594,7 @@ msgstr "" msgid "Choose how dates are displayed in the app" msgstr "" -#: src/strings.ts:2642 +#: src/strings.ts:2639 msgid "Choose how day is displayed in the app" msgstr "" @@ -1614,11 +1610,11 @@ msgstr "" msgid "Choose how you want to secure your notes locally." msgstr "" -#: src/strings.ts:2755 +#: src/strings.ts:2752 msgid "Choose how you want to set up your Inbox PGP keys:" msgstr "" -#: src/strings.ts:2647 +#: src/strings.ts:2644 msgid "Choose what day to display as the first day of the week" msgstr "" @@ -1666,7 +1662,7 @@ msgstr "" msgid "Clear default notebook" msgstr "" -#: src/strings.ts:2809 +#: src/strings.ts:2806 msgid "Clear history" msgstr "" @@ -1709,7 +1705,7 @@ msgstr "" msgid "Clearing trash will permanently delete all the items in your trash. This action is IRREVERSIBLE." msgstr "" -#: src/strings.ts:2629 +#: src/strings.ts:2626 msgid "Click here to directly claim the promotion." msgstr "" @@ -1729,11 +1725,11 @@ msgstr "" msgid "Click to reset {title}" msgstr "" -#: src/strings.ts:2637 +#: src/strings.ts:2634 msgid "Click to save" msgstr "" -#: src/strings.ts:2633 +#: src/strings.ts:2630 msgid "Click to update" msgstr "" @@ -1741,7 +1737,7 @@ msgstr "" msgid "Close" msgstr "" -#: src/strings.ts:2770 +#: src/strings.ts:2767 msgid "Close ({seconds})" msgstr "" @@ -1773,7 +1769,7 @@ msgstr "" msgid "Close to the right" msgstr "" -#: src/strings.ts:2547 +#: src/strings.ts:2544 msgid "cloud storage space for storing images and files." msgstr "" @@ -1814,7 +1810,7 @@ msgstr "" msgid "Color title" msgstr "" -#: src/strings.ts:2802 +#: src/strings.ts:2799 msgid "Colornote password for {filename}" msgstr "" @@ -1838,7 +1834,7 @@ msgstr "" msgid "Community" msgstr "" -#: src/strings.ts:2567 +#: src/strings.ts:2564 msgid "Compare plans" msgstr "" @@ -1858,11 +1854,11 @@ msgstr "" msgid "Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases." msgstr "" -#: src/strings.ts:2771 +#: src/strings.ts:2768 msgid "Compressing" msgstr "" -#: src/strings.ts:2775 +#: src/strings.ts:2772 msgid "Compression failed" msgstr "" @@ -1890,7 +1886,7 @@ msgstr "" msgid "Confirm password" msgstr "" -#: src/strings.ts:2735 +#: src/strings.ts:2732 msgid "Confirm password required" msgstr "" @@ -1898,7 +1894,7 @@ msgstr "" msgid "Confirm pin" msgstr "" -#: src/strings.ts:2661 +#: src/strings.ts:2658 msgid "Confirmation email sent" msgstr "" @@ -1963,7 +1959,7 @@ msgstr "" msgid "Copy link text" msgstr "" -#: src/strings.ts:2794 +#: src/strings.ts:2791 msgid "Copy logs" msgstr "" @@ -2035,11 +2031,11 @@ msgstr "" msgid "Create account" msgstr "" -#: src/strings.ts:2676 +#: src/strings.ts:2673 msgid "Create API Key" msgstr "" -#: src/strings.ts:2693 +#: src/strings.ts:2690 msgid "Create Key" msgstr "" @@ -2071,7 +2067,7 @@ msgstr "" msgid "Create your account" msgstr "" -#: src/strings.ts:2692 +#: src/strings.ts:2689 msgid "Create your first api key to get started." msgstr "" @@ -2079,7 +2075,7 @@ msgstr "" msgid "Created at" msgstr "" -#: src/strings.ts:2712 +#: src/strings.ts:2709 msgid "Created on" msgstr "" @@ -2088,11 +2084,11 @@ msgstr "" msgid "Creating a{0} backup" msgstr "" -#: src/strings.ts:2684 +#: src/strings.ts:2681 msgid "Creating..." msgstr "" -#: src/strings.ts:2786 +#: src/strings.ts:2783 msgid "Creation date cannot be after last edited date" msgstr "" @@ -2117,7 +2113,7 @@ msgstr "" msgid "Current password" msgstr "" -#: src/strings.ts:2744 +#: src/strings.ts:2741 msgid "Current password required" msgstr "" @@ -2214,7 +2210,7 @@ msgstr "" msgid "Date modified" msgstr "" -#: src/strings.ts:2759 +#: src/strings.ts:2756 msgid "Date synced" msgstr "" @@ -2226,7 +2222,7 @@ msgstr "" msgid "Day" msgstr "" -#: src/strings.ts:2641 +#: src/strings.ts:2638 msgid "Day format" msgstr "" @@ -2283,7 +2279,7 @@ msgstr "" msgid "Default screen to open on app launch" msgstr "" -#: src/strings.ts:2500 +#: src/strings.ts:2497 msgid "Default sidebar tab" msgstr "" @@ -2303,15 +2299,15 @@ msgstr "" msgid "Delete account" msgstr "" -#: src/strings.ts:2765 +#: src/strings.ts:2762 msgid "Delete all" msgstr "" -#: src/strings.ts:2811 +#: src/strings.ts:2808 msgid "Delete all version history for this note?" msgstr "" -#: src/strings.ts:2738 +#: src/strings.ts:2735 msgid "Delete attachment" msgstr "" @@ -2323,7 +2319,7 @@ msgstr "" msgid "Delete column" msgstr "" -#: src/strings.ts:2658 +#: src/strings.ts:2655 msgid "Delete data" msgstr "" @@ -2331,7 +2327,7 @@ msgstr "" msgid "Delete group" msgstr "" -#: src/strings.ts:2805 +#: src/strings.ts:2802 msgid "Delete item" msgstr "" @@ -2383,7 +2379,7 @@ msgstr "" msgid "Desktop integration" msgstr "" -#: src/strings.ts:2758 +#: src/strings.ts:2755 msgid "Details" msgstr "" @@ -2403,7 +2399,7 @@ msgstr "" msgid "Disable editor margins" msgstr "" -#: src/strings.ts:2670 +#: src/strings.ts:2667 msgid "Disable Inbox API" msgstr "" @@ -2419,7 +2415,7 @@ msgstr "" msgid "Disabled" msgstr "" -#: src/strings.ts:2672 +#: src/strings.ts:2669 msgid "Disabling will delete all your unsynced inbox items. Additionally, disabling will revoke all existing API keys, they will no longer work. Are you sure?" msgstr "" @@ -2568,7 +2564,7 @@ msgstr "" msgid "Drop your files here to attach" msgstr "" -#: src/strings.ts:2577 +#: src/strings.ts:2574 msgid "Due {date}" msgstr "" @@ -2576,7 +2572,7 @@ msgstr "" msgid "Due date" msgstr "" -#: src/strings.ts:2575 +#: src/strings.ts:2572 msgid "Due today" msgstr "" @@ -2584,7 +2580,7 @@ msgstr "" msgid "Duplicate" msgstr "" -#: src/strings.ts:2678 +#: src/strings.ts:2675 msgid "e.g., Todo integration" msgstr "" @@ -2600,7 +2596,7 @@ msgstr "" msgid "Edit" msgstr "" -#: src/strings.ts:2648 +#: src/strings.ts:2645 msgid "Edit creation date" msgstr "" @@ -2613,7 +2609,7 @@ msgstr "" msgid "Edit link" msgstr "" -#: src/strings.ts:2493 +#: src/strings.ts:2490 msgid "Edit profile" msgstr "" @@ -2634,7 +2630,7 @@ msgstr "" msgid "Editor paging (experimental)" msgstr "" -#: src/strings.ts:2604 +#: src/strings.ts:2601 msgid "Education plan" msgstr "" @@ -2694,7 +2690,7 @@ msgstr "" msgid "Enable editor margins" msgstr "" -#: src/strings.ts:2665 +#: src/strings.ts:2662 msgid "Enable Inbox API" msgstr "" @@ -2714,7 +2710,7 @@ msgstr "" msgid "Enable two-factor authentication to add an extra layer of security to your account." msgstr "" -#: src/strings.ts:2666 +#: src/strings.ts:2663 msgid "Enable/Disable Inbox API" msgstr "" @@ -2734,7 +2730,7 @@ msgstr "" msgid "Encrypted, private, secure." msgstr "" -#: src/strings.ts:2772 +#: src/strings.ts:2769 msgid "Encrypting" msgstr "" @@ -2834,7 +2830,7 @@ msgstr "" msgid "Enter the recovery code to continue logging in" msgstr "" -#: src/strings.ts:2640 +#: src/strings.ts:2637 msgid "Enter title" msgstr "" @@ -2846,11 +2842,11 @@ msgstr "" msgid "Enter your new email" msgstr "" -#: src/strings.ts:2799 +#: src/strings.ts:2796 msgid "Enter your PGP private key" msgstr "" -#: src/strings.ts:2798 +#: src/strings.ts:2795 msgid "Enter your PGP public key" msgstr "" @@ -2894,7 +2890,7 @@ msgstr "" msgid "Errors in {count} attachments" msgstr "" -#: src/strings.ts:2489 +#: src/strings.ts:2486 msgid "Essential plan" msgstr "" @@ -2930,7 +2926,7 @@ msgstr "" msgid "Expand" msgstr "" -#: src/strings.ts:2485 +#: src/strings.ts:2482 msgid "Expand sidebar" msgstr "" @@ -2938,39 +2934,39 @@ msgstr "" msgid "Experience the next level of private note taking\"" msgstr "" -#: src/strings.ts:2714 +#: src/strings.ts:2711 msgid "Expired" msgstr "" -#: src/strings.ts:2679 +#: src/strings.ts:2676 msgid "Expires in" msgstr "" -#: src/strings.ts:2715 +#: src/strings.ts:2712 msgid "Expires on" msgstr "" -#: src/strings.ts:2654 +#: src/strings.ts:2651 msgid "Expiry date" msgstr "" -#: src/strings.ts:2783 +#: src/strings.ts:2780 msgid "Expiry date cannot be more than 1 year in the future" msgstr "" -#: src/strings.ts:2781 +#: src/strings.ts:2778 msgid "Expiry date must be in the future" msgstr "" -#: src/strings.ts:2800 +#: src/strings.ts:2797 msgid "Expiry date removed" msgstr "" -#: src/strings.ts:2784 +#: src/strings.ts:2781 msgid "Expiry date set" msgstr "" -#: src/strings.ts:2558 +#: src/strings.ts:2555 msgid "Explore all plans" msgstr "" @@ -2995,7 +2991,7 @@ msgstr "" msgid "Export as{0}" msgstr "" -#: src/strings.ts:2655 +#: src/strings.ts:2652 msgid "Export CSV" msgstr "" @@ -3023,11 +3019,11 @@ msgstr "" msgid "Failed" msgstr "" -#: src/strings.ts:2760 +#: src/strings.ts:2757 msgid "Failed inbox items" msgstr "" -#: src/strings.ts:2659 +#: src/strings.ts:2656 msgid "Failed to attach file" msgstr "" @@ -3035,12 +3031,12 @@ msgstr "" msgid "Failed to copy note" msgstr "" -#: src/strings.ts:2704 +#: src/strings.ts:2701 msgid "Failed to copy to clipboard" msgstr "" #. placeholder {0}: message ? `: ${message}` : "" -#: src/strings.ts:2683 +#: src/strings.ts:2680 msgid "Failed to create API key{0}" msgstr "" @@ -3064,7 +3060,7 @@ msgstr "" msgid "Failed to install theme." msgstr "" -#: src/strings.ts:2690 +#: src/strings.ts:2687 msgid "Failed to load API keys. Please try again." msgstr "" @@ -3084,7 +3080,7 @@ msgstr "" msgid "Failed to resolve download url" msgstr "" -#: src/strings.ts:2709 +#: src/strings.ts:2706 msgid "Failed to revoke API key" msgstr "" @@ -3120,7 +3116,7 @@ msgstr "" msgid "Fallback method for 2FA enabled" msgstr "" -#: src/strings.ts:2568 +#: src/strings.ts:2565 msgid "FAQs" msgstr "" @@ -3133,7 +3129,7 @@ msgstr "" msgid "Favorites" msgstr "" -#: src/strings.ts:2566 +#: src/strings.ts:2563 msgid "Featured on" msgstr "" @@ -3161,7 +3157,7 @@ msgstr "" msgid "File length mismatch. Expected {expectedSize} but got {currentSize} bytes. Please upload this file again from the attachment manager." msgstr "" -#: src/strings.ts:2780 +#: src/strings.ts:2777 msgid "File links cannot be opened in browsers. Please use the Notesnook desktop app." msgstr "" @@ -3169,7 +3165,7 @@ msgstr "" msgid "File mismatch" msgstr "" -#: src/strings.ts:2774 +#: src/strings.ts:2771 msgid "File size limit exceeded. Please upgrade your plan." msgstr "" @@ -3189,7 +3185,7 @@ msgstr "" msgid "Filter languages" msgstr "" -#: src/strings.ts:2626 +#: src/strings.ts:2623 msgid "Finish your purchase in the browser." msgstr "" @@ -3233,7 +3229,7 @@ msgstr "" msgid "Font size" msgstr "" -#: src/strings.ts:2540 +#: src/strings.ts:2537 msgid "For a monthly subscription, you can get a refund within 7 days of purchase. For a yearly subscription, we offer a full refund within 14 days of purchase. For a 5 year subscription, you can request a refund within 30 days of purchase." msgstr "" @@ -3245,7 +3241,7 @@ msgstr "" msgid "for help regarding how to use the Notesnook Importer." msgstr "" -#: src/strings.ts:2549 +#: src/strings.ts:2546 msgid "for locking your notes as soon as app enters background" msgstr "" @@ -3276,11 +3272,11 @@ msgstr "" msgid "Forgot password?" msgstr "" -#: src/strings.ts:2583 +#: src/strings.ts:2580 msgid "Free {duration} day trial, cancel any time" msgstr "" -#: src/strings.ts:2487 +#: src/strings.ts:2484 msgid "Free plan" msgstr "" @@ -3352,7 +3348,7 @@ msgstr "" msgid "Get started" msgstr "" -#: src/strings.ts:2546 +#: src/strings.ts:2543 msgid "Get this and so much more:" msgstr "" @@ -3368,7 +3364,7 @@ msgstr "" msgid "Getting recovery codes" msgstr "" -#: src/strings.ts:2737 +#: src/strings.ts:2734 msgid "Gift code required" msgstr "" @@ -3376,7 +3372,7 @@ msgstr "" msgid "GNU GENERAL PUBLIC LICENSE Version 3" msgstr "" -#: src/strings.ts:2627 +#: src/strings.ts:2624 msgid "Go back" msgstr "" @@ -3416,7 +3412,7 @@ msgstr "" msgid "Go to web app" msgstr "" -#: src/strings.ts:2557 +#: src/strings.ts:2554 msgid "Google will remind you 2 days before your trial ends." msgstr "" @@ -3444,7 +3440,7 @@ msgstr "" msgid "Having problems with sync?" msgstr "" -#: src/strings.ts:2572 +#: src/strings.ts:2569 msgid "hdImages" msgstr "" @@ -3516,7 +3512,7 @@ msgstr "" msgid "hr" msgstr "" -#: src/strings.ts:2517 +#: src/strings.ts:2514 msgid "I already have an account" msgstr "" @@ -3638,7 +3634,7 @@ msgstr "" msgid "Import completed" msgstr "" -#: src/strings.ts:2656 +#: src/strings.ts:2653 msgid "Import CSV" msgstr "" @@ -3646,15 +3642,15 @@ msgstr "" msgid "import guide" msgstr "" -#: src/strings.ts:2662 +#: src/strings.ts:2659 msgid "Inbox API" msgstr "" -#: src/strings.ts:2722 +#: src/strings.ts:2719 msgid "Inbox keys saved" msgstr "" -#: src/strings.ts:2667 +#: src/strings.ts:2664 msgid "Inbox PGP Keys" msgstr "" @@ -3738,15 +3734,15 @@ msgstr "" msgid "Invalid email" msgstr "" -#: src/strings.ts:2702 +#: src/strings.ts:2699 msgid "Invalid password" msgstr "" -#: src/strings.ts:2721 +#: src/strings.ts:2718 msgid "Invalid PGP key pair. Please check your keys and try again." msgstr "" -#: src/strings.ts:2728 +#: src/strings.ts:2725 msgid "Invalid recovery key. Make sure to input your account recovery key, not a 2FA recovery code." msgstr "" @@ -3779,7 +3775,7 @@ msgstr "" msgid "Item" msgstr "" -#: src/strings.ts:2764 +#: src/strings.ts:2761 msgid "Item deleted" msgstr "" @@ -3832,7 +3828,7 @@ msgstr "" msgid "Keep open" msgstr "" -#: src/strings.ts:2806 +#: src/strings.ts:2803 msgid "Keep screen on" msgstr "" @@ -3840,7 +3836,7 @@ msgstr "" msgid "Keep your data safe" msgstr "" -#: src/strings.ts:2677 +#: src/strings.ts:2674 msgid "Key name" msgstr "" @@ -3852,7 +3848,7 @@ msgstr "" msgid "Last edited at" msgstr "" -#: src/strings.ts:2710 +#: src/strings.ts:2707 msgid "Last used on" msgstr "" @@ -3912,7 +3908,7 @@ msgstr "" msgid "Line {line}, Column {column}" msgstr "" -#: src/strings.ts:2638 +#: src/strings.ts:2635 msgid "Line height" msgstr "" @@ -3932,7 +3928,7 @@ msgstr "" msgid "Link notebooks" msgstr "" -#: src/strings.ts:2494 +#: src/strings.ts:2491 msgid "Link notes" msgstr "" @@ -3981,7 +3977,7 @@ msgstr "" msgid "Loading {0}, please wait..." msgstr "" -#: src/strings.ts:2689 +#: src/strings.ts:2686 msgid "Loading API keys..." msgstr "" @@ -4041,7 +4037,7 @@ msgstr "" msgid "Lock the app with a password or pin" msgstr "" -#: src/strings.ts:2723 +#: src/strings.ts:2720 msgid "Lock vault after" msgstr "" @@ -4085,7 +4081,7 @@ msgstr "" msgid "Login required" msgstr "" -#: src/strings.ts:2745 +#: src/strings.ts:2742 msgid "Login required to restore attachments" msgstr "" @@ -4097,7 +4093,7 @@ msgstr "" msgid "Login to encrypt and sync notes" msgstr "" -#: src/strings.ts:2631 +#: src/strings.ts:2628 msgid "Login to upload attachments. [Read more](https://notesnook.com/help/faqs/login-to-upload-attachments)" msgstr "" @@ -4193,7 +4189,7 @@ msgstr "" msgid "Maximize" msgstr "" -#: src/strings.ts:2789 +#: src/strings.ts:2786 msgid "Maximum reminder date is {maxDate}" msgstr "" @@ -4343,7 +4339,7 @@ msgstr "" msgid "Name" msgstr "" -#: src/strings.ts:2743 +#: src/strings.ts:2740 msgid "Name is required." msgstr "" @@ -4363,7 +4359,7 @@ msgstr "" msgid "Never ask again" msgstr "" -#: src/strings.ts:2713 +#: src/strings.ts:2710 msgid "Never expires" msgstr "" @@ -4375,7 +4371,7 @@ msgstr "" msgid "Never show again" msgstr "" -#: src/strings.ts:2711 +#: src/strings.ts:2708 msgid "Never used" msgstr "" @@ -4487,7 +4483,7 @@ msgstr "" msgid "No encryption key found" msgstr "" -#: src/strings.ts:2762 +#: src/strings.ts:2759 msgid "No failed inbox items" msgstr "" @@ -4507,7 +4503,7 @@ msgstr "" msgid "No note history available for this device." msgstr "" -#: src/strings.ts:2511 +#: src/strings.ts:2508 msgid "No notebooks selected to move" msgstr "" @@ -4515,7 +4511,7 @@ msgstr "" msgid "No one can view this {type} except you." msgstr "" -#: src/strings.ts:2634 +#: src/strings.ts:2631 msgid "No password" msgstr "" @@ -4573,7 +4569,7 @@ msgstr "" msgid "Note does not exist" msgstr "" -#: src/strings.ts:2787 +#: src/strings.ts:2784 msgid "Note duplicated" msgstr "" @@ -4614,7 +4610,7 @@ msgstr "" msgid "Notebook" msgstr "" -#: src/strings.ts:2497 +#: src/strings.ts:2494 msgid "Notebook added" msgstr "" @@ -4653,15 +4649,15 @@ msgstr "" msgid "notes imported" msgstr "" -#: src/strings.ts:2561 +#: src/strings.ts:2558 msgid "Notesnook" msgstr "" -#: src/strings.ts:2619 +#: src/strings.ts:2616 msgid "Notesnook Circle" msgstr "" -#: src/strings.ts:2621 +#: src/strings.ts:2618 msgid "Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom." msgstr "" @@ -4712,7 +4708,6 @@ msgid "of" msgstr "" #: src/strings.ts:1604 -#: src/strings.ts:2482 msgid "Off" msgstr "" @@ -4720,11 +4715,11 @@ msgstr "" msgid "Offline" msgstr "" -#: src/strings.ts:2816 +#: src/strings.ts:2813 msgid "Offline mode" msgstr "" -#: src/strings.ts:2694 +#: src/strings.ts:2691 msgid "OK" msgstr "" @@ -4748,7 +4743,7 @@ msgstr "" msgid "Once your password is changed, please make sure to save the new account recovery key" msgstr "" -#: src/strings.ts:2579 +#: src/strings.ts:2576 msgid "One time purchase, no auto-renewal" msgstr "" @@ -4816,7 +4811,7 @@ msgstr "" msgid "Open the two-factor authentication (TOTP) app to view your authentication code." msgstr "" -#: src/strings.ts:2776 +#: src/strings.ts:2773 msgid "Opening local file" msgstr "" @@ -4849,15 +4844,11 @@ msgstr "" msgid "Outline list" msgstr "" -#: src/strings.ts:2484 -msgid "Pages" -msgstr "" - #: src/strings.ts:2361 msgid "Paragraph" msgstr "" -#: src/strings.ts:2510 +#: src/strings.ts:2507 msgid "Paragraphs" msgstr "" @@ -4901,7 +4892,7 @@ msgstr "" msgid "Password protection" msgstr "" -#: src/strings.ts:2734 +#: src/strings.ts:2731 msgid "Password required" msgstr "" @@ -4933,7 +4924,7 @@ msgstr "" msgid "Paste without formatting" msgstr "" -#: src/strings.ts:2580 +#: src/strings.ts:2577 msgid "Pay once and use for 5 years" msgstr "" @@ -4945,7 +4936,7 @@ msgstr "" msgid "PDF is password protected" msgstr "" -#: src/strings.ts:2796 +#: src/strings.ts:2793 msgid "Permission required to save QR-Code to Gallery" msgstr "" @@ -4977,11 +4968,11 @@ msgstr "" msgid "Pinned" msgstr "" -#: src/strings.ts:2601 +#: src/strings.ts:2598 msgid "Plan limits" msgstr "" -#: src/strings.ts:2561 +#: src/strings.ts:2558 msgid "Plans" msgstr "" @@ -5015,12 +5006,12 @@ msgstr "" msgid "Please enable automatic backups to avoid losing important data." msgstr "" -#: src/strings.ts:2680 +#: src/strings.ts:2677 msgid "Please enter a key name" msgstr "" #: src/strings.ts:1514 -#: src/strings.ts:2736 +#: src/strings.ts:2733 msgid "Please enter a valid email address" msgstr "" @@ -5056,7 +5047,7 @@ msgstr "" msgid "Please enter the password to view this version" msgstr "" -#: src/strings.ts:2700 +#: src/strings.ts:2697 msgid "Please enter your account password to view this API key." msgstr "" @@ -5084,7 +5075,7 @@ msgstr "" msgid "Please grant notifications permission to add new reminders." msgstr "" -#: src/strings.ts:2751 +#: src/strings.ts:2748 msgid "Please login to download attachments." msgstr "" @@ -5214,7 +5205,7 @@ msgstr "" msgid "Prevent note title from appearing in tab/window title." msgstr "" -#: src/strings.ts:2808 +#: src/strings.ts:2805 msgid "Prevent the screen from turning off while the editor is focused." msgstr "" @@ -5258,7 +5249,7 @@ msgstr "" msgid "Privacy mode" msgstr "" -#: src/strings.ts:2596 +#: src/strings.ts:2593 msgid "privacy policy" msgstr "" @@ -5274,15 +5265,15 @@ msgstr "" msgid "private analytics and bug reports." msgstr "" -#: src/strings.ts:2733 +#: src/strings.ts:2730 msgid "Private key is passphrase-protected. Please provide the decrypted key or a key without a passphrase." msgstr "" -#: src/strings.ts:2753 +#: src/strings.ts:2750 msgid "Private key required" msgstr "" -#: src/strings.ts:2719 +#: src/strings.ts:2716 msgid "Private Key:" msgstr "" @@ -5298,7 +5289,7 @@ msgstr "" msgid "Pro" msgstr "" -#: src/strings.ts:2488 +#: src/strings.ts:2485 msgid "Pro plan" msgstr "" @@ -5330,7 +5321,7 @@ msgstr "" msgid "Protect your notes" msgstr "" -#: src/strings.ts:2757 +#: src/strings.ts:2754 msgid "Provide your own keys" msgstr "" @@ -5338,11 +5329,11 @@ msgstr "" msgid "Proxy" msgstr "" -#: src/strings.ts:2752 +#: src/strings.ts:2749 msgid "Public key required" msgstr "" -#: src/strings.ts:2718 +#: src/strings.ts:2715 msgid "Public Key:" msgstr "" @@ -5354,7 +5345,7 @@ msgstr "" msgid "Publish note" msgstr "" -#: src/strings.ts:2635 +#: src/strings.ts:2632 msgid "Publish to the web" msgstr "" @@ -5378,7 +5369,7 @@ msgstr "" msgid "Published note link will be automatically deleted once it is viewed by someone." msgstr "" -#: src/strings.ts:2589 +#: src/strings.ts:2586 msgid "Purchase" msgstr "" @@ -5442,7 +5433,7 @@ msgstr "" msgid "Reading backup file..." msgstr "" -#: src/strings.ts:2563 +#: src/strings.ts:2560 msgid "Ready to take the next step on your private note taking journey?" msgstr "" @@ -5474,7 +5465,7 @@ msgstr "" msgid "Recommended" msgstr "" -#: src/strings.ts:2565 +#: src/strings.ts:2562 msgid "Recommended by Privacy Guides" msgstr "" @@ -5518,7 +5509,7 @@ msgstr "" msgid "Redeem" msgstr "" -#: src/strings.ts:2618 +#: src/strings.ts:2615 msgid "Redeem code" msgstr "" @@ -5741,7 +5732,7 @@ msgstr "" msgid "Reset account password" msgstr "" -#: src/strings.ts:2502 +#: src/strings.ts:2499 msgid "Reset homepage" msgstr "" @@ -5837,7 +5828,7 @@ msgstr "" msgid "Resubscribe to Pro" msgstr "" -#: src/strings.ts:2691 +#: src/strings.ts:2688 msgid "Retry" msgstr "" @@ -5857,7 +5848,7 @@ msgstr "" msgid "Revoke biometric unlocking" msgstr "" -#: src/strings.ts:2705 +#: src/strings.ts:2702 msgid "Revoke Inbox API Key - {name}" msgstr "" @@ -5989,11 +5980,11 @@ msgstr "" msgid "School work" msgstr "" -#: src/strings.ts:2513 +#: src/strings.ts:2510 msgid "Scroll to bottom" msgstr "" -#: src/strings.ts:2512 +#: src/strings.ts:2509 msgid "Scroll to top" msgstr "" @@ -6114,7 +6105,7 @@ msgstr "" msgid "Select a backup file from your device to restore backup" msgstr "" -#: src/strings.ts:2507 +#: src/strings.ts:2504 msgid "Select a notebook to move this notebook into, or unselect to move it to the root level." msgstr "" @@ -6174,7 +6165,7 @@ msgstr "" msgid "Select notebooks you want to add note(s) to." msgstr "" -#: src/strings.ts:2495 +#: src/strings.ts:2492 msgid "Select notes to link to \"{title}\"" msgstr "" @@ -6186,7 +6177,7 @@ msgstr "" msgid "Select profile picture" msgstr "" -#: src/strings.ts:2501 +#: src/strings.ts:2498 msgid "Select the default sidebar tab" msgstr "" @@ -6290,7 +6281,7 @@ msgstr "" msgid "Set as default" msgstr "" -#: src/strings.ts:2499 +#: src/strings.ts:2496 msgid "Set as homepage" msgstr "" @@ -6302,7 +6293,7 @@ msgstr "" msgid "Set automatic trash cleanup interval from Settings > Behaviour > Clean trash interval." msgstr "" -#: src/strings.ts:2652 +#: src/strings.ts:2649 msgid "Set expiry" msgstr "" @@ -6368,7 +6359,7 @@ msgstr "" msgid "Setup app lock pin" msgstr "" -#: src/strings.ts:2797 +#: src/strings.ts:2794 msgid "Setup inbox keys" msgstr "" @@ -6408,7 +6399,7 @@ msgstr "" msgid "Share Notesnook with friends!" msgstr "" -#: src/strings.ts:2664 +#: src/strings.ts:2661 msgid "Share things to Notesnook from anywhere using the Inbox API" msgstr "" @@ -6436,7 +6427,7 @@ msgstr "" msgid "Shortcuts" msgstr "" -#: src/strings.ts:2763 +#: src/strings.ts:2760 msgid "Show" msgstr "" @@ -6500,7 +6491,7 @@ msgstr "" msgid "Spaces" msgstr "" -#: src/strings.ts:2611 +#: src/strings.ts:2608 msgid "Special Offer" msgstr "" @@ -6556,7 +6547,7 @@ msgstr "" msgid "Status" msgstr "" -#: src/strings.ts:2491 +#: src/strings.ts:2488 msgid "Storage" msgstr "" @@ -6580,11 +6571,11 @@ msgstr "" msgid "Submit" msgstr "" -#: src/strings.ts:2590 +#: src/strings.ts:2587 msgid "Subscribe" msgstr "" -#: src/strings.ts:2591 +#: src/strings.ts:2588 msgid "Subscribe and start free trial" msgstr "" @@ -6704,7 +6695,7 @@ msgstr "" msgid "Table settings" msgstr "" -#: src/strings.ts:2552 +#: src/strings.ts:2549 msgid "tables, outlines, block level note linking" msgstr "" @@ -6836,7 +6827,7 @@ msgstr "" msgid "Terms of Service " msgstr "" -#: src/strings.ts:2598 +#: src/strings.ts:2595 msgid "terms of use." msgstr "" @@ -6864,11 +6855,11 @@ msgstr "" msgid "Thank you for reporting!" msgstr "" -#: src/strings.ts:2569 +#: src/strings.ts:2566 msgid "Thank you for subscribing" msgstr "" -#: src/strings.ts:2606 +#: src/strings.ts:2603 msgid "Thank you for the purchase" msgstr "" @@ -6888,7 +6879,7 @@ msgstr "" msgid "The {title} at {url} is not compatible with this client." msgstr "" -#: src/strings.ts:2651 +#: src/strings.ts:2648 msgid "The incoming note could not be unlocked with the provided password. Enter the correct password for the incoming note" msgstr "" @@ -6896,11 +6887,11 @@ msgstr "" msgid "The information above will be publically available at" msgstr "" -#: src/strings.ts:2625 +#: src/strings.ts:2622 msgid "The Notesnook Circle is exclusive to subscribers. Please consider subscribing to gain access to Notesnook Circle and enjoy additional benefits." msgstr "<<<<<<< HEAD" -#: src/strings.ts:2804 +#: src/strings.ts:2801 msgid "The password for decrypting the Colornote backup file." msgstr "" @@ -6972,7 +6963,7 @@ msgstr "" msgid "This error usually means the search index is corrupted." msgstr "" -#: src/strings.ts:2729 +#: src/strings.ts:2726 msgid "This feature is not available on this plan." msgstr "" @@ -6980,7 +6971,7 @@ msgstr "" msgid "This image cannot be previewed" msgstr "" -#: src/strings.ts:2592 +#: src/strings.ts:2589 msgid "This is a one time purchase, no subscription." msgstr "" @@ -6993,7 +6984,7 @@ msgstr "" msgid "This must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co." msgstr "" -#: src/strings.ts:2657 +#: src/strings.ts:2654 msgid "This note is empty" msgstr "" @@ -7054,7 +7045,7 @@ msgstr "" msgid "Title format" msgstr "" -#: src/strings.ts:2742 +#: src/strings.ts:2739 msgid "Title is required" msgstr "" @@ -7107,7 +7098,7 @@ msgstr "" msgid "Trash gets automatically cleaned up daily" msgstr "" -#: src/strings.ts:2559 +#: src/strings.ts:2556 msgid "Try {plan} for free" msgstr "" @@ -7119,7 +7110,7 @@ msgstr "" msgid "Try free for 14 days" msgstr "" -#: src/strings.ts:2545 +#: src/strings.ts:2542 msgid "Try it for free" msgstr "" @@ -7175,7 +7166,7 @@ msgstr "" msgid "Unable to send 2FA code" msgstr "" -#: src/strings.ts:2505 +#: src/strings.ts:2502 msgid "Unarchive" msgstr "" @@ -7191,7 +7182,7 @@ msgstr "" msgid "Unfavorite" msgstr "" -#: src/strings.ts:2602 +#: src/strings.ts:2599 msgid "Unlimited" msgstr "" @@ -7207,7 +7198,7 @@ msgstr "" msgid "Unlock" msgstr "" -#: src/strings.ts:2649 +#: src/strings.ts:2646 msgid "Unlock incoming note" msgstr "" @@ -7224,7 +7215,7 @@ msgstr "" msgid "Unlock note to delete it" msgstr "" -#: src/strings.ts:2660 +#: src/strings.ts:2657 msgid "Unlock note to merge conflicts" msgstr "" @@ -7276,7 +7267,7 @@ msgstr "" msgid "Unregister" msgstr "" -#: src/strings.ts:2653 +#: src/strings.ts:2650 msgid "Unset expiry" msgstr "" @@ -7297,7 +7288,7 @@ msgstr "" msgid "Update now" msgstr "" -#: src/strings.ts:2519 +#: src/strings.ts:2516 msgid "Upgrade" msgstr "" @@ -7305,11 +7296,11 @@ msgstr "" msgid "Upgrade now" msgstr "" -#: src/strings.ts:2518 +#: src/strings.ts:2515 msgid "Upgrade plan" msgstr "" -#: src/strings.ts:2544 +#: src/strings.ts:2541 msgid "Upgrade plan to {plan} to use this feature." msgstr "" @@ -7325,7 +7316,7 @@ msgstr "" msgid "Upgrade to Pro" msgstr "" -#: src/strings.ts:2617 +#: src/strings.ts:2614 msgid "Upgrade to redeem" msgstr "" @@ -7383,7 +7374,7 @@ msgstr "" msgid "Use account password" msgstr "" -#: src/strings.ts:2551 +#: src/strings.ts:2548 msgid "Use advanced note taking features like" msgstr "" @@ -7448,7 +7439,7 @@ msgstr "" msgid "Use this if changes made on this device are not appearing on other devices. This will overwrite the data on the server with the data from this device." msgstr "" -#: src/strings.ts:2492 +#: src/strings.ts:2489 msgid "used" msgstr "" @@ -7460,7 +7451,7 @@ msgstr "" msgid "Using {instance} (v{version})" msgstr "" -#: src/strings.ts:2818 +#: src/strings.ts:2815 msgid "Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly." msgstr "" @@ -7472,7 +7463,7 @@ msgstr "" msgid "v{version} available" msgstr "" -#: src/strings.ts:2731 +#: src/strings.ts:2728 msgid "Value must be between {min} and {max}" msgstr "" @@ -7532,7 +7523,7 @@ msgstr "" msgid "Version" msgstr "" -#: src/strings.ts:2810 +#: src/strings.ts:2807 msgid "Version history cleared" msgstr "" @@ -7548,11 +7539,11 @@ msgstr "" msgid "View all linked notebooks" msgstr "" -#: src/strings.ts:2669 +#: src/strings.ts:2666 msgid "View and edit your inbox public/private key pair" msgstr "" -#: src/strings.ts:2675 +#: src/strings.ts:2672 msgid "View and manage inbox API keys" msgstr "" @@ -7560,7 +7551,7 @@ msgstr "" msgid "View and share debug logs" msgstr "" -#: src/strings.ts:2761 +#: src/strings.ts:2758 msgid "View failed inbox items and error contexts" msgstr "" @@ -7580,7 +7571,7 @@ msgstr "" msgid "View your recovery codes to recover your account in case you lose access to your two-factor authentication methods." msgstr "" -#: src/strings.ts:2632 +#: src/strings.ts:2629 msgid "Views" msgstr "" @@ -7604,11 +7595,11 @@ msgstr "" msgid "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap." msgstr "" -#: src/strings.ts:2791 +#: src/strings.ts:2788 msgid "We couldn't load this theme. Please make sure the file is a valid JSON theme file." msgstr "" -#: src/strings.ts:2793 +#: src/strings.ts:2790 msgid "We couldn't load this theme. The file appears to be incomplete or missing required theme properties." msgstr "" @@ -7616,7 +7607,7 @@ msgstr "" msgid "We have sent you an email confirmation link. Please check your email inbox. If you cannot find the email, check your spam folder." msgstr "" -#: src/strings.ts:2530 +#: src/strings.ts:2527 msgid "We require credit card details to fight abuse and to make it seamless for you to upgrade. Your credit card is NOT charged until your free trial ends and your subscription starts. You will be notified via email of the upcoming charge before your trial ends." msgstr "" @@ -7632,7 +7623,7 @@ msgstr "" msgid "We would love to know what you think!" msgstr "" -#: src/strings.ts:2571 +#: src/strings.ts:2568 msgid "We’re setting up your plan right now. We’ll notify you as soon as everything is ready." msgstr "" @@ -7656,7 +7647,7 @@ msgstr "" msgid "Week" msgstr "" -#: src/strings.ts:2645 +#: src/strings.ts:2642 msgid "Week format" msgstr "" @@ -7673,7 +7664,7 @@ msgstr "" msgid "Welcome back!" msgstr "" -#: src/strings.ts:2605 +#: src/strings.ts:2602 msgid "Welcome to Notesnook {plan}" msgstr "" @@ -7685,11 +7676,11 @@ msgstr "" msgid "What do I do if I am not getting the email?" msgstr "" -#: src/strings.ts:2522 +#: src/strings.ts:2519 msgid "What happens to my data if I switch plans?" msgstr "" -#: src/strings.ts:2538 +#: src/strings.ts:2535 msgid "What is your refund policy?" msgstr "" @@ -7697,7 +7688,7 @@ msgstr "" msgid "What went wrong?" msgstr "" -#: src/strings.ts:2528 +#: src/strings.ts:2525 msgid "Why do you need my credit card details for a free trial?" msgstr "" @@ -7705,7 +7696,7 @@ msgstr "" msgid "Width" msgstr "" -#: src/strings.ts:2508 +#: src/strings.ts:2505 msgid "Words" msgstr "" @@ -7742,7 +7733,7 @@ msgstr "" msgid "Yes" msgstr "" -#: src/strings.ts:2535 +#: src/strings.ts:2532 msgid "Yes, you can cancel your trial anytime. No questions asked." msgstr "" @@ -7750,7 +7741,7 @@ msgstr "" msgid "You also agree to receive marketing emails from us which you can opt-out of from app settings." msgstr "" -#: src/strings.ts:2610 +#: src/strings.ts:2607 msgid "You are already subscribed to this plan." msgstr "" @@ -7782,7 +7773,7 @@ msgstr "" msgid "You can change the theme at any time from Settings or the side menu." msgstr "" -#: src/strings.ts:2613 +#: src/strings.ts:2610 msgid "You can change your subscription plan from the web app" msgstr "" @@ -7854,7 +7845,7 @@ msgstr "" msgid "You have been logged out." msgstr "" -#: src/strings.ts:2609 +#: src/strings.ts:2606 msgid "You have made a one time purchase. To change your plan please contact support." msgstr "" @@ -7890,7 +7881,7 @@ msgstr "" msgid "You must log out in order to change/reset server URLs." msgstr "" -#: src/strings.ts:2747 +#: src/strings.ts:2744 msgid "" "You need to login to restore attachments from a backup file. [Read more](https://notesnook.com/help/faqs/login-to-restore-attachments-in-backup).\n" " \n" @@ -7962,7 +7953,7 @@ msgstr "" msgid "Your archive" msgstr "" -#: src/strings.ts:2504 +#: src/strings.ts:2501 msgid "Your archive is empty" msgstr "" @@ -7982,7 +7973,7 @@ msgstr "" msgid "Your current 2FA method is {method}" msgstr "" -#: src/strings.ts:2616 +#: src/strings.ts:2613 msgid "Your current subscription does not allow changing plans" msgstr "" @@ -7994,7 +7985,7 @@ msgstr "" msgid "Your data recovery key will be used to decrypt your data" msgstr "" -#: src/strings.ts:2524 +#: src/strings.ts:2521 msgid "Your data remains 100% accessible regardless of what plan you are on. That includes your notes, notebooks, attachments, and anything else you might have created." msgstr "" @@ -8002,7 +7993,7 @@ msgstr "" msgid "Your email has been confirmed." msgstr "" -#: src/strings.ts:2515 +#: src/strings.ts:2512 msgid "Your email has been confirmed. You can now securely sync your encrypted notes across all devices." msgstr "" @@ -8030,7 +8021,7 @@ msgstr "" msgid "Your free trial is ending soon" msgstr "" -#: src/strings.ts:2644 +#: src/strings.ts:2641 msgid "Your free trial is on-going. Your subscription will start on {trialExpiryDate}" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index 779efb2c0..f58fb537b 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2479,9 +2479,6 @@ Use this if changes from other devices are not appearing on this device. This wi editorVirtualization: () => t`Editor paging (experimental)`, editorVirtualizationDesc: () => t`Only render the part of a note that is currently on screen. Makes very large notes much faster to open, scroll and type in. While this is on, your browser's find (Ctrl+F) and printing will only cover the visible part of a note — use the editor's own search instead. If you face any issues, please turn it off.`, - editorVirtualizationOff: () => t`Off`, - editorVirtualizationBlocks: () => t`Blocks`, - editorVirtualizationPages: () => t`Pages`, expandSidebar: () => t`Expand sidebar`, viewAllLimits: () => `View all limits`, freePlan: () => t`Free plan`,