Compare commits

...

35 Commits

Author SHA1 Message Date
Ammar Ahmed
f545216dc3 ci: fix artifacts location 2024-11-13 09:31:37 +05:00
Ammar Ahmed
989de21e44 ci: generate debug.keystore 2024-11-13 09:26:12 +05:00
Ammar Ahmed
2c051f9d21 ci: disable gradle caching 2024-11-13 02:18:16 +05:00
Ammar Ahmed
4845d40c0b ci: push 2024-11-13 01:05:52 +05:00
Ammar Ahmed
b4615fa9aa ci: enable build 2024-11-12 22:51:53 +05:00
Ammar Ahmed
f24ff63c06 ci: disable more steps 2024-11-12 22:42:39 +05:00
Ammar Ahmed
7518e6badc ci: skip build 2024-11-12 22:33:21 +05:00
Ammar Ahmed
0fcbdbcdc3 ci: fix ci
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 21:57:07 +05:00
Ammar Ahmed
999856311f ci: free disk space before build 2024-11-12 20:04:26 +05:00
Ammar Ahmed
98f4e08c62 ci: fix 2024-11-12 18:50:05 +05:00
Ammar Ahmed
f3140cbd8f ci: fix 2024-11-12 18:00:00 +05:00
Ammar Ahmed
029cadedc4 ci: run command from root
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 16:46:19 +05:00
Ammar Ahmed
6cdeb48e56 ci: do not clean after build
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 16:05:41 +05:00
Ammar Ahmed
2c8abc4d00 ci: fix working dir path
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 15:11:16 +05:00
Ammar Ahmed
04820f6500 ci: update workflow
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 14:15:42 +05:00
Ammar Ahmed
fc6a843a1a ci: fix action 2024-11-12 14:14:05 +05:00
Ammar Ahmed
129dad06c2 ci: format 2024-11-12 14:12:45 +05:00
Ammar Ahmed
063b387807 ci: install detox-cli 2024-11-12 14:11:07 +05:00
Ammar Ahmed
e0058fe30b ci: change script working dir 2024-11-12 13:48:11 +05:00
Ammar Ahmed
a3c297636e ci: clean runner after build
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 13:10:09 +05:00
Ammar Ahmed
afc534806d ci: clean gh action space 2024-11-12 11:08:43 +05:00
Ammar Ahmed
4456cc2184 mobile: fix intl imports 2024-11-12 10:08:26 +05:00
Ammar Ahmed
2610db90d2 mobile: fix ts errors 2024-11-12 09:54:17 +05:00
Ammar Ahmed
f16c9804ee ci: fix build 2024-11-11 22:48:01 +05:00
Ammar Ahmed
7a3a1aa9c2 ci: update 2024-11-11 19:53:01 +05:00
Ammar Ahmed
57d0904bee mobile: add e2e test workflow 2024-11-11 19:25:56 +05:00
Ammar Ahmed
5ca6f98c6c mobile: fix e2e tests 2024-11-11 18:46:08 +05:00
Abdullah Atta
dbe32e8af6 web: fix more failing tests 2024-11-08 17:06:38 +05:00
Abdullah Atta
e0758bf353 web: fix note & notebook tests 2024-11-08 17:06:38 +05:00
Abdullah Atta
3c1b5d1ee8 intl: update po files 2024-11-08 17:06:38 +05:00
Abdullah Atta
074008ac67 web: localize more missing strings 2024-11-08 17:06:38 +05:00
Ammar Ahmed
7c56a6beb3 mobile: cleanup and fix errors 2024-11-08 17:06:38 +05:00
Abdullah Atta
3e9d7a70d0 web: localize missing strings 2024-11-08 17:06:38 +05:00
Abdullah Atta
e2ca1a0df6 intl: fix type error in vite.config.mts due to duplicate rollup packages 2024-11-08 17:06:38 +05:00
Abdullah Atta
392456a0d9 global: fix many localization related errors and issues 2024-11-08 17:06:38 +05:00
96 changed files with 7425 additions and 6569 deletions

120
.github/workflows/android.e2e.yml vendored Normal file
View File

@@ -0,0 +1,120 @@
name: e2e-android
on: workflow_dispatch
jobs:
e2e-android:
runs-on: ubuntu-latest
env:
API_LEVEL: 34
ARCH: x86_64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-version: wrapper
cache-read-only: false
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
architecture: "x64"
- name: Install Detox CLI
run: npm install detox-cli --global
- name: Generate debug.keystore
working-directory: apps/mobile/native/android/app
run: |
keytool -genkey -v -keystore debug.keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "C=US, O=Android, CN=Android Debug"
- name: Detox build
run: |
yarn build:android
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
- name: Free Disk Space After Build
run: |
echo "Disk space before cleanup:"
df -h
sudo rm -rf /usr/local/lib/android/sdk/ndk
sudo apt-get clean
echo "Disk space after cleanup:"
df -h
- name: Get device name
id: device
run: |
AVD_NAME=$(node -p "require('./apps/mobile/native/.detoxrc.js').devices.emulator.device.avdName")
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: ${{ steps.device.outputs.AVD_NAME }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
avd-name: ${{ steps.device.outputs.AVD_NAME }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Detox test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
avd-name: ${{ steps.device.outputs.AVD_NAME }}
disable-animations: true
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
force-avd-creation: false
script: |
yarn test:android --headless --record-logs failing --record-videos failing --take-screenshots failing --artifacts-location apps/mobile/native/artifacts
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: detox-artifacts
path: apps/mobile/native/artifacts
retention-days: 14

View File

@@ -39,6 +39,7 @@ DerivedData
*.xcuserstate
*.hprof
**/.xcode.env.local
native/cache
# Android/IntelliJ
#
rn-build-deps/

View File

@@ -140,7 +140,7 @@ export async function downloadAttachments(attachments) {
useAttachmentStore.getState().setDownloading({
current: 0,
total: 1,
message: `${strings.savingZipFile()}... ${strings.pleaseWait()}`,
message: strings.savingZipFile(),
groupId
});
// If all goes well, zip the notesnook-attachments folder in cache.
@@ -150,9 +150,7 @@ export async function downloadAttachments(attachments) {
groupId,
current: progress,
total: 1,
message: `${strings.savingZipFile()} (${(progress * 100).toFixed(
1
)}%)... ${strings.pleaseWait()}`
message: strings.savingZipFile((progress * 100).toFixed(1))
});
});
await zip(zipSourceFolder, zipOutputFile);

View File

