mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 19:19:34 +02:00
Compare commits
122 Commits
fix-releas
...
3.2.11-and
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
874352e753 | ||
|
|
49d1ab4be5 | ||
|
|
65a80dbd46 | ||
|
|
e55456f492 | ||
|
|
3f9fce0d34 | ||
|
|
2ae094cf1f | ||
|
|
bc224f1674 | ||
|
|
8bb1582cca | ||
|
|
d02125a93c | ||
|
|
d91ce4e936 | ||
|
|
deb902549b | ||
|
|
e41ed07805 | ||
|
|
f6e8dc0a88 | ||
|
|
044dd5ce59 | ||
|
|
b24830666c | ||
|
|
470802c3e2 | ||
|
|
7e0e1eac93 | ||
|
|
58cbf653b2 | ||
|
|
4b35f2ecd2 | ||
|
|
48d9a863ec | ||
|
|
1d616cf68f | ||
|
|
cdf6a4bb5b | ||
|
|
1b3735e2b2 | ||
|
|
850fc6b066 | ||
|
|
8a62070c8e | ||
|
|
8aa31b8380 | ||
|
|
f445a963f9 | ||
|
|
d6e3f27612 | ||
|
|
4688d74edb | ||
|
|
1985007039 | ||
|
|
039c8faabe | ||
|
|
fb1eb234b6 | ||
|
|
7e70cd96b9 | ||
|
|
f9a89783ed | ||
|
|
301ac781d6 | ||
|
|
ce3c98ae9b | ||
|
|
2447b3bea7 | ||
|
|
63888ff70e | ||
|
|
c6e3d1bd47 | ||
|
|
e03e02d702 | ||
|
|
1abf51ae2b | ||
|
|
c41dfa3185 | ||
|
|
52a07da230 | ||
|
|
f572bb1637 | ||
|
|
773ede4b6c | ||
|
|
e4d42c8bac | ||
|
|
5b85067e15 | ||
|
|
0d111f3a79 | ||
|
|
f6b6f2b4b7 | ||
|
|
d55a2aa04d | ||
|
|
0185eea740 | ||
|
|
d44e8874b8 | ||
|
|
e34d33dd61 | ||
|
|
4b2fb80c23 | ||
|
|
3c908ac079 | ||
|
|
563f068948 | ||
|
|
1344b958cb | ||
|
|
32d7cab70c | ||
|
|
1293fa2dec | ||
|
|
8542cbf37e | ||
|
|
62c6fba9a0 | ||
|
|
2d84154226 | ||
|
|
c4e306d569 | ||
|
|
e86f7491ef | ||
|
|
5151d83af9 | ||
|
|
e462cb0596 | ||
|
|
a30c0e4608 | ||
|
|
f90e7982f7 | ||
|
|
40c69d7cba | ||
|
|
23ee50f688 | ||
|
|
a7c857fb71 | ||
|
|
0f6622360c | ||
|
|
578bc6e546 | ||
|
|
70fd038207 | ||
|
|
b30110a48a | ||
|
|
21978c4c1d | ||
|
|
ec48f2350d | ||
|
|
fe1e3bb847 | ||
|
|
69776f26d8 | ||
|
|
ab4092c088 | ||
|
|
4948a8d7de | ||
|
|
0a607bce18 | ||
|
|
e59820e3d6 | ||
|
|
85a2d25c1b | ||
|
|
3bc7fc84f9 | ||
|
|
249ce1ecbb | ||
|
|
be7f8d9916 | ||
|
|
0a0495f1a7 | ||
|
|
2f7afa8556 | ||
|
|
a6d0177153 | ||
|
|
b9e24888e7 | ||
|
|
9017dae664 | ||
|
|
94acc241d9 | ||
|
|
765f3a476d | ||
|
|
2cdb916233 | ||
|
|
5f14261aba | ||
|
|
20138505c4 | ||
|
|
49c09bfd85 | ||
|
|
475edf6732 | ||
|
|
76ea89b668 | ||
|
|
56b3a8df6b | ||
|
|
e02de0c0f6 | ||
|
|
84bfab2a72 | ||
|
|
a6825a2df2 | ||
|
|
9ba58d1a46 | ||
|
|
29a99d3f97 | ||
|
|
a8b6fe78ba | ||
|
|
4abd69b460 | ||
|
|
5c55b49976 | ||
|
|
c932961dd4 | ||
|
|
47357c3e25 | ||
|
|
3ec5a1cf2c | ||
|
|
13d8a7baf7 | ||
|
|
19b1e74d06 | ||
|
|
82de9b9b94 | ||
|
|
867af58695 | ||
|
|
cdd50f0258 | ||
|
|
8c8b92cc4e | ||
|
|
31dd1619f6 | ||
|
|
afb010e3ae | ||
|
|
ddebdcda43 | ||
|
|
29c69418e9 |
22
.github/workflows/core.tests.yml
vendored
22
.github/workflows/core.tests.yml
vendored
@@ -9,7 +9,13 @@ on:
|
||||
- "packages/core/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/core.tests.yml"
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "packages/core/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/core.tests.yml"
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
@@ -17,11 +23,21 @@ on:
|
||||
- "reopened"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
authorize:
|
||||
environment: ${{ github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
'external' || 'internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo true
|
||||
|
||||
test:
|
||||
needs: authorize
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
6
.github/workflows/desktop.tests.yml
vendored
6
.github/workflows/desktop.tests.yml
vendored
@@ -10,6 +10,12 @@ on:
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "app/desktop/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
6
.github/workflows/editor.tests.yml
vendored
6
.github/workflows/editor.tests.yml
vendored
@@ -10,6 +10,12 @@ on:
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/editor.tests.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "packages/editor/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/editor.tests.yml"
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
|
||||
21
.github/workflows/web.tests.yml
vendored
21
.github/workflows/web.tests.yml
vendored
@@ -9,7 +9,13 @@ on:
|
||||
- "apps/web/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/web.tests.yml"
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/web.tests.yml"
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
@@ -17,12 +23,23 @@ on:
|
||||
- "reopened"
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
environment: ${{ github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
'external' || 'internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo true
|
||||
|
||||
build:
|
||||
needs: authorize
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -54,6 +71,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
@@ -24,7 +24,7 @@ Requirements:
|
||||
|
||||
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
|
||||
|
||||
Once you have completed the setup, the first step is to `clone` the monorepo:
|
||||
1. `clone` the monorepo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/streetwriters/notesnook.git
|
||||
@@ -33,19 +33,28 @@ git clone https://github.com/streetwriters/notesnook.git
|
||||
cd notesnook
|
||||
```
|
||||
|
||||
Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
# this might take a while to complete
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you can finally start the desktop app for development:
|
||||
3. Run the webapp for desktop environment:
|
||||
|
||||
```bash
|
||||
cd apps/web
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
4. In a separate terminal session, run the desktop app from the root of the project:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
### Release mode
|
||||
|
||||
To run the app in release mode:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -17,230 +17,226 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test } from "vitest";
|
||||
import { harness } from "./utils.js";
|
||||
import { testCleanup, test } from "./test-override.js";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { Page } from "playwright";
|
||||
import { gt, lt } from "semver";
|
||||
import { describe } from "vitest";
|
||||
|
||||
test("update starts downloading if version is outdated", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async ({ page }) => {
|
||||
await page.waitForSelector("#authForm");
|
||||
test("update starts downloading if version is outdated", async ({
|
||||
ctx: { page },
|
||||
expect,
|
||||
onTestFinished
|
||||
}) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
await page.waitForSelector("#authForm");
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /updating/i })
|
||||
.waitFor({ state: "attached" });
|
||||
},
|
||||
{ version: "3.0.0" }
|
||||
);
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /updating/i })
|
||||
.waitFor({ state: "attached" });
|
||||
});
|
||||
|
||||
test("update is only shown if version is outdated and auto updates are disabled", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false
|
||||
})
|
||||
);
|
||||
test("update is only shown if version is outdated and auto updates are disabled", async ({
|
||||
ctx,
|
||||
expect,
|
||||
onTestFinished
|
||||
}) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.relaunch();
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false
|
||||
})
|
||||
);
|
||||
|
||||
const { page } = ctx;
|
||||
await ctx.relaunch();
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
const { page } = ctx;
|
||||
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await skipDialog(page);
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i })
|
||||
.waitFor({ state: "attached" });
|
||||
});
|
||||
|
||||
describe("update to stable if it is newer", () => {
|
||||
test.scoped({ options: { version: "3.0.0-beta.0" } });
|
||||
test("test", async ({ ctx, expect, onTestFinished }) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
const updateButton = page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i });
|
||||
await updateButton.waitFor({ state: "visible" });
|
||||
const content = await updateButton.textContent();
|
||||
const version = content?.split(" ")?.[0] || "";
|
||||
expect(gt(version, "3.0.0-beta.0")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update is not available if it latest stable version is older", () => {
|
||||
test.scoped({ options: { version: "99.0.0-beta.0" } });
|
||||
test("test", async ({ ctx, expect, onTestFinished }) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
expect(
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i })
|
||||
.waitFor({ state: "attached" });
|
||||
},
|
||||
{ version: "3.0.0" }
|
||||
);
|
||||
.isHidden()
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("update to stable if it is newer", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
describe("downgrade to stable on switching to stable release track", () => {
|
||||
test.scoped({ options: { version: "99.0.0-beta.0" } });
|
||||
test("test", async ({ ctx, expect, onTestFinished }) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.relaunch();
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "stable"
|
||||
})
|
||||
);
|
||||
|
||||
const { page } = ctx;
|
||||
await ctx.relaunch();
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
const { page } = ctx;
|
||||
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await skipDialog(page);
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
await skipDialog(page);
|
||||
|
||||
const updateButton = page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i });
|
||||
await updateButton.waitFor({ state: "visible" });
|
||||
const content = await updateButton.textContent();
|
||||
const version = content?.split(" ")?.[0] || "";
|
||||
t.expect(gt(version, "3.0.0-beta.0")).toBe(true);
|
||||
},
|
||||
{ version: "3.0.0-beta.0" }
|
||||
);
|
||||
});
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
test("update is not available if it latest stable version is older", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
t.expect(
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i })
|
||||
.isHidden()
|
||||
).toBe(true);
|
||||
},
|
||||
{ version: "99.0.0-beta.0" }
|
||||
);
|
||||
});
|
||||
|
||||
test("downgrade to stable on switching to stable release track", async (t) => {
|
||||
await harness(
|
||||
t,
|
||||
async (ctx) => {
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "stable"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
t.expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
const updateButton = page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i });
|
||||
await updateButton.waitFor({ state: "visible" });
|
||||
const content = await updateButton.textContent();
|
||||
const version = content?.split(" ")?.[0] || "";
|
||||
t.expect(lt(version, "99.0.0-beta.0")).toBe(true);
|
||||
},
|
||||
{ version: "99.0.0-beta.0" }
|
||||
);
|
||||
const updateButton = page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i });
|
||||
await updateButton.waitFor({ state: "visible" });
|
||||
const content = await updateButton.textContent();
|
||||
const version = content?.split(" ")?.[0] || "";
|
||||
expect(lt(version, "99.0.0-beta.0")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
async function skipDialog(page: Page) {
|
||||
await page
|
||||
.waitForSelector(".ReactModal__Content", {
|
||||
timeout: 1000
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(async () => {
|
||||
const positiveButton = page.locator(
|
||||
"button[data-role='positive-button']"
|
||||
);
|
||||
const negativeButton = page.locator(
|
||||
"button[data-role='negative-button']"
|
||||
);
|
||||
if (await positiveButton.isVisible()) await positiveButton.click();
|
||||
else if (await negativeButton.isVisible()) await negativeButton.click();
|
||||
});
|
||||
try {
|
||||
const dialog = page.locator(".ReactModal__Content");
|
||||
const positiveButton = dialog.locator(
|
||||
"button[data-role='positive-button']"
|
||||
);
|
||||
const negativeButton = dialog.locator(
|
||||
"button[data-role='negative-button']"
|
||||
);
|
||||
if (await positiveButton.isVisible())
|
||||
await positiveButton.click({ timeout: 1000 });
|
||||
else if (await negativeButton.isVisible())
|
||||
await negativeButton.click({ timeout: 1000 });
|
||||
} catch (e) {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
24
apps/desktop/__tests__/global-setup.ts
Normal file
24
apps/desktop/__tests__/global-setup.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { buildApp } from "./utils";
|
||||
|
||||
export default async function setup() {
|
||||
await buildApp();
|
||||
}
|
||||
@@ -17,22 +17,24 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test } from "vitest";
|
||||
import { harness } from "./utils.js";
|
||||
import assert from "assert";
|
||||
import { testCleanup, test } from "./test-override.js";
|
||||
|
||||
test("make sure app loads", async (t) => {
|
||||
await harness(t, async ({ page }) => {
|
||||
await page.waitForSelector("#authForm");
|
||||
test("make sure app loads", async ({
|
||||
ctx: { page },
|
||||
expect,
|
||||
onTestFinished
|
||||
}) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
assert.ok(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
);
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
});
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
});
|
||||
|
||||
64
apps/desktop/__tests__/test-override.ts
Normal file
64
apps/desktop/__tests__/test-override.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test as vitestTest, TestContext } from "vitest";
|
||||
import { buildAndLaunchApp, Fixtures, TestOptions } from "./utils";
|
||||
import { mkdir, rm } from "fs/promises";
|
||||
import path from "path";
|
||||
import slugify from "slugify";
|
||||
|
||||
export const test = vitestTest.extend<Fixtures>({
|
||||
options: { version: "3.0.0" } as TestOptions,
|
||||
ctx: async ({ options }, use) => {
|
||||
const ctx = await buildAndLaunchApp(options);
|
||||
await use(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
export async function testCleanup(context: TestContext) {
|
||||
const ctx = (context.task.context as unknown as Fixtures).ctx;
|
||||
if (context.task.result?.state === "fail") {
|
||||
await mkdir("test-results", { recursive: true });
|
||||
await ctx.page.screenshot({
|
||||
path: path.join(
|
||||
"test-results",
|
||||
`${slugify(context.task.name)}-${process.platform}-${
|
||||
process.arch
|
||||
}-error.png`
|
||||
)
|
||||
});
|
||||
}
|
||||
await ctx.app.close();
|
||||
await rm(ctx.userDataDir, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
maxRetries: 3,
|
||||
retryDelay: 5000
|
||||
}).catch(() => {
|
||||
/*ignore */
|
||||
});
|
||||
await rm(ctx.outputDir, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
maxRetries: 3,
|
||||
retryDelay: 5000
|
||||
}).catch(() => {
|
||||
/*ignore */
|
||||
});
|
||||
}
|
||||
@@ -18,146 +18,235 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { mkdir } from "fs/promises";
|
||||
import { cp, readFile, writeFile } from "fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "path";
|
||||
import path, { join, resolve } from "path";
|
||||
import { _electron as electron } from "playwright";
|
||||
import slugify from "slugify";
|
||||
import { TaskContext } from "vitest";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true";
|
||||
const productName = `NotesnookTestHarness`;
|
||||
const SOURCE_DIR = resolve("output", productName);
|
||||
|
||||
interface AppContext {
|
||||
export interface AppContext {
|
||||
app: import("playwright").ElectronApplication;
|
||||
page: import("playwright").Page;
|
||||
configPath: string;
|
||||
userDataDir: string;
|
||||
outputDir: string;
|
||||
relaunch: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface TestOptions {
|
||||
export interface TestOptions {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export async function harness(
|
||||
t: TaskContext,
|
||||
cb: (ctx: AppContext) => Promise<void>,
|
||||
options?: TestOptions
|
||||
) {
|
||||
const ctx = await buildAndLaunchApp(options);
|
||||
|
||||
t.onTestFinished(async (result) => {
|
||||
if (result.state === "fail") {
|
||||
await mkdir("test-results", { recursive: true });
|
||||
await ctx.page.screenshot({
|
||||
path: path.join(
|
||||
"test-results",
|
||||
`${slugify(t.task.name)}-${process.platform}-${
|
||||
process.arch
|
||||
}-error.png`
|
||||
)
|
||||
});
|
||||
}
|
||||
await ctx.app.close();
|
||||
});
|
||||
|
||||
await cb(ctx);
|
||||
export interface Fixtures {
|
||||
options: TestOptions;
|
||||
ctx: AppContext;
|
||||
}
|
||||
|
||||
async function buildAndLaunchApp(options?: TestOptions): Promise<AppContext> {
|
||||
const productName = makeid(10);
|
||||
const executablePath = await buildApp({ ...options, productName });
|
||||
const { app, page, configPath } = await launchApp(executablePath);
|
||||
export async function buildAndLaunchApp(
|
||||
options?: TestOptions
|
||||
): Promise<AppContext> {
|
||||
const productName = `notesnooktest${makeid(10)}`;
|
||||
const outputDir = path.join("test-artifacts", `${productName}-output`);
|
||||
const executablePath = await copyBuild({
|
||||
...options,
|
||||
outputDir
|
||||
});
|
||||
const { app, page, configPath, userDataDir } = await launchApp(
|
||||
executablePath,
|
||||
productName
|
||||
);
|
||||
const ctx: AppContext = {
|
||||
app,
|
||||
page,
|
||||
configPath,
|
||||
userDataDir,
|
||||
outputDir,
|
||||
relaunch: async () => {
|
||||
const { app, page, configPath } = await launchApp(executablePath);
|
||||
const { app, page, configPath, userDataDir } = await launchApp(
|
||||
executablePath,
|
||||
productName
|
||||
);
|
||||
ctx.app = app;
|
||||
ctx.page = page;
|
||||
ctx.userDataDir = userDataDir;
|
||||
ctx.configPath = configPath;
|
||||
}
|
||||
};
|
||||
return ctx;
|
||||
}
|
||||
|
||||
async function launchApp(executablePath: string) {
|
||||
async function launchApp(executablePath: string, packageName: string) {
|
||||
const userDataDir = resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"test-artifacts",
|
||||
"user_data_dirs",
|
||||
packageName
|
||||
);
|
||||
const app = await electron.launch({
|
||||
executablePath,
|
||||
args: IS_DEBUG ? [] : ["--hidden"],
|
||||
env:
|
||||
process.platform === "linux"
|
||||
env: {
|
||||
...(process.platform === "linux"
|
||||
? {
|
||||
...(process.env as Record<string, string>),
|
||||
APPIMAGE: "true"
|
||||
}
|
||||
: (process.env as Record<string, string>)
|
||||
: (process.env as Record<string, string>)),
|
||||
CUSTOM_USER_DATA_DIR: userDataDir
|
||||
}
|
||||
});
|
||||
|
||||
const page = await app.firstWindow();
|
||||
|
||||
const userDataDirectory = await app.evaluate((a) => {
|
||||
return a.app.getPath("userData");
|
||||
});
|
||||
const configPath = path.join(userDataDirectory, "config.json");
|
||||
const configPath = path.join(userDataDir, "UserData", "config.json");
|
||||
return {
|
||||
app,
|
||||
page,
|
||||
configPath
|
||||
configPath,
|
||||
userDataDir
|
||||
};
|
||||
}
|
||||
|
||||
async function buildApp({
|
||||
let MAX_RETRIES = 3;
|
||||
export async function buildApp(version?: string) {
|
||||
if (!existsSync(SOURCE_DIR)) {
|
||||
const args = [
|
||||
"electron-builder",
|
||||
"--dir",
|
||||
`--${process.arch}`,
|
||||
`--config electron-builder.config.js`,
|
||||
`--c.extraMetadata.productName=${productName}`,
|
||||
`--c.compression=store`,
|
||||
"--publish=never"
|
||||
];
|
||||
if (version) args.push(`--c.extraMetadata.version=${version}`);
|
||||
try {
|
||||
execSync(`npx ${args.join(" ")}`, {
|
||||
stdio: IS_DEBUG ? "inherit" : "ignore",
|
||||
env: {
|
||||
...process.env,
|
||||
NOTESNOOK_STAGING: "true",
|
||||
NN_PRODUCT_NAME: productName,
|
||||
NN_APP_ID: `com.notesnook.test.${productName}`,
|
||||
NN_OUTPUT_DIR: SOURCE_DIR
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (--MAX_RETRIES) {
|
||||
console.log("retrying...");
|
||||
return await buildApp(version);
|
||||
} else throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyBuild({
|
||||
version,
|
||||
productName
|
||||
outputDir
|
||||
}: {
|
||||
version?: string;
|
||||
productName: string;
|
||||
outputDir: string;
|
||||
}) {
|
||||
const buildRoot = path.join("test-artifacts", `${productName}-build`);
|
||||
const output = path.join("test-artifacts", `${productName}-output`);
|
||||
execSync(`npm run release -- --root ${buildRoot} --skip-tsc-build`, {
|
||||
stdio: IS_DEBUG ? "inherit" : "ignore"
|
||||
});
|
||||
return process.platform === "win32"
|
||||
? await makeBuildCopyWindows(outputDir, productName, version)
|
||||
: process.platform === "darwin"
|
||||
? await makeBuildCopyMacOS(outputDir, productName, version)
|
||||
: await makeBuildCopyLinux(outputDir, productName, version);
|
||||
}
|
||||
|
||||
const args = [
|
||||
`--config electron-builder.config.js`,
|
||||
`--c.extraMetadata.productName=${productName}`,
|
||||
"--publish=never"
|
||||
];
|
||||
if (version) args.push(`--c.extraMetadata.version=${version}`);
|
||||
|
||||
execSync(`npx electron-builder --dir --${process.arch} ${args.join(" ")}`, {
|
||||
stdio: IS_DEBUG ? "inherit" : "ignore",
|
||||
env: {
|
||||
...process.env,
|
||||
NOTESNOOK_STAGING: "true",
|
||||
NN_BUILD_ROOT: buildRoot,
|
||||
NN_PRODUCT_NAME: productName,
|
||||
NN_APP_ID: `com.notesnook.test.${productName}`,
|
||||
NN_OUTPUT_DIR: output
|
||||
}
|
||||
});
|
||||
|
||||
return path.join(
|
||||
async function makeBuildCopyLinux(
|
||||
outputDir: string,
|
||||
productName: string,
|
||||
version?: string
|
||||
) {
|
||||
const platformDir =
|
||||
process.arch === "arm64" ? "linux-arm64-unpacked" : "linux-unpacked";
|
||||
const appDir = await makeBuildCopy(
|
||||
outputDir,
|
||||
platformDir,
|
||||
"resources",
|
||||
version
|
||||
);
|
||||
return resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
output,
|
||||
process.platform === "linux"
|
||||
? process.arch === "arm64"
|
||||
? "linux-arm64-unpacked"
|
||||
: "linux-unpacked"
|
||||
: process.platform === "darwin"
|
||||
? process.arch === "arm64"
|
||||
? `mac-arm64/${productName}.app/Contents/MacOS/`
|
||||
: `mac/${productName}.app/Contents/MacOS/`
|
||||
: "win-unpacked",
|
||||
process.platform === "win32" ? `${productName}.exe` : productName
|
||||
appDir,
|
||||
productName.toLowerCase().replace(/\s+/g, "-")
|
||||
);
|
||||
}
|
||||
|
||||
async function makeBuildCopyWindows(
|
||||
outputDir: string,
|
||||
productName: string,
|
||||
version?: string
|
||||
) {
|
||||
const platformDir =
|
||||
process.arch === "arm64" ? "win-arm64-unpacked" : "win-unpacked";
|
||||
const appDir = await makeBuildCopy(
|
||||
outputDir,
|
||||
platformDir,
|
||||
"resources",
|
||||
version
|
||||
);
|
||||
return resolve(__dirname, "..", appDir, `${productName}.exe`);
|
||||
}
|
||||
|
||||
async function makeBuildCopyMacOS(
|
||||
outputDir: string,
|
||||
productName: string,
|
||||
version?: string
|
||||
) {
|
||||
const platformDir = process.arch === "arm64" ? "mac-arm64" : "mac";
|
||||
const appDir = await makeBuildCopy(
|
||||
outputDir,
|
||||
platformDir,
|
||||
join(`${productName}.app`, "Contents", "Resources"),
|
||||
version
|
||||
);
|
||||
return resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
appDir,
|
||||
`${productName}.app`,
|
||||
"Contents",
|
||||
"MacOS",
|
||||
productName
|
||||
);
|
||||
}
|
||||
|
||||
async function makeBuildCopy(
|
||||
outputDir: string,
|
||||
platformDir: string,
|
||||
resourcesDir: string,
|
||||
version?: string
|
||||
) {
|
||||
const appDir = outputDir;
|
||||
await cp(join(SOURCE_DIR, platformDir), outputDir, {
|
||||
recursive: true,
|
||||
preserveTimestamps: true,
|
||||
verbatimSymlinks: true,
|
||||
dereference: false,
|
||||
force: true
|
||||
});
|
||||
|
||||
const packageJsonPath = join(appDir, resourcesDir, "app", "package.json");
|
||||
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
|
||||
if (version) {
|
||||
packageJson.version = version;
|
||||
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||||
}
|
||||
|
||||
return appDir;
|
||||
}
|
||||
|
||||
function makeid(length: number) {
|
||||
let result = "";
|
||||
const characters =
|
||||
|
||||
1596
apps/desktop/package-lock.json
generated
1596
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,9 @@
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"sideEffects": [
|
||||
"src/overrides.ts"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"require": {
|
||||
@@ -55,7 +57,7 @@
|
||||
"slugify": "1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^7.8.0",
|
||||
"vitest": "2.1.8"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./overrides";
|
||||
import { app, BrowserWindow, nativeTheme, shell } from "electron";
|
||||
import { isDevelopment } from "./utils";
|
||||
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
|
||||
|
||||
35
apps/desktop/src/overrides.ts
Normal file
35
apps/desktop/src/overrides.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { app } from "electron";
|
||||
import path from "path";
|
||||
|
||||
if (process.env.CUSTOM_USER_DATA_DIR) {
|
||||
app.setPath(
|
||||
"appData",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "AppData")
|
||||
);
|
||||
app.setPath(
|
||||
"userData",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "UserData")
|
||||
);
|
||||
app.setPath(
|
||||
"documents",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "Documents")
|
||||
);
|
||||
}
|
||||
@@ -21,11 +21,13 @@ import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: process.env.CI ? 120 * 1000 : 120 * 1000,
|
||||
testTimeout: 120 * 1000,
|
||||
hookTimeout: 120 * 1000,
|
||||
sequence: {
|
||||
concurrent: true,
|
||||
shuffle: true
|
||||
},
|
||||
globalSetup: "./__tests__/global-setup.ts",
|
||||
dir: "./__tests__/",
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
|
||||
@@ -137,6 +137,7 @@ export async function deleteFile(
|
||||
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
@@ -221,6 +222,7 @@ export async function deleteCacheFileByName(name: string) {
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
await createCacheDir();
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache") || file.startsWith("NN_")) {
|
||||
|
||||
@@ -33,6 +33,9 @@ export function hideAuth() {
|
||||
eSendEvent(eCloseLoginDialog);
|
||||
if (initialAuthMode.current === AuthMode.welcomeSignup) {
|
||||
Navigation.replace("FluidPanelsView");
|
||||
setTimeout(() => {
|
||||
Navigation.resetRootState();
|
||||
}, 300);
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ const Auth = ({ navigation, route }) => {
|
||||
onPress={() => {
|
||||
if (initialAuthMode.current === 2) {
|
||||
Navigation.replace("FluidPanelsView");
|
||||
setTimeout(() => {
|
||||
Navigation.resetRootState();
|
||||
}, 300);
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { BackHandler, Platform, ViewProps } from "react-native";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
SharedValue,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
@@ -67,6 +68,7 @@ export interface TabsRef {
|
||||
setScrollEnabled: () => true;
|
||||
isDrawerOpen: () => boolean;
|
||||
node: RefObject<Animated.View>;
|
||||
tabChangedFromSwipeAction: SharedValue<boolean>;
|
||||
}
|
||||
|
||||
export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
@@ -116,6 +118,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
const isLoaded = useRef(false);
|
||||
const prevWidths = useRef(widths);
|
||||
const isIPhone = Platform.OS === "ios";
|
||||
const tabChangedFromSwipeAction = useSharedValue(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (deviceMode === "tablet" || fullscreen) {
|
||||
@@ -191,6 +194,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
: withTiming(editorPosition);
|
||||
currentTab.value = 2;
|
||||
}
|
||||
tabChangedFromSwipeAction.value = false;
|
||||
},
|
||||
goToIndex: (index: number, animated = true) => {
|
||||
if (deviceMode === "tablet") {
|
||||
@@ -210,6 +214,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
: editorPosition;
|
||||
currentTab.value = 2;
|
||||
}
|
||||
tabChangedFromSwipeAction.value = false;
|
||||
},
|
||||
unlock: () => {
|
||||
forcedLock.value = false;
|
||||
@@ -252,7 +257,8 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
},
|
||||
page: () => (currentTab.value === 1 ? "home" : "editor"),
|
||||
setScrollEnabled: () => true,
|
||||
node: node
|
||||
node: node,
|
||||
tabChangedFromSwipeAction: tabChangedFromSwipeAction
|
||||
}),
|
||||
[
|
||||
currentTab,
|
||||
@@ -262,7 +268,8 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
homePosition,
|
||||
editorPosition,
|
||||
forcedLock,
|
||||
isDrawerOpen
|
||||
isDrawerOpen,
|
||||
tabChangedFromSwipeAction
|
||||
]
|
||||
);
|
||||
|
||||
@@ -362,18 +369,20 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
isDrawerOpen.value = true;
|
||||
currentTab.value = 1;
|
||||
runOnJS(onDrawerStateChange)(true);
|
||||
tabChangedFromSwipeAction.value = true;
|
||||
return;
|
||||
} else if (!isSwipeLeft && finalValue > 100) {
|
||||
translateX.value = withSpring(homePosition, animationConfig);
|
||||
isDrawerOpen.value = false;
|
||||
currentTab.value = 1;
|
||||
runOnJS(onDrawerStateChange)(false);
|
||||
|
||||
tabChangedFromSwipeAction.value = true;
|
||||
return;
|
||||
} else if (!isSwipeLeft && finalValue < 100) {
|
||||
translateX.value = withSpring(0, animationConfig);
|
||||
isDrawerOpen.value = true;
|
||||
currentTab.value = 1;
|
||||
tabChangedFromSwipeAction.value = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -384,6 +393,8 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
translateX.value = withSpring(editorPosition, animationConfig);
|
||||
currentTab.value = 2;
|
||||
isDrawerOpen.value = false;
|
||||
|
||||
tabChangedFromSwipeAction.value = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -398,6 +409,8 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
translateX.value = withSpring(editorPosition, animationConfig);
|
||||
currentTab.value = 2;
|
||||
}
|
||||
|
||||
tabChangedFromSwipeAction.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,9 +73,6 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
|
||||
testID={notesnook.ids.dialogs.actionsheet.color(item.colorCode)}
|
||||
key={item.id}
|
||||
onPress={toggleColor}
|
||||
onLongPress={(event) => {
|
||||
NativeTooltip.show(event, item.title, NativeTooltip.POSITIONS.TOP);
|
||||
}}
|
||||
style={{
|
||||
width: 35,
|
||||
height: 35,
|
||||
|
||||
@@ -197,16 +197,6 @@ export const SelectionHeader = React.memo(
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: strings.move(),
|
||||
onPress: async () => {
|
||||
const ids = selectedItemsList;
|
||||
const notebooks = await db.notebooks.all.items(ids);
|
||||
MoveNotebook.present(notebooks);
|
||||
},
|
||||
visible: renderedInRoute === "Notebooks",
|
||||
icon: "arrow-right-bold-box-outline"
|
||||
},
|
||||
{
|
||||
title: strings.manageTags(),
|
||||
onPress: async () => {
|
||||
|
||||
@@ -310,6 +310,7 @@ const ExportNotesSheet = ({
|
||||
ToastManager.error(e as Error);
|
||||
});
|
||||
} else {
|
||||
await sleep(500);
|
||||
FileViewer.open(result?.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
@@ -338,6 +339,7 @@ const ExportNotesSheet = ({
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
if (Platform.OS === "ios") {
|
||||
await sleep(500);
|
||||
Share.open({
|
||||
url: result?.fileDir + result.fileName
|
||||
}).catch(() => {
|
||||
|
||||
@@ -211,6 +211,14 @@ const TabBar = (
|
||||
const ids = useSideMenuNotebookSelectionStore
|
||||
.getState()
|
||||
.getSelectedItemIds();
|
||||
if (!ids.length) {
|
||||
ToastManager.show({
|
||||
context: "local",
|
||||
type: "error",
|
||||
message: strings.noNotebooksSelectedToMove()
|
||||
});
|
||||
return;
|
||||
}
|
||||
const notebooks = await db.notebooks.all.items(ids);
|
||||
Navigation.navigate("MoveNotebook", {
|
||||
selectedNotebooks: notebooks
|
||||
|
||||
@@ -66,7 +66,7 @@ export const SideMenuNotebooks = () => {
|
||||
if (lastQuery.current) {
|
||||
db.lookup
|
||||
.notebooks(lastQuery.current)
|
||||
.sorted()
|
||||
.sorted(db.settings.getGroupOptions("notebooks"))
|
||||
.then((filtered) => {
|
||||
setFilteredNotebooks(filtered);
|
||||
});
|
||||
|
||||
@@ -220,7 +220,7 @@ export const SideMenuTags = () => {
|
||||
if (lastQuery.current) {
|
||||
db.lookup
|
||||
.tags(lastQuery.current.trim())
|
||||
.sorted()
|
||||
.sorted(db.settings.getGroupOptions("tags"))
|
||||
.then(async (filtered) => {
|
||||
setFilteredTags(filtered);
|
||||
});
|
||||
|
||||
@@ -23,13 +23,11 @@ import { Platform, View } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { PremiumToast } from "../../premium/premium-toast";
|
||||
import { Toast } from "../../toast";
|
||||
import { useAppState } from "../../../hooks/use-app-state";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { getContainerBorder } from "../../../utils/colors";
|
||||
|
||||
import { PremiumToast } from "../../premium/premium-toast";
|
||||
import { Toast } from "../../toast";
|
||||
import { NotesnookModule } from "../../../utils/notesnook-module";
|
||||
/**
|
||||
*
|
||||
* @param {any} param0
|
||||
@@ -63,6 +61,10 @@ const SheetWrapper = ({
|
||||
const locked = useUserStore((state) => state.appLocked);
|
||||
|
||||
let width = dimensions.width > 600 ? 600 : 500;
|
||||
const isGestureNavigationEnabled =
|
||||
NotesnookModule.isGestureNavigationEnabled();
|
||||
|
||||
console.log(isGestureNavigationEnabled);
|
||||
|
||||
const style = React.useMemo(() => {
|
||||
return {
|
||||
@@ -77,14 +79,18 @@ const SheetWrapper = ({
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
...getContainerBorder(colors.primary.border, 0.5),
|
||||
borderBottomWidth: 0
|
||||
borderBottomWidth: 0,
|
||||
paddingBottom: isGestureNavigationEnabled
|
||||
? insets.bottom
|
||||
: insets.bottom || 48
|
||||
};
|
||||
}, [
|
||||
colors.primary.background,
|
||||
colors.primary.border,
|
||||
largeTablet,
|
||||
smallTablet,
|
||||
width
|
||||
width,
|
||||
insets.bottom
|
||||
]);
|
||||
|
||||
const _onOpen = () => {
|
||||
|
||||
@@ -19,9 +19,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { FeatureType } from "./components/sheets/new-feature";
|
||||
|
||||
export const features: FeatureType[] = [
|
||||
{
|
||||
title: "Improved Search",
|
||||
body: "Improved search results with better relevance and highlighting."
|
||||
}
|
||||
];
|
||||
export const features: FeatureType[] = [];
|
||||
|
||||
@@ -515,6 +515,8 @@ export const useActions = ({
|
||||
title: strings.addNotebook(),
|
||||
icon: "plus",
|
||||
onPress: async () => {
|
||||
close();
|
||||
await sleep(300);
|
||||
AddNotebookSheet.present(undefined, item);
|
||||
}
|
||||
},
|
||||
@@ -523,6 +525,8 @@ export const useActions = ({
|
||||
title: strings.editNotebook(),
|
||||
icon: "square-edit-outline",
|
||||
onPress: async () => {
|
||||
close();
|
||||
await sleep(300);
|
||||
AddNotebookSheet.present(item);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -30,6 +30,10 @@ import React, {
|
||||
useState
|
||||
} from "react";
|
||||
import { Dimensions, LayoutChangeEvent, Platform, View } from "react-native";
|
||||
import Orientation, {
|
||||
OrientationType,
|
||||
useDeviceOrientationChange
|
||||
} from "react-native-orientation-locker";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
@@ -67,10 +71,6 @@ import {
|
||||
} from "../utils/events";
|
||||
import { editorRef, fluidTabsRef } from "../utils/global-refs";
|
||||
import { AppNavigationStack } from "./navigation-stack";
|
||||
import Orientation, {
|
||||
OrientationType,
|
||||
useDeviceOrientationChange
|
||||
} from "react-native-orientation-locker";
|
||||
|
||||
const MOBILE_SIDEBAR_SIZE = 0.85;
|
||||
|
||||
@@ -421,7 +421,6 @@ export const FluidPanelsView = React.memo(
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingBottom: Platform.OS === "android" ? insets?.bottom : 0,
|
||||
marginRight:
|
||||
orientation === "LANDSCAPE-RIGHT" && Platform.OS === "ios"
|
||||
? insets.right
|
||||
@@ -530,17 +529,20 @@ const onChangeTab = async (event: { i: number; from: number }) => {
|
||||
activateKeepAwake();
|
||||
eSendEvent(eOnEnterEditor);
|
||||
|
||||
if (!useTabStore.getState().getCurrentNoteId()) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
newNote: true
|
||||
});
|
||||
} else {
|
||||
if (
|
||||
useTabStore.getState().getTab(useTabStore.getState().currentTab)
|
||||
?.session?.locked
|
||||
) {
|
||||
eSendEvent(eUnlockNote);
|
||||
}
|
||||
if (
|
||||
useTabStore.getState().getTab(useTabStore.getState().currentTab)?.session
|
||||
?.locked
|
||||
) {
|
||||
eSendEvent(eUnlockNote);
|
||||
}
|
||||
|
||||
if (
|
||||
fluidTabsRef.current?.tabChangedFromSwipeAction.value &&
|
||||
!useTabStore.getState().getNoteIdForTab(useTabStore.getState().currentTab)
|
||||
) {
|
||||
editorController?.current?.commands?.focus(
|
||||
useTabStore.getState().currentTab
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (event.from === 2) {
|
||||
|
||||
@@ -96,14 +96,16 @@ export const EditorWrapper = ({ widths }: { widths: any }) => {
|
||||
minHeight: "100%",
|
||||
backgroundColor: toolBarColors.primary.background,
|
||||
borderLeftWidth: DDS.isTab ? 1 : 0,
|
||||
borderLeftColor: DDS.isTab ? colors.secondary.background : "transparent"
|
||||
borderLeftColor: DDS.isTab
|
||||
? colors.secondary.background
|
||||
: "transparent",
|
||||
paddingBottom: insets.bottom
|
||||
}}
|
||||
>
|
||||
{loading || !introCompleted ? null : (
|
||||
<KeyboardAvoidingViewIOS
|
||||
behavior="padding"
|
||||
style={{
|
||||
marginBottom: getMarginBottom(),
|
||||
backgroundColor: colors.primary.background,
|
||||
flex: 1
|
||||
}}
|
||||
|
||||
@@ -166,6 +166,20 @@ function closeDrawer() {
|
||||
fluidTabsRef.current?.closeDrawer();
|
||||
}
|
||||
|
||||
function resetRootState() {
|
||||
const state = rootNavigatorRef.getState();
|
||||
const focusedRouteKey = state.routes[state.index].key;
|
||||
const routes = state.routes.filter(
|
||||
(route) => route.name !== "Auth" && route.name !== "Welcome"
|
||||
);
|
||||
const newIndex = routes.findIndex((route) => route.key === focusedRouteKey);
|
||||
rootNavigatorRef.reset({
|
||||
...state,
|
||||
routes: routes,
|
||||
index: newIndex
|
||||
});
|
||||
}
|
||||
|
||||
const Navigation = {
|
||||
navigate,
|
||||
goBack,
|
||||
@@ -177,7 +191,8 @@ const Navigation = {
|
||||
queueRoutesForUpdate,
|
||||
routeNeedsUpdate,
|
||||
routeNames,
|
||||
routeUpdateFunctions
|
||||
routeUpdateFunctions,
|
||||
resetRootState
|
||||
};
|
||||
|
||||
export default Navigation;
|
||||
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getFormattedReminderTime } from "@notesnook/common";
|
||||
import { isReminderActive, Reminder } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import notifee, {
|
||||
@@ -36,6 +37,7 @@ import dayjs, { Dayjs } from "dayjs";
|
||||
import { encodeNonAsciiHTML } from "entities";
|
||||
import { Platform } from "react-native";
|
||||
import { db, setupDatabase } from "../common/database";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
|
||||
import { editorState } from "../screens/editor/tiptap/utils";
|
||||
@@ -52,8 +54,6 @@ import { DDS } from "./device-detection";
|
||||
import { eSendEvent } from "./event-manager";
|
||||
import Navigation from "./navigation";
|
||||
import SettingsService from "./settings";
|
||||
import { getFormattedReminderTime } from "@notesnook/common";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
|
||||
let pinned: DisplayedNotification[] = [];
|
||||
|
||||
@@ -454,7 +454,7 @@ async function loadNote(id: string, jump: boolean) {
|
||||
const tab = useTabStore.getState().getTabForNote(id);
|
||||
if (useTabStore.getState().currentTab !== tab) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
note: note
|
||||
item: note
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -916,8 +916,21 @@ async function pinQuickNote(launch: boolean) {
|
||||
* A function that checks if reminders need to be reconfigured &
|
||||
* reschedules them if anything has changed.
|
||||
*/
|
||||
|
||||
async function setupReminders(checkNeedsScheduling = false) {
|
||||
const reminders = ((await db.reminders?.all.items()) as Reminder[]) || [];
|
||||
let notificationsCancelled = false;
|
||||
if (Platform.OS === "android") {
|
||||
// If the API level has changed, cancel all notifications.
|
||||
// This is to ensure that the app does not crash on Android 14+.
|
||||
const API_LEVEL = MMKV.getInt("android_apiLevel");
|
||||
if (API_LEVEL !== (Platform.Version as number)) {
|
||||
await notifee.cancelAllNotifications();
|
||||
notificationsCancelled = true;
|
||||
MMKV.setInt("android_apiLevel", Platform.Version as number);
|
||||
}
|
||||
}
|
||||
|
||||
const triggers = await notifee.getTriggerNotifications();
|
||||
for (const reminder of reminders) {
|
||||
if (reminder.mode === "permanent") {
|
||||
@@ -927,20 +940,22 @@ async function setupReminders(checkNeedsScheduling = false) {
|
||||
// Skip reminders that are not repeating and their trigger date is in past.
|
||||
if (reminder.mode === "once" && dayjs().isAfter(reminder.date)) continue;
|
||||
|
||||
const pending = triggers.filter((t) =>
|
||||
t.notification.id?.startsWith(reminder.id)
|
||||
);
|
||||
if (!notificationsCancelled) {
|
||||
const pending = triggers.filter((t) =>
|
||||
t.notification.id?.startsWith(reminder.id)
|
||||
);
|
||||
|
||||
let needsReschedule = pending.length === 0 ? true : false;
|
||||
if (!needsReschedule) {
|
||||
needsReschedule = pending[0].notification.data?.dateModified
|
||||
? parseInt(pending[0].notification.data?.dateModified as string) <
|
||||
reminder.dateModified
|
||||
: true;
|
||||
let needsReschedule = pending.length === 0 ? true : false;
|
||||
if (!needsReschedule) {
|
||||
needsReschedule = pending[0].notification.data?.dateModified
|
||||
? parseInt(pending[0].notification.data?.dateModified as string) <
|
||||
reminder.dateModified
|
||||
: true;
|
||||
}
|
||||
|
||||
if (!needsReschedule && checkNeedsScheduling) continue;
|
||||
}
|
||||
|
||||
if (!needsReschedule && checkNeedsScheduling) continue;
|
||||
|
||||
await scheduleNotification(reminder);
|
||||
}
|
||||
// Check for any triggers whose notifications
|
||||
|
||||
@@ -45,10 +45,10 @@ export function initAfterSync(type: "full" | "send" = "send") {
|
||||
useUserStore.setState({
|
||||
profile: db.settings.getProfile()
|
||||
});
|
||||
eSendEvent(eAfterSync);
|
||||
}
|
||||
Notifications.setupReminders(true);
|
||||
NotePreviewWidget.updateNotes();
|
||||
eSendEvent(eAfterSync);
|
||||
}
|
||||
|
||||
export async function initialize() {}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function changeSystemBarColors() {
|
||||
const isDark = useThemeStore.getState().colorScheme === "dark";
|
||||
changeNavigationBarColor(
|
||||
currTheme.scopes.base.primary.background,
|
||||
isDark,
|
||||
!isDark,
|
||||
false
|
||||
);
|
||||
StatusBar.setBackgroundColor("transparent" as any);
|
||||
|
||||
@@ -40,6 +40,7 @@ interface NotesnookModuleInterface {
|
||||
hasWidgetNote: (noteId: string) => Promise<boolean>;
|
||||
updateWidgetNote: (noteId: string, data: string) => void;
|
||||
updateReminderWidget: () => void;
|
||||
isGestureNavigationEnabled: () => boolean;
|
||||
}
|
||||
|
||||
export const NotesnookModule: NotesnookModuleInterface = Platform.select({
|
||||
@@ -59,7 +60,8 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({
|
||||
getWidgetNotes: () => {},
|
||||
hasWidgetNote: () => {},
|
||||
updateWidgetNote: () => {},
|
||||
updateReminderWidget: () => {}
|
||||
updateReminderWidget: () => {},
|
||||
isGestureNavigationEnabled: () => true
|
||||
},
|
||||
android: NativeModules.NNativeModule
|
||||
});
|
||||
|
||||
@@ -124,7 +124,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3062
|
||||
versionCode 3066
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -7,7 +7,9 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.RemoteViews;
|
||||
@@ -217,4 +219,24 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
wm.notifyAppWidgetViewDataChanged(id, R.id.widget_list_view);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public boolean isGestureNavigationEnabled() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
try {
|
||||
String navBarMode = Settings.Secure.getString(
|
||||
mContext.getContentResolver(),
|
||||
"navigation_mode"
|
||||
);
|
||||
return "2".equals(navBarMode);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
- Fix crash on device boot on android 15 devices
|
||||
- Add scroll to top/bottom in editor
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -5,5 +5,5 @@ rootProject.name = 'Notesnook'
|
||||
include ':app'
|
||||
includeBuild('../../node_modules/@react-native/gradle-plugin')
|
||||
|
||||
include ":lazysodium"
|
||||
project(':lazysodium').projectDir = new File(rootProject.projectDir, '../../node_modules/@ammarahmed/react-native-sodium/android/lazysodium')
|
||||
include ":lazysodium-android"
|
||||
project(':lazysodium-android').projectDir = new File(rootProject.projectDir, '../../node_modules/@ammarahmed/react-native-sodium/android/lazysodium-android/app')
|
||||
|
||||
@@ -1089,7 +1089,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2141;
|
||||
CURRENT_PROJECT_VERSION = 2145;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1163,7 +1163,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.2.6;
|
||||
MARKETING_VERSION = 3.2.10;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1194,7 +1194,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2141;
|
||||
CURRENT_PROJECT_VERSION = 2145;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1268,7 +1268,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.2.6;
|
||||
MARKETING_VERSION = 3.2.10;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1427,7 +1427,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2141;
|
||||
CURRENT_PROJECT_VERSION = 2145;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1439,7 +1439,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.2.6;
|
||||
MARKETING_VERSION = 3.2.10;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1470,7 +1470,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2141;
|
||||
CURRENT_PROJECT_VERSION = 2145;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1483,7 +1483,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.2.6;
|
||||
MARKETING_VERSION = 3.2.10;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1513,7 +1513,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2141;
|
||||
CURRENT_PROJECT_VERSION = 2145;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1594,7 +1594,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
|
||||
MARKETING_VERSION = 3.2.6;
|
||||
MARKETING_VERSION = 3.2.10;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1625,7 +1625,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2141;
|
||||
CURRENT_PROJECT_VERSION = 2145;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1707,7 +1707,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
|
||||
MARKETING_VERSION = 3.2.6;
|
||||
MARKETING_VERSION = 3.2.10;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"react-native-screenguard": "^1.0.0",
|
||||
"@formatjs/intl-locale": "4.0.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.14",
|
||||
"@ammarahmed/react-native-sodium": "^1.6.4",
|
||||
"@ammarahmed/react-native-sodium": "^1.6.5",
|
||||
"@react-native-community/datetimepicker": "^8.2.0",
|
||||
"react-native-exit-app": "github:ammarahm-ed/react-native-exit-app",
|
||||
"react-native-begin-background-task": "github:blockfirm/react-native-begin-background-task",
|
||||
|
||||
337
apps/mobile/package-lock.json
generated
337
apps/mobile/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.2.6",
|
||||
"version": "3.2.10",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -56,4 +56,4 @@
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.77.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
apps/mobile/patches/react-native-actions-sheet+0.9.7.patch
Normal file
12
apps/mobile/patches/react-native-actions-sheet+0.9.7.patch
Normal file
@@ -0,0 +1,12 @@
|
||||
diff --git a/node_modules/react-native-actions-sheet/dist/src/index.js b/node_modules/react-native-actions-sheet/dist/src/index.js
|
||||
index 0e124da..3703153 100644
|
||||
--- a/node_modules/react-native-actions-sheet/dist/src/index.js
|
||||
+++ b/node_modules/react-native-actions-sheet/dist/src/index.js
|
||||
@@ -1088,6 +1088,7 @@ export default forwardRef(function ActionSheet(_a, ref) {
|
||||
* Always true, it causes issue with keyboard handling.
|
||||
*/
|
||||
statusBarTranslucent: true,
|
||||
+ navigationBarTranslucent: true
|
||||
}
|
||||
: {
|
||||
testID: ((_b = props.testIDs) === null || _b === void 0 ? void 0 : _b.root) || props.testID,
|
||||
@@ -107,10 +107,12 @@ function generateTableOfContents() {
|
||||
|
||||
export const MonographPage = ({
|
||||
monograph,
|
||||
encodedKey
|
||||
encodedKey,
|
||||
apiHost
|
||||
}: {
|
||||
monograph: Monograph;
|
||||
encodedKey?: string;
|
||||
apiHost: string;
|
||||
}) => {
|
||||
const [reportDialogVisible, setReportDialogVisible] = useState(false);
|
||||
const [tableOfContents, setTableOfContents] = useState<TableOfContent[]>([]);
|
||||
@@ -227,7 +229,7 @@ export const MonographPage = ({
|
||||
)}
|
||||
<Image
|
||||
sx={{ display: "none" }}
|
||||
src={`https://api.notesnook.com/monographs/${monograph.id}/view`}
|
||||
src={`${apiHost}/monographs/${monograph.id}/view`}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
@@ -85,7 +85,8 @@ export async function loader({ params }: LoaderFunctionArgs) {
|
||||
const metadata = getMonographMetadata(monograph);
|
||||
return {
|
||||
monograph,
|
||||
metadata
|
||||
metadata,
|
||||
apiHost: API_HOST
|
||||
};
|
||||
} catch (e) {
|
||||
// console.error(e);
|
||||
@@ -96,19 +97,24 @@ export async function loader({ params }: LoaderFunctionArgs) {
|
||||
fullDescription: "This monograph does not exist.",
|
||||
shortDescription: "This monograph does not exist.",
|
||||
datePublished: ""
|
||||
}
|
||||
},
|
||||
apiHost: API_HOST
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function MonographPost() {
|
||||
const { monograph } = useLoaderData<typeof loader>();
|
||||
const { monograph, apiHost } = useLoaderData<typeof loader>();
|
||||
const [_, hashParams] = useHashLocation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{monograph ? (
|
||||
<MonographPage monograph={monograph} encodedKey={hashParams.key} />
|
||||
<MonographPage
|
||||
monograph={monograph}
|
||||
encodedKey={hashParams.key}
|
||||
apiHost={apiHost}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
4
apps/theme-builder/package-lock.json
generated
4
apps/theme-builder/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/theme-builder",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/theme-builder",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/theme-builder",
|
||||
"description": "Your private note taking space",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
|
||||
@@ -1 +1 @@
|
||||
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made
|
||||
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
|
||||
@@ -1 +1 @@
|
||||
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made
|
||||
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
|
||||
@@ -1 +1 @@
|
||||
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made
|
||||
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
|
||||
@@ -162,9 +162,9 @@ export class SettingsViewModel {
|
||||
|
||||
await appLockSwitch.click();
|
||||
await fillPasswordDialog(this.page, userPassword);
|
||||
await this.page.waitForTimeout(100);
|
||||
await this.page.waitForTimeout(500);
|
||||
await fillConfirmPasswordDialog(this.page, appLockPassword);
|
||||
await this.page.waitForTimeout(100);
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async disableAppLock(appLockPassword: string) {
|
||||
|
||||
@@ -91,6 +91,7 @@ export async function fillReminderDialog(
|
||||
}
|
||||
|
||||
await confirmDialog(dialog);
|
||||
await dialog.waitFor({ state: "hidden" });
|
||||
}
|
||||
|
||||
export async function fillItemDialog(page: Page, item: Item) {
|
||||
@@ -133,7 +134,6 @@ export async function fillConfirmPasswordDialog(page: Page, password: string) {
|
||||
export async function confirmDialog(dialog: Locator) {
|
||||
const dialogConfirm = dialog.locator(getTestId("dialog-yes"));
|
||||
await dialogConfirm.click();
|
||||
// await dialogConfirm.waitFor({ state: "detached" });
|
||||
}
|
||||
|
||||
export async function denyDialog(page: Page) {
|
||||
|
||||
@@ -53,16 +53,17 @@ test("adding a one-time reminder before current time should not be possible", as
|
||||
await app.goto();
|
||||
const reminders = await app.goToReminders();
|
||||
|
||||
await reminders.createReminder({
|
||||
...ONE_TIME_REMINDER,
|
||||
date: 0
|
||||
});
|
||||
|
||||
expect(
|
||||
await app.toasts.waitForToast(
|
||||
const result = await Promise.race([
|
||||
reminders.createReminder({
|
||||
...ONE_TIME_REMINDER,
|
||||
date: 0
|
||||
}),
|
||||
app.toasts.waitForToast(
|
||||
"Reminder time cannot be earlier than the current time."
|
||||
)
|
||||
).toBeTruthy();
|
||||
]);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
for (const recurringMode of ["Daily", "Weekly", "Monthly"] as const) {
|
||||
|
||||
@@ -455,6 +455,7 @@ test("if note is active in multiple tabs, moving the note to trash should close
|
||||
title: "Note 1"
|
||||
});
|
||||
await note?.contextMenu.openInNewTab();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await note?.contextMenu.moveToTrash();
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ kbd {
|
||||
background: var(--background);
|
||||
border-radius: 3px;
|
||||
padding: 2px 5px;
|
||||
color: var(--paragraph-secondary);
|
||||
color: var(--paragraph);
|
||||
}
|
||||
|
||||
.ping {
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useState, Suspense, useEffect, useRef } from "react";
|
||||
import { Suspense, useEffect, useRef } from "react";
|
||||
import { Box, Flex } from "@theme-ui/components";
|
||||
import { ScopedThemeProvider } from "./components/theme-provider";
|
||||
import useMobile from "./hooks/use-mobile";
|
||||
@@ -60,6 +60,14 @@ function App() {
|
||||
useSettingStore.getState().desktopIntegrationSettings?.nativeTitlebar;
|
||||
console.timeEnd("loading app");
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
useStore.setState({
|
||||
isNavPaneCollapsed: false
|
||||
});
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFocused ? null : (
|
||||
@@ -216,7 +224,7 @@ function DesktopAppContents() {
|
||||
initialSize={380}
|
||||
style={{ flex: 1, display: "flex" }}
|
||||
snapSize={120}
|
||||
maxSize={500}
|
||||
maxSize={1000}
|
||||
className="list-pane"
|
||||
>
|
||||
<ScopedThemeProvider
|
||||
|
||||
@@ -53,7 +53,8 @@ import { MenuItem } from "@notesnook/ui";
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
title: strings.addItem("note"),
|
||||
onClick: () => useEditorStore.getState().newSession()
|
||||
onClick: () => useEditorStore.getState().newSession(),
|
||||
onAuxClick: () => useEditorStore.getState().addTab()
|
||||
},
|
||||
notebooks: {
|
||||
title: strings.addItem("notebook"),
|
||||
|
||||
@@ -23,6 +23,9 @@ import { useStore as useSearchStore } from "../stores/search-store";
|
||||
import { useEditorManager } from "../components/editor/manager";
|
||||
import { CommandPaletteDialog } from "../dialogs/command-palette";
|
||||
import { hashNavigate } from "../navigation";
|
||||
import { getKeybinding, keybindings } from "@notesnook/common";
|
||||
import { KeyboardShortcutsDialog } from "../dialogs/keyboard-shortcuts-dialog";
|
||||
import { isMac } from "../utils/platform";
|
||||
|
||||
function isInEditor(e: KeyboardEvent) {
|
||||
return (
|
||||
@@ -30,200 +33,71 @@ function isInEditor(e: KeyboardEvent) {
|
||||
);
|
||||
}
|
||||
|
||||
const KEYMAP = [
|
||||
// {
|
||||
// keys: ["command+n", "ctrl+n", "command+alt+n", "ctrl+alt+n"],
|
||||
// description: "Create a new note",
|
||||
// global: true,
|
||||
// action: (e) => {
|
||||
// e.preventDefault();
|
||||
// hashNavigate("/notes/create", {
|
||||
// addNonce: true,
|
||||
// replace: true,
|
||||
// notify: true,
|
||||
// });
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// keys: [
|
||||
// "command+shift+n",
|
||||
// "ctrl+shift+n",
|
||||
// "command+shift+alt+n",
|
||||
// "ctrl+shift+alt+n",
|
||||
// ],
|
||||
// description: "Create a new notebook",
|
||||
// global: true,
|
||||
// action: (e) => {
|
||||
// e.preventDefault();
|
||||
// hashNavigate("/notebooks/create", {
|
||||
// replace: true,
|
||||
// notify: true,
|
||||
// });
|
||||
// },
|
||||
// },
|
||||
{
|
||||
keys: [
|
||||
"command+option+right",
|
||||
"ctrl+alt+right",
|
||||
"command+option+shift+right",
|
||||
"ctrl+alt+shift+right",
|
||||
"ctrl+tab",
|
||||
"command+tab"
|
||||
],
|
||||
description: "Go to next tab",
|
||||
action: () => useEditorStore.getState().focusNextTab()
|
||||
},
|
||||
{
|
||||
keys: [
|
||||
"command+option+left",
|
||||
"ctrl+alt+left",
|
||||
"command+option+shift+left",
|
||||
"ctrl+alt+shift+left",
|
||||
"ctrl+shift+tab",
|
||||
"command+shift+tab"
|
||||
],
|
||||
description: "Go to previous tab",
|
||||
action: () => useEditorStore.getState().focusPreviousTab()
|
||||
},
|
||||
{
|
||||
keys: ["ctrl+t", "command+t"],
|
||||
description: "Create a new tab",
|
||||
action: () => useEditorStore.getState().addTab()
|
||||
},
|
||||
{
|
||||
keys: ["ctrl+n", "command+n"],
|
||||
description: "Create a new note",
|
||||
action: () => useEditorStore.getState().newSession()
|
||||
},
|
||||
{
|
||||
keys: ["ctrl+w", "command+w"],
|
||||
description:
|
||||
"Close active tab or focus previously activated tab if active tab pinned",
|
||||
action: () => {
|
||||
const activeTab = useEditorStore.getState().getActiveTab();
|
||||
if (activeTab?.pinned) {
|
||||
useEditorStore.getState().focusLastActiveTab();
|
||||
return;
|
||||
}
|
||||
useEditorStore.getState().closeActiveTab();
|
||||
const actions: Partial<
|
||||
Record<keyof typeof keybindings, (() => void) | ((e: KeyboardEvent) => void)>
|
||||
> = {
|
||||
nextTab: () => useEditorStore.getState().focusNextTab(),
|
||||
previousTab: () => useEditorStore.getState().focusPreviousTab(),
|
||||
newTab: () => useEditorStore.getState().addTab(),
|
||||
newNote: () => useEditorStore.getState().newSession(),
|
||||
closeActiveTab: () => {
|
||||
const activeTab = useEditorStore.getState().getActiveTab();
|
||||
if (activeTab?.pinned) {
|
||||
useEditorStore.getState().focusLastActiveTab();
|
||||
return;
|
||||
}
|
||||
useEditorStore.getState().closeActiveTab();
|
||||
},
|
||||
{
|
||||
keys: ["ctrl+shift+w", "command+shift+w"],
|
||||
description: "Close all tabs",
|
||||
action: () => useEditorStore.getState().closeAllTabs()
|
||||
},
|
||||
{
|
||||
keys: ["command+f", "ctrl+f"],
|
||||
description: "Search all notes",
|
||||
global: false,
|
||||
action: (e: KeyboardEvent) => {
|
||||
if (isInEditor(e)) {
|
||||
const activeSession = useEditorStore.getState().getActiveSession();
|
||||
if (activeSession?.type === "readonly") {
|
||||
e.preventDefault();
|
||||
const editor = useEditorManager
|
||||
.getState()
|
||||
.getEditor(activeSession.id);
|
||||
editor?.editor?.startSearch();
|
||||
}
|
||||
return;
|
||||
closeAllTabs: () => useEditorStore.getState().closeAllTabs(),
|
||||
searchInNotes: (e: KeyboardEvent) => {
|
||||
if (isInEditor(e)) {
|
||||
const activeSession = useEditorStore.getState().getActiveSession();
|
||||
if (activeSession?.type === "readonly") {
|
||||
e.preventDefault();
|
||||
const editor = useEditorManager.getState().getEditor(activeSession.id);
|
||||
editor?.editor?.startSearch();
|
||||
}
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
useSearchStore.setState({ isSearching: true, searchType: "notes" });
|
||||
}
|
||||
useSearchStore.setState({ isSearching: true, searchType: "notes" });
|
||||
},
|
||||
// {
|
||||
// keys: ["alt+n"],
|
||||
// description: "Go to Notes",
|
||||
// global: false,
|
||||
// action: (e) => {
|
||||
// e.preventDefault();
|
||||
// navigate("/notes");
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// keys: ["alt+b"],
|
||||
// description: "Go to Notebooks",
|
||||
// global: false,
|
||||
// action: (e) => {
|
||||
// e.preventDefault();
|
||||
// navigate("/notebooks");
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// keys: ["alt+f"],
|
||||
// description: "Go to Favorites",
|
||||
// global: false,
|
||||
// action: (e) => {
|
||||
// e.preventDefault();
|
||||
// navigate("/favorites");
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// keys: ["alt+t"],
|
||||
// description: "Go to Tags",
|
||||
// global: false,
|
||||
// action: (e) => {
|
||||
// e.preventDefault();
|
||||
// navigate("/tags");
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// keys: ["alt+d"],
|
||||
// description: "Go to Trash",
|
||||
// global: false,
|
||||
// action: (e) => {
|
||||
// e.preventDefault();
|
||||
// navigate("/trash");
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// keys: ["alt+s"],
|
||||
// description: "Go to Settings",
|
||||
// global: false,
|
||||
// action: (e) => {
|
||||
// e.preventDefault();
|
||||
// navigate("/settings");
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// keys: ["command+d", "ctrl+d"],
|
||||
// description: "Toggle dark/light mode",
|
||||
// global: true,
|
||||
// action: (e) => {
|
||||
// e.preventDefault();
|
||||
// themestore.get().toggleNightMode();
|
||||
// },
|
||||
// },
|
||||
{
|
||||
keys: ["ctrl+k", "cmd+k", "ctrl+p", "cmd+p"],
|
||||
description: "Open command palette",
|
||||
action: (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
CommandPaletteDialog.close();
|
||||
CommandPaletteDialog.show({
|
||||
isCommandMode: e.key === "k"
|
||||
}).catch(() => {});
|
||||
}
|
||||
openCommandPalette: () => {
|
||||
CommandPaletteDialog.close();
|
||||
CommandPaletteDialog.show({
|
||||
isCommandMode: true
|
||||
}).catch(() => {});
|
||||
},
|
||||
{
|
||||
keys: ["ctrl+,", "command+,"],
|
||||
description: "Open settings",
|
||||
action: () => hashNavigate("/settings", { replace: true })
|
||||
}
|
||||
];
|
||||
openQuickOpen: () => {
|
||||
CommandPaletteDialog.close();
|
||||
CommandPaletteDialog.show({
|
||||
isCommandMode: false
|
||||
}).catch(() => {});
|
||||
},
|
||||
openSettings: (e) => {
|
||||
if (isInEditor(e)) return;
|
||||
hashNavigate("/settings", { replace: true });
|
||||
},
|
||||
openKeyboardShortcuts: () => KeyboardShortcutsDialog.show({})
|
||||
};
|
||||
|
||||
export function registerKeyMap() {
|
||||
hotkeys.filter = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
KEYMAP.forEach((key) => {
|
||||
hotkeys(key.keys.join(","), (e) => {
|
||||
Object.entries(actions).forEach(([id, action]) => {
|
||||
const keys = getKeybinding(
|
||||
id as keyof typeof keybindings,
|
||||
IS_DESKTOP_APP,
|
||||
isMac()
|
||||
);
|
||||
if (!keys || keys.length === 0) return;
|
||||
|
||||
hotkeys(keys.join(","), (e) => {
|
||||
e.preventDefault();
|
||||
key.action?.(e);
|
||||
action(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +46,10 @@ export function ProviderSelector(props: ProviderSelectorProps) {
|
||||
sx={{ mt: 1, color: "paragraph", whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
Can't find your notes app in the list?{" "}
|
||||
<a href="https://github.com/streetwriters/notesnook-importer/issues/new">
|
||||
<a
|
||||
href="https://github.com/streetwriters/notesnook-importer/issues/new"
|
||||
target="_blank"
|
||||
>
|
||||
Send us a request.
|
||||
</a>
|
||||
</Text>
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { PropsWithChildren, useEffect, useRef } from "react";
|
||||
import { Box } from "@theme-ui/components";
|
||||
import { Close, AddReminder } from "../icons";
|
||||
import { Close, AddReminder, Menu } from "../icons";
|
||||
import { useStore as useSearchStore } from "../../stores/search-store";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
import { debounce, usePromise } from "@notesnook/common";
|
||||
@@ -27,6 +27,7 @@ import Field from "../field";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { RouteResult } from "../../navigation/types";
|
||||
import { CREATE_BUTTON_MAP } from "../../common";
|
||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
|
||||
export type RouteContainerButtons = {
|
||||
search?: {
|
||||
@@ -133,6 +134,16 @@ function Header(props: RouteContainerProps) {
|
||||
if (e.key === "Escape") useSearchStore.getState().resetSearch();
|
||||
else useSearchStore.setState({ isSearching: true, searchType: type });
|
||||
}}
|
||||
leftActions={[
|
||||
{
|
||||
icon: Menu,
|
||||
hidden: !isMobile,
|
||||
id: "hamburger-menu",
|
||||
onClick: () => {
|
||||
AppEventManager.publish(AppEvents.toggleSideMenu, true);
|
||||
}
|
||||
}
|
||||
]}
|
||||
rightActions={[
|
||||
{
|
||||
icon: Close,
|
||||
|
||||
@@ -17,7 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { debounce, usePromise } from "@notesnook/common";
|
||||
import {
|
||||
debounce,
|
||||
formatKey,
|
||||
keybindings,
|
||||
usePromise
|
||||
} from "@notesnook/common";
|
||||
import { EVENTS, fuzzy, Note, Notebook, Reminder, Tag } from "@notesnook/core";
|
||||
import { Box, Button, Flex, Input, Text } from "@theme-ui/components";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
@@ -539,7 +544,10 @@ function getCommandPaletteHelp(isCommandMode: boolean) {
|
||||
...(isCommandMode
|
||||
? [
|
||||
{
|
||||
key: isMac() ? "⌘P" : "Ctrl+P",
|
||||
key: keybindings.openQuickOpen
|
||||
.keys(IS_DESKTOP_APP)
|
||||
.map((k) => formatKey(k, isMac(), "+"))
|
||||
.join(" / "),
|
||||
description: strings.quickOpen()
|
||||
}
|
||||
]
|
||||
@@ -553,7 +561,10 @@ function getCommandPaletteHelp(isCommandMode: boolean) {
|
||||
description: strings.createNewNote()
|
||||
},
|
||||
{
|
||||
key: isMac() ? "⌘K" : "Ctrl+K",
|
||||
key: keybindings.openCommandPalette
|
||||
.keys(IS_DESKTOP_APP)
|
||||
.map((k) => formatKey(k, isMac(), "+"))
|
||||
.join(" / "),
|
||||
description: strings.commandPalette()
|
||||
}
|
||||
])
|
||||
|
||||
@@ -50,6 +50,7 @@ import { notebookMenuItems } from "../../components/notebook";
|
||||
import { tagMenuItems } from "../../components/tag";
|
||||
import { useEditorManager } from "../../components/editor/manager";
|
||||
import Config from "../../utils/config";
|
||||
import { KeyboardShortcutsDialog } from "../keyboard-shortcuts-dialog";
|
||||
|
||||
export interface BaseCommand {
|
||||
id: string;
|
||||
@@ -181,6 +182,14 @@ const staticCommands: Command[] = [
|
||||
group: strings.navigate(),
|
||||
type: "command"
|
||||
},
|
||||
{
|
||||
id: "keyboard-shortcuts",
|
||||
title: "Keyboard shortcuts",
|
||||
icon: ArrowTopRight,
|
||||
action: () => KeyboardShortcutsDialog.show({}),
|
||||
group: strings.navigate(),
|
||||
type: "command"
|
||||
},
|
||||
{
|
||||
id: "attachment-manager",
|
||||
title: strings.attachmentManager(),
|
||||
|
||||
112
apps/web/src/dialogs/keyboard-shortcuts-dialog.tsx
Normal file
112
apps/web/src/dialogs/keyboard-shortcuts-dialog.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatKey, getGroupedKeybindings } from "@notesnook/common";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import { DialogManager } from "../common/dialog-manager";
|
||||
import Dialog from "../components/dialog";
|
||||
import { isMac } from "../utils/platform";
|
||||
|
||||
const groupedKeybindings = getGroupedKeybindings(IS_DESKTOP_APP, isMac());
|
||||
|
||||
export const KeyboardShortcutsDialog = DialogManager.register(
|
||||
function KeyboardShortcutsDialog(props) {
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={"Keyboard Shortcuts"}
|
||||
width={750}
|
||||
onClose={() => props.onClose(false)}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
flexWrap: "nowrap",
|
||||
mt: 2,
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
{groupedKeybindings.map((group) => {
|
||||
return (
|
||||
<Flex key={group.category} sx={{ flexDirection: "column" }}>
|
||||
<Text
|
||||
variant="subtitle"
|
||||
sx={{
|
||||
borderBottom: "1px solid var(--border)",
|
||||
mb: 1,
|
||||
pb: 1
|
||||
}}
|
||||
>
|
||||
{group.category}
|
||||
</Text>
|
||||
{group.shortcuts.map((shortcut) => {
|
||||
return (
|
||||
<Flex
|
||||
key={shortcut.description}
|
||||
sx={{
|
||||
mb: 2,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Text variant="body">{shortcut.description}</Text>
|
||||
<Keys keys={shortcut.keys} />
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function Keys({ keys }: { keys: string[] }) {
|
||||
return (
|
||||
<Flex sx={{ gap: 1 }}>
|
||||
{keys.map((key, index) => (
|
||||
<>
|
||||
{key.split(" ").map((k) => (
|
||||
<Text
|
||||
key={k}
|
||||
as="code"
|
||||
sx={{
|
||||
bg: "background",
|
||||
color: "paragraph",
|
||||
px: 1,
|
||||
borderRadius: 5,
|
||||
fontSize: "body",
|
||||
border: "1px solid var(--border)"
|
||||
}}
|
||||
>
|
||||
{formatKey(k, isMac())}
|
||||
</Text>
|
||||
))}
|
||||
{keys.length - 1 !== index && (
|
||||
<Text as="span" sx={{ fontSize: "0.8em" }}>
|
||||
/
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -27,15 +27,15 @@ import {
|
||||
VirtualizedGrouping,
|
||||
createInternalLink
|
||||
} from "@notesnook/core";
|
||||
import { VirtualizedList } from "../components/virtualized-list";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { LinkAttributes } from "@notesnook/editor";
|
||||
import { NoteResolvedData, ResolvedItem } from "@notesnook/common";
|
||||
import { Lock } from "../components/icons";
|
||||
import { ellipsize } from "@notesnook/core";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { CustomScrollbarsVirtualList } from "../components/list-container";
|
||||
|
||||
export type NoteLinkingDialogProps = BaseDialogProps<LinkAttributes | false> & {
|
||||
attributes?: LinkAttributes;
|
||||
@@ -83,7 +83,10 @@ export const NoteLinkingDialog = DialogManager.register(
|
||||
}}
|
||||
noScroll
|
||||
>
|
||||
<Flex variant="columnFill" sx={{ mx: 3, overflow: "hidden" }}>
|
||||
<Flex
|
||||
variant="columnFill"
|
||||
sx={{ mx: 3, overflow: "hidden", height: 500 }}
|
||||
>
|
||||
{selectedNote ? (
|
||||
<>
|
||||
<Field
|
||||
@@ -131,15 +134,15 @@ export const NoteLinkingDialog = DialogManager.register(
|
||||
{strings.noBlocksOnNote()}
|
||||
</Text>
|
||||
) : null}
|
||||
<ScrollContainer>
|
||||
<VirtualizedList
|
||||
items={filteredBlocks || blocks}
|
||||
estimatedSize={34}
|
||||
mode="dynamic"
|
||||
itemGap={5}
|
||||
getItemKey={(i) => blocks[i].id}
|
||||
mt={1}
|
||||
renderItem={({ item }) => (
|
||||
<Virtuoso
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
components={{
|
||||
Scroller: CustomScrollbarsVirtualList
|
||||
}}
|
||||
data={filteredBlocks || blocks}
|
||||
context={{ items: filteredBlocks || blocks }}
|
||||
itemContent={(_, item) => {
|
||||
return (
|
||||
<Button
|
||||
variant="menuitem"
|
||||
sx={{
|
||||
@@ -162,7 +165,10 @@ export const NoteLinkingDialog = DialogManager.register(
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ fontFamily: "monospace", whiteSpace: "pre-wrap" }}
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
whiteSpace: "pre-wrap"
|
||||
}}
|
||||
>
|
||||
{ellipsize(item.content, 200, "end").trim() ||
|
||||
strings.linkNoteEmptyBlock()}
|
||||
@@ -181,9 +187,9 @@ export const NoteLinkingDialog = DialogManager.register(
|
||||
{item.type.toUpperCase()}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</ScrollContainer>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -203,14 +209,14 @@ export const NoteLinkingDialog = DialogManager.register(
|
||||
}}
|
||||
/>
|
||||
{notes && (
|
||||
<ScrollContainer>
|
||||
<VirtualizedList
|
||||
items={notes.placeholders}
|
||||
estimatedSize={28}
|
||||
itemGap={5}
|
||||
getItemKey={notes.key}
|
||||
mt={1}
|
||||
renderItem={({ index }) => (
|
||||
<Virtuoso
|
||||
data={notes.placeholders}
|
||||
components={{
|
||||
Scroller: CustomScrollbarsVirtualList
|
||||
}}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
itemContent={(index) => (
|
||||
<div style={{ height: 28 }}>
|
||||
<ResolvedItem items={notes} index={index} type="note">
|
||||
{({ item: note, data }) => (
|
||||
<Button
|
||||
@@ -239,9 +245,9 @@ export const NoteLinkingDialog = DialogManager.register(
|
||||
</Button>
|
||||
)}
|
||||
</ResolvedItem>
|
||||
)}
|
||||
/>
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -680,7 +680,8 @@ type ResolvedGroup = {
|
||||
};
|
||||
function getGroup(items: TreeNode[], groupId: string): ResolvedGroup | null {
|
||||
const index = items.findIndex((item) => item.id === groupId);
|
||||
const group = items[index];
|
||||
const group = items.at(index);
|
||||
if (!group) return null;
|
||||
if (!isGroup(group) && !isSubgroup(group)) return null;
|
||||
|
||||
const nextGroupIndex = items.findIndex(
|
||||
|
||||
@@ -31,6 +31,7 @@ import { SpellCheckerLanguages } from "./components/spell-checker-languages";
|
||||
import { CustomizeToolbar } from "./components/customize-toolbar";
|
||||
import { DictionaryWords } from "./components/dictionary-words";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { isMac } from "../../utils/platform";
|
||||
|
||||
export const EditorSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -164,7 +165,7 @@ export const EditorSettings: SettingsGroup[] = [
|
||||
key: "spell-checker-languages",
|
||||
title: strings.languages(),
|
||||
description: strings.spellCheckerLanguagesDescription(),
|
||||
isHidden: () => !useSpellChecker.getState().enabled,
|
||||
isHidden: () => !useSpellChecker.getState().enabled || isMac(),
|
||||
onStateChange: (listener) =>
|
||||
useSpellChecker.subscribe((c) => c.enabled, listener),
|
||||
components: [
|
||||
|
||||
@@ -492,6 +492,7 @@ function SessionExpiry(props: BaseAuthComponentProps<"sessionExpiry">) {
|
||||
placeholder={user ? maskEmail(user.email) : undefined}
|
||||
autoFocus
|
||||
disabled
|
||||
required={false}
|
||||
/>
|
||||
<Button
|
||||
data-test-id="auth-forgot-password"
|
||||
@@ -974,10 +975,10 @@ function SubtitleWithAction(props: SubtitleWithActionProps) {
|
||||
export function AuthField(props: FieldProps) {
|
||||
return (
|
||||
<Field
|
||||
required
|
||||
{...props}
|
||||
name={props.name || props.id}
|
||||
data-test-id={props["data-test-id"] || props.id}
|
||||
required
|
||||
sx={{ mt: 2, width: "100%" }}
|
||||
styles={{
|
||||
// label: { fontWeight: "normal" },
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
title: OneNote
|
||||
---
|
||||
|
||||
# How do I import notes from OneNote notes app?
|
||||
|
||||
The following steps will help you import your notes from OneNote easily.
|
||||
|
||||
1. Go to [https://importer.notesnook.com/](https://importer.notesnook.com/) and select `OneNote` from list of apps.
|
||||

|
||||
2. Click on `Start importing`
|
||||

|
||||
3. Login to your account
|
||||

|
||||
4. Give permission to Notesnook Importer to access your notes
|
||||

|
||||
5. Wait while Notesnook importer imports your notes from OneNote account. Once importer completes processing, download the .zip file.
|
||||

|
||||
6. After you have downloaded the `.zip` file, [go to the Notesnook Web App](https://app.notesnook.com/) > Settings > Notesnook Importer. Select the .zip you downloaded earlier and click "Start import" button.
|
||||

|
||||
7. Once importing completes you should see all your notes in Notesnook. If you face any issues during importing, feel free to [report them on GitHub](https://github.com/streetwriters/notesnook-importer).
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
title: Standard Notes
|
||||
---
|
||||
|
||||
# How do I import notes from Standard Notes?
|
||||
|
||||
The following steps will help you import your notes from Standard notes easily.
|
||||
|
||||
1. Open Standard Notes app on Desktop or visit [https://app.standardnotes.org](https://app.standardnotes.org) and Login to your account.
|
||||
2. Click on `Preferences` icon on bottom left corner to open preferences.
|
||||

|
||||
3. Scroll down to `Data backups` section and choose backup type `Decrypted`.
|
||||

|
||||
4. Go to `Backups` from the Sidebar & select "Data backup" to download backup .zip file.
|
||||
5. Open the Notesnook app (web or desktop)
|
||||
6. Go to `Settings > Notesnook Importer` and select `Standard Notes` from list of apps.
|
||||

|
||||
7. Drop the .zip backup file you exported earlier from Standard Notes in the box or click anywhere to open system file picker to select the backup.
|
||||

|
||||
8. Once importing completes you should see all your notes in Notesnook. If you face any issues during importing, [report it on github](https://github.com/streetwriters/notesnook).
|
||||
78
docs/help/contents/keyboard-shortcuts.md
Normal file
78
docs/help/contents/keyboard-shortcuts.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Keyboard Shortcuts
|
||||
description: Keyboard shortcuts for Notesnook
|
||||
---
|
||||
|
||||
# Keyboard shortcuts
|
||||
|
||||
The following keyboard shortcuts will help you navigate Notesnook faster.
|
||||
|
||||
### General
|
||||
|
||||
| Description | Web | Windows/Linux | Mac |
|
||||
| --- | --- | --- | --- |
|
||||
| Search in notes list view if editor is not focused | Ctrl F | Ctrl F | ⌘ F |
|
||||
| Settings | Ctrl , | Ctrl , | ⌘ , |
|
||||
| Keyboard shortcuts | Ctrl / | Ctrl / | ⌘ / |
|
||||
| New note | - | Ctrl N | ⌘ N |
|
||||
|
||||
### Navigation
|
||||
|
||||
| Description | Web | Windows/Linux | Mac |
|
||||
| --- | --- | --- | --- |
|
||||
| Next tab | Ctrl Alt → / Ctrl Alt ⇧ → | Ctrl tab | ⌘ tab |
|
||||
| Previous tab | Ctrl Alt ← / Ctrl Alt ⇧ ← | Ctrl ⇧ tab | ⌘ ⇧ tab |
|
||||
| Command palette | Ctrl ⇧ P | Ctrl ⇧ P | ⌘ ⇧ P |
|
||||
| Quick open | Ctrl P | Ctrl P | ⌘ P |
|
||||
| New tab | - | Ctrl T | ⌘ T |
|
||||
| Close active tab | - | Ctrl W | ⌘ W |
|
||||
| Close all tabs | - | Ctrl ⇧ W | ⌘ ⇧ W |
|
||||
|
||||
### Editor
|
||||
|
||||
| Description | Web | Windows/Linux | Mac |
|
||||
| --- | --- | --- | --- |
|
||||
| Add attachment | Ctrl ⇧ A | Ctrl ⇧ A | ⌘ ⇧ A |
|
||||
| Insert blockquote | Ctrl ⇧ B | Ctrl ⇧ B | ⌘ ⇧ B |
|
||||
| Toggle bold | Ctrl B | Ctrl B | ⌘ B |
|
||||
| Toggle bullet list | Ctrl ⇧ 8 | Ctrl ⇧ 8 | ⌘ ⇧ 8 |
|
||||
| Toggle check list | Ctrl ⇧ 9 | Ctrl ⇧ 9 | ⌘ ⇧ 9 |
|
||||
| Split list item | ↵ | ↵ | ↵ |
|
||||
| Lift list item | ⇧ Tab | ⇧ Tab | ⇧ Tab |
|
||||
| Sink list item | Ctrl ⇧ Down | Ctrl ⇧ Down | ⌘ ⇧ Down |
|
||||
| Toggle code | Ctrl E | Ctrl E | ⌘ E |
|
||||
| Toggle code block | Ctrl ⇧ C | Ctrl ⇧ C | ⌘ ⇧ C |
|
||||
| Insert date | Alt D | Alt D | ⌥ D |
|
||||
| Insert time | Alt T | Alt T | ⌥ T |
|
||||
| Insert date and time | Ctrl Alt D | Ctrl Alt D | ⌘ ⌥ D |
|
||||
| Insert date and time with timezone | Ctrl Alt Z | Ctrl Alt Z | ⌘ ⌥ Z |
|
||||
| Increase font size | Ctrl [ | Ctrl [ | ⌘ [ |
|
||||
| Decrease font size | Ctrl ] | Ctrl ] | ⌘ ] |
|
||||
| Insert paragraph | Ctrl ⇧ 0 | Ctrl ⇧ 0 | ⌘ ⇧ 0 |
|
||||
| Insert heading 1 | Ctrl Alt 1 | Ctrl Alt 1 | ⌘ ⌥ 1 |
|
||||
| Insert heading 2 | Ctrl Alt 2 | Ctrl Alt 2 | ⌘ ⌥ 2 |
|
||||
| Insert heading 3 | Ctrl Alt 3 | Ctrl Alt 3 | ⌘ ⌥ 3 |
|
||||
| Insert heading 4 | Ctrl Alt 4 | Ctrl Alt 4 | ⌘ ⌥ 4 |
|
||||
| Insert heading 5 | Ctrl Alt 5 | Ctrl Alt 5 | ⌘ ⌥ 5 |
|
||||
| Insert heading 6 | Ctrl Alt 6 | Ctrl Alt 6 | ⌘ ⌥ 6 |
|
||||
| Undo | Ctrl Z | Ctrl Z | ⌘ Z |
|
||||
| Redo | Ctrl ⇧ Z / Ctrl Y | Ctrl ⇧ Z / Ctrl Y | ⌘ ⇧ Z / ⌘ Y |
|
||||
| Add image | Ctrl ⇧ I | Ctrl ⇧ I | ⌘ ⇧ I |
|
||||
| Toggle italic | Ctrl I | Ctrl I | ⌘ I |
|
||||
| Remove formatting in selection | Ctrl \ | Ctrl \ | ⌘ \ |
|
||||
| Insert internal link | Ctrl ⇧ K | Ctrl ⇧ K | ⌘ ⇧ K |
|
||||
| Insert link | Ctrl K | Ctrl K | ⌘ K |
|
||||
| Insert math block | Ctrl ⇧ M | Ctrl ⇧ M | ⌘ ⇧ M |
|
||||
| Toggle ordered list | Ctrl ⇧ 7 | Ctrl ⇧ 7 | ⌘ ⇧ 7 |
|
||||
| Toggle outline list | Ctrl ⇧ O | Ctrl ⇧ O | ⌘ ⇧ O |
|
||||
| Toggle outline list expand | Ctrl Space | Ctrl Space | ⌘ Space |
|
||||
| Open search | Ctrl F | Ctrl F | ⌘ F |
|
||||
| Toggle strike | Ctrl ⇧ S | Ctrl ⇧ S | ⌘ ⇧ S |
|
||||
| Toggle subscript | Ctrl , | Ctrl , | ⌘ , |
|
||||
| Toggle superscript | Ctrl . | Ctrl . | ⌘ . |
|
||||
| Toggle task list | Ctrl ⇧ T | Ctrl ⇧ T | ⌘ ⇧ T |
|
||||
| Text align center | Ctrl ⇧ E | Ctrl ⇧ E | ⌘ ⇧ E |
|
||||
| Text align justify | Ctrl ⇧ J | Ctrl ⇧ J | ⌘ ⇧ J |
|
||||
| Text align left | Ctrl ⇧ L | Ctrl ⇧ L | ⌘ ⇧ L |
|
||||
| Text align right | Ctrl ⇧ R | Ctrl ⇧ R | ⌘ ⇧ R |
|
||||
| Underline | Ctrl U | Ctrl U | ⌘ U |
|
||||
@@ -54,6 +54,7 @@ navigation:
|
||||
- path: deleting-your-account.md
|
||||
- path: app-lock.md
|
||||
- path: gift-cards.md
|
||||
- path: keyboard-shortcuts.md
|
||||
|
||||
- path: privacy-mode.md
|
||||
- path: web-clipper
|
||||
|
||||
49
docs/help/package-lock.json
generated
Normal file
49
docs/help/package-lock.json
generated
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@notesnook/docs-help",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/docs-help",
|
||||
"version": "1.0.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@notesnook/common": "file:../../packages/common"
|
||||
}
|
||||
},
|
||||
"../../../packages/common": {
|
||||
"extraneous": true
|
||||
},
|
||||
"../../packages/common": {
|
||||
"name": "@notesnook/common",
|
||||
"version": "2.1.3",
|
||||
"dev": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/core": "file:../core",
|
||||
"@readme/data-urls": "^3.0.0",
|
||||
"dayjs": "1.11.13",
|
||||
"pathe": "^1.1.2",
|
||||
"timeago.js": "4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@notesnook/core": "file:../core",
|
||||
"@types/react": "18.3.5",
|
||||
"react": "18.3.1",
|
||||
"vitest": "2.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"timeago.js": "4.0.2"
|
||||
}
|
||||
},
|
||||
"../common": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@notesnook/common": {
|
||||
"resolved": "../../packages/common",
|
||||
"link": true
|
||||
}
|
||||
}
|
||||
}
|
||||
25
docs/help/package.json
Normal file
25
docs/help/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@notesnook/docs-help",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"document-keyboard-shortcuts": "node scripts/document-keyboard-shortcuts.mjs"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/streetwriters/notesnook.git"
|
||||
},
|
||||
"keywords": [
|
||||
"notesnook",
|
||||
"docs",
|
||||
"help"
|
||||
],
|
||||
"author": "",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/streetwriters/notesnook/issues"
|
||||
},
|
||||
"homepage": "https://github.com/streetwriters/notesnook#readme",
|
||||
"devDependencies": {
|
||||
"@notesnook/common": "file:../../packages/common"
|
||||
}
|
||||
}
|
||||
100
docs/help/scripts/document-keyboard-shortcuts.mjs
Normal file
100
docs/help/scripts/document-keyboard-shortcuts.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { writeFileSync } from "fs";
|
||||
import {
|
||||
getGroupedKeybindings,
|
||||
formatKey,
|
||||
macify,
|
||||
CATEGORIES
|
||||
} from "@notesnook/common";
|
||||
|
||||
console.log("Generating keyboard shortcuts documentation...");
|
||||
|
||||
const keyboardShortcutFilePath = "./contents/keyboard-shortcuts.md";
|
||||
|
||||
const frontmatter = `---
|
||||
title: Keyboard Shortcuts
|
||||
description: Keyboard shortcuts for Notesnook
|
||||
---
|
||||
`;
|
||||
|
||||
const content = `# Keyboard shortcuts
|
||||
|
||||
The following keyboard shortcuts will help you navigate Notesnook faster.`;
|
||||
|
||||
const markdownTable = getGroupedTableKeybindingsMarkdown();
|
||||
|
||||
writeFileSync(
|
||||
keyboardShortcutFilePath,
|
||||
frontmatter + "\n" + content + "\n\n" + markdownTable,
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
console.log("Keyboard shortcuts documentation updated successfully!");
|
||||
|
||||
/**
|
||||
* @returns markdown formatted table of keyboard shortcuts grouped by category.
|
||||
*/
|
||||
function getGroupedTableKeybindingsMarkdown() {
|
||||
const desktopKeybindings = getGroupedKeybindings(true, false);
|
||||
const webKeybindings = getGroupedKeybindings(false, false);
|
||||
|
||||
const header = `| Description | Web | Windows/Linux | Mac |
|
||||
| --- | --- | --- | --- |`;
|
||||
|
||||
return CATEGORIES.map((category) => {
|
||||
const webShortcuts =
|
||||
webKeybindings.find((g) => g.category === category)?.shortcuts || [];
|
||||
const desktopShortcuts =
|
||||
desktopKeybindings.find((g) => g.category === category)?.shortcuts || [];
|
||||
|
||||
const mergedShortcuts = {};
|
||||
|
||||
webShortcuts.forEach(({ description, keys }) => {
|
||||
if (!mergedShortcuts[description]) {
|
||||
mergedShortcuts[description] = {};
|
||||
}
|
||||
mergedShortcuts[description].web = keys;
|
||||
});
|
||||
desktopShortcuts.forEach(({ description, keys }) => {
|
||||
if (!mergedShortcuts[description]) {
|
||||
mergedShortcuts[description] = {};
|
||||
}
|
||||
mergedShortcuts[description].desktop = keys;
|
||||
});
|
||||
|
||||
const rows = Object.entries(mergedShortcuts)
|
||||
.map(([description, { web, desktop }]) => {
|
||||
const webKeys = web?.map((k) => formatKey(k)).join(" / ") || "-";
|
||||
const windowsLinuxKeys =
|
||||
desktop?.map((k) => formatKey(k)).join(" / ") || "-";
|
||||
const macKeys =
|
||||
desktop
|
||||
?.map(macify)
|
||||
.map((k) => formatKey(k, true))
|
||||
.join(" / ") || "-";
|
||||
|
||||
return `| ${description} | ${webKeys} | ${windowsLinuxKeys} | ${macKeys} |`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return `### ${category}\n\n${header}\n${rows}`;
|
||||
}).join("\n\n");
|
||||
}
|
||||
5
fastlane/metadata/android/en-US/changelogs/15319.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/15319.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
- Fix crash on device boot on android 15 devices
|
||||
- Fix note not opening in editor randomly
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
3
fastlane/metadata/android/en-US/changelogs/15324.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15324.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
4
fastlane/metadata/android/en-US/changelogs/15329.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/15329.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
- Fix crash on android devices upgraded to android 16
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
4
fastlane/metadata/android/en-US/changelogs/15334.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/15334.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
- Add scroll to top/bottom in editor
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
3
nx.json
3
nx.json
@@ -5,7 +5,8 @@
|
||||
"outputs": [
|
||||
"{projectRoot}/build",
|
||||
"{projectRoot}/dist",
|
||||
"{projectRoot}/languages"
|
||||
"{projectRoot}/languages",
|
||||
"{projectRoot}/src/extensions/code-block/languages"
|
||||
],
|
||||
"cache": true
|
||||
},
|
||||
|
||||
@@ -28,3 +28,4 @@ export * from "./migrate-toolbar.js";
|
||||
export * from "./export-notes.js";
|
||||
export * from "./dataurl.js";
|
||||
export * from "./tab-session-history.js";
|
||||
export * from "./keybindings.js";
|
||||
|
||||
485
packages/common/src/utils/keybindings.ts
Normal file
485
packages/common/src/utils/keybindings.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
interface Hotkeys {
|
||||
keys: (isDesktop: boolean) => string[];
|
||||
description: string;
|
||||
category: Category;
|
||||
type: "hotkeys";
|
||||
}
|
||||
|
||||
interface TipTapKey {
|
||||
keys: string | string[];
|
||||
description: string;
|
||||
category: Category;
|
||||
type: "tiptap";
|
||||
}
|
||||
|
||||
type Category = (typeof CATEGORIES)[number];
|
||||
|
||||
export const CATEGORIES = ["General", "Navigation", "Editor"] as const;
|
||||
|
||||
/**
|
||||
* consumed by hotkeys-js
|
||||
*/
|
||||
export const hotkeys = {
|
||||
nextTab: {
|
||||
keys: normalizeKeys({
|
||||
web: ["ctrl+alt+right", "ctrl+alt+shift+right"],
|
||||
desktop: ["ctrl+tab"]
|
||||
}),
|
||||
description: "Next tab",
|
||||
category: "Navigation",
|
||||
type: "hotkeys"
|
||||
},
|
||||
previousTab: {
|
||||
keys: normalizeKeys({
|
||||
web: ["ctrl+alt+left", "ctrl+alt+shift+left"],
|
||||
desktop: ["ctrl+shift+tab"]
|
||||
}),
|
||||
description: "Previous tab",
|
||||
category: "Navigation",
|
||||
type: "hotkeys"
|
||||
},
|
||||
newTab: {
|
||||
keys: normalizeKeys({
|
||||
desktop: ["ctrl+t"]
|
||||
}),
|
||||
description: "New tab",
|
||||
category: "Navigation",
|
||||
type: "hotkeys"
|
||||
},
|
||||
closeActiveTab: {
|
||||
keys: normalizeKeys({
|
||||
desktop: ["ctrl+w"]
|
||||
}),
|
||||
description: "Close active tab",
|
||||
category: "Navigation",
|
||||
type: "hotkeys"
|
||||
},
|
||||
closeAllTabs: {
|
||||
keys: normalizeKeys({
|
||||
desktop: ["ctrl+shift+w"]
|
||||
}),
|
||||
description: "Close all tabs",
|
||||
category: "Navigation",
|
||||
type: "hotkeys"
|
||||
},
|
||||
newNote: {
|
||||
keys: normalizeKeys({
|
||||
desktop: ["ctrl+n"]
|
||||
}),
|
||||
description: "New note",
|
||||
category: "General",
|
||||
type: "hotkeys"
|
||||
},
|
||||
searchInNotes: {
|
||||
keys: normalizeKeys(["ctrl+f"]),
|
||||
description: "Search in notes list view if editor is not focused",
|
||||
category: "General",
|
||||
type: "hotkeys"
|
||||
},
|
||||
openCommandPalette: {
|
||||
keys: normalizeKeys(["ctrl+shift+p"]),
|
||||
description: "Command palette",
|
||||
category: "Navigation",
|
||||
type: "hotkeys"
|
||||
},
|
||||
openQuickOpen: {
|
||||
keys: normalizeKeys(["ctrl+p"]),
|
||||
description: "Quick open",
|
||||
category: "Navigation",
|
||||
type: "hotkeys"
|
||||
},
|
||||
openSettings: {
|
||||
keys: normalizeKeys(["ctrl+,"]),
|
||||
description: "Settings",
|
||||
category: "General",
|
||||
type: "hotkeys"
|
||||
},
|
||||
openKeyboardShortcuts: {
|
||||
keys: normalizeKeys(["ctrl+/"]),
|
||||
description: "Keyboard shortcuts",
|
||||
category: "General",
|
||||
type: "hotkeys"
|
||||
}
|
||||
} satisfies Record<string, Hotkeys>;
|
||||
|
||||
/**
|
||||
* consumed by tiptap
|
||||
*/
|
||||
export const tiptapKeys = {
|
||||
addAttachment: {
|
||||
keys: "Mod-Shift-A",
|
||||
description: "Add attachment",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertBlockquote: {
|
||||
keys: "Mod-Shift-B",
|
||||
description: "Insert blockquote",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleBold: {
|
||||
keys: "Mod-b",
|
||||
description: "Toggle bold",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleBulletList: {
|
||||
keys: "Mod-Shift-8",
|
||||
description: "Toggle bullet list",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleCheckList: {
|
||||
keys: "Mod-Shift-9",
|
||||
description: "Toggle check list",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
splitListItem: {
|
||||
keys: "Enter",
|
||||
description: "Split list item",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
liftListItem: {
|
||||
keys: "Shift-Tab",
|
||||
description: "Lift list item",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
sinkListItem: {
|
||||
keys: "Mod-Shift-Down",
|
||||
description: "Sink list item",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleCode: {
|
||||
keys: "Mod-e",
|
||||
description: "Toggle code",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleCodeBlock: {
|
||||
keys: "Mod-Shift-C",
|
||||
description: "Toggle code block",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertDate: {
|
||||
keys: "Alt-d",
|
||||
description: "Insert date",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertTime: {
|
||||
keys: "Alt-t",
|
||||
description: "Insert time",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertDateTime: {
|
||||
keys: "Mod-Alt-d",
|
||||
description: "Insert date and time",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertDateTimeWithTimezone: {
|
||||
keys: "Mod-Alt-z",
|
||||
description: "Insert date and time with timezone",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
increaseFontSize: {
|
||||
keys: "Ctrl-[",
|
||||
description: "Increase font size",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
decreaseFontSize: {
|
||||
keys: "Ctrl-]",
|
||||
description: "Decrease font size",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertParagraph: {
|
||||
keys: "Mod-Shift-0",
|
||||
description: "Insert paragraph",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertHeading1: {
|
||||
keys: "Mod-Alt-1",
|
||||
description: "Insert heading 1",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertHeading2: {
|
||||
keys: "Mod-Alt-2",
|
||||
description: "Insert heading 2",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertHeading3: {
|
||||
keys: "Mod-Alt-3",
|
||||
description: "Insert heading 3",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertHeading4: {
|
||||
keys: "Mod-Alt-4",
|
||||
description: "Insert heading 4",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertHeading5: {
|
||||
keys: "Mod-Alt-5",
|
||||
description: "Insert heading 5",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertHeading6: {
|
||||
keys: "Mod-Alt-6",
|
||||
description: "Insert heading 6",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
undo: {
|
||||
keys: "Mod-z",
|
||||
description: "Undo",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
redo: {
|
||||
keys: ["Mod-Shift-z", "Mod-y"],
|
||||
description: "Redo",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
addImage: {
|
||||
keys: "Mod-Shift-I",
|
||||
description: "Add image",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleItalic: {
|
||||
keys: "Mod-i",
|
||||
description: "Toggle italic",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
removeFormattingInSelection: {
|
||||
keys: "Mod-\\",
|
||||
description: "Remove formatting in selection",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertInternalLink: {
|
||||
keys: "Mod-Shift-K",
|
||||
description: "Insert internal link",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertLink: {
|
||||
keys: "Mod-k",
|
||||
description: "Insert link",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
insertMathBlock: {
|
||||
keys: "Mod-Shift-M",
|
||||
description: "Insert math block",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleOrderedList: {
|
||||
keys: "Mod-Shift-7",
|
||||
description: "Toggle ordered list",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleOutlineList: {
|
||||
keys: "Mod-Shift-O",
|
||||
description: "Toggle outline list",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleOutlineListExpand: {
|
||||
keys: "Mod-Space",
|
||||
description: "Toggle outline list expand",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
openSearch: {
|
||||
keys: "Mod-f",
|
||||
description: "Open search",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleStrike: {
|
||||
keys: "Mod-Shift-S",
|
||||
description: "Toggle strike",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleSubscript: {
|
||||
keys: "Mod-,",
|
||||
description: "Toggle subscript",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleSuperscript: {
|
||||
keys: "Mod-.",
|
||||
description: "Toggle superscript",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleTaskList: {
|
||||
keys: "Mod-Shift-T",
|
||||
description: "Toggle task list",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
textAlignCenter: {
|
||||
keys: "Mod-Shift-E",
|
||||
description: "Text align center",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
textAlignJustify: {
|
||||
keys: "Mod-Shift-J",
|
||||
description: "Text align justify",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
textAlignLeft: {
|
||||
keys: "Mod-Shift-L",
|
||||
description: "Text align left",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
textAlignRight: {
|
||||
keys: "Mod-Shift-R",
|
||||
description: "Text align right",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
underline: {
|
||||
keys: "Mod-u",
|
||||
description: "Underline",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
}
|
||||
} satisfies Record<string, TipTapKey>;
|
||||
|
||||
export const keybindings = {
|
||||
...hotkeys,
|
||||
...tiptapKeys
|
||||
};
|
||||
|
||||
export function getKeybinding(
|
||||
key: keyof typeof keybindings,
|
||||
isDesktop = false,
|
||||
isMac = false
|
||||
) {
|
||||
const keybinding = keybindings[key];
|
||||
if (keybinding.type === "hotkeys") {
|
||||
const hotkeys = keybinding.keys(isDesktop);
|
||||
return isMac ? hotkeys.map(macify) : hotkeys;
|
||||
}
|
||||
const tiptapKeys = Array.isArray(keybinding.keys)
|
||||
? keybinding.keys
|
||||
: [keybinding.keys];
|
||||
return isMac ? tiptapKeys.map(macify) : tiptapKeys;
|
||||
}
|
||||
|
||||
function normalizeKeys(
|
||||
keys: string[] | { web?: string[]; desktop?: string[] }
|
||||
): (isDesktop?: boolean) => string[] {
|
||||
return (isDesktop = false) => {
|
||||
let keyList: string[] = [];
|
||||
if (Array.isArray(keys)) {
|
||||
keyList = keys;
|
||||
} else {
|
||||
keyList = isDesktop ? keys.desktop ?? [] : keys.web ?? [];
|
||||
}
|
||||
return keyList;
|
||||
};
|
||||
}
|
||||
|
||||
export function macify(key: string) {
|
||||
return key
|
||||
.replace(/ctrl/gi, "Command")
|
||||
.replace(/alt/gi, "Option")
|
||||
.replace(/mod/gi, "Command");
|
||||
}
|
||||
|
||||
export function formatKey(key: string, isMac = false, separator = " ") {
|
||||
return key
|
||||
.replace(/\+|-/g, separator)
|
||||
.replace(/\bcommand\b/gi, isMac ? "⌘" : "Ctrl")
|
||||
.replace(/\bctrl\b/gi, isMac ? "⌘" : "Ctrl")
|
||||
.replace(/\bmod\b/gi, isMac ? "⌘" : "Ctrl")
|
||||
.replace(/\balt\b/gi, isMac ? "⌥" : "Alt")
|
||||
.replace(/\boption\b/gi, isMac ? "⌥" : "Alt")
|
||||
.replace(/\bshift\b/gi, "⇧")
|
||||
.replace(/\bright\b/gi, "→")
|
||||
.replace(/\bleft\b/gi, "←")
|
||||
.replace(/\benter\b/gi, "↵")
|
||||
.replace(/\b\w\b/gi, (e) => e.toUpperCase())
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function getGroupedKeybindings(isDesktop: boolean, isMac: boolean) {
|
||||
const grouped: {
|
||||
shortcuts: { keys: string[]; description: string }[];
|
||||
category: Category;
|
||||
}[] = CATEGORIES.map((c) => ({
|
||||
category: c,
|
||||
shortcuts: []
|
||||
}));
|
||||
|
||||
const allKeybindings = { ...hotkeys, ...tiptapKeys };
|
||||
|
||||
for (const key in allKeybindings) {
|
||||
const binding = allKeybindings[key as keyof typeof allKeybindings];
|
||||
let keys =
|
||||
typeof binding.keys === "function"
|
||||
? binding.keys(isDesktop)
|
||||
: binding.keys;
|
||||
if (!keys || !keys.length) continue;
|
||||
|
||||
if (isMac) {
|
||||
keys = Array.isArray(keys) ? keys.map(macify) : macify(keys);
|
||||
}
|
||||
|
||||
const group = grouped.find((g) => g.category === binding.category);
|
||||
if (!group) throw new Error("Invalid group category: " + binding.category);
|
||||
|
||||
group.shortcuts.push({
|
||||
keys: Array.isArray(keys) ? keys : [keys],
|
||||
description: binding.description
|
||||
});
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
toBlobURL,
|
||||
usePermissionHandler
|
||||
} from "@notesnook/editor";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import FingerprintIcon from "mdi-react/FingerprintIcon";
|
||||
import {
|
||||
@@ -47,7 +48,6 @@ import StatusBar from "./statusbar";
|
||||
import Tags from "./tags";
|
||||
import TiptapEditorWrapper from "./tiptap";
|
||||
import Title from "./title";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
|
||||
|
||||
@@ -380,11 +380,15 @@ const Tiptap = ({
|
||||
|
||||
const editor = editors[tab.id];
|
||||
|
||||
const firstChild = editor?.state.doc.firstChild;
|
||||
const isParagraph = firstChild?.type.name === "paragraph";
|
||||
const isFirstChildEmpty =
|
||||
!firstChild?.textContent || firstChild?.textContent?.length === 0;
|
||||
if (isParagraph && isFirstChildEmpty) {
|
||||
const firstChildNodeType = editor?.state.doc.firstChild?.type.name;
|
||||
const isSimpleNode =
|
||||
firstChildNodeType !== "image" &&
|
||||
firstChildNodeType !== "embed" &&
|
||||
firstChildNodeType !== "attachment" &&
|
||||
firstChildNodeType !== "mathBlock" &&
|
||||
firstChildNodeType !== "horizontalRule" &&
|
||||
firstChildNodeType !== "table";
|
||||
if (isSimpleNode) {
|
||||
editor?.commands.focus("end");
|
||||
return;
|
||||
}
|
||||
@@ -405,11 +409,15 @@ const Tiptap = ({
|
||||
const editor = editors[tab.id];
|
||||
const docSize = editor?.state.doc.content.size;
|
||||
if (!docSize) return;
|
||||
const lastChild = editor?.state.doc.lastChild;
|
||||
const isParagraph = lastChild?.type.name === "paragraph";
|
||||
const isLastChildEmpty =
|
||||
!lastChild?.textContent || lastChild?.textContent?.length === 0;
|
||||
if (isParagraph && isLastChildEmpty) {
|
||||
const lastChildNodeType = editor?.state.doc.lastChild?.type.name;
|
||||
const isSimpleNode =
|
||||
lastChildNodeType !== "image" &&
|
||||
lastChildNodeType !== "embed" &&
|
||||
lastChildNodeType !== "attachment" &&
|
||||
lastChildNodeType !== "mathBlock" &&
|
||||
lastChildNodeType !== "horizontalRule" &&
|
||||
lastChildNodeType !== "table";
|
||||
if (isSimpleNode) {
|
||||
editor?.commands.focus("end");
|
||||
return;
|
||||
}
|
||||
@@ -544,10 +552,12 @@ const Tiptap = ({
|
||||
<div
|
||||
onScroll={controller.scroll}
|
||||
ref={containerRef}
|
||||
id="editor-container-scroller"
|
||||
style={{
|
||||
overflowY: controller.loading ? "hidden" : "scroll",
|
||||
height: "100%",
|
||||
display: "block",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
@@ -865,9 +875,10 @@ const Tiptap = ({
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
width: "100%",
|
||||
minHeight: 300
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
minHeight: 100
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { ControlledMenu, MenuItem as MenuItemInner } from "@szhsin/react-menu";
|
||||
import ArrowBackIcon from "mdi-react/ArrowBackIcon";
|
||||
import ArrowTopIcon from "mdi-react/ArrowTopIcon";
|
||||
import ArrowDownIcon from "mdi-react/ArrowDownIcon";
|
||||
import ArrowForwardIcon from "mdi-react/ArrowForwardIcon";
|
||||
import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon";
|
||||
import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon";
|
||||
@@ -428,6 +430,30 @@ function Header({
|
||||
tab.session?.noteId
|
||||
);
|
||||
break;
|
||||
case "scroll-top":
|
||||
{
|
||||
const element = document.getElementById(
|
||||
"editor-container-scroller"
|
||||
);
|
||||
element?.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "scroll-bottom":
|
||||
{
|
||||
const element = document.getElementById(
|
||||
"editor-container-scroller"
|
||||
);
|
||||
element?.scrollTo({
|
||||
top: element?.scrollHeight,
|
||||
left: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "properties":
|
||||
post(
|
||||
EditorEvents.properties,
|
||||
@@ -590,6 +616,47 @@ function Header({
|
||||
{strings.toc()}
|
||||
</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="scroll-top"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 10,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<ArrowTopIcon
|
||||
size={22 * settings.fontScale}
|
||||
color="var(--nn_primary_icon)"
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--nn_primary_paragraph)"
|
||||
}}
|
||||
>
|
||||
{strings.scrollToTop()}
|
||||
</span>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
value="scroll-bottom"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 10,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<ArrowDownIcon
|
||||
size={22 * settings.fontScale}
|
||||
color="var(--nn_primary_icon)"
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--nn_primary_paragraph)"
|
||||
}}
|
||||
>
|
||||
{strings.scrollToBottom()}
|
||||
</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="properties"
|
||||
style={{
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Attribute } from "@tiptap/core";
|
||||
import { createNodeView } from "../react/index.js";
|
||||
import { AttachmentComponent } from "./component.js";
|
||||
import { Attachment } from "./types.js";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
|
||||
export type AttachmentType = "image" | "file" | "camera";
|
||||
export interface AttachmentOptions {
|
||||
@@ -161,7 +162,7 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-Shift-A": () =>
|
||||
[tiptapKeys.addAttachment.keys]: () =>
|
||||
this.editor.storage.openAttachmentPicker?.("file") || true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
import { getParentAttributes } from "../../utils/prosemirror.js";
|
||||
import { wrappingInputRule } from "@tiptap/core";
|
||||
import TiptapBlockquote, { inputRegex } from "@tiptap/extension-blockquote";
|
||||
@@ -47,5 +48,13 @@ export const Blockquote = TiptapBlockquote.extend({
|
||||
getAttributes: () => getParentAttributes(this.editor)
|
||||
})
|
||||
];
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
[tiptapKeys.insertBlockquote.keys]: () =>
|
||||
this.editor.commands.toggleBlockquote()
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { keybindings } from "@notesnook/common";
|
||||
import { KeyboardShortcutCommand, mergeAttributes, Node } from "@tiptap/core";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
|
||||
@@ -80,7 +81,8 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
|
||||
[key: string]: KeyboardShortcutCommand;
|
||||
} = {
|
||||
Enter: () => this.editor.commands.splitListItem(this.name),
|
||||
"Shift-Tab": () => this.editor.commands.liftListItem(this.name)
|
||||
[keybindings.liftListItem.keys]: () =>
|
||||
this.editor.commands.liftListItem(this.name)
|
||||
};
|
||||
|
||||
if (!this.options.nested) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { mergeAttributes, Node, wrappingInputRule } from "@tiptap/core";
|
||||
import { inputRegex } from "@tiptap/extension-task-item";
|
||||
import { getParentAttributes } from "../../utils/prosemirror.js";
|
||||
import { ListItem } from "../list-item/index.js";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
|
||||
export interface CheckListOptions {
|
||||
itemTypeName: string;
|
||||
@@ -123,7 +124,8 @@ export const CheckList = Node.create<CheckListOptions>({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-Shift-9": () => this.editor.commands.toggleCheckList()
|
||||
[tiptapKeys.toggleCheckList.keys]: () =>
|
||||
this.editor.commands.toggleCheckList()
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ import stripIndent from "strip-indent";
|
||||
import { nanoid } from "nanoid";
|
||||
import Languages from "./languages.json";
|
||||
import { CaretPosition, CodeLine } from "./utils.js";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
|
||||
interface Indent {
|
||||
type: "tab" | "space";
|
||||
@@ -292,7 +293,8 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-Shift-C": () => this.editor.commands.toggleCodeBlock(),
|
||||
[tiptapKeys.toggleCodeBlock.keys]: () =>
|
||||
this.editor.commands.toggleCodeBlock(),
|
||||
"Mod-a": ({ editor }) => {
|
||||
const { $anchor } = this.editor.state.selection;
|
||||
if ($anchor.parent.type.name !== this.name) {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
InputRuleFinder,
|
||||
ExtendedRegExpMatchArray
|
||||
} from "@tiptap/core";
|
||||
import { formatDate } from "@notesnook/common";
|
||||
import { formatDate, tiptapKeys } from "@notesnook/common";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
@@ -67,10 +67,14 @@ export const DateTime = Extension.create<DateTimeOptions>({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Alt-t": ({ editor }) => editor.commands.insertTime(),
|
||||
"Alt-d": ({ editor }) => editor.commands.insertDate(),
|
||||
"Mod-Alt-d": ({ editor }) => editor.commands.insertDateTime(),
|
||||
"Mod-Alt-z": ({ editor }) => editor.commands.insertDateTimeWithTimeZone()
|
||||
[tiptapKeys.insertTime.keys]: ({ editor }) =>
|
||||
editor.commands.insertTime(),
|
||||
[tiptapKeys.insertDate.keys]: ({ editor }) =>
|
||||
editor.commands.insertDate(),
|
||||
[tiptapKeys.insertDateTime.keys]: ({ editor }) =>
|
||||
editor.commands.insertDateTime(),
|
||||
[tiptapKeys.insertDateTimeWithTimezone.keys]: ({ editor }) =>
|
||||
editor.commands.insertDateTimeWithTimeZone()
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
|
||||
import { Editor, Extension } from "@tiptap/core";
|
||||
|
||||
@@ -86,7 +87,7 @@ export const FontSize = Extension.create<FontSizeOptions>({
|
||||
},
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"ctrl-]": ({ editor }) => {
|
||||
[tiptapKeys.decreaseFontSize.keys]: ({ editor }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
@@ -94,7 +95,7 @@ export const FontSize = Extension.create<FontSizeOptions>({
|
||||
.run();
|
||||
return true;
|
||||
},
|
||||
"Ctrl-[": ({ editor }) => {
|
||||
[tiptapKeys.increaseFontSize.keys]: ({ editor }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
import { textblockTypeInputRule } from "@tiptap/core";
|
||||
import { Heading as TiptapHeading } from "@tiptap/extension-heading";
|
||||
|
||||
@@ -49,7 +50,8 @@ export const Heading = TiptapHeading.extend({
|
||||
(items, level) => ({
|
||||
...items,
|
||||
...{
|
||||
[`Mod-Alt-${level}`]: () => this.editor.commands.setHeading({ level })
|
||||
[tiptapKeys[`insertHeading${level}`].keys]: () =>
|
||||
this.editor.commands.setHeading({ level })
|
||||
}
|
||||
}),
|
||||
{}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { createNodeView } from "../react/index.js";
|
||||
import { TextDirections } from "../text-direction/index.js";
|
||||
import { ImageComponent } from "./component.js";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
|
||||
export interface ImageOptions {
|
||||
inline: boolean;
|
||||
@@ -199,7 +200,7 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-Shift-I": () =>
|
||||
[tiptapKeys.addImage.keys]: () =>
|
||||
this.editor.storage.openAttachmentPicker?.("image") || true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { isInTable } from "@tiptap/pm/tables";
|
||||
import { CodeBlock } from "../code-block/index.js";
|
||||
import { showLinkPopup } from "../../toolbar/popups/link-popup.js";
|
||||
import { isListActive } from "../../utils/list.js";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
|
||||
export const KeyMap = Extension.create({
|
||||
name: "key-map",
|
||||
@@ -42,7 +43,7 @@ export const KeyMap = Extension.create({
|
||||
if (isListActive(editor)) return false;
|
||||
return true;
|
||||
},
|
||||
"Mod-\\": ({ editor }) => {
|
||||
[tiptapKeys.removeFormattingInSelection.keys]: ({ editor }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
@@ -52,7 +53,7 @@ export const KeyMap = Extension.create({
|
||||
.run();
|
||||
return true;
|
||||
},
|
||||
"Shift-Mod-L": ({ editor }) => {
|
||||
[tiptapKeys.insertInternalLink.keys]: ({ editor }) => {
|
||||
editor.storage.createInternalLink?.().then((link) => {
|
||||
if (!link) return;
|
||||
|
||||
@@ -67,7 +68,7 @@ export const KeyMap = Extension.create({
|
||||
});
|
||||
return true;
|
||||
},
|
||||
"Shift-Mod-k": ({ editor }) => {
|
||||
[tiptapKeys.insertLink.keys]: ({ editor }) => {
|
||||
showLinkPopup(editor);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -30,46 +30,48 @@ export function clickHandler(options: ClickHandlerOptions): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey("handleClickLink"),
|
||||
props: {
|
||||
handleClick: (view, pos, event) => {
|
||||
const isMainClick = event.button === 0;
|
||||
const isAuxClick = event.button === 1;
|
||||
if (!isMainClick && !isAuxClick) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let a = event.target as HTMLElement;
|
||||
const els = [];
|
||||
|
||||
while (a.nodeName !== "DIV") {
|
||||
els.push(a);
|
||||
a = a.parentNode as HTMLElement;
|
||||
}
|
||||
|
||||
if (!els.find((value) => value.nodeName === "A")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attrs = getAttributes(view.state, options.type.name);
|
||||
const link = event.target as HTMLLinkElement;
|
||||
|
||||
const href = link?.href ?? attrs.href;
|
||||
// const target = link?.target ?? attrs.target;
|
||||
|
||||
if (link && href) {
|
||||
if (options.editor.storage.openLink) {
|
||||
event.preventDefault();
|
||||
setTimeout(() =>
|
||||
options.editor.storage.openLink?.(
|
||||
href,
|
||||
isAuxClick || event.ctrlKey || event.metaKey
|
||||
)
|
||||
);
|
||||
handleDOMEvents: {
|
||||
click: (view, event) => {
|
||||
const isMainClick = event.button === 0;
|
||||
const isAuxClick = event.button === 1;
|
||||
if (!isMainClick && !isAuxClick) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
let a = event.target as HTMLElement;
|
||||
const els = [];
|
||||
|
||||
return false;
|
||||
while (a.nodeName !== "DIV") {
|
||||
els.push(a);
|
||||
a = a.parentNode as HTMLElement;
|
||||
}
|
||||
|
||||
if (!els.find((value) => value.nodeName === "A")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attrs = getAttributes(view.state, options.type.name);
|
||||
const link = event.target as HTMLLinkElement;
|
||||
|
||||
const href = link?.href ?? attrs.href;
|
||||
// const target = link?.target ?? attrs.target;
|
||||
|
||||
if (link && href) {
|
||||
if (options.editor.storage.openLink) {
|
||||
event.preventDefault();
|
||||
setTimeout(() =>
|
||||
options.editor.storage.openLink?.(
|
||||
href,
|
||||
isAuxClick || event.ctrlKey || event.metaKey
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -232,9 +232,10 @@ export const Link = Mark.create<LinkOptions>({
|
||||
}
|
||||
}),
|
||||
markPasteRule({
|
||||
find: (text) => {
|
||||
find: (text, ev) => {
|
||||
const foundLinks: PasteRuleMatch[] = [];
|
||||
|
||||
const html = ev?.clipboardData?.getData("text/html");
|
||||
if (html && html.includes("<a")) return [];
|
||||
if (text) {
|
||||
const links = find(text).filter((item) => item.isLink);
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`paste text > with html link 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><p><a target="_blank" rel="noopener noreferrer nofollow" spellcheck="false" href="nn://note/68798972093c8c6ea22efca2">example.py</a></p></div></div>"`;
|
||||
|
||||
exports[`paste text > with markdown link 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><p><a target="_blank" rel="noopener noreferrer nofollow" spellcheck="false" href="example.com">test</a></p></div></div>"`;
|
||||
|
||||
exports[`paste text > with multiple markdown links 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><p><a target="_blank" rel="noopener noreferrer nofollow" spellcheck="false" href="example.com">test</a> some text <a target="_blank" rel="noopener noreferrer nofollow" spellcheck="false" href="example2.com">test2</a></p></div></div>"`;
|
||||
|
||||
@@ -114,4 +114,33 @@ describe("paste text", () => {
|
||||
|
||||
expect(editorElement.outerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("with html link", async () => {
|
||||
const editorElement = h("div");
|
||||
const { editor } = createEditor({
|
||||
element: editorElement,
|
||||
extensions: {
|
||||
link: Link
|
||||
}
|
||||
});
|
||||
|
||||
const clipboardEvent = new Event("paste", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true
|
||||
});
|
||||
|
||||
(clipboardEvent as unknown as any)["clipboardData"] = {
|
||||
getData: (type: string) =>
|
||||
type === "text/html"
|
||||
? `<meta charset='utf-8'><html><head></head><body><a href="nn://note/68798972093c8c6ea22efca2">example.py</a></body></html>`
|
||||
: ""
|
||||
};
|
||||
|
||||
editor.view.dom.dispatchEvent(clipboardEvent);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(editorElement.outerHTML).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { insertMathNode } from "./plugin/index.js";
|
||||
import { NodeSelection } from "prosemirror-state";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
@@ -66,7 +67,8 @@ export const MathBlock = Node.create({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-Shift-M": () => this.editor.commands.insertMathBlock()
|
||||
[tiptapKeys.insertMathBlock.keys]: () =>
|
||||
this.editor.commands.insertMathBlock()
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "@tiptap/core";
|
||||
import { findParentNodeOfTypeClosestToPos } from "../../utils/prosemirror.js";
|
||||
import { OutlineList } from "../outline-list/outline-list.js";
|
||||
import { keybindings, tiptapKeys } from "@notesnook/common";
|
||||
|
||||
export interface ListItemOptions {
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
@@ -75,7 +76,7 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-Space": ({ editor }) => {
|
||||
[tiptapKeys.toggleOutlineListExpand.keys]: ({ editor }) => {
|
||||
const { selection } = editor.state;
|
||||
const { $from, empty } = selection;
|
||||
|
||||
@@ -104,7 +105,8 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
||||
return this.editor.commands.splitListItem(this.name);
|
||||
},
|
||||
Tab: () => this.editor.commands.sinkListItem(this.name),
|
||||
"Shift-Tab": () => this.editor.commands.liftListItem(this.name)
|
||||
[keybindings.liftListItem.keys]: () =>
|
||||
this.editor.commands.liftListItem(this.name)
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user