@@ -336,10 +336,8 @@ export const AttachmentDialog = ({
onPress() {
if (!attachments) return;
presentDialog({
title: strings.doAction(
"attachment",
attachments.placeholders.length,
"download"
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
@@ -390,10 +388,8 @@ export const AttachmentDialog = ({
onPress={() => {
if (!attachments) return;
presentDialog({
title: strings.doAction(
"attachment",
attachments.placeholders.length,
"download"
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {

View File

@@ -17,8 +17,11 @@ 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 React, { useEffect, useRef, useState } from "react";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { db } from "../../common/database/index";
import useTimer from "../../hooks/use-timer";
import {
@@ -26,19 +29,14 @@ import {
presentSheet,
ToastManager
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { eCloseSheet } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
import { ScrollView } from "react-native-actions-sheet";
import { strings } from "@notesnook/intl";
const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
const { colors } = useThemeColors();

View File

@@ -311,8 +311,10 @@ export class VaultDialog extends Component {
loading: true
});
try {
let verified = await db.user.verifyPassword(this.password);
if (!(await db.user.getUser())) verified = true;
let verified = true;
if (await db.user.getUser()) {
verified = await db.user.verifyPassword(this.password);
}
if (verified) {
let noteIds = [];
if (this.state.deleteAll) {
@@ -320,20 +322,21 @@ export class VaultDialog extends Component {
const relations = await db.relations.from(vault, "note").get();
noteIds = relations.map((item) => item.toId);
}
await db.vault.delete(this.state.deleteAll);
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
if (this.state.deleteAll) {
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
}
console.log("VAULT UPDATED EVENT");
eSendEvent("vaultUpdated");
this.setState({
loading: false
@@ -517,7 +520,6 @@ export class VaultDialog extends Component {
});
this.close();
} else {
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.vaultCreated(),
type: "success",
@@ -525,6 +527,7 @@ export class VaultDialog extends Component {
});
this.close();
}
eSendEvent("vaultUpdated");
}
_permanantUnlock() {

View File

@@ -33,7 +33,6 @@ import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
import { Title } from "./title";
import { useNavigation } from "@react-navigation/native";
type HeaderRightButton = {
title: string;

View File

@@ -17,30 +17,23 @@ 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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import {
Linking,
ScrollView,
TouchableOpacity,
useWindowDimensions,
View
} from "react-native";
import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
import { SwiperFlatList } from "react-native-swiper-flatlist";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { getElevationStyle } from "../../utils/elevation";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
import { getElevationStyle } from "../../utils/elevation";
import { eOpenLoginDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { AuthMode } from "../auth";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import Navigation from "../../services/navigation";
import { eOpenLoginDialog } from "../../utils/events";
import { AuthMode } from "../auth";
import { eSendEvent } from "../../services/event-manager";
import { strings } from "@notesnook/intl";
const Intro = ({ navigation }) => {
const { colors } = useThemeColors();

View File

@@ -34,12 +34,13 @@ import {
Reminder,
Tag,
TrashItem,
VirtualizedGrouping
VirtualizedGrouping,
getSortValue
} from "@notesnook/core";
import { getSortValue } from "@notesnook/core";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
import { eSendEvent } from "../../services/event-manager";
import { RouteName } from "../../stores/use-navigation-store";
import { eOpenJumpToDialog } from "../../utils/events";
@@ -48,7 +49,6 @@ import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;

View File

@@ -102,9 +102,8 @@ export const SelectionHeader = React.memo(
const deleteItem = async () => {
presentDialog({
title: strings.deleteItems(type as string, selectedItemsList.length),
paragraph: strings.deleteItemsConfirmation(
type as string,
title: strings.doActions.delete[type](selectedItemsList.length),
paragraph: strings.actionConfirmations.delete[type](
selectedItemsList.length
),
positiveText: strings.delete(),

View File

@@ -81,7 +81,7 @@ export const AddNotebookSheet = ({
id: notebook?.id
});
if (parentNotebook) {
if (parentNotebook && id) {
await db.relations.add(parentNotebook, {
type: "notebook",
id: id
@@ -94,7 +94,8 @@ export const AddNotebookSheet = ({
useNotebookStore.getState().refresh();
const parent =
parentNotebook?.id || (await getParentNotebookId(notebook?.id || id));
parentNotebook?.id ||
(await getParentNotebookId(notebook?.id || (id as string)));
eSendEvent(eOnNotebookUpdated, parent);
if (notebook) {
@@ -104,7 +105,7 @@ export const AddNotebookSheet = ({
});
}
if (!notebook && showMoveNotesOnComplete) {
if (!notebook && showMoveNotesOnComplete && id) {
MoveNotes.present(await db.notebooks.notebook(id));
} else {
close?.(true);

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
@@ -49,13 +50,11 @@ import DialogHeader from "../../dialog/dialog-header";
import SheetProvider from "../../sheet-provider";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import { NotebookItem } from "./notebook-item";
import { useNotebookItemSelectionStore } from "./store";
import { AddNotebookSheet } from "../add-notebook";
import Input from "../../ui/input";
import { presentDialog } from "../../dialog/functions";
import { strings } from "@notesnook/intl";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations

View File

@@ -171,7 +171,7 @@ const ExportNotesSheet = ({
>
<DialogHeader
icon="export"
title={`${strings.export()} ${strings.notes(ids.length)}`}
title={strings.exportNotes(ids.length)}
/>
</View>

View File

@@ -43,7 +43,7 @@ const NewFeature = ({
version
}: {
features: FeatureType[];
version?: string | null;
version?: string;
}) => {
const { colors } = useThemeColors();
@@ -57,7 +57,7 @@ const NewFeature = ({
}}
>
<Heading color={colors.secondary.heading} size={SIZE.md}>
{!version ? `${strings.newVersion()} ` : `v${version} `}Highlights 🎉
{strings.newVersionHighlights(version)}
</Heading>
<Seperator />
@@ -122,7 +122,7 @@ NewFeature.present = () => {
component: (
<NewFeature
features={features}
version={SettingsService.getProperty("version")}
version={SettingsService.getProperty("version") || undefined}
/>
),
disableClosing: true

View File

@@ -206,7 +206,7 @@ export const NotebookSheet = () => {
backgroundInteractionEnabled
gestureEnabled
>
<View
{/* <View
style={{
position: "absolute",
right: 24 + normalize(50),
@@ -214,17 +214,11 @@ export const NotebookSheet = () => {
}}
>
<Pressable
testID={notesnook.buttons.add}
testID="add-notebook-button"
type="secondary"
onPress={() => {
if (!notebook) return;
AddNotebookSheet.present(
undefined,
notebook,
undefined,
undefined,
false
);
}}
style={{
borderRadius: 100
@@ -245,7 +239,7 @@ export const NotebookSheet = () => {
/>
</View>
</Pressable>
</View>
</View> */}
<View
style={{
@@ -404,6 +398,27 @@ export const NotebookSheet = () => {
height: 40 * fontScale
}}
/>
<IconButton
testID="add-notebook-button"
name="notebook-plus"
onPress={() => {
if (!notebook) return;
AddNotebookSheet.present(
undefined,
notebook,
undefined,
undefined,
false
);
}}
color={colors.primary.icon}
size={22}
style={{
width: 40 * fontScale,
height: 40 * fontScale
}}
/>
</>
)}
</View>

View File

@@ -121,8 +121,8 @@ const PublishNoteSheet = ({
}}
>
<DialogHeader
title={`${strings.publish()} ${strings.note()}`}
paragraph={strings.publishDesc()}
title={strings.publishNote()}
paragraph={strings.publishNoteDesc()}
/>
{publishing ? (

View File

@@ -16,8 +16,8 @@ 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 { VirtualizedGrouping } from "@notesnook/core";
import { Item, ItemReference } from "@notesnook/core";
import { Item, ItemReference, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native";
@@ -37,8 +37,6 @@ import SheetProvider from "../../sheet-provider";
import { Button } from "../../ui/button";
import { PressableProps } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { isStringLiteralOrJsxExpression } from "typescript";
import { strings } from "@notesnook/intl";
type RelationsListProps = {
actionSheetRef: RefObject<ActionSheetRef>;
@@ -125,7 +123,9 @@ export const RelationsList = ({
// width="100%"
type="inverted"
icon="plus"
title={strings.addItem(referenceType)}
title={strings.addItem(
referenceType as "notebook" | "tag" | "reminder" | "note"
)}
/>
</View>
) : (

View File

@@ -70,7 +70,6 @@ interface InputProps extends TextInputProps {
};
buttons?: React.ReactNode;
onBlurInput?: () => void;
onPress?: () => void;
height?: number;
fontSize?: number;
onFocusInput?: () => void;

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-inner-declarations */
import { VAULT_ERRORS } from "@notesnook/core";
import {
Color,
ItemReference,
@@ -25,8 +24,11 @@ import {
Notebook,
Reminder,
Tag,
TrashItem
TrashItem,
VAULT_ERRORS,
createInternalLink
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { DisplayedNotification } from "@notifee/react-native";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useRef, useState } from "react";
@@ -39,8 +41,10 @@ import NoteHistory from "../components/note-history";
import { AddNotebookSheet } from "../components/sheets/add-notebook";
import MoveNoteSheet from "../components/sheets/add-to";
import ExportNotesSheet from "../components/sheets/export-notes";
import { MoveNotebookSheet } from "../components/sheets/move-notebook";
import { MoveNotes } from "../components/sheets/move-notes/movenote";
import PublishNoteSheet from "../components/sheets/publish-note";
import { ReferencesList } from "../components/sheets/references";
import { RelationsList } from "../components/sheets/relations-list/index";
import ReminderSheet from "../components/sheets/reminder";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
@@ -60,15 +64,10 @@ import { useRelationStore } from "../stores/use-relation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { useTagStore } from "../stores/use-tag-store";
import { useUserStore } from "../stores/use-user-store";
import Errors from "../utils/errors";
import { eOpenLoginDialog, eUpdateNoteInEditor } from "../utils/events";
import { deleteItems } from "../utils/functions";
import { convertNoteToText } from "../utils/note-to-text";
import { sleep } from "../utils/time";
import { ReferencesList } from "../components/sheets/references";
import { createInternalLink } from "@notesnook/core";
import { MoveNotebookSheet } from "../components/sheets/move-notebook";
import { strings } from "@notesnook/intl";
export const useActions = ({
close,
@@ -255,8 +254,8 @@ export const useActions = ({
item.type === "color"
) {
presentDialog({
title: strings.deleteItem(item.type),
paragraph: strings.deleteItemConfirmation(item.type),
title: strings.doActions.delete[item.type](1),
paragraph: strings.actionConfirmations.delete[item.type](1),
positivePress: async () => {
if (item.type === "reminder") {
await db.reminders.remove(item.id);
@@ -304,8 +303,8 @@ export const useActions = ({
close();
await sleep(300);
presentDialog({
title: strings.delete(),
paragraph: strings.deleteItemsConfirmation(item.itemType, 1),
title: strings.doActions.delete[item.itemType](1),
paragraph: strings.actionConfirmations.delete[item.itemType](1),
positiveText: strings.delete(),
negativeText: strings.cancel(),
positivePress: async () => {
@@ -314,7 +313,10 @@ export const useActions = ({
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: strings.itemDeleted(1, item.itemType),
heading:
strings.actions.deleted[
item.itemType as keyof typeof strings.actions.deleted
](1),
type: "success",
context: "local"
});
@@ -955,7 +957,11 @@ export const useActions = ({
id: "trash",
title:
item.type !== "notebook" && item.type !== "note"
? strings.doAction(item.type, 1, "delete")
? strings.doActions.delete[
item.type === "trash"
? item.itemType
: (item.type as keyof typeof strings.doActions.delete)
](1)
: strings.moveToTrash(),
icon: "delete-outline",
type: "error",

View File

@@ -18,11 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect } from "react";
import { db } from "../common/database";
import BiometricService from "../services/biometrics";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { db } from "../common/database";
const VaultStatusCache = {
const VaultStatusDefaults = {
exists: false,
biometryEnrolled: false,
isBiometryAvailable: false
@@ -35,18 +35,12 @@ export type VaultStatusType = {
};
export const useVaultStatus = () => {
const [vaultStatus, setVaultStatus] = React.useState(VaultStatusCache);
const [vaultStatus, setVaultStatus] = React.useState(VaultStatusDefaults);
const checkVaultStatus = useCallback(() => {
db.vault?.exists().then(async (exists) => {
const available = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
if (
VaultStatusCache.exists === exists &&
VaultStatusCache.biometryEnrolled === fingerprint &&
VaultStatusCache.isBiometryAvailable === available
)
return;
setVaultStatus({
exists: exists,
biometryEnrolled: fingerprint,

View File

@@ -76,7 +76,7 @@ export const Search = ({ route, navigation }: NavigationProps<"Search">) => {
await results.item(0);
setResults(results);
if (results.placeholders?.length === 0) {
setSearchStatus(`${strings.noResultsFound(query)}`);
setSearchStatus(strings.noResultsFound(query));
} else {
setSearchStatus(undefined);
}
@@ -125,10 +125,8 @@ export const Search = ({ route, navigation }: NavigationProps<"Search">) => {
loading={loading}
placeholder={{
title: route.name,
paragraph:
searchStatus ||
`${strings.typeAKeywordToSearchIn(route.params?.title)}`,
loading: `${strings.searchingFor(currentQuery.current as string)}...`
paragraph: searchStatus || strings.searchInRoute(route.params?.title),
loading: strings.searchingFor(currentQuery.current as string)
}}
/>
</>

View File

@@ -153,7 +153,7 @@ const restoreBackup = async (options: {
if (path === ".nnbackup" || path === "attachments") continue;
updateProgress({
progress: `${strings.restoringBackup()}... (${count++}/${
progress: `${strings.restoringBackup()} (${count++}/${
extractedBackupFiles.length
})`
});
@@ -242,7 +242,7 @@ const restoreBackup = async (options: {
await db.transaction(async () => {
updateProgress({
progress: strings.restoringBackup() + "..."
progress: strings.restoringBackup()
});
await db.backup.import(backup, {
encryptionKey,

View File

@@ -351,9 +351,7 @@ export const settingsGroups: SettingSection[] = [
});
},
description(current) {
return `${strings.clearCacheDesc()}. ${strings.currentCacheSize(
`${current}`
)}`;
return strings.clearCacheDesc(current as number);
},
useHook: () => {
const [cacheSize, setCacheSize] = React.useState(0);
@@ -444,7 +442,7 @@ export const settingsGroups: SettingSection[] = [
}
updateProgress({
progress: `${strings.loggingOut()}... ${strings.pleaseWait()}`
progress: strings.loggingOut()
});
await db.user?.logout();

View File

@@ -17,8 +17,10 @@ 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 { strings } from "@notesnook/intl";
import React from "react";
import { Linking, Platform, View } from "react-native";
import Config from "react-native-config";
import { Button } from "../../components/ui/button";
import { usePricing } from "../../hooks/use-pricing";
import {
@@ -28,14 +30,9 @@ import {
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useUserStore } from "../../stores/use-user-store";
import {
SUBSCRIPTION_PROVIDER,
SUBSCRIPTION_STATUS
} from "../../utils/constants";
import { SUBSCRIPTION_STATUS } from "../../utils/constants";
import { eOpenPremiumDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import Config from "react-native-config";
import { strings } from "@notesnook/intl";
export const Subscription = () => {
const user = useUserStore((state) => state.user);
const monthlyPlan = usePricing("monthly");

View File

@@ -33,7 +33,7 @@ import { strings } from "@notesnook/intl";
function confirmDeleteAllNotes(items, type, context) {
return new Promise((resolve) => {
presentDialog({
title: strings.doAction("type", items.length, "delete"),
title: strings.doActions.delete[type](items.length),
positiveText: strings.delete(),
negativeText: strings.cancel(),
positivePress: (_inputValue, value) => {
@@ -112,10 +112,10 @@ export const deleteItems = async (items, type, context) => {
}
} else if (type === "tag") {
presentDialog({
title: strings.deleteTags(ids.length),
title: strings.doActions.delete.tag(ids.length),
positiveText: strings.delete(),
negativeText: strings.cancel(),
paragraph: strings.deleteTagsConfirm(),
paragraph: strings.actionConfirmations.delete.tag(2),
positivePress: async () => {
await db.tags.remove(...ids);
useTagStore.getState().refresh();
@@ -128,7 +128,7 @@ export const deleteItems = async (items, type, context) => {
let deletedIds = [...ids];
if (type === "notebook" || type === "note") {
let message = strings.movedToTrash(type, ids.length);
let message = strings.actions.movedToTrash[type](ids.length);
ToastManager.show({
heading: message,
type: "success",

View File

@@ -0,0 +1,29 @@
import { execSync } from "child_process";
import { pathExists, ensureDir } from "fs-extra";
import { resolveConfig } from "detox/internals";
import { globalSetup } from "detox/runners/jest";
export default async function customGlobalSetup() {
const config = await resolveConfig();
if (config.device.type === "android.emulator") {
await downloadTestButlerAPK();
}
await globalSetup();
}
async function downloadTestButlerAPK() {
const version = "2.2.1";
const artifactUrl = `https://repo1.maven.org/maven2/com/linkedin/testbutler/test-butler-app/${version}/test-butler-app-${version}.apk`;
const filePath = `cache/test-butler-app.apk`;
await ensureDir("cache");
if (!(await pathExists(filePath))) {
console.log(`\nDownloading Test-Butler APK v${version}...`);
execSync(`curl -f -o ${filePath} ${artifactUrl}`);
}
}
module.exports = customGlobalSetup;

View File

@@ -1,20 +1,21 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
rootDir: "..",
testMatch: ["<rootDir>/e2e/**/*.e2e.js"],
testTimeout: 120000,
maxWorkers: 1,
setupFilesAfterEnv: ["./e2e/setup.js"],
globalSetup: "detox/runners/jest/globalSetup",
testTimeout: 120000,
globalSetup: "./e2e/globalSetup.ts",
globalTeardown: "detox/runners/jest/globalTeardown",
reporters: ["detox/runners/jest/reporter"],
setupFilesAfterEnv: ["./e2e/setup.ts"],
testEnvironment: "detox/runners/jest/testEnvironment",
verbose: true,
reporters: ["detox/runners/jest/reporter"],
testRunner: "jest-circus/runner",
testMatch: ["<rootDir>/e2e/**/*.e2e.(js|ts)"],
transform: {
"^.+\\.(js|jsx|ts|tsx)$": [
"\\.tsx?$": "ts-jest",
"^.+\\.(js|jsx)$": [
"babel-jest",
{ configFile: "../native/babel.config.js" }
]
},
transformIgnorePatterns: ["<rootDir>/../node_modules/"]
verbose: true
};

View File

@@ -74,7 +74,7 @@ async function login() {
// }
describe("AUTH", () => {
it("Login", async () => {
it.skip("Login", async () => {
await prepare();
await openSideMenu();
await login();

View File

@@ -17,23 +17,22 @@ 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 { navigate, tapByText, prepare, sleep } from "./utils";
import { Tests } from "./utils";
describe("APP LAUNCH AND NAVIGATION", () => {
it("App should launch successfully & hide welcome screen", async () => {
await prepare();
await Tests.prepare();
});
it("Basic navigation should work", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await navigate("Favorites");
await navigate("Trash");
await navigate("Tags");
await navigate("Settings");
await navigate("Monographs");
await navigate("Reminders");
await navigate("Notes");
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.navigate("Favorites");
await Tests.navigate("Trash");
await Tests.navigate("Tags");
await Tests.navigate("Settings");
await Tests.navigate("Monographs");
await Tests.navigate("Reminders");
await Tests.navigate("Notes");
});
});

View File

@@ -1,140 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import {
navigate,
tapById,
visibleByText,
createNote,
prepare,
visibleById,
notVisibleById,
sleep,
exitEditor,
tapByText,
elementByText,
elementById
} from "./utils";
describe("NOTE TESTS", () => {
it("Create a note in editor", async () => {
await prepare();
await createNote();
});
it("Open and close a note", async () => {
await prepare();
await createNote();
await tapById(notesnook.ids.note.get(0));
await exitEditor();
});
it.only("Notes properties should show", async () => {
await prepare();
let note = await createNote();
await tapById(notesnook.listitem.menu);
await waitFor(elementByText("Created at:")).toBeVisible().withTimeout(500);
});
it("Favorite and unfavorite a note", async () => {
await prepare();
let note = await createNote();
await tapById(notesnook.listitem.menu);
await tapById("icon-favorite");
await visibleById("icon-star");
await navigate("Favorites");
await visibleByText(note.body);
await sleep(500);
await tapById(notesnook.listitem.menu);
await tapById("icon-favorite");
await expect(element(by.text(note.body))).not.toBeVisible();
await navigate("Notes");
});
it("Pin a note to top", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await tapById("icon-pin");
await visibleByText("Pinned");
await visibleById("icon-pinned");
await tapById(notesnook.listitem.menu);
await tapById("icon-pin");
expect(element(by.id("icon-pinned"))).not.toBeVisible();
});
it("Pin a note in notifications", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await tapById("icon-pin-to-notifications");
await visibleByText("Unpin from notifications");
await sleep(500);
await tapById("icon-pin-to-notifications");
await sleep(500);
await visibleByText("Pin to notifications");
});
it("Copy note", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await sleep(1000);
await waitFor(elementById("icon-copy")).toBeVisible().withTimeout(500);
await tapById("icon-copy");
});
it("Assign colors to a note", async () => {
await prepare();
let note = await createNote();
await tapById(notesnook.listitem.menu);
await sleep(1000);
await tapByText("Add color");
await sleep(500);
await elementById("color-title-input").typeText("Test color");
await tapByText("Add color");
await sleep(3000);
await visibleById("icon-check");
await tapById("icon-color-#efefef");
await notVisibleById("icon-check");
await tapById("icon-color-#efefef");
await device.pressBack();
await navigate("Test color");
await visibleByText(note.body);
});
it("Delete & restore a note", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await sleep(500);
await tapById("icon-trash");
await navigate("Trash");
await sleep(500);
await tapById(notesnook.listitem.menu);
await sleep(500);
await tapByText("Restore note");
await device.pressBack();
await sleep(500);
await visibleByText(
"Test note description that is very long and should not fit in text."
);
});
});

View File

@@ -0,0 +1,116 @@
/*
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 { notesnook } from "../test.ids";
import { Tests } from "./utils";
describe("NOTE TESTS", () => {
it("Create a note in editor", async () => {
await Tests.prepare();
await Tests.createNote();
});
it("Open and close a note", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.ids.note.get(0)).waitAndTap();
await Tests.exitEditor();
});
it("Notes properties should show", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromText("Created at").isVisible();
});
it("Favorite and unfavorite a note", async () => {
await Tests.prepare();
let note = await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-favorite").waitAndTap();
await Tests.fromId("icon-star").isVisible();
await Tests.navigate("Favorites");
await Tests.fromText(note.body).isVisible();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-favorite").waitAndTap();
await Tests.fromText(note.body).isNotVisible();
await Tests.navigate("Notes");
});
it("Pin a note to top", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-pin").waitAndTap();
await Tests.fromText("Pinned").isVisible();
await Tests.fromId("icon-pinned").isVisible();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-pin").waitAndTap();
await Tests.fromText("icon-pinned").isNotVisible();
});
it("Pin a note in notifications", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-pin-to-notifications").waitAndTap();
await Tests.fromText("Unpin from notifications").isVisible();
await Tests.fromId("icon-pin-to-notifications").waitAndTap();
await Tests.fromText("Pin to notifications").isVisible();
});
it("Copy note", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-copy").isVisible();
await Tests.fromId("icon-copy").waitAndTap();
});
it("Assign colors to a note", async () => {
await Tests.prepare();
let note = await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromText("Add color").waitAndTap();
await Tests.fromId("color-title-input").element.typeText("Test color");
await Tests.fromText("Add color").waitAndTap();
await Tests.fromId("icon-check").isVisible();
await Tests.fromId("icon-color-#efefef").waitAndTap();
await Tests.fromId("icon-check").isNotVisible();
await Tests.fromId("icon-color-#efefef").waitAndTap();
await device.pressBack();
await Tests.navigate("Test color");
await Tests.fromText(note.body).isVisible();
});
it.only("Delete & restore a note", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-trash").waitAndTap();
await Tests.navigate("Trash");
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromText("Restore note").waitAndTap();
await device.pressBack();
await Tests.fromText(
"Test note description that is very long and should not fit in text."
).isVisible();
});
});

View File

@@ -1,309 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import {
tapById,
elementById,
visibleByText,
tapByText,
createNote,
prepare,
notVisibleById,
navigate,
elementByText,
sleep,
notVisibleByText,
visibleById,
createNotebook
} from "./utils";
// async function addTopic(title = "Topic") {
// await tapById(notesnook.buttons.add);
// await elementById("input-title").typeText(title);
// await tapByText("Add");
// await sleep(500);
// }
describe("NOTEBOOKS", () => {
it("Create a notebook with title only", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", false, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await visibleByText("Notebook 1");
});
it("Create a notebook title & description", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await visibleByText("Notebook 1");
});
it("Create a notebook, move notes", async () => {
await prepare();
let note = await createNote();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, true);
await sleep(500);
await tapById("listitem.select");
await tapByText("Move selected notes");
await sleep(500);
await tapByText("Notebook 1");
await sleep(500);
await visibleByText(note.body);
});
it("Add a sub notebook to a notebook", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await tapByText("Notebook 1");
await sleep(500);
await tapById("add-notebook-button");
await createNotebook("Sub notebook", true, true);
await sleep(500);
await device.pressBack();
await visibleByText("Sub notebook");
await tapById(notesnook.ids.notebook.menu);
await tapByText("Move to trash");
await sleep(300);
await tapByText("Delete");
await notVisibleByText("Sub notebook");
});
it("Remove a sub notebook from notebook", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await tapByText("Notebook 1");
await sleep(500);
await tapById("add-notebook-button");
await createNotebook("Sub notebook", true, true);
await sleep(500);
await device.pressBack();
await visibleByText("Sub notebook");
});
it("Edit notebook", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapById(notesnook.buttons.add);
await createNotebook("Notebook 1", true);
await sleep(500);
await device.pressBack();
await tapById(notesnook.ids.notebook.menu);
await tapByText("Edit notebook");
await elementById(notesnook.ids.dialogs.notebook.inputs.title).typeText(
" (edited)"
);
await tapByText("Save");
await visibleByText("Notebook 1 (edited)");
});
it("Edit a sub notebook", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapById(notesnook.buttons.add);
await createNotebook("Notebook 1", true);
await sleep(500);
await device.pressBack();
await sleep(500);
await tapByText("Notebook 1");
await sleep(500);
await tapById("add-notebook-button");
await createNotebook("Sub notebook", true, true);
await device.pressBack();
await sleep(500);
await tapById(notesnook.ids.notebook.menu);
await tapByText("Edit notebook");
await sleep(500);
await elementById(notesnook.ids.dialogs.notebook.inputs.title).typeText(
" (edited)"
);
await tapByText("Save");
await sleep(500);
await visibleByText("Sub notebook (edited)");
});
it("Add a note to notebook", async () => {
await prepare();
await navigate("Notebooks");
await tapByText("Add your first notebook");
await sleep(500);
await createNotebook("Notebook 1", true, true);
await sleep(500);
await device.pressBack();
await sleep(500);
await tapByText("Notebook 1");
await createNote();
});
it("Remove note from Notebook", async () => {
await prepare();
await navigate("Notebooks");
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, true);
await sleep(500);
await device.pressBack();
await sleep(500);
await tapByText("Notebook 1");
await sleep(500);
let note = await createNote();
await elementByText(note.body).longPress();
await tapById("select-minus");
await notVisibleById(note.title);
});
it("Add/Remove note to notebook from home", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await navigate("Notes");
await createNote();
await tapById(notesnook.listitem.menu);
await tapById("icon-notebooks");
await sleep(500);
await tapByText("Notebook 1");
await tapByText("Save");
await sleep(300);
await visibleByText("Notebook 1");
});
it("Edit notebook title, description", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook();
await sleep(500);
await device.pressBack();
await sleep(500);
await visibleByText("Notebook 1");
await tapById(notesnook.ids.notebook.menu);
await tapByText("Edit notebook");
await sleep(500);
await elementById(notesnook.ids.dialogs.notebook.inputs.title).typeText(
" (Edited)"
);
await elementById(
notesnook.ids.dialogs.notebook.inputs.description
).clearText();
await elementById(
notesnook.ids.dialogs.notebook.inputs.description
).typeText("Description of Notebook 1 (Edited)");
await tapByText("Save");
await sleep(500);
await visibleByText("Notebook 1 (Edited)");
await visibleByText("Description of Notebook 1 (Edited)");
});
it.skip("Move notebook to trash", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", false, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await visibleByText("Notebook 1");
await tapById(notesnook.ids.notebook.menu);
await tapByText("Move to trash");
await sleep(2000);
await tapByText("Delete");
await sleep(4000);
await navigate("Trash");
await visibleByText("Notebook 1");
});
it("Move notebook to trash with notes", async () => {
await prepare();
let note = await createNote();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", false, true);
await sleep(500);
await tapById("listitem.select");
await tapByText("Move selected notes");
await sleep(500);
await visibleByText("Notebook 1");
await tapById(notesnook.ids.notebook.menu);
await tapByText("Move to trash");
await sleep(2000);
await tapByText("Move all notes in this notebook to trash");
await sleep(500);
await tapByText("Delete");
await sleep(4000);
await navigate("Trash");
await visibleByText("Notebook 1");
await visibleByText(note.body);
});
it("Pin notebook to side menu", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", false, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await visibleByText("Notebook 1");
await tapById(notesnook.ids.notebook.menu);
await tapByText("Add Shortcut");
let menu = elementById(notesnook.ids.default.header.buttons.left);
await menu.tap();
await visibleByText("Notebook 1");
});
});

View File

@@ -0,0 +1,233 @@
/*
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 { notesnook } from "../test.ids";
import { Tests } from "./utils";
describe("NOTEBOOKS", () => {
it("Create a notebook with title only", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", false);
await device.pressBack();
await Tests.fromText("Notebook 1").isVisible();
});
it("Create a notebook title & description", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").isVisible();
});
it("Create a notebook, move notes", async () => {
await Tests.prepare();
let note = await Tests.createNote();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await Tests.fromId("listitem.select").waitAndTap();
await Tests.fromText("Move selected notes").waitAndTap();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.fromText(note.body).isVisible();
});
it("Add a sub notebook to a notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.sleep(500);
await Tests.fromId("add-notebook-button").waitAndTap();
await Tests.createNotebook("Sub notebook", true);
await Tests.fromText("Sub notebook").isVisible();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Move to trash").waitAndTap();
await Tests.fromText("Delete").waitAndTap();
await Tests.fromText("Sub notebook").isNotVisible();
});
it("Remove a sub notebook from notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.sleep(500);
await Tests.fromId("add-notebook-button").waitAndTap();
await Tests.createNotebook("Sub notebook", true);
await Tests.fromText("Sub notebook").isVisible();
});
it("Edit notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromId(notesnook.buttons.add).waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.fromText("Edit notebook").waitAndTap();
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.title
).element.typeText(" (edited)");
await Tests.fromText("Save").waitAndTap();
await Tests.fromText("Notebook 1 (edited)").isVisible();
});
it("Edit a sub notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromId(notesnook.buttons.add).waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.sleep(500);
await Tests.fromId("add-notebook-button").waitAndTap();
await Tests.createNotebook("Sub notebook", true);
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Edit notebook").waitAndTap();
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.title
).element.typeText(" (edited)");
await Tests.fromText("Save").waitAndTap();
await Tests.fromText("Sub notebook (edited)").isVisible();
});
it("Add a note to notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.createNote();
});
it.skip("Remove note from Notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.sleep(500);
let note = await Tests.createNote();
await Tests.fromText(note.body).element.longPress();
await Tests.fromId("select-minus").waitAndTap();
await Tests.fromId(note.title).isNotVisible();
});
it("Add/Remove note to notebook from home", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.navigate("Notes");
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromId("icon-notebooks").waitAndTap();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.fromText("Save").waitAndTap();
await Tests.fromText("Notebook 1").isVisible();
});
it("Edit notebook title, description", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook();
await device.pressBack();
await Tests.fromText("Notebook 1").isVisible();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Edit notebook").waitAndTap();
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.title
).element.typeText(" (Edited)");
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.description
).element.clearText();
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.description
).element.typeText("Description of Notebook 1 (Edited)");
await Tests.fromText("Save").waitAndTap();
await Tests.fromText("Notebook 1 (Edited)").isVisible();
await Tests.fromText("Description of Notebook 1 (Edited)").isVisible();
});
it.skip("Move notebook to trash", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", false);
await device.pressBack();
await Tests.fromText("Notebook 1").isVisible();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Move to trash").waitAndTap();
await Tests.fromText("Delete").waitAndTap();
await Tests.navigate("Trash");
await Tests.fromText("Notebook 1").isVisible();
});
it("Move notebook to trash with notes", async () => {
await Tests.prepare();
let note = await Tests.createNote();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", false);
await Tests.fromId("listitem.select").waitAndTap();
await Tests.fromText("Move selected notes").waitAndTap();
await Tests.fromText("Notebook 1").isVisible();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Move to trash").waitAndTap();
await Tests.fromText(
"Move all notes in this notebook to trash"
).waitAndTap();
await Tests.fromText("Delete").waitAndTap();
await Tests.navigate("Trash");
await Tests.fromText("Notebook 1").isVisible();
await Tests.fromText(note.body).isVisible();
});
it.skip("Pin notebook to side menu", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", false);
await device.pressBack();
await Tests.fromText("Notebook 1").isVisible();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add Shortcut").waitAndTap();
await Tests.fromId(notesnook.ids.default.header.buttons.left).waitAndTap();
await Tests.fromText("Notebook 1").isVisible();
});
});

View File

@@ -17,29 +17,17 @@ 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 { notesnook } from "../test.ids";
import {
tapById,
visibleByText,
createNote,
prepare,
elementById,
sleep,
tapByText
} from "./utils";
import { Tests } from "./utils";
describe("Search", () => {
it("Search for a note", async () => {
await prepare();
let note = await createNote();
await tapById("icon-search");
await sleep(300);
await elementById("search-input").typeText("Test");
await sleep(1000);
await tapByText(note.body);
await sleep(1000);
await Tests.prepare();
let note = await Tests.createNote();
await Tests.fromId("icon-search").waitAndTap();
await Tests.fromId("search-input").element.typeText("Test");
await Tests.fromText(note.body).waitAndTap();
await device.pressBack();
await device.pressBack();
await visibleByText(note.body);
await Tests.fromText(note.body).isVisible();
});
});

View File

@@ -1,121 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import {
tapById,
visibleByText,
createNote,
prepare,
tapByText,
notVisibleByText,
sleep,
navigate,
createNotebook
} from "./utils";
async function sortBy(sorting, elementText = "Default") {
await tapByText(elementText);
await tapByText(sorting);
await device.pressBack();
}
describe("Sort & filter", () => {
it("Sort by date-edited/date-created", async () => {
await prepare();
let webview = web(by.id(notesnook.editor.id));
await createNote("Note 1", "Note 1");
await sleep(300);
await createNote("Note 2", "Note 2");
await sleep(300);
await tapByText("Note 1");
await sleep(500);
await expect(webview.element(by.web.className("ProseMirror"))).toExist();
await webview.element(by.web.className("ProseMirror")).tap();
await webview
.element(by.web.className("ProseMirror"))
.typeText("Edited ", true);
await device.pressBack();
await device.pressBack();
await sortBy("Date created");
await tapById(notesnook.listitem.menu);
//await visibleByText("Note 2");
await device.pressBack();
await sortBy("Date edited");
await tapById(notesnook.listitem.menu);
//await visibleByText("Edited Note 1");
await device.pressBack();
});
it("Disable grouping", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await sortBy("None");
await sleep(300);
await visibleByText("None");
});
it("Group by Abc", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await sortBy("Abc");
await visibleByText("N");
});
it("Group by Year", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await sortBy("Year");
await sleep(300);
await visibleByText("Year");
});
it("Group by Week", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await sortBy("Week");
await sleep(300);
await visibleByText("Week");
});
it("Group by Month", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await sortBy("Month");
await sleep(300);
await visibleByText("Month");
});
it("Compact mode", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await tapById("icon-compact-mode");
await sleep(300);
await notVisibleByText("Note 1");
await tapById("icon-compact-mode");
await sleep(300);
await visibleByText("Note 1");
});
});

View File

@@ -0,0 +1,93 @@
/*
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 { notesnook } from "../test.ids";
import { Tests } from "./utils";
async function sortBy(sorting: string, elementText = "Default") {
await Tests.fromText(elementText).waitAndTap();
await Tests.fromText(sorting).waitAndTap();
await device.pressBack();
}
describe("Sort & filter", () => {
it("Sort by date-edited/date-created", async () => {
await Tests.prepare();
let webview = web();
await Tests.createNote("Note 1", "Note 1");
await Tests.createNote("Note 2", "Note 2");
await Tests.fromText("Note 1").waitAndTap();
await webview.element(by.web.className("ProseMirror")).tap();
await webview
.element(by.web.className("ProseMirror"))
.typeText("Edited ", true);
await device.pressBack();
await device.pressBack();
await sortBy("Date created");
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await device.pressBack();
await sortBy("Date edited");
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await device.pressBack();
});
it("Disable grouping", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await sortBy("None");
await Tests.fromText("Default").isNotVisible();
});
it("Group by Abc", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await sortBy("Abc");
await Tests.fromText("N").isVisible();
});
it("Group by Year", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await sortBy("Year");
await Tests.fromText("Year").isVisible();
});
it("Group by Week", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await sortBy("Week");
await Tests.fromText("Week").isVisible();
});
it("Group by Month", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await sortBy("Month");
await Tests.fromText("Month").isVisible();
});
it("Compact mode", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await Tests.fromId("icon-compact-mode").waitAndTap();
await Tests.fromText("Note 1").isNotVisible();
await Tests.fromId("icon-compact-mode").waitAndTap();
await Tests.fromText("Note 1").isVisible();
});
});

View File

@@ -1,103 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import {
navigate,
tapById,
visibleByText,
createNote,
prepare,
elementById,
tapByText,
notVisibleByText,
sleep
} from "./utils";
describe("Tags", () => {
it("Tag a note", async () => {
await prepare();
let note = await createNote();
await tapById(notesnook.listitem.menu);
await tapByText("Add tags");
await elementById("tag-input").typeText("testtag");
await tapByText('Add "#testtag"');
await visibleByText("#testtag");
await device.pressBack();
await device.pressBack();
await navigate("Tags");
await tapByText("#testtag");
await visibleByText(note.body);
});
it("Untag a note", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await tapByText("Add tags");
await elementById("tag-input").typeText("testtag");
await tapByText('Add "#testtag"');
await visibleByText("#testtag");
await tapByText("#testtag");
await device.pressBack();
await device.pressBack();
await notVisibleByText("#testtag");
});
it("Creat shortcut of a tag", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await tapByText("Add tags");
await elementById("tag-input").typeText("testtag");
await tapByText('Add "#testtag"');
await visibleByText("#testtag");
await device.pressBack();
await device.pressBack();
await navigate("Tags");
await sleep(500);
await tapById(notesnook.ids.tag.menu);
await sleep(500);
await tapByText("Add Shortcut");
let menu = elementById(notesnook.ids.default.header.buttons.left);
await menu.tap();
await visibleByText("#testtag");
});
it("Rename a tag", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await tapByText("Add tags");
await elementById("tag-input").typeText("testtag");
await tapByText('Add "#testtag"');
await visibleByText("#testtag");
await device.pressBack();
await device.pressBack();
await navigate("Tags");
await tapById(notesnook.ids.tag.menu);
await sleep(500);
await tapByText("Rename tag");
await sleep(500);
await elementById("input-value").clearText();
await elementById("input-value").typeText("testtag_edited");
await tapByText("Save");
await visibleByText("#testtag_edited");
});
});

View File

@@ -0,0 +1,95 @@
/*
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 { notesnook } from "../test.ids";
import { Tests } from "./utils";
describe("Tags", () => {
it("Tag a note", async () => {
await Tests.prepare();
let note = await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add tag").waitAndTap();
await Tests.fromId("tag-input").element.typeText("testtag");
await Tests.fromText('Add "#testtag"').waitAndTap();
await Tests.fromText("#testtag").isVisible();
await device.pressBack();
await device.pressBack();
await Tests.navigate("Tags");
await Tests.fromText("#testtag").waitAndTap();
await Tests.fromText(note.body).isVisible();
});
it("Untag a note", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add tag").waitAndTap();
await Tests.fromId("tag-input").element.typeText("testtag");
await Tests.fromText('Add "#testtag"').waitAndTap();
await Tests.fromText("#testtag").isVisible();
await Tests.fromText("#testtag").waitAndTap();
await device.pressBack();
await device.pressBack();
await Tests.fromText("#testtag").isNotVisible();
});
it("Create shortcut of a tag", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add tag").waitAndTap();
await Tests.fromId("tag-input").element.typeText("testtag");
await Tests.fromText('Add "#testtag"').waitAndTap();
await Tests.fromText("#testtag").isVisible();
await device.pressBack();
await device.pressBack();
await Tests.navigate("Tags");
await Tests.fromId(notesnook.ids.tag.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add shortcut").waitAndTap();
await Tests.fromId(notesnook.ids.default.header.buttons.left).waitAndTap();
await Tests.fromText("testtag").isVisible();
});
it("Rename a tag", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add tag").waitAndTap();
await Tests.fromId("tag-input").element.typeText("testtag");
await Tests.fromText('Add "#testtag"').waitAndTap();
await Tests.fromText("#testtag").isVisible();
await device.pressBack();
await device.pressBack();
await Tests.navigate("Tags");
await Tests.fromId(notesnook.ids.tag.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Rename").waitAndTap();
await Tests.sleep(100);
await Tests.fromId("input-value").element.clearText();
await Tests.fromId("input-value").element.typeText("testtag_edited");
await Tests.fromText("Save").waitAndTap();
await Tests.fromText("#testtag_edited").isVisible();
});
});

View File

@@ -1,166 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import { readFileSync } from "fs";
import { expect as jestExpect } from "@jest/globals";
import { toMatchImageSnapshot } from "jest-image-snapshot";
import { device as _device } from "detox";
jestExpect.extend({ toMatchImageSnapshot });
const sleep = (duration) =>
new Promise((resolve) =>
setTimeout(() => {
resolve();
}, duration)
);
async function LaunchApp() {
await waitFor(element(by.id(notesnook.ids.default.root)))
.toBeVisible()
.withTimeout(500);
}
function elementById(id) {
return element(by.id(id)).atIndex(0);
}
function elementByText(text) {
return element(by.text(text)).atIndex(0);
}
async function tapById(id) {
await elementById(id).tap();
}
async function tapByText(text) {
await elementByText(text).tap();
}
async function visibleByText(text) {
await expect(elementByText(text)).toBeVisible();
}
async function visibleById(id) {
await expect(elementById(id)).toBeVisible();
}
async function notVisibleById(id) {
await expect(elementById(id)).not.toBeVisible();
}
async function notVisibleByText(text) {
await expect(elementByText(text)).not.toBeVisible();
}
async function exitEditor() {
await _device.pressBack();
await _device.pressBack();
}
async function createNote(_title, _body) {
let title = _title || "Test note description that ";
let body =
_body ||
"Test note description that is very long and should not fit in text.";
await tapById(notesnook.buttons.add);
let webview = web(by.id(notesnook.editor.id));
await expect(webview.element(by.web.className("ProseMirror"))).toExist();
await webview.element(by.web.className("ProseMirror")).tap();
await webview.element(by.web.className("ProseMirror")).typeText(body, true);
await exitEditor();
await waitFor(element(by.text(body)))
.toBeVisible()
.withTimeout(500);
return { title, body };
}
async function openSideMenu() {
let menu = elementById(notesnook.ids.default.header.buttons.left);
await menu.tap();
}
async function navigate(screen) {
let menu = elementById(notesnook.ids.default.header.buttons.left);
await waitFor(menu).toBeVisible().withTimeout(300);
await menu.tap();
await waitFor(elementByText(screen)).toBeVisible().withTimeout(300);
await elementByText(screen).tap();
}
const testvars = {
isFirstTest: true
};
async function prepare() {
await device.disableSynchronization();
if (testvars.isFirstTest) {
testvars.isFirstTest = false;
return await LaunchApp();
}
await device.reverseTcpPort(8081);
await device.uninstallApp();
await device.installApp();
await device.launchApp({ newInstance: true });
await LaunchApp();
}
async function matchSnapshot(element, name) {
let path = await element.takeScreenshot(name);
const bitmapBuffer = readFileSync(path);
jestExpect(bitmapBuffer).toMatchImageSnapshot({
failureThreshold: 200,
failureThresholdType: "pixel"
});
}
async function createNotebook(title = "Notebook 1", description = true) {
await elementById(notesnook.ids.dialogs.notebook.inputs.title).typeText(
title
);
if (description) {
await elementById(
notesnook.ids.dialogs.notebook.inputs.description
).typeText(`Description of ${title}`);
}
await tapByText("Add");
await sleep(300);
}
export {
matchSnapshot,
createNotebook,
prepare,
LaunchApp,
createNote,
navigate,
openSideMenu,
notVisibleById,
notVisibleByText,
visibleById,
visibleByText,
tapById,
tapByText,
elementByText,
elementById,
sleep,
exitEditor
};

View File

@@ -0,0 +1,149 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect as jestExpect } from "@jest/globals";
import { device as _device, expect } from "detox";
import { readFileSync } from "fs";
import { toMatchImageSnapshot } from "jest-image-snapshot";
import type { RouteName } from "../../app/stores/use-navigation-store";
import { notesnook } from "../test.ids";
jestExpect.extend({ toMatchImageSnapshot });
const testvars = {
isFirstTest: true
};
class Element {
element: Detox.NativeElement;
constructor(public type: "id" | "text", public value: string) {
if (type == "id") {
this.element = element(by.id(value)).atIndex(0);
} else {
this.element = element(by.text(value)).atIndex(0);
}
}
isVisible(timeout?: number) {
return waitFor(this.element)
.toBeVisible()
.withTimeout(timeout || 500);
}
isNotVisible(timeout?: number) {
return waitFor(this.element)
.not.toBeVisible()
.withTimeout(timeout || 500);
}
async waitAndTap(timeout?: number) {
await waitFor(this.element)
.toBeVisible()
.withTimeout(timeout || 500);
await this.element.tap();
}
tap(point?: Detox.Point2D): Promise<void> {
return this.element.tap(point);
}
static fromId(id: string) {
return new Element("id", id);
}
static fromText(text: string) {
return new Element("text", text);
}
}
const Tests = {
awaitLaunch: async () => {
await waitFor(element(by.id(notesnook.ids.default.root)))
.toBeVisible()
.withTimeout(500);
},
sleep: (duration: number) => {
return new Promise((resolve) =>
setTimeout(() => {
resolve(undefined);
}, duration)
);
},
fromId: Element.fromId,
fromText: Element.fromText,
async exitEditor() {
await _device.pressBack();
await _device.pressBack();
},
async createNote(_title?: string, _body?: string) {
let title = _title || "Test note description that ";
let body =
_body ||
"Test note description that is very long and should not fit in text.";
await Tests.fromId(notesnook.buttons.add).tap();
await expect(web().element(by.web.className("ProseMirror"))).toExist();
// await web().element(by.web.className("ProseMirror")).tap();
await web().element(by.web.className("ProseMirror")).typeText(body, true);
await Tests.exitEditor();
await Tests.fromText(body).isVisible();
return { title, body };
},
async navigate(screen: RouteName | ({} & string)) {
let menu = Tests.fromId(notesnook.ids.default.header.buttons.left);
await menu.waitAndTap();
await Tests.fromText(screen).waitAndTap();
},
async openSideMenu() {
await Tests.fromId(notesnook.ids.default.header.buttons.left).waitAndTap();
},
async prepare() {
await device.disableSynchronization();
if (testvars.isFirstTest) {
testvars.isFirstTest = false;
return await Tests.awaitLaunch();
}
await device.reverseTcpPort(8081);
await device.uninstallApp();
await device.installApp();
await device.launchApp({ newInstance: true });
await Tests.awaitLaunch();
},
async createNotebook(title = "Notebook 1", description = true) {
await Tests.sleep(1000);
const titleInput = Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.title
);
await titleInput.isVisible();
await titleInput.element.typeText(title);
if (description) {
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.description
).element.typeText(`Description of ${title}`);
}
await Tests.fromText("Add").waitAndTap();
},
async matchSnapshot(element: Element, name: string) {
let path = await element.element.takeScreenshot(name);
const bitmapBuffer = readFileSync(path);
(jestExpect(bitmapBuffer) as any).toMatchImageSnapshot({
failureThreshold: 200,
failureThresholdType: "pixel"
});
}
};
export { Element, Tests };

View File

@@ -1,145 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import {
tapById,
elementById,
visibleByText,
tapByText,
createNote,
prepare,
visibleById,
matchSnapshot,
notVisibleById,
navigate
} from "./utils";
import { sleep } from "./utils";
async function lockNote() {
await tapById(notesnook.listitem.menu);
await tapById("icon-lock-unlock");
await sleep(1000);
await visibleByText("Lock");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await elementById(notesnook.ids.dialogs.vault.pwdAlt).typeText("1234");
await tapByText("Lock");
await sleep(500);
await visibleById("note-locked-icon");
}
async function removeFromVault() {
await tapById(notesnook.listitem.menu);
await tapById("icon-lock-unlock");
await sleep(1000);
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await tapByText("Unlock");
await sleep(1000);
await notVisibleById("note-locked-icon");
}
async function openLockedNote(pwd) {
await tapById(notesnook.ids.note.get(1));
await sleep(1000);
await visibleByText("Open");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText(pwd || "1234");
await tapByText("Open");
await sleep(3000);
await matchSnapshot(elementById("editor-wrapper"), "note-after-vault-unlock");
}
async function goToPrivacySecuritySettings() {
await navigate("Settings");
await tapByText("Vault");
await sleep(500);
}
describe("VAULT", () => {
it("Create vault from settings", async () => {
await prepare();
await goToPrivacySecuritySettings();
await tapByText("Create vault");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await elementById(notesnook.ids.dialogs.vault.pwdAlt).typeText("1234");
await tapByText("Create");
await sleep(500);
await visibleByText("Clear vault");
});
it("Change vault password", async () => {
await prepare();
await createNote();
await lockNote();
await goToPrivacySecuritySettings();
await tapByText("Change vault password");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await elementById(notesnook.ids.dialogs.vault.changePwd).typeText("2362");
await tapByText("Change");
await sleep(2000);
await device.pressBack();
await device.pressBack();
await device.pressBack();
await openLockedNote("2362");
});
it("Delete vault", async () => {
await prepare();
await createNote();
await lockNote();
await goToPrivacySecuritySettings();
await tapByText("Delete vault");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await tapByText("Delete");
await sleep(500);
await visibleByText("Create vault");
await device.pressBack();
await device.pressBack();
await visibleById(notesnook.listitem.menu);
});
it("Delete vault with locked notes", async () => {
await prepare();
await createNote();
await lockNote();
await goToPrivacySecuritySettings();
await tapByText("Delete vault");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await tapByText("Delete all notes");
await tapByText("Delete");
await sleep(500);
await visibleByText("Create vault");
await device.pressBack();
await device.pressBack();
await notVisibleById(notesnook.listitem.menu);
});
it("Add a note to vault", async () => {
await prepare();
await createNote();
await lockNote();
await openLockedNote();
});
it("Remove note from vault", async () => {
await prepare();
await createNote();
await lockNote();
await removeFromVault();
});
});

View File

@@ -0,0 +1,143 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect } from "detox";
import { notesnook } from "../test.ids";
import { Tests } from "./utils";
async function lockNote() {
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromId("icon-lock-unlock").waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Lock").isVisible();
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText("1234");
await Tests.fromId(notesnook.ids.dialogs.vault.pwdAlt).element.typeText(
"1234"
);
await Tests.fromText("Lock").waitAndTap();
await Tests.fromId("note-locked-icon").isVisible();
}
async function removeFromVault() {
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromId("icon-lock-unlock").waitAndTap();
await Tests.sleep(500);
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText("1234");
await Tests.fromText("Unlock").waitAndTap();
await Tests.fromId("note-locked-icon").isNotVisible();
}
async function openLockedNote(pwd?: string) {
await Tests.fromId(notesnook.ids.note.get(0)).waitAndTap();
await Tests.sleep(500);
await web()
.element(by.web.name("password"))
.typeText(pwd || "1234", false);
await web().element(by.web.className("unlock-note")).tap();
await Tests.sleep(500);
await expect(web().element(by.web.className("unlock-note"))).not.toExist();
}
async function goToPrivacySecuritySettings() {
await Tests.navigate("Settings");
await Tests.fromText("Vault").waitAndTap();
}
describe("VAULT", () => {
it("Create vault from settings", async () => {
await Tests.prepare();
await goToPrivacySecuritySettings();
await Tests.fromText("Create vault").waitAndTap();
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText(
"1234"
);
await Tests.fromId(notesnook.ids.dialogs.vault.pwdAlt).element.typeText(
"1234"
);
await Tests.fromText("Create").waitAndTap();
await Tests.fromText("Clear vault").isVisible();
});
it("Change vault password", async () => {
await Tests.prepare();
await Tests.createNote();
await lockNote();
await goToPrivacySecuritySettings();
await Tests.fromText("Change vault password").waitAndTap();
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText(
"1234"
);
await Tests.fromId(notesnook.ids.dialogs.vault.changePwd).element.typeText(
"2362"
);
await Tests.fromText("Change").waitAndTap();
await device.pressBack();
await device.pressBack();
await Tests.sleep(500);
await openLockedNote("2362");
});
it("Delete vault", async () => {
await Tests.prepare();
await Tests.createNote();
await lockNote();
await goToPrivacySecuritySettings();
await Tests.fromText("Delete vault").waitAndTap();
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText(
"1234"
);
await Tests.fromText("Delete").waitAndTap();
await Tests.fromText("Create vault").isVisible();
await device.pressBack();
await device.pressBack();
await Tests.fromId(notesnook.listitem.menu).isVisible();
});
it("Delete vault with locked notes", async () => {
await Tests.prepare();
await Tests.createNote();
await lockNote();
await goToPrivacySecuritySettings();
await Tests.fromText("Delete vault").waitAndTap();
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText(
"1234"
);
await Tests.fromText("Delete notes in this vault").waitAndTap();
await Tests.fromText("Delete").waitAndTap();
await Tests.fromText("Create vault").isVisible();
await device.pressBack();
await device.pressBack();
await Tests.fromId(notesnook.listitem.menu).isNotVisible();
});
it("Add a note to vault", async () => {
await Tests.prepare();
await Tests.createNote();
await lockNote();
await openLockedNote();
});
it.only("Remove note from vault", async () => {
await Tests.prepare();
await Tests.createNote();
await lockNote();
await removeFromVault();
});
});

View File

@@ -52,7 +52,7 @@ module.exports = {
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_5_API_31'
avdName: 'Pixel_5_API_34'
}
}
},

View File

@@ -16,6 +16,7 @@ org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.configuration-cache=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK

View File

@@ -18,10 +18,10 @@ import { i18n } from "@lingui/core";
import { ScriptManager, Script } from '@callstack/repack/client';
import {
messages as $en
} from "@notesnook/intl/locales/$en.json";
} from "@notesnook/intl/dist/locales/$en.json";
import {
messages as $pseudo
} from "@notesnook/intl/locales/$pseudo-LOCALE.json";
} from "@notesnook/intl/dist/locales/$pseudo-LOCALE.json";
i18n.load({
en: __DEV__ ? $pseudo : $en

View File

@@ -10,8 +10,6 @@
},
"dependencies": {
"@ammarahmed/notifee-react-native": "7.4.7",
"@ammarahmed/react-native-share-extension": "^3.0.0",
"@ammarahmed/react-native-sodium": "1.5.4",
"@bam.tech/react-native-image-resizer": "3.0.5",
"@callstack/repack": "^4.1.1",
"@react-native-clipboard/clipboard": "^1.9.0",
@@ -73,7 +71,10 @@
"react-native-url-polyfill": "^2.0.0",
"react-native-screenguard": "^1.0.0",
"@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-pluralrules": "5.2.14"
"@formatjs/intl-pluralrules": "5.2.14",
"detox": "^20.27.6",
"@ammarahmed/react-native-share-extension": "^2.6.0",
"@ammarahmed/react-native-sodium": "1.5.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",
@@ -86,7 +87,6 @@
"@react-native/metro-config": "0.74.86",
"@tsconfig/react-native": "^3.0.2",
"@types/html-to-text": "^8.0.1",
"@types/jest": "^29.2.1",
"@types/metro-config": "^0.76.3",
"@types/react": "^18.2.6",
"@types/react-native": "^0.69.1",
@@ -98,19 +98,13 @@
"babel-loader": "^8.2.5",
"babel-plugin-module-resolver": "^4.1.0",
"babel-plugin-transform-remove-console": "6.9.4",
"detox": "^20.11.2",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-native": "^4.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"expect": "^29.6.2",
"jest": "^29.6.3",
"jest-circus": "^29.6.2",
"jest-image-snapshot": "^6.2.0",
"@react-native/babel-preset": "0.74.86",
"pixelmatch": "^5.3.0",
"prettier": "2.8.8",
"react-native-actions-shortcuts": "^1.0.1",
"react-native-bundle-visualizer": "^3.1.1",
@@ -118,10 +112,16 @@
"react-refresh": "0.14.0",
"react-test-renderer": "18.2.0",
"terser-webpack-plugin": "^5.3.5",
"ts-jest": "^29.1.1",
"webpack-cli": "^5.1.4",
"webpack": "^5.88.2",
"metro-react-native-babel-preset": "0.77.0",
"acorn-import-attributes": "1.9.5"
"acorn-import-attributes": "1.9.5",
"expect": "^29.7.0",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"jest-image-snapshot": "^6.4.0",
"ts-jest": "^29.2.5",
"pixelmatch": "^6.0.0",
"@types/jest": "^29.5.14"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
diff --git a/node_modules/react-native-actions-shortcuts/android/build.gradle b/node_modules/react-native-actions-shortcuts/android/build.gradle
index 19bd311..d15cdaf 100644
index 19bd311..bc37238 100644
--- a/node_modules/react-native-actions-shortcuts/android/build.gradle
+++ b/node_modules/react-native-actions-shortcuts/android/build.gradle
@@ -29,7 +29,7 @@ android {
@@ -7,7 +7,7 @@ index 19bd311..d15cdaf 100644
buildToolsVersion getExtOrDefault('buildToolsVersion')
defaultConfig {
- minSdkVersion 16
+ minSdkVersion 21
+ minSdkVersion getExtOrDefault('minSdkVersion')
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
versionCode 1
versionName "1.0"

View File

@@ -0,0 +1,11 @@
diff --git a/node_modules/react-native-fingerprint-scanner/android/build.gradle b/node_modules/react-native-fingerprint-scanner/android/build.gradle
index eaf3829..6c392ba 100644
--- a/node_modules/react-native-fingerprint-scanner/android/build.gradle
+++ b/node_modules/react-native-fingerprint-scanner/android/build.gradle
@@ -49,5 +49,5 @@ dependencies {
// 1.2.3 is the minimum version compatible with androidx.
// See https://github.com/uccmawei/FingerprintIdentify/issues/74
// (translation https://translate.google.com/translate?sl=zh-CN&tl=en&u=https://github.com/uccmawei/FingerprintIdentify/issues/74)
- implementation "com.wei.android.lib:fingerprintidentify:${safeExtGet("fingerprintidentify", "1.2.6")}"
+ implementation "com.github.uccmawei:FingerprintIdentify:1.2.6"
}

View File

@@ -39,7 +39,7 @@ index 0ccfd23..7771e6f 100644
s.preserve_paths = 'LICENSE', 'package.json'
s.source_files = '**/*.{h,m}'
diff --git a/node_modules/react-native-tooltips/android/build.gradle b/node_modules/react-native-tooltips/android/build.gradle
index 978045f..401b458 100644
index 978045f..59c64f1 100644
--- a/node_modules/react-native-tooltips/android/build.gradle
+++ b/node_modules/react-native-tooltips/android/build.gradle
@@ -3,8 +3,9 @@ buildscript {
@@ -53,24 +53,36 @@ index 978045f..401b458 100644
}
dependencies {
@@ -15,12 +16,12 @@ buildscript {
@@ -14,18 +15,19 @@ buildscript {
apply plugin: 'com.android.library'
android {
-android {
- compileSdkVersion 29
- buildToolsVersion '29.0.2'
+ compileSdkVersion 31
+ buildToolsVersion '31.0.0'
+def safeExtGet(prop, fallback) {
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
+}
+
+android {
+ compileSdkVersion safeExtGet('compileSdkVersion', 29)
+ buildToolsVersion safeExtGet('buildToolsVersion', '29.0.2')
defaultConfig {
- minSdkVersion 16
- targetSdkVersion 29
+ minSdkVersion 21
+ targetSdkVersion 31
+ minSdkVersion safeExtGet('minSdkVersion', 21)
+ targetSdkVersion safeExtGet('targetSdkVersion', 29)
versionCode 1
versionName "1.0"
- versionName "1.0"
- }
- lintOptions {
- abortOnError false
+ versionName "0.1"
}
@@ -36,5 +37,5 @@ repositories {
}
@@ -36,5 +38,5 @@ repositories {
dependencies {
implementation 'com.facebook.react:react-native:+'
@@ -104,7 +116,7 @@ index fb96466..75a6fdb 100644
@interface TooltipDelegate : NSObject <SexyTooltipDelegate>
@@ -7,9 +9,15 @@ @interface TooltipDelegate : NSObject <SexyTooltipDelegate>
@@ -7,9 +9,15 @@
@end
@implementation TooltipDelegate
@@ -120,7 +132,7 @@ index fb96466..75a6fdb 100644
}
@end
@@ -17,77 +25,120 @@ - (void)tooltipDidDismiss:(SexyTooltip *)tooltip {
@@ -17,77 +25,120 @@
@implementation RNTooltips

View File

@@ -73,8 +73,6 @@ test("creating a color shouldn't be possible on basic plan", async ({
await note?.contextMenu.newColor({ title: "red", color: "#ff0000" });
expect(
await app.toasts.waitForToast(
"Please upgrade your account to Pro to add colors."
)
await app.toasts.waitForToast("Upgrade to Notesnook Pro to add colors.")
).toBe(true);
});

View File

@@ -82,7 +82,7 @@ export class SettingsViewModel {
}
async isLoggedIn() {
const item = await this.navigation.findItem("Subscription");
const item = await this.navigation.findItem("Subscription details");
return !!(await item?.getTitle());
}

View File

@@ -92,7 +92,7 @@ test("delete a notebook", async ({ page }) => {
await notebook?.moveToTrash();
expect(await notebook?.isPresent()).toBe(false);
expect(await app.toasts.waitForToast("1 notebook moved to trash")).toBe(true);
expect(await app.toasts.waitForToast("Notebook moved to trash")).toBe(true);
const trash = await app.goToTrash();
expect(await trash.findItem(NOTEBOOK.title)).toBeDefined();
});

View File

@@ -45,7 +45,7 @@ test("delete a note", async ({ page }) => {
await note?.contextMenu.moveToTrash();
expect(await app.toasts.waitForToast("1 note moved to trash")).toBe(true);
expect(await app.toasts.waitForToast("Note moved to trash")).toBe(true);
expect(await note?.isPresent()).toBe(false);
});
@@ -60,7 +60,7 @@ test("restore a note", async ({ page }) => {
const trashItem = await trash.findItem(NOTE.title);
await trashItem?.restore();
expect(await app.toasts.waitForToast("1 item restored")).toBe(true);
expect(await app.toasts.waitForToast("Item restored")).toBe(true);
await app.goToNotes();
await notes.waitForItem(NOTE.title);
const restoredNote = await notes.findNote(NOTE);
@@ -78,9 +78,7 @@ test("permanently delete a note", async ({ page }) => {
const trashItem = await trash.findItem(NOTE.title);
await trashItem?.delete();
expect(await app.toasts.waitForToast("1 item permanently deleted")).toBe(
true
);
expect(await app.toasts.waitForToast("Item permanently deleted")).toBe(true);
expect(await trash.findItem(NOTE.title)).toBeUndefined();
});

View File

@@ -86,7 +86,7 @@ test("delete a tag", async ({ page }) => {
await tag?.delete();
expect(await app.toasts.waitForToast("1 tag deleted")).toBe(true);
expect(await app.toasts.waitForToast("Tag deleted")).toBe(true);
expect(await tags?.findItem(TAG)).toBeUndefined();
});
@@ -235,9 +235,7 @@ test("creating more than 5 tags shouldn't be possible on basic plan", async ({
const result = await Promise.race([
tags.createItem({ title: "tag6" }),
app.toasts.waitForToast(
"Please upgrade your account to Pro to add more tags."
)
app.toasts.waitForToast("Upgrade to Notesnook Pro to create more tags.")
]);
expect(result).toBe(true);
});

View File

@@ -77,7 +77,7 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
sentence = strings.upgradeToProToUseFeature("color");
break;
case CHECK_IDS.noteTag:
sentence = strings.upgradeToProToUseFeature("tag");
sentence = strings.upgradeToProToUseFeature("tags");
break;
case CHECK_IDS.notebookAdd:
sentence = strings.upgradeToProToUseFeature("notebook");

View File

@@ -44,7 +44,6 @@ import { ReminderDialog } from "../dialogs/reminder-dialog";
import { Cipher, SerializedKey } from "@notesnook/crypto";
import { ChunkedStream } from "../utils/streams/chunked-stream";
import { isFeatureSupported } from "../utils/feature-check";
import { NNCrypto } from "../interfaces/nncrypto";
import { strings } from "@notesnook/intl";
export const CREATE_BUTTON_MAP = {
@@ -215,7 +214,7 @@ export async function restoreBackupFile(backupFile: File) {
} else {
const error = await TaskManager.startTask<Error | void>({
title: strings.restoringBackup(),
subtitle: `${strings.restoringBackupDesc()}...`,
subtitle: strings.restoringBackupDesc(),
type: "modal",
action: async (report) => {
let cachedPassword: string | undefined = undefined;
@@ -344,14 +343,14 @@ async function restoreWithProgress(
current: number;
}) => {
report({
text: `Restoring ${collection}...`,
text: strings.restoringCollection(collection),
current,
total
});
}
);
report({ text: `Restoring...` });
report({ text: strings.restoring() });
return restore(backup, password, key);
}
});

View File

@@ -48,49 +48,64 @@ async function moveNotesToTrash(ids: string[], confirm = true) {
await TaskManager.startTask({
type: "status",
id: "deleteNotes",
title: strings.deletingItems("note", items.length),
title: strings.inProgressActions.deleting.note(items.length),
action: async (report) => {
report({
text: `${strings.inProgressAction("note", items.length, "deleting")}...`
text: strings.inProgressActions.deleting.note(items.length)
});
await noteStore.delete(...items);
}
});
showToast("success", strings.movedToTrash("note", items.length));
showToast("success", strings.actions.movedToTrash.note(items.length));
}
async function moveNotebooksToTrash(ids: string[]) {
if (!ids.length) return;
const isMultiselect = ids.length > 1;
if (isMultiselect) {
if (!(await showMultiDeleteConfirmation(ids.length))) return;
const result = await ConfirmDialog.show({
title: strings.doActions.delete.notebook(ids.length),
positiveButtonText: strings.yes(),
negativeButtonText: strings.no(),
checks: {
deleteContainingNotes: {
text: strings.deleteContainingNotes(ids.length)
}
}
});
if (!result) return;
if (result.deleteContainingNotes) {
await Multiselect.moveNotesToTrash(
Array.from(
new Set(
(await Promise.all(ids.map((id) => db.notebooks.notes(id)))).flat()
)
),
false
);
}
await TaskManager.startTask({
type: "status",
id: "deleteNotebooks",
title: strings.deletingItems("notebook", ids.length),
title: strings.inProgressActions.deleting.notebook(ids.length),
action: async (report) => {
report({
text: `${strings.inProgressAction(
"notebook",
ids.length,
"deleting"
)}...`
text: strings.inProgressActions.deleting.notebook(ids.length)
});
await notebookStore.delete(...ids);
}
});
showToast("success", strings.movedToTrash("notebook", ids.length));
showToast("success", strings.actions.movedToTrash.notebook(ids.length));
}
async function deleteAttachments(ids: string[]) {
if (
!(await ConfirmDialog.show({
title: strings.doAction("attachment", ids.length, "permanentlyDelete"),
title: strings.doActions.permanentlyDelete.attachment(ids.length),
message: strings.irreverisibleAction(),
negativeButtonText: strings.no(),
positiveButtonText: strings.yes()
@@ -101,7 +116,7 @@ async function deleteAttachments(ids: string[]) {
await TaskManager.startTask({
type: "status",
id: "deleteAttachments",
title: strings.deletingItems("attachment", ids.length),
title: strings.inProgressActions.deleting.attachment(ids.length),
action: async (report) => {
for (let i = 0; i < ids.length; ++i) {
const id = ids[i];
@@ -109,11 +124,7 @@ async function deleteAttachments(ids: string[]) {
if (!attachment) continue;
report({
text: `${strings.inProgressAction(
"attachment",
ids.length,
"deleting"
)}...`,
text: strings.inProgressActions.deleting.attachment(ids.length),
current: i,
total: ids.length
});
@@ -121,7 +132,7 @@ async function deleteAttachments(ids: string[]) {
}
}
});
showToast("success", strings.deleted("attachment", ids.length));
showToast("success", strings.actions.deleted.attachment(ids.length));
}
async function moveRemindersToTrash(ids: string[]) {
@@ -133,20 +144,16 @@ async function moveRemindersToTrash(ids: string[]) {
await TaskManager.startTask({
type: "status",
id: "deleteReminders",
title: strings.deletingItems("reminder", ids.length),
title: strings.inProgressActions.deleting.reminder(ids.length),
action: async (report) => {
report({
text: `${strings.inProgressAction(
"reminder",
ids.length,
"deleting"
)}...`
text: strings.inProgressActions.deleting.reminder(ids.length)
});
await reminderStore.delete(...ids);
}
});
showToast("success", strings.deleted("reminder", ids.length));
showToast("success", strings.actions.deleted.reminder(ids.length));
}
async function deleteTags(ids: string[]) {
@@ -161,7 +168,7 @@ async function deleteTags(ids: string[]) {
title: "Deleting tags",
action: async (report) => {
report({
text: `${strings.inProgressAction("tag", ids.length, "deleting")}...`
text: strings.inProgressActions.deleting.tag(ids.length)
});
for (const id of ids) {
await db.tags.remove(id);
@@ -172,7 +179,7 @@ async function deleteTags(ids: string[]) {
}
});
showToast("success", strings.deleted("tag", ids.length));
showToast("success", strings.actions.deleted.tag(ids.length));
}
export const Multiselect = {

View File

@@ -1,63 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { db } from "./db";
import { store as notestore } from "../stores/note-store";
import { store as nbstore } from "../stores/notebook-store";
import { showToast } from "../utils/toast";
import { strings } from "@notesnook/intl";
async function showUnpinnedToast(itemId, itemType) {
const messageText = strings.action(itemType, 1, "unpinned");
const undoAction = async () => {
toast.hide();
if (itemType === "note") await notestore.pin(itemId);
else if (itemType === "notebook") await nbstore.pin(itemId);
};
let actions = [{ text: "Undo", onClick: undoAction }];
var toast = showToast("success", messageText, actions);
}
function showItemDeletedToast(item) {
const messageText = strings.action(item.type, 1, "deleted");
const undoAction = async () => {
toast.hide();
let trashItem = db.trash.all.find((i) => i.id === item.id);
if (!trashItem) return;
await db.trash.restore(trashItem.id);
nbstore.refresh();
notestore.refresh();
};
let actions = [{ text: "Undo", onClick: undoAction }];
var toast = showToast("success", messageText, actions);
}
async function showUndoableToast(message, onAction, onPermanentAction, onUndo) {
await onAction();
const timeoutId = setTimeout(onPermanentAction, 5000);
const undoAction = async () => {
clearTimeout(timeoutId);
toast.hide();
onUndo();
};
let actions = [{ text: strings.undo(), onClick: undoAction }];
var toast = showToast("success", message, actions);
}
export { showUnpinnedToast, showItemDeletedToast, showUndoableToast };

View File

@@ -303,7 +303,7 @@ const AttachmentMenuItems: (
icon: Rename.path,
onClick: async () => {
const newName = await PromptDialog.show({
title: strings.doAction("attachment", 1, "rename"),
title: strings.doActions.rename.attachment(1),
description: attachment.filename,
defaultValue: attachment.filename
});

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Button, Flex, Text } from "@theme-ui/components";
import { Flex, Text } from "@theme-ui/components";
import { SaveState, useEditorStore } from "../../stores/editor-store";
import { Loading, Saved, NotSaved } from "../icons";
import { useNoteStatistics } from "./manager";
@@ -58,8 +58,8 @@ function EditorFooter() {
variant="subBody"
sx={{ color: "paragraph" }}
>
{words.total + " " + strings.words()}
{words.selected ? ` (${words.selected} ${strings.selected()})` : ""}
{strings.totalWords(words.total)}
{words.selected ? ` (${strings.selectedWords(words.selected)})` : ""}
</Text>
{dateEdited > 0 ? (
<Text

View File

@@ -33,7 +33,7 @@ function EditorLoading({ text }: { text?: string }) {
>
<Loading color="accent" sx={{ mt: 2 }} />
<Text variant="body" mt={2} sx={{ textAlign: "center" }}>
{text || `${strings.loadingEditor()}. ${strings.pleaseWait()}...`}
{text || strings.loadingEditor()}
</Text>
</Flex>
);

View File

@@ -368,12 +368,12 @@ function NavigationMenu(props: NavigationMenuProps) {
{
type: "button",
key: "rename-color",
title: strings.doAction("color", 1, "rename"),
title: strings.renameColor(),
onClick: () => RenameColorDialog.show(color)
},
{
type: "button",
key: strings.doAction("color", 1, "remove"),
key: "remove-color",
title: strings.removeColor(),
onClick: async () => {
await db.colors.remove(color.id);
@@ -432,7 +432,7 @@ function NavigationMenu(props: NavigationMenuProps) {
{
type: "button",
key: "removeshortcut",
title: strings.doAction("shortcut", 1, "remove"),
title: strings.doActions.remove.shortcut(1),
onClick: async () => {
await db.shortcuts.remove(item.id);
refreshNavItems();

View File

@@ -244,7 +244,7 @@ function Note(props: NoteProps) {
data-test-id={`tag-item`}
key={tag.id}
variant="anchor"
title={`${strings.goTo()} #${tag.title}`}
title={strings.goToTag(tag.title)}
onClick={(e) => {
e.stopPropagation();
if (!tag.id)
@@ -721,7 +721,7 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
{
type: "button",
key: "assign-tags",
title: `${strings.assignTo()}...`,
title: strings.assignTo(),
icon: Plus.path,
onClick: () => AddTagsDialog.show({ noteIds: ids })
},
@@ -815,7 +815,7 @@ async function copyNote(noteId: string, format: "md" | "txt") {
disableTemplate: true,
unlockVault: Vault.unlockVault
});
if (!result) throw new Error(`${strings.couldNotConvertNote(format)}.`);
if (!result) throw new Error(strings.couldNotConvertNote(format));
await navigator.clipboard.writeText(result);
showToast("success", strings.noteCopied());

View File

@@ -37,13 +37,11 @@ import {
import { hashNavigate, navigate } from "../../navigation";
import IconTag from "../icon-tag";
import { Multiselect } from "../../common/multi-select";
import { pluralize } from "@notesnook/common";
import { getFormattedDate } from "@notesnook/common";
import { MenuItem } from "@notesnook/ui";
import { Notebook as NotebookType } from "@notesnook/core";
import { handleDrop } from "../../common/drop-handler";
import { useDragHandler } from "../../hooks/use-drag-handler";
import { ConfirmDialog } from "../../dialogs/confirm";
import { useStore as useSelectionStore } from "../../stores/selection-store";
import { strings } from "@notesnook/intl";
@@ -210,30 +208,7 @@ export const notebookMenuItems: (
title: strings.moveToTrash(),
variant: "dangerous",
icon: Trash.path,
onClick: async () => {
const result = await ConfirmDialog.show({
title: `Delete ${pluralize(ids.length, "notebook")}?`,
positiveButtonText: strings.yes(),
negativeButtonText: strings.no(),
checks: {
deleteContainingNotes: {
text: `Move all notes in ${
ids.length > 1 ? "these notebooks" : "this notebook"
} to trash`
}
}
});
if (result) {
if (result.deleteContainingNotes) {
await Multiselect.moveNotesToTrash(
await db.notebooks.notes(notebook.id),
false
);
}
await Multiselect.moveNotebooksToTrash(ids);
}
},
onClick: () => Multiselect.moveNotebooksToTrash(ids),
multiSelect: true
}
];

View File

@@ -21,7 +21,6 @@ import { Button, Flex, Text } from "@theme-ui/components";
import { TipContext, useTip } from "../../hooks/use-tip";
import { Info, Sync } from "../icons";
import { useStore as useAppStore } from "../../stores/app-store";
import { toTitleCase } from "@notesnook/common";
import { strings } from "@notesnook/intl";
type PlaceholderProps = { context: TipContext; text?: string };
@@ -54,13 +53,13 @@ function Placeholder(props: PlaceholderProps) {
>
<Sync color="accent" size={12} sx={{ mr: "small" }} />
<Text variant="subBody" sx={{ fontSize: 10 }} color="accent">
{strings.syncingYour(context)}
{strings.syncingYourNotes()}
</Text>
</Flex>
<Text variant="subBody" sx={{ fontSize: "body", mt: 1 }}>
{toTitleCase(syncStatus.type || "sync")}ing {syncStatus.progress}{" "}
{strings.items()}
{strings.networkProgress(syncStatus.type || "sync")}{" "}
{syncStatus.progress} {strings.items()}
</Text>
</Flex>
);

View File

@@ -203,12 +203,12 @@ function PublishView(props: PublishViewProps) {
password
});
setPublishId(publishId);
showToast("success", strings.action("note", 1, "published"));
showToast("success", strings.actions.published.note(1));
} catch (e) {
console.error(e);
showToast(
"error",
`${strings.actionError("note", 1, "published")}: ${
`${strings.actionErrors.published.note(1)}: ${
(e as Error).message
}`
);
@@ -228,12 +228,12 @@ function PublishView(props: PublishViewProps) {
await unpublishNote(note.id);
setPublishId(undefined);
onClose(true);
showToast("success", strings.action("note", 1, "unpublished"));
showToast("success", strings.actions.unpublished.note(1));
} catch (e) {
console.error(e);
showToast(
"error",
`${strings.actionError("note", 1, "unpublished")}: ` +
`${strings.actionErrors.unpublished.note(1)}: ` +
(e as Error).message
);
} finally {

View File

@@ -152,7 +152,7 @@ const menuItems: (reminder: ReminderType, items?: string[]) => MenuItem[] = (
icon: Trash.path,
onClick: async () => {
ConfirmDialog.show({
title: strings.doAction("reminder", items.length, "delete"),
title: strings.doActions.delete.reminder(items.length),
message: strings.irreverisibleAction(),
positiveButtonText: strings.yes(),
negativeButtonText: strings.no()

View File

@@ -194,13 +194,11 @@ function statusToInfoText(status: UpdateStatus) {
return type === "checking"
? strings.checkingForUpdates()
: type === "downloading"
? `${Math.round(status.progress)}% ${strings.updating()}...`
? strings.updating(Math.round(status.progress))
: type === "completed"
? `v${
status.version
} ${strings.network.downloaded()} (${strings.restartRequired()})`
? strings.updateCompleted(status.version)
: type === "available"
? `v${status.version} ${strings.available()}`
? strings.updateNewVersionAvailable(status.version)
: "";
}

View File

@@ -65,7 +65,9 @@ export const AddNotebookDialog = DialogManager.register(
showToast(
"success",
strings.action("notebook", 1, props.edit ? "edited" : "created")
props.edit
? strings.actions.edited.notebook(1)
: strings.actions.created.notebook(1)
);
onClose(true);
}, [props.notebook?.id, props.edit, onClose, parentId]);

View File

@@ -191,7 +191,7 @@ export function CheckoutSideBar(props: SideBarProps) {
if (e instanceof Error)
showToast(
"error",
`${strings.couldNotActivateTrial()}. ${strings.pleaseTryAgain()} Error: ${
`${strings.couldNotActivateTrial()} ${strings.error()}: ${
e.message
}`
);

View File

@@ -26,7 +26,6 @@ import { db } from "../common/db";
import { getChangelog } from "../utils/version";
import { downloadUpdate } from "../utils/updater";
import { ErrorText } from "../components/error-text";
import { pluralize } from "@notesnook/common";
import { strings } from "@notesnook/intl";
type Check = { text: string; default?: boolean };
@@ -145,7 +144,7 @@ export const ConfirmDialog = DialogManager.register(function ConfirmDialog<
export function showMultiDeleteConfirmation(length: number) {
return ConfirmDialog.show({
title: strings.doAction("item", length, "delete"),
title: strings.doActions.delete.item(length),
message: strings.moveToTrashDesc(
db.settings.getTrashCleanupInterval() || 7
),
@@ -156,7 +155,7 @@ export function showMultiDeleteConfirmation(length: number) {
export function showMultiPermanentDeleteConfirmation(length: number) {
return ConfirmDialog.show({
title: strings.doAction("item", length, "permanentlyDelete"),
title: strings.doActions.permanentlyDelete.item(length),
message: strings.irreverisibleAction(),
positiveButtonText: strings.yes(),
negativeButtonText: strings.no()

View File

@@ -106,7 +106,7 @@ export const CreateTagDialog = {
)
return;
showToast("success", strings.action("tag", 1, "created"));
showToast("success", strings.actions.created.tag(1));
useTagStore.getState().refresh();
})
};
@@ -114,7 +114,7 @@ export const CreateTagDialog = {
export const EditTagDialog = {
show: (tag: Tag) =>
ItemDialog.show({
title: strings.doAction("tag", 1, "edit"),
title: strings.doActions.edit.tag(1),
subtitle: strings.editingTagDesc(tag.title),
defaultValue: tag.title
}).then(async (title) => {
@@ -126,7 +126,7 @@ export const EditTagDialog = {
}))
)
return;
showToast("success", strings.action("tag", 1, "edited"));
showToast("success", strings.actions.edited.tag(1));
await useTagStore.getState().refresh();
await useNoteStore.getState().refresh();
await useAppStore.getState().refreshNavItems();
@@ -148,7 +148,7 @@ export const RenameColorDialog = {
}))
)
return;
showToast("success", strings.action("color", 1, "renamed"));
showToast("success", strings.actions.renamed.color(1));
useAppStore.getState().refreshNavItems();
})
};

View File

@@ -55,13 +55,13 @@ export const MigrationDialog = DialogManager.register(function MigrationDialog(
EVENTS.migrationProgress,
({ collection, total, current }: MigrationProgressEvent) => {
task({
text: `${strings.processing()} ${collection}...`,
text: strings.processingCollection(collection),
current,
total
});
}
);
task({ text: `Processing...` });
task({ text: strings.processing() });
try {
await db.migrations.migrate();

View File

@@ -45,7 +45,6 @@ import {
TreeEnvironmentRef
} from "react-complex-tree";
import { FlexScrollContainer } from "../components/scroll-container";
import { pluralize } from "@notesnook/common";
import Field from "../components/field";
import { AddNotebookDialog } from "./add-notebook-dialog";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
@@ -153,11 +152,13 @@ export const MoveNoteDialog = DialogManager.register(function MoveNoteDialog({
await noteStore.refresh();
await notebookStore.refresh();
const stringified = stringifySelected(selected);
if (stringified) {
const added = selected.filter((a) => a.new && a.op === "add").length;
const removed = selected.filter((a) => a.op === "remove").length;
if (added > 0 || removed > 0) {
showToast(
"success",
`${strings.notes(noteIds.length)} ${stringified}`
strings.assignedToNotebookMessage(noteIds.length, added, removed)
);
}
@@ -562,37 +563,3 @@ function selectSingle(topic: Notebook, array: NotebookReference[]) {
return selected;
}
function stringifySelected(suggestion: NotebookReference[]) {
const added = suggestion.filter((a) => a.new && a.op === "add");
// .map(resolveReference)
// .filter(Boolean);
const removed = suggestion.filter((a) => a.op === "remove");
// .map(resolveReference)
// .filter(Boolean);
if (!added.length && !removed.length) return;
const parts = [];
if (added.length > 0) parts.push(strings.addedToNotebook(added.length));
// if (added.length >= 1) parts.push(added[0]);
// if (added.length > 1) parts.push(`and ${added.length - 1} others`);
if (removed.length >= 1) {
if (parts.length > 0) parts.push("&");
parts.push(`removed from ${strings.removedFromNotebook(removed.length)}`);
}
// if (removed.length > 1) parts.push(`and ${removed.length - 1} others`);
return parts.join(" ") + ".";
}
// function resolveReference(ref: NotebookReference): string | undefined {
// const notebook = db.notebooks.notebook(ref.id);
// if (!notebook) return undefined;
// // if (ref.topic) {
// // return notebook.topics.topic(ref.topic)?._topic?.title;
// // } else {
// return notebook.title;
// // }
// }

View File

@@ -17,12 +17,16 @@ 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 { createBackup, verifyAccount } from "../../common";
import { createBackup, verifyAccount, importBackup } from "../../common";
import { db } from "../../common/db";
import { exportNotes } from "../../common/export";
import { SettingsGroup } from "./types";
import { strings } from "@notesnook/intl";
import { useStore as useSettingStore } from "../../stores/setting-store";
import { useStore as useAppStore } from "../../stores/app-store";
import { useStore as useUserStore } from "../../stores/user-store";
import { desktop } from "../../common/desktop-bridge";
import { PATHS } from "@notesnook/desktop";
export const BackupExportSettings: SettingsGroup[] = [
{
@@ -51,6 +55,142 @@ export const BackupExportSettings: SettingsGroup[] = [
}
}
]
},
{
key: "restore-backup",
title: strings.restoreBackup(),
description: strings.restoreBackupDesc(),
components: [
{
type: "button",
title: "Restore",
action: async () => {
if (await importBackup()) {
await useAppStore.getState().refresh();
}
},
variant: "secondary"
}
]
},
{
key: "auto-backup",
title: strings.automaticBackups(),
description: strings.automaticBackupsDesc(),
// isHidden: () => !isUserPremium(),
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.backupReminderOffset, listener),
components: [
{
type: "dropdown",
options: [
{ value: "0", title: strings.never(), premium: true },
{ value: "1", title: strings.daily(), premium: true },
{ value: "2", title: strings.weekly(), premium: true },
{ value: "3", title: strings.monthly(), premium: true }
],
selectedOption: () =>
useSettingStore.getState().backupReminderOffset.toString(),
onSelectionChanged: async (value) => {
const verified =
useSettingStore.getState().encryptBackups ||
(await verifyAccount());
if (verified)
useSettingStore
.getState()
.setBackupReminderOffset(parseInt(value));
}
}
]
},
{
key: "auto-backup-with-attachments",
title: strings.automaticBackupsWithAttachments(),
description: strings.automaticBackupsWithAttachmentsDesc().join("\n\n"),
onStateChange: (listener) =>
useSettingStore.subscribe(
(s) => s.fullBackupReminderOffset,
listener
),
components: [
{
type: "dropdown",
options: [
{ value: "0", title: strings.never(), premium: true },
{ value: "1", title: strings.weekly(), premium: true },
{ value: "2", title: strings.monthly(), premium: true }
],
selectedOption: () =>
useSettingStore.getState().fullBackupReminderOffset.toString(),
onSelectionChanged: async (value) => {
const verified =
useSettingStore.getState().encryptBackups ||
(await verifyAccount());
if (verified)
useSettingStore
.getState()
.setFullBackupReminderOffset(parseInt(value));
}
}
]
},
{
key: "encrypt-backups",
title: strings.backupEncryption(),
description: strings.backupEncryptionDesc(),
isHidden: () => !useUserStore.getState().isLoggedIn,
onStateChange: (listener) => {
const subscriptions = [
useUserStore.subscribe((s) => s.isLoggedIn, listener),
useSettingStore.subscribe((s) => s.encryptBackups, listener)
];
return () => subscriptions.forEach((s) => s());
},
components: [
{
type: "toggle",
isToggled: () =>
!!useUserStore.getState().isLoggedIn &&
useSettingStore.getState().encryptBackups,
toggle: async () => {
const verified =
!useSettingStore.getState().encryptBackups ||
(await verifyAccount());
if (verified) useSettingStore.getState().toggleEncryptBackups();
}
}
]
},
{
key: "backup-directory",
title: strings.selectBackupDir(),
description: strings.selectBackupDirDesc(),
isHidden: () => !IS_DESKTOP_APP,
components: [
{
type: "button",
title: strings.select(),
action: async () => {
const verified =
useSettingStore.getState().encryptBackups ||
(await verifyAccount());
if (!verified) return;
const backupStorageLocation =
useSettingStore.getState().backupStorageLocation ||
PATHS.backupsDirectory;
const location = await desktop?.integration.selectDirectory.query(
{
title: strings.selectBackupDir(),
defaultPath: backupStorageLocation
}
);
if (!location) return;
useSettingStore.getState().setBackupStorageLocation(location);
},
variant: "secondary"
}
]
}
]
},

View File

@@ -130,9 +130,9 @@ export const BehaviourSettings: SettingsGroup[] = [
useSettingStore.getState().trashCleanupInterval.toString(),
options: [
{ value: "1", title: strings.daily() },
{ value: "7", title: `7 ${strings.days()}` },
{ value: "30", title: `30 ${strings.days()}` },
{ value: "365", title: `365 ${strings.days()}` },
{ value: "7", title: strings.days(7) },
{ value: "30", title: strings.days(30) },
{ value: "365", title: strings.days(365) },
{ value: "-1", title: strings.never(), premium: true }
]
}

View File

@@ -171,12 +171,10 @@ export function ServersConfiguration() {
.then((r) => r.json() as Promise<VersionResponse>)
.catch(() => undefined);
if (!version)
throw new Error(
`${strings.couldNotConnectTo(server.title)} .`
);
throw new Error(strings.couldNotConnectTo(server.title));
if (version.id !== server.id)
throw new Error(
`${strings.incorrectServerUrl(url, server.title)}.`
strings.incorrectServerUrl(url, server.title)
);
if (!isServerCompatible(version.version))
throw new Error(

View File

@@ -137,7 +137,7 @@ function ThemesList() {
if (e instanceof Error)
showToast(
"error",
`${strings.failedToInstallTheme()}. Error: ` + e.message
`${strings.failedToInstallTheme()} ${strings.error()}: ` + e.message
);
} finally {
setIsApplying(false);

View File

@@ -45,7 +45,7 @@ class TrashStore extends BaseStore<TrashStore> {
restore = async (...ids: string[]) => {
const restored = await db.trash.restore(...ids);
if (restored === false) return;
showToast("success", strings.action("item", ids.length, "restored"));
showToast("success", strings.actions.restored.item(ids.length));
await this.get().refresh();
await appStore.refreshNavItems();
await noteStore.refresh();

View File

@@ -137,10 +137,7 @@ function useAuthenticateUser({
const user = await db.user.fetchUser();
setUser(user);
} catch (e) {
showToast(
"error",
`${strings.biometricsAuthFailed()}. ${strings.pleaseTryAgain()}.`
);
showToast("error", strings.biometricsAuthFailed());
openURL("/");
} finally {
setIsAuthenticating(false);
@@ -325,7 +322,7 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
title={strings.accountRecovery()}
subtitle={strings.accountRecoveryWithKey()}
loading={{
title: `${strings.network.downloading()} (${progress})`,
title: strings.network.downloading(progress),
subtitle: strings.keyRecoveryProgressDesc()
}}
onSubmit={async (form) => {
@@ -461,7 +458,7 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
title={strings.resetAccountPassword()}
subtitle={strings.accountPassDesc()}
loading={{
title: `${strings.resettingAccountPassword()} (${progress})`,
title: strings.resettingAccountPassword(progress),
subtitle: strings.resetPasswordWait()
}}
onSubmit={async (form) => {

View File

@@ -61,7 +61,9 @@ function Trash() {
if (e instanceof Error)
showToast(
"error",
`${strings.couldNotClearTrash()}. Error: ${e.message}`
`${strings.couldNotClearTrash()} ${strings.error()}: ${
e.message
}`
);
}
}

View File

@@ -646,6 +646,7 @@ const Tiptap = ({
/>
<button
className="unlock-note"
style={{
backgroundColor: colors.primary.accent,
borderRadius: 5,

View File

@@ -38,20 +38,20 @@ function StatusBar({
const stickyRef = useRef(false);
const prevScroll = useRef(0);
const lastStickyChangeTime = useRef(0);
const [words, setWords] = useState(`0 ${strings.words()}`);
const [words, setWords] = useState(strings.totalWords(0));
const currentWords = useRef(words);
const statusBar = useRef({
set: setStatus,
updateWords: () => {
const editor = editors[tab.id];
if (!editor) return;
const words = getTotalWords(editor as Editor) + ` ${strings.words()}`;
const words = strings.totalWords(getTotalWords(editor as Editor));
if (currentWords.current === words) return;
setWords(words);
},
resetWords: () => {
currentWords.current = `0 ${strings.words()}`;
setWords(`0 ${strings.words()}`);
currentWords.current = strings.totalWords(0);
setWords(currentWords.current);
}
});

View File

@@ -31,8 +31,11 @@ import "./index.css";
if (globalThis.__DEV__) {
const logFn = global.console.log;
global.console.log = function () {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
// eslint-disable-next-line prefer-rest-params
logFn.apply(console, arguments);
// eslint-disable-next-line prefer-rest-params
globalThis.logger("info", ...arguments);
};
}
@@ -52,7 +55,7 @@ function loadApp() {
})
);
locale.then((locale) => {
locale.then((locale: { [name: string]: any }) => {
i18n.load(locale);
i18n.activate(globalThis.LINGUI_LOCALE || "en");
setI18nGlobal(i18n);

View File

@@ -5,7 +5,9 @@
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "react-jsx",
"noEmit": true,
"allowJs": true
"allowJs": true,
"moduleResolution": "nodenext",
"module": "nodenext"
},
"include": ["src/"]
}

View File

@@ -1,2 +1,3 @@
.swc
locales
locales
generated/*.ts

View File

@@ -28,7 +28,7 @@ module.exports = {
catalogs: [
{
path: "<rootDir>/locale/{locale}",
include: ["src"]
include: ["src", "generated"]
}
],
format: "po",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,9 @@
"@lingui/swc-plugin": "^4.0.10",
"@types/react": "^18.2.39",
"babel-plugin-macros": "^3.1.0",
"nodemon": "^3.1.7",
"react": "18.2.0",
"rollup": "^4.24.4",
"typescript": "5.5.3",
"vite": "^5.4.8",
"vite-plugin-dts": "^4.2.3",
@@ -877,9 +879,9 @@
}
},
"node_modules/@lingui/babel-plugin-extract-messages": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-4.11.4.tgz",
"integrity": "sha512-7gUOsYJ4wIjv/0tGxAGiGpgWKCybFPP0tCQMz6baa9xcsk8Vp7Xmuf9og1AD6EMawjStibQsQyE6xaRnJgpoHg==",
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-4.14.0.tgz",
"integrity": "sha512-lLabYkUsiF0un4ezColFm9Yf23c+efPR/5DRM9cFwW9uZJ5ajN4Ic00dBxJK9HOpy6fpaANnaHFmrm4NScijvQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -887,22 +889,22 @@
}
},
"node_modules/@lingui/cli": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@lingui/cli/-/cli-4.11.4.tgz",
"integrity": "sha512-PauBkvi++YkYAYq6w9MwkBmE6KiDE9wRh5DkN8yFPRcfj64vkE2l1HFENCqL/jg63kr8esOAiueD9+CtUGVyDg==",
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/@lingui/cli/-/cli-4.14.0.tgz",
"integrity": "sha512-g9aJ76wpl6Fj06SEGiGnilAErEuZG6/woAU1HM81E0ZZ8ib1DF+cf81PJE+8Lwoxc0yMqY8qMScwhwcUhnR0aA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.21.0",
"@babel/generator": "^7.21.1",
"@babel/parser": "^7.21.2",
"@babel/parser": "^7.22.0",
"@babel/runtime": "^7.21.0",
"@babel/types": "^7.21.2",
"@lingui/babel-plugin-extract-messages": "4.11.4",
"@lingui/conf": "4.11.4",
"@lingui/core": "4.11.4",
"@lingui/format-po": "4.11.4",
"@lingui/message-utils": "4.11.4",
"@lingui/babel-plugin-extract-messages": "4.14.0",
"@lingui/conf": "4.14.0",
"@lingui/core": "4.14.0",
"@lingui/format-po": "4.14.0",
"@lingui/message-utils": "4.14.0",
"babel-plugin-macros": "^3.0.1",
"chalk": "^4.1.0",
"chokidar": "3.5.1",
@@ -930,6 +932,53 @@
"node": ">=16.0.0"
}
},
"node_modules/@lingui/cli/node_modules/@lingui/conf": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/@lingui/conf/-/conf-4.14.0.tgz",
"integrity": "sha512-wHZ4s79wqPnROa3xEoUTNep7sjqwbDHCMLBhsHbcvk/3ACDKgm0TMvDnIRjMRwrPRsK98yxSmWaE+NKhT0Ggyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"chalk": "^4.1.0",
"cosmiconfig": "^8.0.0",
"jest-validate": "^29.4.3",
"jiti": "^1.17.1",
"lodash.get": "^4.4.2"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/cli/node_modules/@lingui/core": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-4.14.0.tgz",
"integrity": "sha512-rsuBKG3mupWpfP5RYacufu0ToavrqgFPnEI/zOcuvMNL24IlZ38O2/P3POZCEA8XCyhON70FICy8xs3ZvQ+uWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui/message-utils": "4.14.0",
"unraw": "^3.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/cli/node_modules/@lingui/message-utils": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.14.0.tgz",
"integrity": "sha512-izZod/6P2cXtqZt06r+VcbG3oCskPY8A1dVA8hyx0UZBCaufrZk6NgzCMYTMRXyOuXt6MafQyUfBJSrE4n3FIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@messageformat/parser": "^5.0.0",
"js-sha256": "^0.10.1"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/cli/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -1110,14 +1159,14 @@
}
},
"node_modules/@lingui/format-po": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@lingui/format-po/-/format-po-4.11.4.tgz",
"integrity": "sha512-PiWbTiiNgYZTFVuBHYirtAG98cDxrT0IwmSvETQk4YbaqCdn28/J7fRFZScsKqe8LmpnxX4EXZLs/R0MP2jLHA==",
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/@lingui/format-po/-/format-po-4.14.0.tgz",
"integrity": "sha512-3Bmk9f7lRuBoZsBG5c5hq5ipgAqeux+nDVnAIXSfsp4f8jj+z+cHwW0xuHcW48mHQFwfcTGAYZcKnwhuwwut6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@lingui/conf": "4.11.4",
"@lingui/message-utils": "4.11.4",
"@lingui/conf": "4.14.0",
"@lingui/message-utils": "4.14.0",
"date-fns": "^3.6.0",
"pofile": "^1.1.4"
},
@@ -1125,6 +1174,114 @@
"node": ">=16.0.0"
}
},
"node_modules/@lingui/format-po/node_modules/@lingui/conf": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/@lingui/conf/-/conf-4.14.0.tgz",
"integrity": "sha512-wHZ4s79wqPnROa3xEoUTNep7sjqwbDHCMLBhsHbcvk/3ACDKgm0TMvDnIRjMRwrPRsK98yxSmWaE+NKhT0Ggyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"chalk": "^4.1.0",
"cosmiconfig": "^8.0.0",
"jest-validate": "^29.4.3",
"jiti": "^1.17.1",
"lodash.get": "^4.4.2"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/format-po/node_modules/@lingui/message-utils": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.14.0.tgz",
"integrity": "sha512-izZod/6P2cXtqZt06r+VcbG3oCskPY8A1dVA8hyx0UZBCaufrZk6NgzCMYTMRXyOuXt6MafQyUfBJSrE4n3FIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@messageformat/parser": "^5.0.0",
"js-sha256": "^0.10.1"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/format-po/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@lingui/format-po/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@lingui/format-po/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/@lingui/format-po/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/@lingui/format-po/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@lingui/format-po/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@lingui/macro": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@lingui/macro/-/macro-4.11.4.tgz",
@@ -1160,6 +1317,55 @@
"node": ">=16.0.0"
}
},
"node_modules/@lingui/react": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@lingui/react/-/react-4.13.0.tgz",
"integrity": "sha512-5remR9rVwosiiX/RnEWETHA8cpqQiP7U87OXXMPz67LuyG3XP8RP+ic75rVn284DHLHgpjDbauz7vYIz855ZoQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui/core": "4.13.0"
},
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@lingui/react/node_modules/@lingui/core": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-4.13.0.tgz",
"integrity": "sha512-kRqQWeEVoqNrDtEdyHPWGsAHRStN8ObYc5a1gdyuBhoj1zaoUS/DMK5C7B1ZeTtj6rCCmZRs6d2tN12hsZ2zJA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui/message-utils": "4.13.0",
"unraw": "^3.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/react/node_modules/@lingui/message-utils": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.13.0.tgz",
"integrity": "sha512-tI/WBVZym+APwqk0O3xTaF0k+RQIv5E4PqGHdXqwbofycHly2C+izH+hg6UeNctc6jd19GRwqu/4ga9knkdAlQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@messageformat/parser": "^5.0.0",
"js-sha256": "^0.10.1"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/swc-plugin": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/@lingui/swc-plugin/-/swc-plugin-4.0.10.tgz",
@@ -1380,9 +1586,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
"integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.4.tgz",
"integrity": "sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==",
"cpu": [
"arm"
],
@@ -1394,9 +1600,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz",
"integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.4.tgz",
"integrity": "sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==",
"cpu": [
"arm64"
],
@@ -1408,9 +1614,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz",
"integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.4.tgz",
"integrity": "sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ==",
"cpu": [
"arm64"
],
@@ -1422,9 +1628,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz",
"integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.4.tgz",
"integrity": "sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==",
"cpu": [
"x64"
],
@@ -1435,10 +1641,38 @@
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.4.tgz",
"integrity": "sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.4.tgz",
"integrity": "sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz",
"integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.4.tgz",
"integrity": "sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==",
"cpu": [
"arm"
],
@@ -1450,9 +1684,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz",
"integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.4.tgz",
"integrity": "sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==",
"cpu": [
"arm"
],
@@ -1464,9 +1698,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz",
"integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.4.tgz",
"integrity": "sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==",
"cpu": [
"arm64"
],
@@ -1478,9 +1712,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz",
"integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.4.tgz",
"integrity": "sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==",
"cpu": [
"arm64"
],
@@ -1492,9 +1726,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz",
"integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.4.tgz",
"integrity": "sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==",
"cpu": [
"ppc64"
],
@@ -1506,9 +1740,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz",
"integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.4.tgz",
"integrity": "sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==",
"cpu": [
"riscv64"
],
@@ -1520,9 +1754,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz",
"integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.4.tgz",
"integrity": "sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==",
"cpu": [
"s390x"
],
@@ -1534,9 +1768,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz",
"integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.4.tgz",
"integrity": "sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==",
"cpu": [
"x64"
],
@@ -1548,9 +1782,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
"integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.4.tgz",
"integrity": "sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==",
"cpu": [
"x64"
],
@@ -1562,9 +1796,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz",
"integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.4.tgz",
"integrity": "sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==",
"cpu": [
"arm64"
],
@@ -1576,9 +1810,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz",
"integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.4.tgz",
"integrity": "sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==",
"cpu": [
"ia32"
],
@@ -1590,9 +1824,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
"integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.4.tgz",
"integrity": "sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==",
"cpu": [
"x64"
],
@@ -2330,6 +2564,7 @@
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
@@ -3117,6 +3352,13 @@
}
]
},
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
"dev": true,
"license": "ISC"
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -3840,6 +4082,86 @@
"integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==",
"dev": true
},
"node_modules/nodemon": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz",
"integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2",
"pstree.remy": "^1.1.8",
"semver": "^7.5.3",
"simple-update-notifier": "^2.0.0",
"supports-color": "^5.5.0",
"touch": "^3.1.0",
"undefsafe": "^2.0.5"
},
"bin": {
"nodemon": "bin/nodemon.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nodemon"
}
},
"node_modules/nodemon/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/nodemon/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/nodemon/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -4209,6 +4531,13 @@
"node": ">=16.0.0"
}
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"dev": true,
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4357,9 +4686,9 @@
}
},
"node_modules/rollup": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
"integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.4.tgz",
"integrity": "sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4373,22 +4702,24 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.24.0",
"@rollup/rollup-android-arm64": "4.24.0",
"@rollup/rollup-darwin-arm64": "4.24.0",
"@rollup/rollup-darwin-x64": "4.24.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
"@rollup/rollup-linux-arm-musleabihf": "4.24.0",
"@rollup/rollup-linux-arm64-gnu": "4.24.0",
"@rollup/rollup-linux-arm64-musl": "4.24.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
"@rollup/rollup-linux-riscv64-gnu": "4.24.0",
"@rollup/rollup-linux-s390x-gnu": "4.24.0",
"@rollup/rollup-linux-x64-gnu": "4.24.0",
"@rollup/rollup-linux-x64-musl": "4.24.0",
"@rollup/rollup-win32-arm64-msvc": "4.24.0",
"@rollup/rollup-win32-ia32-msvc": "4.24.0",
"@rollup/rollup-win32-x64-msvc": "4.24.0",
"@rollup/rollup-android-arm-eabi": "4.24.4",
"@rollup/rollup-android-arm64": "4.24.4",
"@rollup/rollup-darwin-arm64": "4.24.4",
"@rollup/rollup-darwin-x64": "4.24.4",
"@rollup/rollup-freebsd-arm64": "4.24.4",
"@rollup/rollup-freebsd-x64": "4.24.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.24.4",
"@rollup/rollup-linux-arm-musleabihf": "4.24.4",
"@rollup/rollup-linux-arm64-gnu": "4.24.4",
"@rollup/rollup-linux-arm64-musl": "4.24.4",
"@rollup/rollup-linux-powerpc64le-gnu": "4.24.4",
"@rollup/rollup-linux-riscv64-gnu": "4.24.4",
"@rollup/rollup-linux-s390x-gnu": "4.24.4",
"@rollup/rollup-linux-x64-gnu": "4.24.4",
"@rollup/rollup-linux-x64-musl": "4.24.4",
"@rollup/rollup-win32-arm64-msvc": "4.24.4",
"@rollup/rollup-win32-ia32-msvc": "4.24.4",
"@rollup/rollup-win32-x64-msvc": "4.24.4",
"fsevents": "~2.3.2"
}
},
@@ -4478,6 +4809,32 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/simple-update-notifier/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/source-map": {
"version": "0.8.0-beta.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
@@ -4628,6 +4985,16 @@
"node": ">=8.0"
}
},
"node_modules/touch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
"dev": true,
"license": "ISC",
"bin": {
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/tr46": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
@@ -4675,6 +5042,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",

View File

@@ -23,11 +23,11 @@
"url": "git://github.com/streetwriters/notesnook.git"
},
"scripts": {
"extract": "lingui extract",
"extract": "lingui extract --clean --overwrite",
"compile": "lingui compile --typescript",
"build-locale": "npm run extract && npm run compile",
"build-locale": "node scripts/generate-strings.mjs && npm run extract && npm run compile",
"build": "npm run build-locale && npx vite build && node scripts/postbuild.mjs",
"watch": "npx vite build --watch",
"watch": "nodemon --watch scripts/ --watch src --ext ts,mjs --exec \"npm run build\"",
"postinstall": "patch-package"
},
"devDependencies": {
@@ -36,7 +36,9 @@
"@lingui/swc-plugin": "^4.0.10",
"@types/react": "^18.2.39",
"babel-plugin-macros": "^3.1.0",
"nodemon": "^3.1.7",
"react": "18.2.0",
"rollup": "^4.24.4",
"typescript": "5.5.3",
"vite": "^5.4.8",
"vite-plugin-dts": "^4.2.3",

View File

@@ -0,0 +1,315 @@
/*
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 { mkdirSync, writeFileSync } from "fs";
const DO_ACTIONS = [
{
action: "delete",
label: "Delete",
dataTypes: [
"note",
"item",
"reminder",
"notebook",
"tag",
"color",
"attachment"
]
},
{ action: "unpin", label: "Unpin", dataTypes: ["note", "notebook"] },
{ action: "pin", label: "Pin", dataTypes: ["note", "notebook"] },
{ action: "unpublish", label: "Unpublish", dataTypes: ["note"] },
{ action: "publish", label: "Publish", dataTypes: ["note"] },
{
action: "permanentlyDelete",
label: "Permanently delete",
dataTypes: ["attachment", "item"]
},
{
action: "restore",
label: "Restore",
dataTypes: ["note", "notebook"]
},
{
action: "edit",
label: "Edit",
dataTypes: ["tag", "notebook", "reminder"]
},
{
action: "rename",
label: "Rename",
dataTypes: ["attachment", "color", "tag"]
},
{
action: "remove",
label: "Remove",
dataTypes: ["shortcut", "color", "attachment"]
},
{
action: "download",
label: "Download",
dataTypes: ["attachment"]
}
];
const ACTIONS = [
{
action: "deleted",
label: "deleted",
dataTypes: ["attachment", "reminder", "tag", "note"]
},
{
action: "movedToTrash",
label: "moved to trash",
dataTypes: ["note", "notebook"]
},
{
action: "permanentlyDeleted",
label: "permanently deleted",
dataTypes: ["note", "notebook"]
},
{ action: "published", label: "published", dataTypes: ["note"] },
{
action: "restored",
label: "restored",
dataTypes: ["note", "notebook", "item"]
},
{
action: "edited",
label: "edited",
dataTypes: ["tag", "notebook"]
},
{
action: "created",
label: "created",
dataTypes: ["notebook", "tag", "shortcut"]
},
{
action: "renamed",
label: "renamed",
dataTypes: ["color"]
}
];
const ACTION_CONFIRMATIONS = [
{
action: "delete",
label: "delete",
dataTypes: [
"note",
"item",
"reminder",
"notebook",
"tag",
"color",
"attachment"
]
},
{
action: "permanentlyDelete",
label: "permanently delete",
dataTypes: ["note", "notebook"]
}
];
const ACTION_ERRORS = [
{
action: "unpublished",
label: "unpublished",
dataTypes: ["note"]
},
{
action: "published",
label: "published",
dataTypes: ["note"]
}
];
const IN_PROGRESS_ACTIONS = [
{
action: "deleting",
label: "Deleting",
dataTypes: ["note", "notebook", "attachment", "tag", "reminder"]
}
];
const DATA_TYPES = {
note: {
singular: `note`,
singularCamelCase: `Note`,
plural: `notes`,
pluralCamelCase: `Notes`
},
notebook: {
singular: `notebook`,
singularCamelCase: `Notebook`,
plural: `notebooks`,
pluralCamelCase: `Notebooks`
},
tag: {
singular: `tag`,
singularCamelCase: `Tag`,
plural: `tags`,
pluralCamelCase: `Tags`
},
reminder: {
singular: `reminder`,
singularCamelCase: `Reminder`,
plural: `reminders`,
pluralCamelCase: `Reminders`
},
color: {
singular: `color`,
singularCamelCase: `Color`,
plural: `colors`,
pluralCamelCase: `Colors`
},
attachment: {
singular: `attachment`,
singularCamelCase: `Attachment`,
plural: `attachments`,
pluralCamelCase: `Attachments`
},
item: {
singular: `item`,
singularCamelCase: `Item`,
plural: `items`,
pluralCamelCase: `Items`
},
shortcut: {
singular: `shortcut`,
singularCamelCase: `Shortcut`,
plural: `shortcuts`,
pluralCamelCase: `Shortcuts`
}
};
const DO_ACTIONS_TEMPLATE = (action, dataTypes) => `${action}: {
${dataTypes}
}`;
const DATA_TYPES_TEMPLATE = (
type,
singularTemplate,
pluralTemplate
) => `${type}: (count: number) => plural(count, {
one: \`${singularTemplate}\`,
other: \`${pluralTemplate}\`
})`;
const MODULE_TEMPLATE = (exportName, strings) =>
`/* eslint-disable header/header */
// THIS FILE IS GENERATED. DO NOT EDIT MANUALLY.
import { t, plural } from "@lingui/macro";
export const ${exportName} = {
${strings}
};
`;
function generateDoActionsStrings() {
return generateStrings(
DO_ACTIONS,
(action, type) => `${action} ${DATA_TYPES[type].singular}`,
(action, type) => `${action} # ${DATA_TYPES[type].plural}`
);
}
function generateActionsStrings() {
return generateStrings(
ACTIONS,
(action, type) => `${DATA_TYPES[type].singularCamelCase} ${action}`,
(action, type) => `# ${DATA_TYPES[type].plural} ${action}`
);
}
function generateActionConfirmationStrings() {
return generateStrings(
ACTION_CONFIRMATIONS,
(action, type) =>
`Are you sure you want to ${action} this ${DATA_TYPES[type].singular}?`,
(action, type) =>
`Are you sure you to ${action} these ${DATA_TYPES[type].plural}?`
);
}
function generateActionErrorStrings() {
return generateStrings(
ACTION_ERRORS,
(action, type) =>
`${DATA_TYPES[type].singularCamelCase} could not be ${action}`,
(action, type) => `# ${DATA_TYPES[type].plural} could not be ${action}`
);
}
function generateInProgressActionsStrings() {
return generateStrings(
IN_PROGRESS_ACTIONS,
(action, type) => `${action} ${DATA_TYPES[type].singular}...`,
(action, type) => `${action} # ${DATA_TYPES[type].plural}...`
);
}
function generateStrings(actions, singular, plural) {
let result = [];
for (const action of actions) {
const subResults = [];
for (const type of action.dataTypes) {
const actionName = action.label;
subResults.push(
DATA_TYPES_TEMPLATE(
type,
singular(actionName, type),
plural(actionName, type)
)
);
}
result.push(DO_ACTIONS_TEMPLATE(action.action, subResults.join(",\n")));
}
return result.join(",\n");
}
mkdirSync("./generated/", { recursive: true });
writeFileSync(
"./generated/do-actions.ts",
MODULE_TEMPLATE("doActions", generateDoActionsStrings())
);
writeFileSync(
"./generated/actions.ts",
MODULE_TEMPLATE("actions", generateActionsStrings())
);
writeFileSync(
"./generated/in-progress-actions.ts",
MODULE_TEMPLATE("inProgressActions", generateInProgressActionsStrings())
);
writeFileSync(
"./generated/action-errors.ts",
MODULE_TEMPLATE("actionErrors", generateActionErrorStrings())
);
writeFileSync(
"./generated/action-confirmations.ts",
MODULE_TEMPLATE("actionConfirmations", generateActionConfirmationStrings())
);

View File

@@ -17,44 +17,27 @@ 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 { plural, select, t } from "@lingui/macro";
import { fail } from "assert";
import { doActions } from "../generated/do-actions";
import { actions } from "../generated/actions";
import { inProgressActions } from "../generated/in-progress-actions";
import { actionErrors } from "../generated/action-errors";
import { actionConfirmations } from "../generated/action-confirmations";
const actions = {
deleted: () => t`deleted`,
unpinned: () => t`unpinned`,
pinned: () => t`pinned`,
unpublished: () => t`unpublished`,
published: () => t`published`,
permanentlyDeleted: () => t`permanently deleted`,
restored: () => t`restored`,
edited: () => t`edited`,
created: () => t`created`,
renamed: () => t`renamed`
const SEARCH_IN_ROUTE_STRINGS = {
Notes: () => t`Search in in Notes`,
Notebooks: () => t`Search in in Notebooks`,
Notebook: () => t`Search in in Notebook`,
Favorites: () => t`Search in in Favorites`,
Reminders: () => t`Search in in Reminders`,
Trash: () => t`Search in in Trash`,
Settings: () => t`Search in in Settings`,
Tags: () => t`Search in in Tags`,
Editor: () => t`Search in in Editor`,
Home: () => t`Search in in Home`,
Search: () => t`Search in in Search`,
Monographs: () => t`Search in in Monographs`
};
const doActions = {
delete: () => t`Delete`,
unpin: () => t`Unpin`,
pin: () => t`Pin`,
unpublish: () => t`Unpublish`,
publish: () => t`Publish`,
permanentlyDelete: () => t`Permanently delete`,
restore: () => t`Restore`,
edit: () => t`Edit`,
create: () => t`Created`,
rename: () => t`Rename`,
remove: () => t`Remove`,
download: () => t`Download`
};
const inProgressActions = {
deleting: () => t`Delete`
};
type Actions = keyof typeof actions;
type DoActions = keyof typeof doActions;
type InProgressActions = keyof typeof inProgressActions;
export const strings = {
done: () => t`Done`,
verifyItsYou: () => t`Please verify it's you`,
@@ -67,10 +50,11 @@ export const strings = {
}),
downloading: () => t`Downloading`,
uploading: () => t`Uploading`,
networkProgress: (type: "upload" | "download") =>
networkProgress: (type: "upload" | "download" | "sync") =>
select(type, {
upload: "Uploading",
download: "Downloading",
sync: "Syncing",
other: "Loading"
}),
tapToCancel: () => t`Tap to cancel`,
@@ -326,10 +310,14 @@ export const strings = {
item: () => t`Items`,
shortcut: () => t`Shortcuts`
},
addItem: (referenceType: string) =>
t`Add a ${strings.dataTypes[
referenceType as keyof typeof strings.dataTypes
]()}`,
addItem: (itemType: "tag" | "notebook" | "reminder" | "note") =>
select(itemType, {
tag: `Add a tag`,
notebook: `Add a notebook`,
reminder: `Add a reminder`,
note: `Add a note`,
other: `Add an item`
}),
reminderRepeatStrings: {
day: (date: string) => t`Repeats daily at ${date}`,
week: {
@@ -340,21 +328,32 @@ export const strings = {
month: {
selectDays: () => t`Select nth day of the month to repeat the reminder.`
},
repeats: (freq: number, mode: string, selectedDays: string, date: string) =>
plural(freq, {
one: `Repeats every ${strings.reminderRepeatMode[
mode as keyof typeof strings.reminderRepeatMode
]()} on ${selectedDays} at ${date}`,
other: `Repeats every ${freq} ${strings.reminderRepeatMode[
mode as keyof typeof strings.reminderRepeatMode
]()} every ${selectedDays} at ${date}`
})
},
reminderRepeatMode: {
day: () => t`day`,
week: () => t`week`,
month: () => t`month`,
year: () => t`year`
repeats: (
freq: number,
mode: string,
selectedDays: string,
date: string
) => {
const strings = {
day: plural(freq, {
one: `Repeats every day on ${selectedDays} at ${date}`,
other: `Repeats every # day every ${selectedDays} at ${date}`
}),
week: plural(freq, {
one: `Repeats every week on ${selectedDays} at ${date}`,
other: `Repeats every # week every ${selectedDays} at ${date}`
}),
month: plural(freq, {
one: `Repeats every month on ${selectedDays} at ${date}`,
other: `Repeats every # month every ${selectedDays} at ${date}`
}),
year: plural(freq, {
one: `Repeats every year on ${selectedDays} at ${date}`,
other: `Repeats every # year every ${selectedDays} at ${date}`
})
};
return strings[mode as keyof typeof strings];
}
},
remindMeIn: () => t`Remind me in`,
referencedIn: () => t`REFERENCED IN`,
@@ -362,13 +361,13 @@ export const strings = {
t`Select the folder that includes your backup files to list them here.`,
noBackupsFound: () => t`No backups found`,
restoring: () => t`Restoring`,
restoringCollection: (collection: string) => t`Restoring ${collection}...`,
checkNewVersion: () => t`Checking for new version`,
noUpdates: () => t`No updates available`,
updateAvailable: () => t`Update available`,
versionReleased: (version: string, type: "github" | "store") =>
select(type, {
github: `v${version} has been released on GitHub`,
store: `v${version} has been released`,
other: `v${version} has been released`
}),
readReleaseNotes: () => t`Read full release notes on Github`,
@@ -387,7 +386,7 @@ export const strings = {
gettingRecoveryCodes: () => t`Getting recovery codes`,
protectNotes: () => t`Protect your notes`,
protectNotesDesc: () => t`Choose how you want to secure your notes locally.`,
loggingOut: () => t`Logging out`,
loggingOut: () => t`Logging out. Please wait...`,
loggingOutDesc: () => t`Please wait while we log you out.`,
by: () => t`By`,
noResultsForSearch: (query: string) => t`No results found for "${query}"`,
@@ -425,13 +424,12 @@ $headline$: Use starting line of the note as title.`,
other: ""
}),
createVault: () => t`Create Vault`,
createVaultDesc: () =>
t`A vault stores your notes in a password-encrypted storage.`,
vaultFingerprintUnlock: () => t`Vault Fingerprint Unlock`,
revokeVaultFingerprintUnlock: () => t`Revoke Vault Fingerprint Unlock`,
changeVaultPassword: () => t`Change Vault Password`,
deleteNote: () => t`Delete note`,
createVault: () => t`Create vault`,
createVaultDesc: () => t`A vault stores your notes in a encrypted storage.`,
vaultFingerprintUnlock: () => t`Vault fingerprint unlock`,
revokeVaultFingerprintUnlock: () => t`Revoke vault fingerprint unlock`,
changeVaultPassword: () => t`Change vault password`,
deleteNote: () => doActions.delete.note(1),
shareNote: () => t`Share note`,
copyNote: () => t`Copy note`,
goToEditor: () => t`Unlock note`,
@@ -448,6 +446,8 @@ $headline$: Use starting line of the note as title.`,
t`Your account email will be changed without affecting your subscription or any other settings.`,
changeEmailNotice: () => t`You will be logged out from all your devices`,
export: () => t`Export`,
exportNotes: (notes: number) =>
plural(notes, { one: "Export note", other: "Export # notes" }),
issueTitle: () => t`Report issue`,
issueDesc: () =>
t`We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap.`,
@@ -463,7 +463,8 @@ $headline$: Use starting line of the note as title.`,
}),
addNotesToNotebook: (title: string) => t`Add notes to ${title}`,
publish: () => t`Publish`,
publishDesc: () =>
publishNote: () => t`Publish note`,
publishNoteDesc: () =>
t`Publish your note to share it with others. You can set a password to protect it.`,
saveRecoveryKey: () => t`Save account recovery key`,
saveRecoveryKeyDesc: () =>
@@ -481,7 +482,8 @@ $headline$: Use starting line of the note as title.`,
twoFactorAuthEnabled: () => t`Two-factor authentication enabled`,
listOf: () => t`List of`,
network: {
downloading: () => t`Downloading`,
downloading: (progress?: string | number) =>
progress ? t`Downloading (${progress})` : t`Downloading`,
downloaded: () => t`Downloaded`,
download: () => t`Download`,
upload: () => t`Upload`,
@@ -498,7 +500,7 @@ $headline$: Use starting line of the note as title.`,
redo: () => t`Redo`,
createYourAccount: () => t`Create your account`,
pinned: () => t`Pinned`,
editNotebook: () => t`Edit notebook`,
editNotebook: () => doActions.edit.notebook(1),
newNotebook: () => t`New notebook`,
newInternalLink: () => t`Link to note`,
editInternalLink: () => t`Edit internal link`,
@@ -506,7 +508,9 @@ $headline$: Use starting line of the note as title.`,
tabs: () => t`Tabs`,
add: () => t`Add`,
newVersion: () => t`New version`,
editReminder: () => t`Edit reminder`,
newVersionHighlights: (version?: string) =>
t`${version ? `v${version} ` : "New version"} Highlights 🎉`,
editReminder: () => doActions.edit.reminder(1),
newReminder: () => t`New reminder`,
sortBy: () => t`Sort by`,
groupBy: () => t`Group by`,
@@ -542,7 +546,6 @@ $headline$: Use starting line of the note as title.`,
keep: () => t`Keep`,
restore: () => t`Restore`,
deletePermanently: () => t`Delete permanently`,
deletedPermanently: () => t`deleted permanently`,
viewAllLinkedNotebooks: () => t`View all linked notebooks`,
learnMore: () => t`Learn more`,
addTag: () => t`Add tag`,
@@ -635,7 +638,7 @@ $headline$: Use starting line of the note as title.`,
},
downloadUpdate: () => t`Download update`,
stopReordering: () => t`Tap to stop reordering`,
removeShortcut: () => t`Remove shortcut`,
removeShortcut: () => doActions.remove.shortcut(1),
createShortcut: () => t`Create a shortcut`,
tip: () => t`TIP`,
neverShowAgain: () => t`Never show again`,
@@ -749,7 +752,10 @@ $headline$: Use starting line of the note as title.`,
enterPassword: () => t`Enter password`,
failedToDownloadFile: () => t`Failed to download file`,
zipping: () => t`Zipping`,
savingZipFile: () => t`Saving zip file`,
savingZipFile: (progress?: string) =>
progress
? t`Saving zip file (${progress}%). Please wait...`
: t`Saving zip file. Please wait...`,
failedToZipFiles: () => t`Failed to zip files`,
fileVerificationFailed: () => t`Uploaded file verification failed.`,
fileLengthError: () =>
@@ -793,34 +799,15 @@ $headline$: Use starting line of the note as title.`,
user: "@andrewsayer on Twitter"
}
],
shortcutCreated: () => t`Shortcut created`,
notebookRestored: () => t`Notebook restored`,
restoreNotebook: () => t`Restore notebook`,
permanentlyDeletedNotebook: () => t`Permanently deleted notebook`,
shortcutCreated: () => actions.created.shortcut(1),
notebookRestored: () => actions.restored.notebook(1),
restoreNotebook: () => doActions.restore.notebook(1),
permanentlyDeletedNotebook: () => actions.permanentlyDeleted.notebook(1),
noteRestoredFromHistory: () => t`Note restored from history`,
noteRestored: () => t`Note restored`,
deleteNoteConfirmation: () =>
t`Are you sure you want to delete this note permanently?`,
noteDeleted: () => t`Note deleted`,
noteRestored: () => actions.restored.note(1),
deleteNoteConfirmation: () => actionConfirmations.permanentlyDelete.note(1),
noteDeleted: () => actions.deleted.note(1),
restored: () => t`Restored successfully`,
deleteItems: (type: string, count: number) =>
plural(count, {
one: `Delete ${strings.dataTypes[
type as keyof typeof strings.dataTypes
]()}`,
other: `Delete ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypes
]()}`
}),
deleteItemsConfirmation: (type: string, count: number) =>
plural(count, {
one: `Are you sure you want to delete this ${strings.dataTypes[
type as keyof typeof strings.dataTypes
]()} permanently?`,
other: `Are you sure you want to delete these ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypes
]()} permanently?`
}),
manageTags: () => t`Manage tags`,
linkNotebook: () => t`Link to notebook`,
move: () => t`Move`,
@@ -840,7 +827,7 @@ $headline$: Use starting line of the note as title.`,
}),
failedToPublish: () => t`Failed to publish note`,
failedToUnpublish: () => t`Failed to unpublish note`,
notePublished: () => t`Note published`,
notePublished: () => actions.published.note(1),
monographUrlCopied: () => t`Monograph URL copied`,
recoveryKeySaved: () => t`Did you save recovery key?`,
recoveryKeySavedDesc: () =>
@@ -855,22 +842,13 @@ $headline$: Use starting line of the note as title.`,
},
backupEncrypted: () => t`Backup is encrypted`,
password: () => t`Password`,
renameTag: () => t`Rename tag`,
renameColor: () => t`Rename color`,
renameTag: () => doActions.rename.tag(1),
renameColor: () => doActions.rename.color(1),
renameColorDesc: (color: string) => t`You are renaming color ${color}`,
name: () => t`Name`,
unlockToDelete: () => t`Unlock note to delete it`,
backupRestored: () => t`Backup restored`,
restoreFailed: () => t`Restore failed`,
itemDeleted: (count: number, type: string) =>
plural(count, {
one: `1 ${strings.dataTypesCamelCase[
type as keyof typeof strings.dataTypesCamelCase
]()} deleted`,
other: `# ${strings.dataTypes[
type as keyof typeof strings.dataTypes
]()} deleted`
}),
reorder: () => t`Reorder`,
turnOffReminder: () => t`Turn off reminder`,
turnOnReminder: () => t`Turn on reminder`,
@@ -894,12 +872,15 @@ $headline$: Use starting line of the note as title.`,
readOnly: () => t`Read only`,
syncOff: () => t`Sync off`,
syncOffConfirm: (count: number) =>
t`Prevent ${strings.itemsPlural("note", count)} from syncing`,
plural(count, {
one: `Prevent note from syncing`,
other: `Prevent # notes from syncing`
}),
syncOffDesc: (count: number) =>
`${strings.itemsPlural(
"note",
count
)} will be automatically deleted from all other devices & any future changes won't get synced. Are you sure you want to continue?`,
plural(count, {
one: `Note will be automatically deleted from all other devices & any future changes won't get synced. Are you sure you want to continue?`,
other: `# notes will be automatically deleted from all other devices & any future changes won't get synced. Are you sure you want to continue?`
}),
duplicate: () => t`Duplicate`,
remindMe: () => t`Remind me`,
published: () => t`Published`,
@@ -908,7 +889,7 @@ $headline$: Use starting line of the note as title.`,
linkNotebooks: () => t`Link notebooks`,
unlinkFromAll: () => t`Unlink from all`,
removeFromAll: () => t`Remove from all`,
assignTo: () => t`Assign to`,
assignTo: () => t`Assign to...`,
addTags: () => t`Add tags`,
addTagsDesc: () => t`Add tags to multiple notes at once`,
references: () => t`References`,
@@ -1190,10 +1171,10 @@ $headline$: Use starting line of the note as title.`,
automaticBackupsDesc: () =>
t`Set the interval to create a partial backup (without attachments) automatically.`,
automaticBackupsWithAttachments: () => t`Automatic backups with attachments`,
automaticBackupsWithAttachmentsDesc: () =>
t`Set the interval to create a backup (with attachments) automatically.
NOTE: Creating a backup with attachments can take a while, and also fail completely. The app will try to resume/restart the backup in case of interruptions.`,
automaticBackupsWithAttachmentsDesc: () => [
t`Set the interval to create a backup (with attachments) automatically.`,
t`NOTE: Creating a backup with attachments can take a while, and also fail completely. The app will try to resume/restart the backup in case of interruptions.`
],
selectBackupDir: () => t`Select backup directory`,
selectBackupDirDesc: () => t`Choose where to save your backups`,
noDirectorySelected: () => t`No directory selected`,
@@ -1310,18 +1291,17 @@ NOTE: Creating a backup with attachments can take a while, and also fail complet
backupDataDesc: () =>
t`All your backups are stored in 'Phone Storage/Notesnook/backups/' folder`,
backupSuccess: () => t`Backup successful`,
biometricsAuthFailed: () => t`Biometrics authentication failed`,
biometricsAuthFailed: () =>
t`Biometrics authentication failed. Please try again.`,
biometricsAuthFailedDesc: () => t`Wait 30 seconds to try again`,
biometricsAuthCancelled: () => t`Authentication cancelled by user`,
biometricsAuthError: () => t`Authentication failed`,
tryAgain: () => t`Tap to try again`,
rateAppMessage: () => t`We would love to know what you think!`,
rateAppActionText: (platform: string) =>
t`Rate Notesnook on ${
platform === "ios" ? strings.appStore() : strings.playStore()
}`,
appStore: () => t`App Store`,
playStore: () => t`Play Store`,
platform === "ios"
? t`Rate Notesnook on App Store`
: t`Rate Notesnook on Play Store`,
recoveryKeyMessage: () => t`Keep your data safe`,
recoveryKeyMessageActionText: () => t`Save your account recovery key`,
loginMessage: () => t`You are not logged in`,
@@ -1441,20 +1421,6 @@ NOTE: Creating a backup with attachments can take a while, and also fail complet
],
someNotesPublished: () => t`Some notes are published`,
unpublishToDelete: () => t`Unpublish notes to delete them`,
deleteTags: (count: number) =>
plural(count, {
one: "Delete tag",
other: "Delete # tags"
}),
deleteTagsConfirm: () => t`Are you sure you want to delete these tags?`,
deleteItemConfirmation: (itemType: string) =>
t`Are you sure you want to delete this ${strings.dataTypes[
itemType as keyof typeof strings.dataTypes
]()}?`,
deleteItem: (itemType: string) =>
t`Delete ${strings.dataTypes[
itemType as keyof typeof strings.dataTypes
]()}`,
filterAttachments: () => t`Filter attachments by filename, type or hash`,
oldPassword: () => t`Old password`,
newPassword: () => t`New password`,
@@ -1485,11 +1451,7 @@ For example:
searchANote: () => t`Search a note`,
remindeMeOf: () => t`Remind me of...`,
addShortNote: () => t`Add a short note`,
typeAKeywordToSearchIn: (route: string) =>
t`Type a keyword to search in ${strings.routes[
route as keyof typeof strings.routes
]()}...`,
searchingFor: (query: string) => t`Searching for ${query}`,
searchingFor: (query: string) => t`Searching for ${query}...`,
typeAKeyword: () => t`Type a keyword`,
search: () => t`Search`,
enterEmailAddress: () => t`Enter email address`,
@@ -1511,10 +1473,15 @@ For example:
Search: () => t`Search`,
Monographs: () => t`Monographs`
},
searchInRoute: (routeName: string) =>
t`Type a keyword to search in ${
strings.routes[routeName as keyof typeof strings.routes]?.() || routeName
}`,
searchInRoute: (
routeName: keyof typeof SEARCH_IN_ROUTE_STRINGS | ({} & string)
) => {
return (
SEARCH_IN_ROUTE_STRINGS[
routeName as keyof typeof SEARCH_IN_ROUTE_STRINGS
]() || t`Search in ${routeName}`
);
},
logoutConfirmation: () =>
t`Are you sure you want to logout and clear all data stored on THIS DEVICE?`,
backupDataBeforeLogout: () => t`Take a backup before logging out`,
@@ -1533,11 +1500,15 @@ For example:
t`Please grant notifications permission to add new reminders.`,
selectDayError: () => t`Please select the day to repeat the reminder on`,
setTitleError: () => t`Please set title of the reminder`,
dateError: () => t`Reminder date must be set in future`,
dateError: () => t`Reminder time cannot be earlier than the current time.`,
failedToDecryptBackup: () => t`Failed to decrypt backup`,
backupDirectoryNotSelected: () => t`Backup directory not selected`,
legal: () => t`legal`,
days: () => t`days`,
days: (days: number) =>
plural(days, {
one: `1 day`,
other: `# days`
}),
daily: () => t`Daily`,
weekly: () => t`Weekly`,
monthly: () => t`Monthly`,
@@ -1568,7 +1539,8 @@ For example:
noteLockedBlockLink: () =>
t`Linking to a specific block is not available for locked notes.`,
dismiss: () => t`Dismiss`,
words: () => t`words`,
totalWords: (words: number) =>
plural(words, { one: "# word", other: "# words" }),
addATag: () => t`Add a tag`,
startWritingNote: () => t`Start writing your note...`,
off: () => t`Off`,
@@ -1582,7 +1554,7 @@ For example:
restoreFromFiles: () => t`Restore from files`,
recentBackups: () => t`RECENT BACKUPS`,
restoringBackup: () => t`Restoring backup...`,
restoringBackupDesc: () => t`Please wait while we restore your backup`,
restoringBackupDesc: () => t`Please wait while we restore your backup...`,
decryptingBackup: () => t`Backup is encrypted, decrypting...`,
preparingBackupRestore: () => t`Preparing to restore backup file...`,
readingBackupFile: () => t`Reading backup file...`,
@@ -1608,9 +1580,9 @@ For example:
connectedToServer: () => t`Connected to all servers sucessfully.`,
allServerUrlsRequired: () => t`All server urls are required.`,
serverNotFound: (host: string) => t`Server with host ${host} not found.`,
couldNotConnectTo: (server: string) => t`Could not connect to ${server}`,
couldNotConnectTo: (server: string) => t`Could not connect to ${server}.`,
incorrectServerUrl: (url: string, server: string) =>
t`The URL you have given (${url}) does not point to the ${server}`,
t`The URL you have given (${url}) does not point to the ${server}.`,
serverVersionMismatch: (title: string, url: string) =>
t`The ${title} at ${url} is not compatible with this client.`,
testConnectionBeforeSave: () =>
@@ -1631,9 +1603,9 @@ For example:
],
restoreThisVersion: () => t`Restore this version`,
autoSaveOff: () => t`Auto save: off`,
selected: () => t`selected`,
selectedWords: (words: number) => plural(words, { other: "# selected" }),
dropFilesToAttach: () => t`Drop your files here to attach`,
loadingEditor: () => t`Loading editor`,
loadingEditor: () => t`Loading editor. Please wait...`,
noHeadingsFound: () => t`No headings found`,
somethingWentWrong: () => t`Something went wrong`,
whatWentWrong: () => t`What went wrong?`,
@@ -1671,22 +1643,14 @@ For example:
zoomOut: () => t`Zoom out`,
zoomIn: () => t`Zoom in`,
enterFullScreen: () => t`Enter fullscreen`,
syncingYour: (context: string) =>
t`Syncing your ${strings.routes[
(context.slice(0, 1).toUpperCase() +
context.slice(1)) as keyof typeof strings.routes
]()}`,
syncingYourNotes: () => t`Syncing your notes`,
items: () => t`items`,
downloadingImages: () => t`Downloading images`,
checkingForUpdates: () => t`Checking for updates`,
updating: () => t`updating`,
restartRequired: () => t`restart required`,
available: () => t`available`,
itemsRestored: (count: number) =>
plural(count, {
one: `# item restored`,
other: `# items restored`
}),
updating: (percentage: number) => t`${percentage}% updating...`,
updateCompleted: (version: string) =>
`v${version} downloaded (click to install)`,
updateNewVersionAvailable: (version: string) => t`v${version} available`,
unlocking: () => t`Unlocking`,
reminderStarts: (date: string, time: string) =>
t`The reminder will start on ${date} at ${time}.`,
@@ -1801,6 +1765,7 @@ For example:
dontShowAgainConfirm: () => t`Don't show again on this device?`,
toggleDarkLightMode: () => t`Toggle dark/light mode`,
goTo: () => t`Go to`,
goToTag: (tag: string) => t`Go to #${tag}`,
tagNotFound: () => `Tag not found`,
downloadAllAttachments: () => t`Download all attachments`,
selectProfilePicture: () => t`Select profile picture`,
@@ -1855,7 +1820,8 @@ For example:
properties: () => t`Properties`,
clickToPreview: () => t`Click to preview`,
clearCache: () => t`Clear cache`,
clearCacheDesc: () => t`Clear all cached attachments`,
clearCacheDesc: (cacheSize: number) =>
t`Clear all cached attachments. Current cache size: ${cacheSize}`,
clearCacheConfirm: () => t`Clear attachments cache?`,
clearCacheConfirmDesc:
() => t`Clearing attachments cache will perform the following actions:
@@ -1898,94 +1864,22 @@ All attachments will be downloaded & cached again on access.
backingUpDataWait: () =>
t`We are creating a backup of your data. Please wait...`,
resetAccountPassword: () => t`Reset account password`,
resettingAccountPassword: () => t`Resetting account password`,
resettingAccountPassword: (progress: number) =>
t`Resetting account password (${progress})`,
resetPasswordWait: () => t`Please wait while we reset your account password.`,
recoverySuccess: () => t`Recovery successful!`,
recoverySuccessDesc: () => t`Your account has been recovered.`,
upgradeNow: () => t`Upgrade now`,
backupSavedAt: (path: string) => t`Backup saved at ${path}`,
movedToTrash: (type: string, count: number) =>
plural(count, {
one: `1 ${strings.dataTypes[
type as keyof typeof strings.dataTypes
]()} moved to trash`,
other: `# ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypesPlural
]()} moved to trash`
}),
irreverisibleAction: () => t`This action is IRREVERSIBLE.`,
doAction: (type: string, count: number, action: DoActions) =>
plural(count, {
one: `${doActions[action]()} ${strings.dataTypesCamelCase[
type as keyof typeof strings.dataTypes
]()}?`,
other: `${doActions[action]()} # ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypesPlural
]()}?`
}),
action: (type: string, count: number, action: Actions) =>
plural(count, {
one: `${strings.dataTypesCamelCase[
type as keyof typeof strings.dataTypes
]()} ${actions[action]()}`,
other: `# ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypesPlural
]()} ${actions[action]()}`
}),
inProgressAction: (type: string, count: number, action: InProgressActions) =>
plural(count, {
one: `${inProgressActions[action]()} ${strings.dataTypesCamelCase[
type as keyof typeof strings.dataTypes
]()}`,
other: `${inProgressActions[action]()} # ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypesPlural
]()}`
}),
actionError: (type: string, count: number, action: Actions) =>
plural(count, {
one: `${strings.dataTypesCamelCase[
type as keyof typeof strings.dataTypes
]()} could not be ${actions[action]}`,
other: `# ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypesPlural
]()} could not be ${actions[action]}`
}),
deleted: (type: string, count: number) =>
plural(count, {
one: `${strings.dataTypesCamelCase[
type as keyof typeof strings.dataTypes
]()} deleted`,
other: `# ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypesPlural
]()} deleted`
}),
unpinned: (type: string, count: number) =>
plural(count, {
one: `${strings.dataTypesCamelCase[
type as keyof typeof strings.dataTypes
]()} unpinned`,
other: `# ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypesPlural
]()} unpinned`
}),
deleting: () => t`Deleting`,
deletingItems: (type: string, count: number) =>
plural(count, {
one: `Deleting ${strings.dataTypes[
type as keyof typeof strings.dataTypes
]()}`,
other: `Deleting ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypesPlural
]()}`
}),
itemsPlural: (type: string, count: number) =>
plural(count, {
one: `${strings.dataTypes[type as keyof typeof strings.dataTypes]()}`,
other: `# ${strings.dataTypesPlural[
type as keyof typeof strings.dataTypesPlural
]()}`
}),
doActions,
actions,
inProgressActions,
actionErrors,
actionConfirmations,
deleting: () => t`Deleting`,
backupReadyToDownload: () => t`Your backup is ready to download`,
vaultUnlocked: () => t`Vault unlocked`,
vaultLocked: () => t`Vault locked`,
@@ -2008,23 +1902,42 @@ All attachments will be downloaded & cached again on access.
},
noteDoesNotExist: () => t`Note does not exist`,
couldNotConvertNote: (format: string) =>
t`Could not convert note to ${format}`,
t`Could not convert note to ${format}.`,
remindersNotSupported: () =>
t`Reminders will not be active on this device as it does not support notifications.`,
invalidHexColor: () => t`Please enter a valid hex color (e.g. #ffffff)`,
profileUpdated: () => t`Profile updated`,
addedToNotebook: (count: number) =>
plural(count, {
one: `added to 1 notebook`,
other: `added to # notebooks`
}),
removedFromNotebook: (count: number) =>
plural(count, {
one: `removed from 1 notebook`,
other: `removed from # notebooks`
}),
assignedToNotebookMessage: (
notes: number,
added: number,
removed: number
) => {
const n = plural(notes, {
one: `1 note`,
other: `# notes`
});
if (added === 0 && removed > 0)
return t`${n} ${plural(removed, {
one: `removed from 1 notebook`,
other: `removed from # notebooks`
})}.`;
else if (added > 0 && removed === 0)
return t`${n} ${plural(added, {
one: `added to 1 notebook`,
other: `added to # notebooks`
})}.`;
else
return t`${n} ${plural(added, {
one: `added to 1 notebook`,
other: `added to # notebooks`
})} and ${plural(removed, {
one: `removed from 1 notebook`,
other: `removed from # notebooks`
})}.`;
},
noEncryptionKeyFound: () => t`No encryption key found`,
couldNotActivateTrial: () => t`Could not activate trial`,
couldNotActivateTrial: () =>
t`Could not activate trial. Please try again later.`,
pleaseTryAgain: () => t`Please try again`,
securityKeyRegistered: () => t`Security key successfully registered.`,
restartNow: () => t`Restart now`,
@@ -2036,13 +1949,13 @@ All attachments will be downloaded & cached again on access.
subCanceled: () => t`Your subscription has been canceled.`,
refundIssued: () =>
t`Your refund has been issued. Please wait 24 hours before reaching out to us in case you do not receive your funds.`,
failedToInstallTheme: () => t`Failed to install theme`,
failedToInstallTheme: () => t`Failed to install theme.`,
fullNameUpdated: () => t`Full name updated`,
shortcutRemoved: () => t`Shortcut removed`,
recheckFailed: () => t`Rechecking failed`,
failedToDelete: () => t`Failed to delete`,
failedToRegisterTask: () => t`Failed to register task`,
couldNotClearTrash: () => t`Could not clear trash`,
couldNotClearTrash: () => t`Could not clear trash.`,
automaticBackupsDisabled: () => t`Automatic backups disabled`,
automaticBackupsDisabledDesc: () =>
t`Please upgrade to Pro to enable automatic backups.`,
@@ -2069,7 +1982,7 @@ All attachments will be downloaded & cached again on access.
rotateRight: () => t`Rotate right`,
website: () => t`Website`,
resetSidebar: () => t`Reset sidebar`,
removeColor: () => t`Remove color`,
removeColor: () => doActions.remove.color(1),
favorite: () => t`Favorite`,
assignColor: () => t`Assign color`,
print: () => t`Print`,
@@ -2084,7 +1997,8 @@ All attachments will be downloaded & cached again on access.
editingTagDesc: (tag: string) => t`You are editing #${tag}`,
applyingChanges: () => t`Applying changes`,
thisMayTakeAWhile: () => t`This may take a while`,
processing: () => t`Processing`,
processing: () => t`Processing...`,
processingCollection: (collection: string) => t`Processing ${collection}...`,
root: () => t`Root`,
credientials: () => t`Credentials`,
thankYouForReporting: () => t`Thank you for reporting!`,
@@ -2440,7 +2354,6 @@ Use this if changes from other devices are not appearing on this device. This wi
passed: () => t`Passed`,
failed: () => t`Failed`,
cacheClearedDesc: () => t`All cached attachments have been cleared.`,
currentCacheSize: (size: string) => t`Current cache size: ${size}`,
restoreBackupConfirm: () => t`Restore backup?`,
serversConfigurationDesc: () => t`Configure server URLs for Notesnook`,
prioritySupport: () => t`Get Priority support`,
@@ -2460,5 +2373,11 @@ Use this if changes from other devices are not appearing on this device. This wi
schoolWork: () => t`School work`,
schoolWorkDesc: () => t`Everything related to my school in one place.`,
recipes: () => t`Recipes`,
recipesDesc: () => t`I love cooking and collecting recipes.`
recipesDesc: () => t`I love cooking and collecting recipes.`,
error: () => t`Error`,
deleteContainingNotes: (count: number) =>
plural(count, {
one: `Move all notes in this notebook to trash`,
other: `Move all notes in these notebooks to trash`
})
};

View File

@@ -17,10 +17,14 @@ 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 { resolve } from "path";
import path, { resolve } from "path";
import { defineConfig } from "vite";
import swc from "vite-plugin-swc-transform";
import dts from "vite-plugin-dts";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig({
plugins: [
@@ -32,13 +36,17 @@ export default defineConfig({
syntax: "typescript",
tsx: true
},
baseUrl: __dirname,
paths: {
"$src/*": ["src/*"]
},
experimental: {
plugins: [
[
"@lingui/swc-plugin",
{
runtimeModules: {
i18n: ["./setup", "i18n"]
i18n: ["$src/setup", "i18n"]
}
}
]