mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
128 Commits
fix-signup
...
editor/inl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6ff0a2430 | ||
|
|
eab0bdee8b | ||
|
|
0d94797bc1 | ||
|
|
372c49d81e | ||
|
|
97d21c818e | ||
|
|
3beb60d8b6 | ||
|
|
6343212d7e | ||
|
|
f728ad8826 | ||
|
|
b96af6c4cf | ||
|
|
6069a41fa4 | ||
|
|
7190207e5c | ||
|
|
015453a40b | ||
|
|
dad92a186c | ||
|
|
7e5e19e72c | ||
|
|
8e1484a029 | ||
|
|
4a3b3a14ec | ||
|
|
758157cf78 | ||
|
|
a1a171d648 | ||
|
|
92ba4d7624 | ||
|
|
d44ee5ca05 | ||
|
|
d7dde74675 | ||
|
|
5fde1c0f07 | ||
|
|
ffddbe2d7a | ||
|
|
eb3998d72d | ||
|
|
fca61afd7c | ||
|
|
33559250d8 | ||
|
|
38daca0099 | ||
|
|
36c8b8cc25 | ||
|
|
dd4ca817c7 | ||
|
|
594ceb1f52 | ||
|
|
fbc74ec359 | ||
|
|
0dfc1dfa52 | ||
|
|
b9c14ef9aa | ||
|
|
3fec2b397f | ||
|
|
ec841caef7 | ||
|
|
c163fc3be4 | ||
|
|
1e04315596 | ||
|
|
9aef28b03d | ||
|
|
88628c650f | ||
|
|
9523fd52d8 | ||
|
|
8cc6df92f4 | ||
|
|
042656856a | ||
|
|
5921c61e98 | ||
|
|
c5cd214d97 | ||
|
|
b30c35e59d | ||
|
|
ce4fa13758 | ||
|
|
1f47df0f45 | ||
|
|
91109278b6 | ||
|
|
78e4672871 | ||
|
|
dbecd6a466 | ||
|
|
e690973107 | ||
|
|
17b14e0ac6 | ||
|
|
0f8da747f9 | ||
|
|
df7d91e4f3 | ||
|
|
7eefecd961 | ||
|
|
2ec764d862 | ||
|
|
5a9b9da47e | ||
|
|
c5a82bd779 | ||
|
|
9c2a807025 | ||
|
|
4fa1ea4380 | ||
|
|
351d412fdf | ||
|
|
38600bf2a5 | ||
|
|
4720456d63 | ||
|
|
5a2221c5d3 | ||
|
|
91ef98df4f | ||
|
|
68b921887b | ||
|
|
dea91c48a2 | ||
|
|
f2e91d4a5c | ||
|
|
47be00355d | ||
|
|
05c4051cc9 | ||
|
|
71fe6bbebc | ||
|
|
74ed529f16 | ||
|
|
24a6d8c3f9 | ||
|
|
14c1567c07 | ||
|
|
fb778f56ed | ||
|
|
1eb0ef0cbe | ||
|
|
a81c8d0064 | ||
|
|
36099c6b4d | ||
|
|
8f7a15e692 | ||
|
|
de4a660eb2 | ||
|
|
c53fab603f | ||
|
|
a3e11b62e0 | ||
|
|
5d1775151d | ||
|
|
a0f5880e2c | ||
|
|
3dc7e70890 | ||
|
|
2fa1fc8662 | ||
|
|
879a65a41c | ||
|
|
d5b68bf0da | ||
|
|
fd460d3e90 | ||
|
|
a74bd4972c | ||
|
|
0ed8839ea9 | ||
|
|
d7b0f2684a | ||
|
|
b5be9b3abf | ||
|
|
911105c409 | ||
|
|
200c842671 | ||
|
|
5e5930e9e8 | ||
|
|
fd64c1cf2d | ||
|
|
48e2916874 | ||
|
|
a4b0e5bfef | ||
|
|
0b56121a26 | ||
|
|
f3f61d0e19 | ||
|
|
faaf1eeadf | ||
|
|
4fbaecc24e | ||
|
|
4fb056095c | ||
|
|
2d6142381b | ||
|
|
d97d14e830 | ||
|
|
f2f3f7e393 | ||
|
|
50176d2b3a | ||
|
|
ece7b29005 | ||
|
|
8cd8e93487 | ||
|
|
8900a6b080 | ||
|
|
19d90fa221 | ||
|
|
2e5ff859eb | ||
|
|
06747575ee | ||
|
|
98bb8ca186 | ||
|
|
d5f68602b6 | ||
|
|
312999aa5b | ||
|
|
607a4d26f4 | ||
|
|
a88b6615e4 | ||
|
|
f900098201 | ||
|
|
d51aaa8eb1 | ||
|
|
e9a48ae028 | ||
|
|
90a0f619ec | ||
|
|
ed3662a636 | ||
|
|
26eda3b105 | ||
|
|
e239597eb1 | ||
|
|
bf1ef07d3a | ||
|
|
7d2e1dab4c |
5
.github/workflows/android.e2e.yml
vendored
5
.github/workflows/android.e2e.yml
vendored
@@ -47,6 +47,11 @@ jobs:
|
||||
- name: Install Detox CLI
|
||||
run: npm install detox-cli --global
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Detox build
|
||||
run: |
|
||||
yarn build:android
|
||||
|
||||
@@ -73,6 +73,12 @@ jobs:
|
||||
- name: CCache Stats Before Build
|
||||
run: ccache -sv
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
|
||||
6
.github/workflows/android.publish.yml
vendored
6
.github/workflows/android.publish.yml
vendored
@@ -73,6 +73,12 @@ jobs:
|
||||
- name: CCache Stats Before Build
|
||||
run: ccache -sv
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
|
||||
6
.github/workflows/ios.publish.yml
vendored
6
.github/workflows/ios.publish.yml
vendored
@@ -64,6 +64,12 @@ jobs:
|
||||
bundle install
|
||||
RCT_NEW_ARCH_ENABLED=0 bundle exec pod install
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: CCache Stats Before Build
|
||||
run: ccache -sv
|
||||
|
||||
|
||||
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.2",
|
||||
"version": "3.3.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.2",
|
||||
"version": "3.3.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.2",
|
||||
"version": "3.3.5",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./overrides";
|
||||
import { app, BrowserWindow, nativeTheme, shell } from "electron";
|
||||
import { app, BrowserWindow, nativeTheme, shell, dialog } from "electron";
|
||||
import { isDevelopment } from "./utils";
|
||||
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
|
||||
import { configureAutoUpdater } from "./utils/autoupdater";
|
||||
@@ -182,6 +182,17 @@ async function createWindow() {
|
||||
app.once("ready", async () => {
|
||||
console.info("App ready. Opening window.");
|
||||
|
||||
if (app.runningUnderARM64Translation) {
|
||||
console.log("App is running under ARM64 translation");
|
||||
dialog.showMessageBoxSync({
|
||||
message:
|
||||
"Notesnook detected that it is running under ARM64 translation. For the best performance, please download the ARM64 build of Notesnook from our website.",
|
||||
type: "warning",
|
||||
buttons: ["Okay"],
|
||||
title: "Degraded Performance Warning"
|
||||
});
|
||||
}
|
||||
|
||||
if (config.customDns) enableCustomDns();
|
||||
else disableCustomDns();
|
||||
|
||||
|
||||
@@ -94,7 +94,9 @@ export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(
|
||||
DB_KEY_CIPHER
|
||||
) as Cipher;
|
||||
const databaseKey = (await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
@@ -135,7 +137,9 @@ export async function clearAppLockVerificationCipher() {
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
const appLockCipher: Cipher = CipherStorage.getMap(
|
||||
APPLOCK_CIPHER
|
||||
) as Cipher;
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
@@ -159,7 +163,9 @@ export function clearDatabaseKey() {
|
||||
export async function getDatabaseKey(appLockPassword?: string) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(
|
||||
"databaseKeyCipher"
|
||||
) as Cipher;
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
@@ -293,7 +299,7 @@ export async function deriveCryptoKey(data: SerializedKey) {
|
||||
|
||||
export async function getCryptoKey() {
|
||||
try {
|
||||
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER) as Cipher;
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return undefined;
|
||||
|
||||
@@ -136,6 +136,12 @@ export const Storage: IStorage = {
|
||||
clear(): Promise<void> {
|
||||
return DefaultStorage.clear();
|
||||
},
|
||||
generateCryptoKeyPair() {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
decryptAsymmetric() {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
getAllKeys(): Promise<string[]> {
|
||||
return DefaultStorage.getAllKeys();
|
||||
},
|
||||
|
||||
@@ -99,7 +99,7 @@ export const useLogin = (
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if (e.message === "invalid_grant") {
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
|
||||
@@ -35,29 +35,35 @@ import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import Progress from "../dialogs/progress";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const isAppLoading = useSettingStore((state) => state.isAppLoading);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppLockPassword />
|
||||
<LoadingDialog />
|
||||
<Dialog context="global" />
|
||||
<AuthModal colors={colors} />
|
||||
<MergeConflicts />
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<SheetProvider />
|
||||
<SheetProvider context="sync_progress" />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
<Dialog context="global" />
|
||||
<Progress />
|
||||
|
||||
{isAppLoading ? null : (
|
||||
<>
|
||||
<MergeConflicts />
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
positiveText: strings.restore(),
|
||||
negativeText: strings.delete(),
|
||||
positivePress: async () => {
|
||||
if ((await db.trash.restore(item.id)) === false) return;
|
||||
await db.trash.restore(item.id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(undefined);
|
||||
ToastManager.show({
|
||||
|
||||
@@ -117,7 +117,8 @@ export default function NotePreview({ session, content, note }) {
|
||||
{!session?.locked && !locked ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1
|
||||
flex: 1,
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
>
|
||||
<ReadonlyEditor
|
||||
|
||||
@@ -44,6 +44,7 @@ import Config from "react-native-config";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
//@ts-ignore
|
||||
import ToggleSwitch from "toggle-switch-react-native";
|
||||
import {
|
||||
ANDROID_POLICE_SVG,
|
||||
|
||||
@@ -39,9 +39,7 @@ import {
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import Exporter from "../../../services/exporter";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
@@ -83,7 +81,6 @@ const ExportNotesSheet = ({
|
||||
type: "pdf" | "txt" | "md" | "html" | "md-frontmatter"
|
||||
) => {
|
||||
if (exporting) return;
|
||||
if (!PremiumService.get() && type !== "txt") return;
|
||||
setExporting(true);
|
||||
update?.({ disableClosing: true } as PresentSheetOptions);
|
||||
setComplete(false);
|
||||
@@ -145,8 +142,7 @@ const ExportNotesSheet = ({
|
||||
await exportNoteAs("txt");
|
||||
},
|
||||
icon: "card-text",
|
||||
id: notesnook.ids.dialogs.export.text,
|
||||
pro: true
|
||||
id: notesnook.ids.dialogs.export.text
|
||||
},
|
||||
{
|
||||
title: "HTML",
|
||||
@@ -194,8 +190,7 @@ const ExportNotesSheet = ({
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: 0,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
opacity: item.pro ? 1 : 0.5
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
View
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
//@ts-ignore
|
||||
import ToggleSwitch from "toggle-switch-react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
|
||||
@@ -171,7 +171,7 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
|
||||
eSendEvent(eOnLoadNote, { newNote: true });
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
return;
|
||||
} else if (url.startsWith("https://notesnook.com/open_note")) {
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_note")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const note = await db.notes.note(id);
|
||||
@@ -182,13 +182,13 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
}
|
||||
}
|
||||
} else if (url.startsWith("https://notesnook.com/open_reminder")) {
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_reminder")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const reminder = await db.reminders.reminder(id);
|
||||
if (reminder) AddReminder.present(reminder);
|
||||
}
|
||||
} else if (url.startsWith("https://notesnook.com/new_reminder")) {
|
||||
} else if (url.startsWith("https://app.notesnook.com/new_reminder")) {
|
||||
const reminderFeature = await isFeatureAvailable("activeReminders");
|
||||
if (!reminderFeature.isAllowed) {
|
||||
ToastManager.show({
|
||||
@@ -780,13 +780,11 @@ export const useAppEvents = () => {
|
||||
if (!isAppLoading && !appLocked) {
|
||||
setTimeout(() => {
|
||||
sub = AppState.addEventListener("change", onAppStateChanged);
|
||||
if (
|
||||
refValues.current.initialUrl &&
|
||||
!refValues.current.initialUrl?.includes("open_note")
|
||||
) {
|
||||
if (refValues.current.initialUrl) {
|
||||
onAppOpenedFromURL({
|
||||
url: refValues.current.initialUrl!
|
||||
});
|
||||
refValues.current.initialUrl = undefined;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
|
||||
@@ -69,36 +69,6 @@ export default function useFeatureManager() {
|
||||
db.settings.setDefaultTag(undefined);
|
||||
}
|
||||
}
|
||||
const isAppLocked = useUserStore.getState().appLocked;
|
||||
let unsub: () => void;
|
||||
|
||||
if (isAppLocked) {
|
||||
unsub = useUserStore.subscribe((state) => {
|
||||
if (!state.appLocked && !features?.appLock?.isAllowed) {
|
||||
unsub();
|
||||
SettingsService.setProperty("appLockEnabled", false);
|
||||
setTimeout(() => {
|
||||
presentDialog({
|
||||
title: "App Lock Disabled",
|
||||
paragraph: features?.appLock?.error,
|
||||
positiveText: strings.upgrade(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
if (SettingsService.getProperty("serverUrls")) return;
|
||||
Navigation.navigate("PayWall", {
|
||||
context: "logged-in"
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsub?.();
|
||||
};
|
||||
}, [features, plan]);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,67 +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 { useEffect, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { Subscription } from "react-native-iap";
|
||||
import PremiumService from "../services/premium";
|
||||
import { db } from "../common/database";
|
||||
import { Product } from "@notesnook/core";
|
||||
|
||||
const skuInfos: { [name: string]: Product | undefined } = {};
|
||||
|
||||
export const usePricing = (period: "monthly" | "yearly") => {
|
||||
const [current, setCurrent] = useState<{
|
||||
period: string;
|
||||
info?: Product;
|
||||
product?: Subscription;
|
||||
}>();
|
||||
|
||||
const getDefaultSku = (period: "monthly" | "yearly") => {
|
||||
return period === "monthly"
|
||||
? "com.streetwriters.notesnook.sub.mo"
|
||||
: "com.streetwriters.notesnook.sub.yr";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const skuInfo =
|
||||
skuInfos[period] ||
|
||||
(await db.pricing?.sku(
|
||||
Platform.OS === "android" ? "android" : "ios",
|
||||
period
|
||||
));
|
||||
skuInfos[period] = skuInfo;
|
||||
|
||||
const products = (await (
|
||||
await PremiumService.loadProductsAndSubs()
|
||||
).subs) as Subscription[];
|
||||
let product = products.find((p) => p.productId === skuInfo?.sku);
|
||||
if (!product)
|
||||
product = products.find((p) => p.productId === getDefaultSku(period));
|
||||
setCurrent({
|
||||
info: skuInfo,
|
||||
period,
|
||||
product
|
||||
});
|
||||
})();
|
||||
}, [period]);
|
||||
|
||||
return current;
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import React, { useCallback, useEffect } from "react";
|
||||
import { db } from "../common/database";
|
||||
import BiometricService from "../services/biometrics";
|
||||
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
|
||||
const VaultStatusDefaults = {
|
||||
exists: false,
|
||||
@@ -36,6 +37,7 @@ export type VaultStatusType = {
|
||||
|
||||
export const useVaultStatus = () => {
|
||||
const [vaultStatus, setVaultStatus] = React.useState(VaultStatusDefaults);
|
||||
const isAppLoading = useSettingStore((state) => state.isAppLoading);
|
||||
|
||||
const checkVaultStatus = useCallback(() => {
|
||||
db.vault?.exists().then(async (exists) => {
|
||||
@@ -50,12 +52,13 @@ export const useVaultStatus = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAppLoading) return;
|
||||
checkVaultStatus();
|
||||
eSubscribeEvent("vaultUpdated", () => checkVaultStatus());
|
||||
return () => {
|
||||
eUnSubscribeEvent("vaultUpdated", () => checkVaultStatus());
|
||||
};
|
||||
}, [checkVaultStatus]);
|
||||
}, [checkVaultStatus, isAppLoading]);
|
||||
|
||||
return vaultStatus;
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@ import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { getFormattedDate, useIsFeatureAvailable } from "@notesnook/common";
|
||||
import PaywallSheet from "../../components/sheets/paywall";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
|
||||
const ReminderModes =
|
||||
Platform.OS === "ios"
|
||||
@@ -89,6 +90,7 @@ const ReminderNotificationModes = {
|
||||
|
||||
export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
const { reminder, reference } = props.route.params;
|
||||
useNavigationFocus(props.navigation, { focusOnInit: true });
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const [reminderMode, setReminderMode] = useState<Reminder["mode"]>(
|
||||
reminder?.mode || "once"
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
import { fluidTabsRef } from "../../utils/global-refs";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { useVaultStatus } from "../../hooks/use-vault-status";
|
||||
|
||||
const style: ViewStyle = {
|
||||
height: "100%",
|
||||
@@ -197,6 +198,7 @@ const Editor = React.memo(
|
||||
export default Editor;
|
||||
|
||||
const useLockedNoteHandler = () => {
|
||||
const vaultStatus = useVaultStatus();
|
||||
const tab = useTabStore((state) => state.getTab(state.currentTab));
|
||||
const tabRef = useRef(tab);
|
||||
tabRef.current = tab;
|
||||
@@ -217,15 +219,13 @@ const useLockedNoteHandler = () => {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const biometry = await BiometricService.isBiometryAvailable();
|
||||
const fingerprint = await BiometricService.hasInternetCredentials();
|
||||
useTabStore.setState({
|
||||
biometryAvailable: !!biometry,
|
||||
biometryEnrolled: !!fingerprint
|
||||
biometryAvailable: !!vaultStatus.isBiometryAvailable,
|
||||
biometryEnrolled: !!vaultStatus.biometryEnrolled
|
||||
});
|
||||
syncTabs("biometry");
|
||||
})();
|
||||
}, [tab?.id]);
|
||||
}, [tab?.id, vaultStatus.biometryEnrolled, vaultStatus.isBiometryAvailable]);
|
||||
|
||||
useEffect(() => {
|
||||
const unlockWithBiometrics = async () => {
|
||||
|
||||
@@ -59,6 +59,7 @@ export type Settings = {
|
||||
fontScale: number;
|
||||
markdownShortcuts: boolean;
|
||||
features: Record<any, any>;
|
||||
loggedIn: boolean;
|
||||
};
|
||||
|
||||
export type EditorProps = {
|
||||
|
||||
@@ -82,6 +82,7 @@ import { editorState, openInternalLink } from "./utils";
|
||||
import AddReminder from "../../add-reminder";
|
||||
import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
|
||||
import PaywallSheet from "../../../components/sheets/paywall";
|
||||
import useNavigationStore from "../../../stores/use-navigation-store";
|
||||
|
||||
const publishNote = async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
@@ -164,6 +165,7 @@ export const useEditorEvents = (
|
||||
state.timeFormat
|
||||
]);
|
||||
const handleBack = useRef<NativeEventSubscription>();
|
||||
const loggedIn = useUserStore((state) => !!state.user);
|
||||
const { fontScale } = useWindowDimensions();
|
||||
|
||||
const doubleSpacedLines = useSettingStore(
|
||||
@@ -223,7 +225,8 @@ export const useEditorEvents = (
|
||||
timeFormat: db.settings?.getTimeFormat(),
|
||||
fontScale,
|
||||
markdownShortcuts,
|
||||
features
|
||||
features,
|
||||
loggedIn
|
||||
});
|
||||
}, [
|
||||
fullscreen,
|
||||
@@ -241,7 +244,8 @@ export const useEditorEvents = (
|
||||
timeFormat,
|
||||
loading,
|
||||
fontScale,
|
||||
markdownShortcuts
|
||||
markdownShortcuts,
|
||||
loggedIn
|
||||
]);
|
||||
|
||||
const onBackPress = useCallback(async () => {
|
||||
@@ -274,7 +278,15 @@ export const useEditorEvents = (
|
||||
|
||||
const onHardwareBackPress = useCallback(() => {
|
||||
if (fluidTabsRef.current?.page() === "editor") {
|
||||
onBackPress();
|
||||
if (
|
||||
useNavigationStore.getState().currentRoute === "ManageTags" ||
|
||||
useNavigationStore.getState().currentRoute === "LinkNotebooks" ||
|
||||
useNavigationStore.getState().currentRoute === "AddReminder"
|
||||
) {
|
||||
Navigation.goBack();
|
||||
} else {
|
||||
onBackPress();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}, [onBackPress]);
|
||||
@@ -547,9 +559,17 @@ export const useEditorEvents = (
|
||||
if (editor.state.current?.isFocused) {
|
||||
editor.state.current.isFocused = true;
|
||||
}
|
||||
PaywallSheet.present(
|
||||
await isFeatureAvailable(editorMessage.value.feature)
|
||||
);
|
||||
if (editorMessage.value.feature === "insertAttachment") {
|
||||
ToastManager.show({
|
||||
type: "info",
|
||||
message: strings.loginRequired()
|
||||
});
|
||||
} else {
|
||||
PaywallSheet.present(
|
||||
await isFeatureAvailable(editorMessage.value.feature)
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case EditorEvents.monograph:
|
||||
publishNote();
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { TextInput } from "react-native-gesture-handler";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
@@ -36,6 +36,7 @@ export const SearchBar = ({
|
||||
onChangeText: (value: string) => void;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
const [clearButton, setClearButton] = useState(false);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const isFocused = useNavigationStore(
|
||||
(state) => state.focusedRouteId === "Search"
|
||||
@@ -45,6 +46,7 @@ export const SearchBar = ({
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const _onChangeText = (value: string) => {
|
||||
onChangeText(value);
|
||||
setClearButton(!!value);
|
||||
};
|
||||
|
||||
return selectionMode && isFocused ? null : (
|
||||
@@ -100,6 +102,23 @@ export const SearchBar = ({
|
||||
autoCorrect={false}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
{clearButton ? (
|
||||
<IconButton
|
||||
name="close"
|
||||
size={AppFontSize.xxl}
|
||||
top={10}
|
||||
testID="clear-search"
|
||||
bottom={10}
|
||||
onPress={() => {
|
||||
inputRef.current?.clear();
|
||||
onChangeText("");
|
||||
setClearButton(false);
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
type="plain"
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -185,7 +185,7 @@ export default function DebugLogs() {
|
||||
paragraph: strings.clearLogsConfirmation(currentLog.key),
|
||||
negativeText: strings.cancel(),
|
||||
positiveText: strings.clear(),
|
||||
positivePress: () => {
|
||||
positivePress: async () => {
|
||||
const index = logs.findIndex((l) => (l.key = currentLog.key));
|
||||
logManager?.delete(currentLog.key);
|
||||
if (logs.length > 1) {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { CirclePartner, SubscriptionStatus } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import { db } from "../../common/database";
|
||||
import AppIcon from "../../components/ui/AppIcon";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Notice } from "../../components/ui/notice";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -21,8 +23,8 @@ import PremiumService from "../../services/premium";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import { Notice } from "../../components/ui/notice";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
|
||||
export const NotesnookCircle = () => {
|
||||
const user = useUserStore((state) => state.user);
|
||||
@@ -156,32 +158,53 @@ const Partner = ({
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.secondary.border,
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
Clipboard.setString(code);
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.lg}
|
||||
color={colors.secondary.paragraph}
|
||||
<>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.secondary.border,
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
Clipboard.setString(code);
|
||||
}}
|
||||
>
|
||||
{code}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
size={AppFontSize.lg}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{code}
|
||||
</Paragraph>
|
||||
|
||||
<AppIcon name="content-copy" />
|
||||
</TouchableOpacity>
|
||||
<AppIcon name="content-copy" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{item.codeRedeemUrl ? (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (item.codeRedeemUrl) {
|
||||
openLinkInBrowser(
|
||||
item.codeRedeemUrl.replace("{{code}}", code)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xxs}
|
||||
>
|
||||
{strings.clickToDirectlyClaimPromo()}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -322,6 +322,7 @@ export const RestoreBackup = () => {
|
||||
});
|
||||
|
||||
setFiles(files);
|
||||
setLoading(false);
|
||||
BACKUP_FILES_CACHE.splice(0, BACKUP_FILES_CACHE.length, ...files);
|
||||
} catch (e) {
|
||||
e;
|
||||
@@ -519,7 +520,11 @@ const BackupItem = ({
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>{itemName}</Paragraph>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
|
||||
@@ -26,18 +26,17 @@ import {
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, TextInput, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
//@ts-ignore
|
||||
import { FeatureResult, useIsFeatureAvailable } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
//@ts-ignore
|
||||
import ToggleSwitch from "toggle-switch-react-native";
|
||||
import PaywallSheet from "../../components/sheets/paywall";
|
||||
import AppIcon from "../../components/ui/AppIcon";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import Input from "../../components/ui/input";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
import Seperator from "../../components/ui/seperator";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { SettingStore, useSettingStore } from "../../stores/use-setting-store";
|
||||
@@ -45,7 +44,6 @@ import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { components } from "./components";
|
||||
import { RouteParams, SettingSection } from "./types";
|
||||
import AppIcon from "../../components/ui/AppIcon";
|
||||
|
||||
const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
@@ -32,6 +32,7 @@ import React from "react";
|
||||
import { Appearance, Linking, Platform } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import * as RNIap from "react-native-iap";
|
||||
//@ts-ignore
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
@@ -81,15 +82,22 @@ export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
id: "subscription-status",
|
||||
useHook: () => useUserStore((state) => state.user),
|
||||
hidden: (current) =>
|
||||
!current ||
|
||||
(current as User).subscription?.plan === SubscriptionPlan.FREE,
|
||||
hidden: (current) => {
|
||||
const user = current as User;
|
||||
return (
|
||||
!user ||
|
||||
!user.subscription ||
|
||||
user.subscription.provider === undefined ||
|
||||
!strings.subscriptionProviderInfo[user?.subscription?.provider] ||
|
||||
user.subscription?.plan === SubscriptionPlan.FREE
|
||||
);
|
||||
},
|
||||
name: (current) => {
|
||||
const user = (current as User) || useUserStore.getState().user;
|
||||
return (
|
||||
strings.subscriptionProviderInfo[
|
||||
user?.subscription?.provider
|
||||
].title() || "Unknown provider"
|
||||
]?.title() || `Unknown provider id: ${user?.subscription?.provider}`
|
||||
);
|
||||
},
|
||||
icon: "credit-card",
|
||||
@@ -99,6 +107,8 @@ export const settingsGroups: SettingSection[] = [
|
||||
const subscriptionProviderInfo =
|
||||
strings.subscriptionProviderInfo[user?.subscription?.provider];
|
||||
|
||||
if (!subscriptionProviderInfo) return;
|
||||
|
||||
const isCurrentPlatform =
|
||||
(user.subscription?.provider === SubscriptionProvider.APPLE &&
|
||||
Platform.OS === "ios") ||
|
||||
@@ -1426,7 +1436,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
id: "docs-link",
|
||||
name: strings.documentation(),
|
||||
modifer: async () => {
|
||||
Linking.openURL("https://docs.notesnook.com");
|
||||
Linking.openURL("https://help.notesnook.com/");
|
||||
},
|
||||
description: strings.documentationDesc(),
|
||||
icon: "file-document"
|
||||
|
||||
@@ -580,7 +580,7 @@ function openSettingsDialog(context: string) {
|
||||
positivePress:
|
||||
Platform.OS === "ios"
|
||||
? undefined
|
||||
: () => {
|
||||
: async () => {
|
||||
resolve(true);
|
||||
},
|
||||
onClose: () => {
|
||||
|
||||
@@ -17,8 +17,15 @@ 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 { isFeatureAvailable } from "@notesnook/common";
|
||||
import { Profile, User } from "@notesnook/core";
|
||||
import create, { State } from "zustand";
|
||||
import SettingsService from "../services/settings";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { eSendEvent } from "../services/event-manager";
|
||||
import { eCloseSimpleDialog } from "../utils/events";
|
||||
import Navigation from "../services/navigation";
|
||||
|
||||
export enum SyncStatus {
|
||||
Passed,
|
||||
@@ -57,6 +64,28 @@ export const useUserStore = create<UserStore>((set) => ({
|
||||
setLastSynced: (lastSynced) => set({ lastSynced: lastSynced }),
|
||||
lockApp: (appLocked) => {
|
||||
set({ appLocked });
|
||||
if (!appLocked) {
|
||||
isFeatureAvailable("appLock").then((feature) => {
|
||||
if (!feature.isAllowed) {
|
||||
SettingsService.setProperty("appLockEnabled", false);
|
||||
setTimeout(() => {
|
||||
presentDialog({
|
||||
title: "App Lock Disabled",
|
||||
paragraph: feature?.error,
|
||||
positiveText: strings.upgrade(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
if (SettingsService.getProperty("serverUrls")) return;
|
||||
Navigation.navigate("PayWall", {
|
||||
context: "logged-in"
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
lastSyncStatus: SyncStatus.Never,
|
||||
disableAppLockRequests: false,
|
||||
|
||||
@@ -33,6 +33,7 @@ import { useNotebookStore } from "../stores/use-notebook-store";
|
||||
import { useRelationStore } from "../stores/use-relation-store";
|
||||
import { useTagStore } from "../stores/use-tag-store";
|
||||
import { eUpdateNoteInEditor } from "./events";
|
||||
import { unlockVault } from "./unlock-vault";
|
||||
|
||||
export function getObfuscatedEmail(email: string) {
|
||||
if (!email) return "";
|
||||
@@ -56,7 +57,7 @@ function confirmDeleteAllNotes(
|
||||
title: strings.doActions.delete.notebook(items.length),
|
||||
positiveText: strings.delete(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: (_inputValue, value) => {
|
||||
positivePress: async (_inputValue, value) => {
|
||||
setTimeout(() => {
|
||||
resolve({ delete: true, deleteNotes: value });
|
||||
});
|
||||
@@ -98,6 +99,30 @@ export const deleteItems = async (
|
||||
await db.reminders.remove(...itemIds);
|
||||
useRelationStore.getState().update();
|
||||
} else if (type === "note") {
|
||||
let someNotesLocked = false;
|
||||
|
||||
for (const id of itemIds) {
|
||||
if (
|
||||
await db.vaults.itemExists({
|
||||
id: id,
|
||||
type: "note"
|
||||
})
|
||||
) {
|
||||
someNotesLocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (someNotesLocked) {
|
||||
const unlocked = await unlockVault({
|
||||
title: strings.unlockVault(),
|
||||
paragraph: strings.unlockVaultDesc(),
|
||||
context: "global",
|
||||
requirePassword: true
|
||||
});
|
||||
if (!unlocked) return;
|
||||
}
|
||||
|
||||
for (const id of itemIds) {
|
||||
if (db.monographs.isPublished(id)) {
|
||||
ToastManager.show({
|
||||
@@ -108,6 +133,7 @@ export const deleteItems = async (
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
await db.notes.moveToTrash(id);
|
||||
|
||||
eSendEvent(
|
||||
@@ -157,7 +183,7 @@ export const deleteItems = async (
|
||||
heading: message,
|
||||
type: "success",
|
||||
func: async () => {
|
||||
if ((await db.trash.restore(...deletedIds)) === false) return;
|
||||
await db.trash.restore(...deletedIds);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useMenuStore.getState().setMenuPins();
|
||||
useMenuStore.getState().setColorNotes();
|
||||
|
||||
@@ -27,18 +27,20 @@ let unlockPromise: Promise<any> | undefined = undefined;
|
||||
export async function unlockVault({
|
||||
context,
|
||||
title,
|
||||
paragraph
|
||||
paragraph,
|
||||
requirePassword
|
||||
}: {
|
||||
context?: string;
|
||||
title: string;
|
||||
paragraph: string;
|
||||
requirePassword?: boolean;
|
||||
}) {
|
||||
if (unlockPromise) {
|
||||
return unlockPromise;
|
||||
}
|
||||
unlockPromise = new Promise(async (resolve) => {
|
||||
const result = await (async () => {
|
||||
if (db.vault.unlocked) return true;
|
||||
if (db.vault.unlocked && !requirePassword) return true;
|
||||
const biometry = await BiometricService.isBiometryAvailable();
|
||||
const fingerprint = await BiometricService.hasInternetCredentials();
|
||||
if (biometry && fingerprint) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execSync } from "child_process";
|
||||
|
||||
//@ts-ignore
|
||||
import { pathExists, ensureDir } from "fs-extra";
|
||||
|
||||
import { resolveConfig } from "detox/internals";
|
||||
|
||||
@@ -28,6 +28,9 @@ describe("Search", () => {
|
||||
.typeTextById("search-input", "Test")
|
||||
.wait(1000)
|
||||
.isVisibleByText("1")
|
||||
.waitAndTapById("clear-search")
|
||||
.wait(2000)
|
||||
.isNotVisibleByText("1")
|
||||
.run();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ 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";
|
||||
//@ts-ignore
|
||||
import { toMatchImageSnapshot } from "jest-image-snapshot";
|
||||
import type { RouteName } from "../../app/stores/use-navigation-store";
|
||||
import { notesnook } from "../test.ids";
|
||||
|
||||
@@ -124,7 +124,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3074
|
||||
versionCode 3078
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -33,7 +33,7 @@ public class NotePreviewWidget extends AppWidgetProvider {
|
||||
intent.putExtra(OpenNoteId, note.getId());
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(RCTNNativeModule.IntentType, "OpenNote");
|
||||
intent.setData(Uri.parse("https://notesnook.com/open_note?id=" + note.getId()));
|
||||
intent.setData(Uri.parse("https://app.notesnook.com/open_note?id=" + note.getId()));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
|
||||
views.setOnClickPendingIntent(R.id.open_note, pendingIntent);
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor
|
||||
final Intent fillInIntent = new Intent();
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString(ReminderViewsService.OpenReminderId, reminder.getId());
|
||||
fillInIntent.setData(Uri.parse("https://notesnook.com/open_reminder?id=" + reminder.getId()));
|
||||
fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId()));
|
||||
fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder");
|
||||
fillInIntent.putExtras(extras);
|
||||
views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent);
|
||||
|
||||
@@ -43,7 +43,7 @@ public class ReminderWidgetProvider extends AppWidgetProvider {
|
||||
new_reminder_intent.putExtra(NewReminder, NewReminder);
|
||||
new_reminder_intent.setAction(Intent.ACTION_VIEW);
|
||||
new_reminder_intent.putExtra(RCTNNativeModule.IntentType, "NewReminder");
|
||||
new_reminder_intent.setData(Uri.parse("https://notesnook.com/new_reminder"));
|
||||
new_reminder_intent.setData(Uri.parse("https://app.notesnook.com/new_reminder"));
|
||||
PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
|
||||
views.setOnClickPendingIntent(R.id.add_button, pendingIntent2);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="10dp"
|
||||
android:id="@+id/reminder_item_btn">
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="10dp"
|
||||
android:id="@+id/reminder_item_btn">
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
android:text="Upcoming Reminders"/>
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="30dp"
|
||||
android:layout_width="35dp"
|
||||
android:id="@+id/add_button"
|
||||
android:layout_height="30dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:background="@drawable/ic_newnote" />
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
- Collapsible Headings
|
||||
- Notesnook Circle
|
||||
- Minor bug fixes and improvements
|
||||
- Bug fixes and minor improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -654,6 +654,7 @@
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Notesnook/Pods-Notesnook-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
@@ -665,6 +666,7 @@
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
@@ -821,6 +823,7 @@
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Notesnook-NotesnookTests/Pods-Notesnook-NotesnookTests-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
@@ -832,6 +835,7 @@
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
@@ -1091,7 +1095,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
CURRENT_PROJECT_VERSION = 2157;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1165,7 +1169,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
MARKETING_VERSION = 3.3.9;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1196,7 +1200,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
CURRENT_PROJECT_VERSION = 2157;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1270,7 +1274,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
MARKETING_VERSION = 3.3.9;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1429,7 +1433,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
CURRENT_PROJECT_VERSION = 2157;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1441,7 +1445,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
MARKETING_VERSION = 3.3.9;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1472,7 +1476,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
CURRENT_PROJECT_VERSION = 2157;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1485,7 +1489,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
MARKETING_VERSION = 3.3.9;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1515,7 +1519,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
CURRENT_PROJECT_VERSION = 2157;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1596,7 +1600,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
MARKETING_VERSION = 3.3.9;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1627,7 +1631,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
CURRENT_PROJECT_VERSION = 2157;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1709,7 +1713,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
MARKETING_VERSION = 3.3.9;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1786,7 +1790,10 @@
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
@@ -1851,7 +1858,10 @@
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>3B52.1</string>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
@@ -1273,11 +1273,11 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-keep-awake (1.3.1):
|
||||
- React-Core
|
||||
- react-native-mmkv-storage (0.11.2):
|
||||
- react-native-mmkv-storage (12.0.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- MMKV (~> 1.3.9)
|
||||
- MMKV (~> 1.3.14)
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
@@ -1372,7 +1372,7 @@ PODS:
|
||||
- react-native-screenguard (1.0.0):
|
||||
- React-Core
|
||||
- SDWebImage (~> 5.11.1)
|
||||
- react-native-share-extension (2.9.0):
|
||||
- react-native-share-extension (2.9.5):
|
||||
- React
|
||||
- react-native-sodium (1.6.5):
|
||||
- React
|
||||
@@ -1770,15 +1770,53 @@ PODS:
|
||||
- Yoga
|
||||
- RNIap (12.16.2):
|
||||
- React-Core
|
||||
- RNImageCropPicker (0.40.2):
|
||||
- RNImageCropPicker (0.51.1):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-RCTImage
|
||||
- RNImageCropPicker/QBImagePickerController (= 0.40.2)
|
||||
- TOCropViewController
|
||||
- RNImageCropPicker/QBImagePickerController (0.40.2):
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNImageCropPicker/QBImagePickerController (= 0.51.1)
|
||||
- TOCropViewController (~> 2.8.0)
|
||||
- Yoga
|
||||
- RNImageCropPicker/QBImagePickerController (0.51.1):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-RCTImage
|
||||
- TOCropViewController
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- TOCropViewController (~> 2.8.0)
|
||||
- Yoga
|
||||
- RNKeychain (4.0.5):
|
||||
- React
|
||||
- RNNotifee (7.4.9):
|
||||
@@ -1975,7 +2013,7 @@ PODS:
|
||||
- SwiftyRSA (1.7.0):
|
||||
- SwiftyRSA/ObjC (= 1.7.0)
|
||||
- SwiftyRSA/ObjC (1.7.0)
|
||||
- TOCropViewController (2.7.4)
|
||||
- TOCropViewController (2.8.0)
|
||||
- toolbar-android (0.2.1):
|
||||
- React
|
||||
- Yoga (0.0.0)
|
||||
@@ -2425,7 +2463,7 @@ SPEC CHECKSUMS:
|
||||
react-native-image-resizer: 24c5d06fae2176dc0caed4b6396e02befb44064a
|
||||
react-native-in-app-review: b3d1eed3d1596ebf6539804778272c4c65e4a400
|
||||
react-native-keep-awake: 03b74eebe4f2bb5e8478fc8f420651a92463b6f8
|
||||
react-native-mmkv-storage: 51c33c6f6678d67061059cce8189a57c3e47192e
|
||||
react-native-mmkv-storage: 935d661dc3913a41be70d041aa3ff860d6aa4873
|
||||
react-native-netinfo: 66c9ac0d0ae92a57a9ed99ab8a75429740700c73
|
||||
react-native-notification-sounds: ce106d58df0dd384bccbd2e84fb53accab7cc068
|
||||
react-native-orientation-locker: cc6f357b289a2e0dd2210fea0c52cb8e0727fdaa
|
||||
@@ -2434,7 +2472,7 @@ SPEC CHECKSUMS:
|
||||
react-native-quick-sqlite: 1bfc7f1e9acbe9a5aa5c4cc81712e9bde3ab7672
|
||||
react-native-safe-area-context: 9d72abf6d8473da73033b597090a80b709c0b2f1
|
||||
react-native-screenguard: 82437eeb0086a90b5e5d7e54130bb04fb406373e
|
||||
react-native-share-extension: bcb7e466390a9e50c742f4b1019d6f181aedd7ad
|
||||
react-native-share-extension: fdc6aaab51591a2d445df239c446aaa3a99658ec
|
||||
react-native-sodium: 285eec063e4232cb67347ef6a434b85e588d38cb
|
||||
react-native-theme-switch-animation: d90fe2de0d9e87a63cd6235d98cba6e7054e9a10
|
||||
react-native-webview: 079eca50edf657503318b66687dadfb903731aa8
|
||||
@@ -2479,7 +2517,7 @@ SPEC CHECKSUMS:
|
||||
RNFlashList: ff5a0b3113c4cda0eaf4b94df8572ccad3c40fd5
|
||||
RNGestureHandler: 92e89a04cd0d1c77f383a55d14c15e7f423f4c00
|
||||
RNIap: f94647b3a3dbd5fa08ad7ed847eb588aa5fe95fe
|
||||
RNImageCropPicker: 30d770b383d84e1067d82ea7b5ed4fff851bbbb2
|
||||
RNImageCropPicker: aa81bda9f887e20542b569938cde5ce50a4ec404
|
||||
RNKeychain: ffd0513e676445c637410b47249460cbf56bc9cb
|
||||
RNNotifee: dabf3cdd7bfd9340bb84358cc78f635af4bc80e2
|
||||
RNPrivacySnapshot: ccad3a548338c2f526bb7b1789af3fb0618b7d1d
|
||||
@@ -2495,7 +2533,7 @@ SPEC CHECKSUMS:
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6
|
||||
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
|
||||
TOCropViewController: 797deaf39c90e6e9ddd848d88817f6b9a8a09888
|
||||
toolbar-android: c426ed5bd3dcccfed20fd79533efc0d1ae0ef018
|
||||
Yoga: 31a098f74c16780569aebd614a0f37a907de0189
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -21,6 +21,7 @@
|
||||
"@react-navigation/native": "^6.0.10",
|
||||
"@react-navigation/native-stack": "6.6.2",
|
||||
"@sayem314/react-native-keep-awake": "^1.3.1",
|
||||
"react-native-image-crop-picker": "^0.51.1",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.77.2",
|
||||
"react-native-actions-shortcuts": "^1.0.1",
|
||||
@@ -48,7 +49,7 @@
|
||||
"react-native-navigation-bar-color": "2.0.2",
|
||||
"react-native-notification-sounds": "0.5.5",
|
||||
"@shopify/flash-list": "^1.8.0",
|
||||
"react-native-mmkv-storage": "^0.11.2",
|
||||
"react-native-mmkv-storage": "^12.0.0",
|
||||
"react-native-quick-sqlite": "^8.2.7",
|
||||
"react-native-svg": "^15.12.0",
|
||||
"react-native-webview": "^13.13.5",
|
||||
@@ -61,7 +62,6 @@
|
||||
"react-native-zip-archive": "6.0.9",
|
||||
"react-native-theme-switch-animation": "^0.6.0",
|
||||
"@ammarahmed/react-native-background-fetch": "^4.2.2",
|
||||
"react-native-image-crop-picker": "^0.40.2",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-screenguard": "^1.0.0",
|
||||
"@formatjs/intl-locale": "4.0.0",
|
||||
@@ -72,10 +72,10 @@
|
||||
"react-native-begin-background-task": "github:blockfirm/react-native-begin-background-task",
|
||||
"react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot",
|
||||
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
|
||||
"@ammarahmed/react-native-share-extension": "^2.9.0",
|
||||
"react-native-pager-view": "^6.5.1",
|
||||
"react-native-tab-view": "^4.0.2",
|
||||
"react-native-orientation-locker": "^1.7.0"
|
||||
"react-native-orientation-locker": "^1.7.0",
|
||||
"@ammarahmed/react-native-share-extension": "^2.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
|
||||
29
apps/mobile/package-lock.json
generated
29
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.5",
|
||||
"version": "3.3.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.5",
|
||||
"version": "3.3.7",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -28326,7 +28326,7 @@
|
||||
"@ammarahmed/react-native-background-fetch": "^4.2.2",
|
||||
"@ammarahmed/react-native-eventsource": "1.1.0",
|
||||
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
|
||||
"@ammarahmed/react-native-share-extension": "^2.9.0",
|
||||
"@ammarahmed/react-native-share-extension": "^2.9.5",
|
||||
"@ammarahmed/react-native-sodium": "^1.6.5",
|
||||
"@bam.tech/react-native-image-resizer": "3.0.11",
|
||||
"@callstack/repack": "~5.1.2",
|
||||
@@ -28361,10 +28361,10 @@
|
||||
"react-native-gzip": "1.1.0",
|
||||
"react-native-html-to-pdf-lite": "^0.9.1",
|
||||
"react-native-iap": "12.16.2",
|
||||
"react-native-image-crop-picker": "^0.40.2",
|
||||
"react-native-image-crop-picker": "^0.51.1",
|
||||
"react-native-in-app-review": "4.3.3",
|
||||
"react-native-keychain": "4.0.5",
|
||||
"react-native-mmkv-storage": "^0.11.2",
|
||||
"react-native-mmkv-storage": "^12.0.0",
|
||||
"react-native-modal-datetime-picker": "14.0.0",
|
||||
"react-native-navigation-bar-color": "2.0.2",
|
||||
"react-native-notification-sounds": "0.5.5",
|
||||
@@ -28472,9 +28472,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ammarahmed/react-native-share-extension": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.9.0.tgz",
|
||||
"integrity": "sha512-iEkpJQFZL5/WDPh7iRCLZREANgaaAHthDsPaAYT6sI4wieQcHZMBAnfRMwBEnC0BZ0WVxbQZdzJYez+dmwpr2g==",
|
||||
"version": "2.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.9.5.tgz",
|
||||
"integrity": "sha512-UTE8TSxkx+RFNgSJCCfD5FSdQ5IuMOsZzuuy0BVy46emRrpPw1fedJTtDbqyKrU8fwtKEIbzJWeQtjjRZlHxjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-native": "^0.63.1"
|
||||
@@ -44519,10 +44519,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-image-crop-picker": {
|
||||
"version": "0.40.2",
|
||||
"version": "0.51.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-crop-picker/-/react-native-image-crop-picker-0.51.1.tgz",
|
||||
"integrity": "sha512-GIFRyXJgv1dPceKd/hraK9q9V38v45rSg2ONR6RiSePcOJemkHpc/PMU86pq6lWPilDYHSxbZmea2pNMk85ayw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react-native": ">=0.40.0"
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-image-pan-zoom": {
|
||||
@@ -44590,9 +44593,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-mmkv-storage": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-0.11.2.tgz",
|
||||
"integrity": "sha512-/jbYNOUrwgVU09WyXDK6lFGXqBs+23oR9X37z3N68rwHNiXF5WDyXnT38dU2tF07ZlvmsobNHgdxgTu4kGQUKQ==",
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-12.0.0.tgz",
|
||||
"integrity": "sha512-sssZInILQBquytDDfjosjEdvevwMPk3fqQQjdjfnH362IcsBsApvtT8yJS406Mz9aJ6VhqVsZw/awltsUuPSYg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mmkv-link": "autolink/postlink/run.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.5",
|
||||
"version": "3.3.9",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -56,4 +56,4 @@
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.77.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
|
||||
index 5de0845..1b158d8 100644
|
||||
--- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
|
||||
+++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
|
||||
@@ -692,6 +692,7 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
|
||||
image.putString("mime", options.outMimeType);
|
||||
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java
|
||||
index b9c494e..34ef800 100644
|
||||
--- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java
|
||||
+++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java
|
||||
@@ -679,6 +679,7 @@ class ImageCropPicker implements ActivityEventListener {
|
||||
image.putInt("size", (int) new File(compressedImagePath).length());
|
||||
image.putString("modificationDate", String.valueOf(modificationDate));
|
||||
image.putString("filename", new File(path).getName());
|
||||
+ image.putString("sourceURL", path);
|
||||
|
||||
if (includeBase64) {
|
||||
image.putString("data", getBase64StringFromFile(compressedImagePath));
|
||||
diff --git a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
|
||||
index 9f20973..5e14da8 100644
|
||||
--- a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
|
||||
+++ b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
|
||||
@@ -595,8 +595,10 @@ - (void)qb_imagePickerController:
|
||||
NSString *mimeType = [self determineMimeTypeFromImageData:imageData];
|
||||
diff --git a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.mm b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.mm
|
||||
index 354ad09..622f779 100644
|
||||
--- a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.mm
|
||||
+++ b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.mm
|
||||
@@ -603,7 +603,10 @@ - (void)qb_imagePickerController:(QBImagePickerController *)imagePickerControlle
|
||||
Boolean isKnownMimeType = [mimeType length] > 0;
|
||||
|
||||
+ Boolean isHeicOrHeif = [mimeType isEqualToString:@"image/heic"] || [mimeType isEqualToString:@"image/heif"];
|
||||
+
|
||||
ImageResult *imageResult = [[ImageResult alloc] init];
|
||||
- if (isLossless && useOriginalWidth && useOriginalHeight && isKnownMimeType && !forceJpg) {
|
||||
+
|
||||
+ Boolean isHeicOrHeif = [mimeType isEqualToString:@"image/heic"] || [mimeType isEqualToString:@"image/heif"];
|
||||
+
|
||||
+ if (isLossless && useOriginalWidth && useOriginalHeight && isKnownMimeType && !forceJpg && !isHeicOrHeif) {
|
||||
// Use original, unmodified image
|
||||
imageResult.data = imageData;
|
||||
169
apps/mobile/patches/react-native-keychain+4.0.5.patch
Normal file
169
apps/mobile/patches/react-native-keychain+4.0.5.patch
Normal file
@@ -0,0 +1,169 @@
|
||||
diff --git a/node_modules/react-native-keychain/android/build.gradle b/node_modules/react-native-keychain/android/build.gradle
|
||||
index bd2fe04..53ce4b1 100755
|
||||
--- a/node_modules/react-native-keychain/android/build.gradle
|
||||
+++ b/node_modules/react-native-keychain/android/build.gradle
|
||||
@@ -51,5 +51,4 @@ dependencies {
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation 'com.facebook.react:react-native:+' // From node_modules
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
- implementation 'com.facebook.conceal:conceal:1.1.3@aar'
|
||||
}
|
||||
diff --git a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
|
||||
index 61d061e..be49f8a 100644
|
||||
--- a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
|
||||
+++ b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
|
||||
@@ -15,7 +15,6 @@ import com.oblador.keychain.PrefsStorage.ResultSet;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorage;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorage.DecryptionResult;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorage.EncryptionResult;
|
||||
-import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAESCBC;
|
||||
import com.oblador.keychain.exceptions.CryptoFailedException;
|
||||
import com.oblador.keychain.exceptions.EmptyParameterException;
|
||||
@@ -48,7 +47,6 @@ public class KeychainModule extends ReactContextBaseJavaModule {
|
||||
super(reactContext);
|
||||
prefsStorage = new PrefsStorage(reactContext);
|
||||
|
||||
- addCipherStorageToMap(new CipherStorageFacebookConceal(reactContext));
|
||||
addCipherStorageToMap(new CipherStorageKeystoreAESCBC());
|
||||
}
|
||||
|
||||
diff --git a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/PrefsStorage.java b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/PrefsStorage.java
|
||||
index 99448b1..792c24c 100644
|
||||
--- a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/PrefsStorage.java
|
||||
+++ b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/PrefsStorage.java
|
||||
@@ -7,7 +7,6 @@ import android.util.Base64;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.oblador.keychain.cipherStorage.CipherStorage.EncryptionResult;
|
||||
-import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal;
|
||||
|
||||
public class PrefsStorage {
|
||||
public static final String KEYCHAIN_DATA = "RN_KEYCHAIN";
|
||||
@@ -35,10 +34,6 @@ public class PrefsStorage {
|
||||
byte[] bytesForPassword = getBytesForPassword(service);
|
||||
String cipherStorageName = getCipherStorageName(service);
|
||||
if (bytesForUsername != null && bytesForPassword != null) {
|
||||
- if (cipherStorageName == null) {
|
||||
- // If the CipherStorage name is not found, we assume it is because the entry was written by an older version of this library. The older version used Facebook Conceal, so we default to that.
|
||||
- cipherStorageName = CipherStorageFacebookConceal.CIPHER_STORAGE_NAME;
|
||||
- }
|
||||
return new ResultSet(cipherStorageName, bytesForUsername, bytesForPassword);
|
||||
}
|
||||
return null;
|
||||
diff --git a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java
|
||||
deleted file mode 100644
|
||||
index 3162f99..0000000
|
||||
--- a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java
|
||||
+++ /dev/null
|
||||
@@ -1,110 +0,0 @@
|
||||
-package com.oblador.keychain.cipherStorage;
|
||||
-
|
||||
-import android.os.Build;
|
||||
-import androidx.annotation.NonNull;
|
||||
-
|
||||
-import com.facebook.android.crypto.keychain.AndroidConceal;
|
||||
-import com.facebook.android.crypto.keychain.SharedPrefsBackedKeyChain;
|
||||
-import com.facebook.crypto.Crypto;
|
||||
-import com.facebook.crypto.CryptoConfig;
|
||||
-import com.facebook.crypto.Entity;
|
||||
-import com.facebook.crypto.keychain.KeyChain;
|
||||
-import com.facebook.react.bridge.ReactApplicationContext;
|
||||
-import com.oblador.keychain.SecurityLevel;
|
||||
-import com.oblador.keychain.exceptions.CryptoFailedException;
|
||||
-
|
||||
-import java.nio.charset.Charset;
|
||||
-
|
||||
-public class CipherStorageFacebookConceal implements CipherStorage {
|
||||
- public static final String CIPHER_STORAGE_NAME = "FacebookConceal";
|
||||
- public static final String KEYCHAIN_DATA = "RN_KEYCHAIN";
|
||||
- private final Crypto crypto;
|
||||
-
|
||||
- public CipherStorageFacebookConceal(ReactApplicationContext reactContext) {
|
||||
- KeyChain keyChain = new SharedPrefsBackedKeyChain(reactContext, CryptoConfig.KEY_256);
|
||||
- this.crypto = AndroidConceal.get().createDefaultCrypto(keyChain);
|
||||
- }
|
||||
-
|
||||
- @Override
|
||||
- public String getCipherStorageName() {
|
||||
- return CIPHER_STORAGE_NAME;
|
||||
- }
|
||||
-
|
||||
- @Override
|
||||
- public int getMinSupportedApiLevel() {
|
||||
- return Build.VERSION_CODES.JELLY_BEAN;
|
||||
- }
|
||||
-
|
||||
- @Override
|
||||
- public SecurityLevel securityLevel() {
|
||||
- return SecurityLevel.ANY;
|
||||
- }
|
||||
-
|
||||
- @Override
|
||||
- public boolean supportsSecureHardware() {
|
||||
- return false;
|
||||
- }
|
||||
-
|
||||
- @Override
|
||||
- public EncryptionResult encrypt(@NonNull String service, @NonNull String username, @NonNull String password, SecurityLevel level) throws CryptoFailedException {
|
||||
-
|
||||
- if (!this.securityLevel().satisfiesSafetyThreshold(level)) {
|
||||
- throw new CryptoFailedException(String.format("Insufficient security level (wants %s; got %s)", level, this.securityLevel()));
|
||||
- }
|
||||
-
|
||||
- if (!crypto.isAvailable()) {
|
||||
- throw new CryptoFailedException("Crypto is missing");
|
||||
- }
|
||||
- Entity usernameEntity = createUsernameEntity(service);
|
||||
- Entity passwordEntity = createPasswordEntity(service);
|
||||
-
|
||||
- try {
|
||||
- byte[] encryptedUsername = crypto.encrypt(username.getBytes(Charset.forName("UTF-8")), usernameEntity);
|
||||
- byte[] encryptedPassword = crypto.encrypt(password.getBytes(Charset.forName("UTF-8")), passwordEntity);
|
||||
-
|
||||
- return new EncryptionResult(encryptedUsername, encryptedPassword, this);
|
||||
- } catch (Exception e) {
|
||||
- throw new CryptoFailedException("Encryption failed for service " + service, e);
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- @Override
|
||||
- public DecryptionResult decrypt(@NonNull String service, @NonNull byte[] username, @NonNull byte[] password) throws CryptoFailedException {
|
||||
- if (!crypto.isAvailable()) {
|
||||
- throw new CryptoFailedException("Crypto is missing");
|
||||
- }
|
||||
- Entity usernameEntity = createUsernameEntity(service);
|
||||
- Entity passwordEntity = createPasswordEntity(service);
|
||||
-
|
||||
- try {
|
||||
- byte[] decryptedUsername = crypto.decrypt(username, usernameEntity);
|
||||
- byte[] decryptedPassword = crypto.decrypt(password, passwordEntity);
|
||||
-
|
||||
- return new DecryptionResult(
|
||||
- new String(decryptedUsername, Charset.forName("UTF-8")),
|
||||
- new String(decryptedPassword, Charset.forName("UTF-8")),
|
||||
- SecurityLevel.ANY);
|
||||
- } catch (Exception e) {
|
||||
- throw new CryptoFailedException("Decryption failed for service " + service, e);
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- @Override
|
||||
- public void removeKey(@NonNull String service) {
|
||||
- // Facebook Conceal stores only one key across all services, so we cannot delete the key (otherwise decryption will fail for encrypted data of other services).
|
||||
- }
|
||||
-
|
||||
- private Entity createUsernameEntity(String service) {
|
||||
- String prefix = getEntityPrefix(service);
|
||||
- return Entity.create(prefix + "user");
|
||||
- }
|
||||
-
|
||||
- private Entity createPasswordEntity(String service) {
|
||||
- String prefix = getEntityPrefix(service);
|
||||
- return Entity.create(prefix + "pass");
|
||||
- }
|
||||
-
|
||||
- private String getEntityPrefix(String service) {
|
||||
- return KEYCHAIN_DATA + ":" + service;
|
||||
- }
|
||||
-}
|
||||
@@ -1,14 +0,0 @@
|
||||
diff --git a/node_modules/react-native-mmkv-storage/android/build.gradle b/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
index abb696e..ba35baa 100644
|
||||
--- a/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
+++ b/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
@@ -98,7 +98,8 @@ android {
|
||||
cmake {
|
||||
cppFlags "-O3 -frtti -fexceptions -Wall -fstack-protector-all"
|
||||
arguments "-DANDROID_STL=c++_shared",
|
||||
- "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}"
|
||||
+ "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}",
|
||||
+ "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||
abiFilters (*reactNativeArchitectures())
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import RNFetchBlob from "react-native-blob-util";
|
||||
import WebView from "react-native-webview";
|
||||
import { Config } from "./store";
|
||||
import { db } from "../app/common/database";
|
||||
import { SUBSCRIPTION_STATUS } from "../app/utils/constants";
|
||||
|
||||
export const fetchHandle = createRef();
|
||||
export const HtmlLoadingWebViewAgent = React.memo(
|
||||
@@ -40,6 +39,7 @@ export const HtmlLoadingWebViewAgent = React.memo(
|
||||
const webview = useRef();
|
||||
const premium = useRef(false);
|
||||
const corsProxy = Config.corsProxy;
|
||||
const [isLoggedIn, setIsLoggedIn] = useState();
|
||||
|
||||
useImperativeHandle(
|
||||
fetchHandle,
|
||||
@@ -72,11 +72,8 @@ export const HtmlLoadingWebViewAgent = React.memo(
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const user = await db.user.getUser();
|
||||
const subscriptionStatus =
|
||||
user?.subscription?.type || SUBSCRIPTION_STATUS.BASIC;
|
||||
premium.current =
|
||||
user && subscriptionStatus !== SUBSCRIPTION_STATUS.BASIC;
|
||||
|
||||
setIsLoggedIn(!!user);
|
||||
console.log("USER", !!user);
|
||||
const clipperPath =
|
||||
Platform.OS === "ios"
|
||||
? RNFetchBlob.fs.dirs.MainBundleDir +
|
||||
@@ -91,6 +88,7 @@ export const HtmlLoadingWebViewAgent = React.memo(
|
||||
.catch((e) => console.log(e));
|
||||
})();
|
||||
}, []);
|
||||
console.log(isLoggedIn);
|
||||
|
||||
return !source || !clipper ? null : (
|
||||
<WebView
|
||||
@@ -103,7 +101,8 @@ export const HtmlLoadingWebViewAgent = React.memo(
|
||||
height: 100,
|
||||
position: "absolute",
|
||||
opacity: 0,
|
||||
zIndex: -1
|
||||
zIndex: -1,
|
||||
pointerEvents: "none"
|
||||
}}
|
||||
useSharedProcessPool={false}
|
||||
pointerEvents="none"
|
||||
@@ -123,7 +122,11 @@ export const HtmlLoadingWebViewAgent = React.memo(
|
||||
console.log("Error handling webview message", e);
|
||||
}
|
||||
}}
|
||||
injectedJavaScriptBeforeContentLoaded={script(clipper, premium.current)}
|
||||
injectedJavaScriptBeforeContentLoaded={script(
|
||||
clipper,
|
||||
corsProxy,
|
||||
isLoggedIn
|
||||
)}
|
||||
onError={() => {
|
||||
console.log("Error loading page");
|
||||
loadHandler.current?.();
|
||||
@@ -137,9 +140,11 @@ export const HtmlLoadingWebViewAgent = React.memo(
|
||||
() => true
|
||||
);
|
||||
|
||||
const script = (clipper, pro) => `
|
||||
const script = (clipper, corsProxy, loggedIn) => `
|
||||
globalThis.module = {};
|
||||
${clipper}
|
||||
|
||||
|
||||
function postMessage(type, value) {
|
||||
if (window.ReactNativeWebView) {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
@@ -158,10 +163,10 @@ function postMessage(type, value) {
|
||||
postMessage("error", globalThis.Clipper.clipPage);
|
||||
} else {
|
||||
globalThis.Clipper.clipPage(document,false, {
|
||||
images: ${pro},
|
||||
images: ${loggedIn ? "true" : "false"},
|
||||
inlineImages: false,
|
||||
styles: false,
|
||||
corsProxy: undefined
|
||||
corsProxy: ${corsProxy ? `"${corsProxy}"` : "undefined"}
|
||||
}).then(result => {
|
||||
postMessage("html", result);
|
||||
}).catch(e => {
|
||||
|
||||
@@ -267,7 +267,6 @@ const ShareView = () => {
|
||||
);
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
console.log(noteContent.current, "current...");
|
||||
eSendEvent(eOnLoadNote + "shareEditor", {
|
||||
id: null,
|
||||
content: {
|
||||
@@ -747,7 +746,7 @@ const ShareView = () => {
|
||||
</Paragraph>
|
||||
{rawData.value && isURL(rawData.value) ? (
|
||||
<Button
|
||||
type={mode === 2 ? "inverted" : "transparent"}
|
||||
type={mode === 2 ? "inverted" : "plain"}
|
||||
icon={mode === 2 ? "radiobox-marked" : "radiobox-blank"}
|
||||
onPress={() => changeMode(2)}
|
||||
title={modes[2].title}
|
||||
@@ -758,9 +757,9 @@ const ShareView = () => {
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
type={mode === 1 ? "inverted" : "transparent"}
|
||||
type={mode === 1 ? "inverted" : "plain"}
|
||||
icon={mode === 1 ? "radiobox-marked" : "radiobox-blank"}
|
||||
onPress={() => changeMode(2)}
|
||||
onPress={() => changeMode(1)}
|
||||
title={modes[1].title}
|
||||
height={30}
|
||||
style={{
|
||||
|
||||
@@ -68,11 +68,10 @@ export const useShareStore = create((set) => ({
|
||||
let appendNote = MMKV.getString(StorageKeys.appendNote);
|
||||
let selectedNotebooks = MMKV.getString(StorageKeys.selectedNotebooks);
|
||||
let selectedTags = MMKV.getString(StorageKeys.selectedTag);
|
||||
appendNote = JSON.parse(appendNote);
|
||||
set({
|
||||
appendNote: appendNote,
|
||||
selectedNotebooks: selectedNotebooks ? JSON.parse(selectedNotebooks) : [],
|
||||
selectedTag: selectedTags ? JSON.parse(selectedTags) : []
|
||||
selectedTags: selectedTags ? JSON.parse(selectedTags) : []
|
||||
});
|
||||
},
|
||||
selectedTags: [],
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
"@notesnook/theme": ["../../packages/theme"],
|
||||
"@types/react": ["./node_modules/@types/react"],
|
||||
"react": ["./node_modules/react"]
|
||||
},
|
||||
"incremental": true,
|
||||
"maxNodeModuleJsDepth": 5,
|
||||
"downlevelIteration": true
|
||||
}
|
||||
},
|
||||
"exclude": ["native", "e2e"]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
apps/monograph/package-lock.json
generated
2
apps/monograph/package-lock.json
generated
@@ -141,7 +141,7 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook-importer/core": "^2.2.2",
|
||||
"@notesnook-importer/core": "^2.2.5",
|
||||
"@notesnook/common": "file:../common",
|
||||
"@notesnook/intl": "file:../intl",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
|
||||
4
apps/theme-builder/package-lock.json
generated
4
apps/theme-builder/package-lock.json
generated
@@ -987,7 +987,7 @@
|
||||
},
|
||||
"../web": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -1001,7 +1001,7 @@
|
||||
"@lingui/react": "5.1.2",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/react": "1.6.1",
|
||||
"@notesnook-importer/core": "^2.2.2",
|
||||
"@notesnook-importer/core": "^2.2.5",
|
||||
"@notesnook/common": "file:../../packages/common",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
|
||||
1410
apps/web/package-lock.json
generated
1410
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.2",
|
||||
"version": "3.3.5",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
useStore as useAppStore,
|
||||
store as appstore
|
||||
} from "../../stores/app-store";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { useStore as useSearchStore } from "../../stores/search-store";
|
||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
import { FlexScrollContainer } from "../scroll-container";
|
||||
@@ -77,6 +78,7 @@ import { Pane, SplitPane } from "../split-pane";
|
||||
import { TITLE_BAR_HEIGHT } from "../title-bar";
|
||||
import { isMobile } from "../../hooks/use-mobile";
|
||||
import { isTablet } from "../../hooks/use-tablet";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
|
||||
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
||||
|
||||
@@ -604,6 +606,15 @@ export function Editor(props: EditorProps) {
|
||||
}
|
||||
}}
|
||||
onInsertAttachment={async (type) => {
|
||||
if (!useUserStore.getState().isLoggedIn) {
|
||||
ConfirmDialog.show({
|
||||
title: strings.notLoggedIn(),
|
||||
message: strings.loginToUploadAttachments(),
|
||||
positiveButtonText: strings.okay()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const mime = type === "file" ? "*/*" : "image/*";
|
||||
const attachments = await insertAttachments(mime);
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
import { IEditor, MAX_AUTO_SAVEABLE_WORDS } from "./types";
|
||||
import { useEditorConfig, useToolbarConfig, useEditorManager } from "./manager";
|
||||
import { useStore as useSettingsStore } from "../../stores/setting-store";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { debounce, useAreFeaturesAvailable } from "@notesnook/common";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
@@ -65,6 +66,8 @@ import { EDITOR_ZOOM } from "./common";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { showFeatureNotAllowedToast } from "../../common/toasts";
|
||||
import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export type OnChangeHandler = (
|
||||
content: () => string,
|
||||
@@ -185,7 +188,6 @@ function TipTap(props: TipTapProps) {
|
||||
|
||||
const autoSave = useRef(true);
|
||||
const { toolbarConfig } = useToolbarConfig();
|
||||
|
||||
const features = useAreFeaturesAvailable([
|
||||
"callout",
|
||||
"outlineList",
|
||||
@@ -196,9 +198,19 @@ function TipTap(props: TipTapProps) {
|
||||
claims: {
|
||||
callout: !!features?.callout?.isAllowed,
|
||||
outlineList: !!features?.outlineList?.isAllowed,
|
||||
taskList: !!features?.taskList?.isAllowed
|
||||
taskList: !!features?.taskList?.isAllowed,
|
||||
insertAttachment: !!useUserStore.getState().isLoggedIn
|
||||
},
|
||||
onPermissionDenied: (claim, silent) => {
|
||||
if (claim === "insertAttachment") {
|
||||
ConfirmDialog.show({
|
||||
title: strings.notLoggedIn(),
|
||||
message: strings.loginToUploadAttachments(),
|
||||
positiveButtonText: strings.okay()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (silent) {
|
||||
console.log(features, features?.[claim]);
|
||||
if (features?.[claim]) showFeatureNotAllowedToast(features[claim]);
|
||||
|
||||
@@ -82,6 +82,7 @@ import { mdToHtml } from "../../utils/md";
|
||||
import { InboxSettings } from "./inbox-settings";
|
||||
import { withFeatureCheck } from "../../common";
|
||||
import { NotesnookCircleSettings } from "./notesnook-circle-settings";
|
||||
import { hashNavigate } from "../../navigation";
|
||||
|
||||
type SettingsDialogProps = BaseDialogProps<false> & {
|
||||
activeSection?: SectionKeys;
|
||||
@@ -271,6 +272,12 @@ function SettingsSideBar(props: SettingsSideBarProps) {
|
||||
const [route, setRoute] = useState<SectionKeys>(activeSection || "profile");
|
||||
useUserStore((store) => store.isLoggedIn);
|
||||
|
||||
useEffect(() => {
|
||||
hashNavigate(`/settings/${route}`, {
|
||||
notify: false
|
||||
});
|
||||
}, [route, activeSection]);
|
||||
|
||||
return (
|
||||
<FlexScrollContainer
|
||||
id="settings-side-menu"
|
||||
|
||||
@@ -20,29 +20,32 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { FeatureId } from "@notesnook/common";
|
||||
import { Icon } from "../../components/icons";
|
||||
|
||||
export type SectionKeys =
|
||||
| "profile"
|
||||
| "auth"
|
||||
| "subscription"
|
||||
| "sync"
|
||||
| "appearance"
|
||||
| "behaviour"
|
||||
| "desktop"
|
||||
| "notifications"
|
||||
| "servers"
|
||||
| "editor"
|
||||
| "backup-export"
|
||||
| "export"
|
||||
| "importer"
|
||||
| "vault"
|
||||
| "app-lock"
|
||||
| "privacy"
|
||||
| "support"
|
||||
| "legal"
|
||||
| "developer"
|
||||
| "about"
|
||||
| "inbox"
|
||||
| "circle";
|
||||
const SectionKeys = [
|
||||
"profile",
|
||||
"auth",
|
||||
"subscription",
|
||||
"sync",
|
||||
"appearance",
|
||||
"behaviour",
|
||||
"desktop",
|
||||
"notifications",
|
||||
"servers",
|
||||
"editor",
|
||||
"backup-export",
|
||||
"export",
|
||||
"importer",
|
||||
"vault",
|
||||
"app-lock",
|
||||
"privacy",
|
||||
"support",
|
||||
"legal",
|
||||
"developer",
|
||||
"about",
|
||||
"inbox",
|
||||
"circle"
|
||||
] as const;
|
||||
|
||||
export type SectionKeys = (typeof SectionKeys)[number];
|
||||
|
||||
export type SectionGroupKeys =
|
||||
| "account"
|
||||
@@ -153,3 +156,7 @@ export type TextInputSettingComponent = BaseSettingComponent<"input"> & {
|
||||
export type CustomSettingComponent = BaseSettingComponent<"custom"> & {
|
||||
component: () => JSX.Element | null;
|
||||
};
|
||||
|
||||
export function isSectionKey(key: string): key is SectionKeys {
|
||||
return SectionKeys.includes(key as SectionKeys);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { FeatureDialog } from "../dialogs/feature-dialog";
|
||||
import { CreateTagDialog } from "../dialogs/item-dialog";
|
||||
import { OnboardingDialog } from "../dialogs/onboarding-dialog";
|
||||
import { isSectionKey, SectionKeys } from "../dialogs/settings/types";
|
||||
|
||||
const hashroutes = defineHashRoutes({
|
||||
"/": () => {},
|
||||
@@ -68,6 +69,11 @@ const hashroutes = defineHashRoutes({
|
||||
},
|
||||
"/settings": () => {
|
||||
SettingsDialog.show({}).then(afterAction);
|
||||
},
|
||||
"/settings/:section": ({ section }) => {
|
||||
SettingsDialog.show(
|
||||
isSectionKey(section) ? { activeSection: section as SectionKeys } : {}
|
||||
).then(afterAction);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -73,7 +73,11 @@ export class WebExtensionServer implements Server {
|
||||
async saveClip(clip: Clip) {
|
||||
let clipContent = "";
|
||||
|
||||
if (clip.mode === "simplified" || clip.mode === "screenshot") {
|
||||
if (
|
||||
clip.mode === "simplified" ||
|
||||
clip.mode === "screenshot" ||
|
||||
clip.mode === "bookmark"
|
||||
) {
|
||||
clipContent += clip.data;
|
||||
} else {
|
||||
const clippedFile = new File(
|
||||
@@ -105,11 +109,17 @@ export class WebExtensionServer implements Server {
|
||||
if (isCipher(content)) return;
|
||||
|
||||
content += clipContent;
|
||||
content += h("div", [
|
||||
h("hr"),
|
||||
h("p", ["Clipped from ", h("a", [clip.title], { href: clip.url })]),
|
||||
h("p", [`Date clipped: ${getFormattedDate(Date.now())}`])
|
||||
]).innerHTML;
|
||||
content +=
|
||||
clip.mode === "bookmark"
|
||||
? h("div", [
|
||||
h("p", [`Date bookmarked: ${getFormattedDate(Date.now())}`]),
|
||||
h("hr")
|
||||
]).innerHTML
|
||||
: h("div", [
|
||||
h("hr"),
|
||||
h("p", ["Clipped from ", h("a", [clip.title], { href: clip.url })]),
|
||||
h("p", [`Date clipped: ${getFormattedDate(Date.now())}`])
|
||||
]).innerHTML;
|
||||
|
||||
const id = await db.notes.add({
|
||||
id: note?.id,
|
||||
|
||||
8
docs/help/contents/faqs/login-to-upload-attachments.md
Normal file
8
docs/help/contents/faqs/login-to-upload-attachments.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Login to upload attachments
|
||||
description: We require users to be logged in to upload attachments.
|
||||
---
|
||||
|
||||
# Login to upload attachments
|
||||
|
||||
We require users to be logged in to upload attachments. This is because attachments are encrypted using a sub-key derived from your database encryption key. Without a login, we cannot encrypt/upload/sync attachments.
|
||||
@@ -73,6 +73,10 @@ The `Selected nodes` mode allows you to select exactly which nodes you want to c
|
||||
|
||||
The clipping mode controls how the final clip should look.
|
||||
|
||||
### Bookmark
|
||||
|
||||
`Bookmark` mode saves only the URL of the page along with the title. It is best suited to save pages for later reading/reference.
|
||||
|
||||
### Simplified
|
||||
|
||||
`Simplified` mode doesn't include any styles. It is best suited for long-form content such as articles & blogs. All clips in `Simplified` mode are saved directly as is i.e. they do not appear as web clip embeds in the Notesnook editor.
|
||||
|
||||
@@ -84,3 +84,4 @@ navigation:
|
||||
children:
|
||||
- path: faqs/what-are-merge-conflicts.md
|
||||
- path: faqs/is-there-an-eta.md
|
||||
- paht: faqs/login-to-upload-attachments.md
|
||||
|
||||
656
extensions/web-clipper/package-lock.json
generated
656
extensions/web-clipper/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/web-clipper",
|
||||
"version": "0.3.4",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
@@ -105,7 +105,7 @@
|
||||
"style-loader": "^3.3.1",
|
||||
"terser-webpack-plugin": "^5.3.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"web-ext": "^7.6.2",
|
||||
"web-ext": "^7.12.0",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ThemeDefinition } from "@notesnook/theme";
|
||||
|
||||
export type ClipArea = "full-page" | "visible" | "selection" | "article";
|
||||
|
||||
export type ClipMode = "simplified" | "screenshot" | "complete";
|
||||
export type ClipMode = "bookmark" | "simplified" | "screenshot" | "complete";
|
||||
|
||||
export type User = {
|
||||
email?: string;
|
||||
|
||||
@@ -57,6 +57,7 @@ export const Icons = {
|
||||
visible: mdiViewDayOutline,
|
||||
selection: mdiCursorDefaultClickOutline,
|
||||
|
||||
bookmark: mdiBookmarkOutline,
|
||||
simplified: mdiTextBoxOutline,
|
||||
screenshot: mdiFitToScreenOutline,
|
||||
complete: mdiViewDashboardOutline,
|
||||
|
||||
@@ -68,6 +68,11 @@ const clipAreas: { name: string; id: ClipArea; icon: string }[] = [
|
||||
|
||||
const clipModes: { name: string; id: ClipMode; icon: string; pro?: boolean }[] =
|
||||
[
|
||||
{
|
||||
name: "Bookmark",
|
||||
id: "bookmark",
|
||||
icon: Icons.bookmark
|
||||
},
|
||||
{
|
||||
name: "Simplified",
|
||||
id: "simplified",
|
||||
@@ -87,6 +92,20 @@ const clipModes: { name: string; id: ClipMode; icon: string; pro?: boolean }[] =
|
||||
}
|
||||
];
|
||||
|
||||
enum ClipperState {
|
||||
Idle = "idle",
|
||||
Clipping = "clipping",
|
||||
Clipped = "clipped",
|
||||
Error = "error"
|
||||
}
|
||||
|
||||
const clipperButtonLabelMap: Record<ClipperState, string> = {
|
||||
[ClipperState.Clipping]: "Clipping...",
|
||||
[ClipperState.Clipped]: "Save clip",
|
||||
[ClipperState.Error]: "Retry Clip",
|
||||
[ClipperState.Idle]: "Start clip"
|
||||
};
|
||||
|
||||
export function Main() {
|
||||
const [error, setError] = useState<string>();
|
||||
// const [colorMode, setColorMode] = useColorMode();
|
||||
@@ -98,7 +117,6 @@ export function Main() {
|
||||
const [title, setTitle] = useState<string>();
|
||||
const [hasPermission, setHasPermission] = useState<boolean>(false);
|
||||
const [url, setUrl] = useState<string>();
|
||||
const [clipNonce, setClipNonce] = useState(0);
|
||||
const [clipMode, setClipMode] = usePersistentState<ClipMode>(
|
||||
"clipMode",
|
||||
"simplified"
|
||||
@@ -107,10 +125,12 @@ export function Main() {
|
||||
"clipArea",
|
||||
"article"
|
||||
);
|
||||
const [isClipping, setIsClipping] = useState(false);
|
||||
const [note, setNote] = usePersistentState<ItemReference>("note");
|
||||
const [refs, setRefs] = usePersistentState<SelectedReference[]>("refs", []);
|
||||
const [clipData, setClipData] = useState<ClipData>();
|
||||
const [clipperState, setClipperState] = useState<ClipperState>(
|
||||
ClipperState.Idle
|
||||
);
|
||||
const pageTitle = useRef<string>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -129,7 +149,6 @@ export function Main() {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!clipArea || !clipMode) return;
|
||||
if (
|
||||
!isPremium &&
|
||||
(clipMode === "complete" || clipMode === "screenshot")
|
||||
@@ -137,25 +156,8 @@ export function Main() {
|
||||
setClipMode("simplified");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsClipping(true);
|
||||
setClipData(
|
||||
await clip(clipArea, clipMode, {
|
||||
...DEFAULT_SETTINGS,
|
||||
...settings,
|
||||
images: isPremium,
|
||||
inlineImages: isPremium
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error) setError(e.message);
|
||||
} finally {
|
||||
setIsClipping(false);
|
||||
}
|
||||
})();
|
||||
}, [isPremium, clipArea, clipMode, clipNonce]);
|
||||
}, [isPremium, clipArea, clipMode]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -168,6 +170,32 @@ export function Main() {
|
||||
})();
|
||||
}, [settings]);
|
||||
|
||||
async function startClip() {
|
||||
if (!clipArea || !clipMode || clipMode === "bookmark") return;
|
||||
|
||||
try {
|
||||
setError(undefined);
|
||||
setClipperState(ClipperState.Clipping);
|
||||
setClipData(
|
||||
await clip(clipArea, clipMode, {
|
||||
...DEFAULT_SETTINGS,
|
||||
...settings,
|
||||
images: isPremium,
|
||||
inlineImages: isPremium
|
||||
})
|
||||
);
|
||||
setClipperState(ClipperState.Clipped);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
}
|
||||
setClipperState(ClipperState.Error);
|
||||
}
|
||||
}
|
||||
|
||||
const isClipping = clipperState === ClipperState.Clipping;
|
||||
|
||||
if (!hasPermission && !!settings?.corsProxy) {
|
||||
return (
|
||||
<FlexScrollContainer style={{ maxHeight: 560 }}>
|
||||
@@ -241,10 +269,15 @@ export function Main() {
|
||||
key={item.id}
|
||||
variant="icon"
|
||||
onClick={() => {
|
||||
setError(undefined);
|
||||
setClipperState(ClipperState.Idle);
|
||||
setClipArea(item.id);
|
||||
setClipNonce((s) => ++s);
|
||||
}}
|
||||
disabled={isClipping}
|
||||
disabled={
|
||||
isClipping ||
|
||||
clipperState === ClipperState.Clipped ||
|
||||
clipMode === "bookmark"
|
||||
}
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderRadius: "default",
|
||||
@@ -283,10 +316,15 @@ export function Main() {
|
||||
key={item.id}
|
||||
variant="icon"
|
||||
onClick={() => {
|
||||
setError(undefined);
|
||||
setClipperState(ClipperState.Idle);
|
||||
setClipMode(item.id);
|
||||
setClipNonce((s) => ++s);
|
||||
}}
|
||||
disabled={isClipping || (item.pro && !isPremium)}
|
||||
disabled={
|
||||
isClipping ||
|
||||
clipperState === ClipperState.Clipped ||
|
||||
(item.pro && !isPremium)
|
||||
}
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderRadius: "default",
|
||||
@@ -313,33 +351,58 @@ export function Main() {
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{clipData && clipData.data && !isClipping && (
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
mt: 1,
|
||||
bg: "shade",
|
||||
color: "accent",
|
||||
p: 1,
|
||||
border: "1px solid var(--accent)",
|
||||
borderRadius: "default",
|
||||
cursor: "pointer",
|
||||
":hover": {
|
||||
filter: "brightness(80%)"
|
||||
}
|
||||
}}
|
||||
onClick={async () => {
|
||||
const winUrl = URL.createObjectURL(
|
||||
new Blob(["\ufeff", clipData.data], { type: "text/html" })
|
||||
);
|
||||
await browser.windows.create({
|
||||
url: winUrl
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clip done. Click here to preview.
|
||||
</Text>
|
||||
)}
|
||||
{clipMode !== "bookmark" &&
|
||||
clipData &&
|
||||
clipData.data &&
|
||||
!isClipping && (
|
||||
<Flex sx={{ gap: 1, justifyContent: "space-between" }}>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
flex: 1,
|
||||
mt: 1,
|
||||
bg: "shade",
|
||||
color: "accent",
|
||||
p: 1,
|
||||
border: "1px solid var(--accent)",
|
||||
borderRadius: "default",
|
||||
cursor: "pointer",
|
||||
":hover": {
|
||||
filter: "brightness(80%)"
|
||||
}
|
||||
}}
|
||||
onClick={async () => {
|
||||
const winUrl = URL.createObjectURL(
|
||||
new Blob(["\ufeff", clipData.data], { type: "text/html" })
|
||||
);
|
||||
await browser.windows.create({
|
||||
url: winUrl
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clip done. Click here to preview.
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
mt: 1,
|
||||
bg: "background-secondary",
|
||||
p: 1,
|
||||
borderRadius: "default",
|
||||
cursor: "pointer",
|
||||
":hover": {
|
||||
filter: "brightness(80%)"
|
||||
}
|
||||
}}
|
||||
onClick={async () => {
|
||||
setClipData(undefined);
|
||||
setClipperState(ClipperState.Idle);
|
||||
}}
|
||||
>
|
||||
Discard
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Text
|
||||
@@ -357,7 +420,7 @@ export function Main() {
|
||||
}
|
||||
}}
|
||||
onClick={async () => {
|
||||
setClipNonce((s) => ++s);
|
||||
await startClip();
|
||||
}}
|
||||
>
|
||||
{ERROR_MAP[error] || error}
|
||||
@@ -400,9 +463,27 @@ export function Main() {
|
||||
<Button
|
||||
variant="accent"
|
||||
sx={{ mt: 1 }}
|
||||
disabled={!clipData}
|
||||
disabled={isClipping}
|
||||
onClick={async () => {
|
||||
if (!clipData || !title || !clipArea || !clipMode || !url) return;
|
||||
if (
|
||||
clipMode !== "bookmark" &&
|
||||
(clipperState === ClipperState.Idle ||
|
||||
clipperState === ClipperState.Error)
|
||||
) {
|
||||
await startClip();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!title || !clipArea || !clipMode || !url) return;
|
||||
|
||||
const data =
|
||||
clipMode === "bookmark"
|
||||
? {
|
||||
data: createBookmark(url, title)
|
||||
}
|
||||
: clipData;
|
||||
|
||||
if (!data) return;
|
||||
|
||||
const notesnook = await connectApi(false);
|
||||
if (!notesnook) {
|
||||
@@ -417,7 +498,7 @@ export function Main() {
|
||||
note,
|
||||
refs,
|
||||
pageTitle: pageTitle.current,
|
||||
...clipData
|
||||
...data
|
||||
});
|
||||
|
||||
setClipData(undefined);
|
||||
@@ -433,7 +514,9 @@ export function Main() {
|
||||
window.close();
|
||||
}}
|
||||
>
|
||||
Save clip
|
||||
{clipMode === "bookmark"
|
||||
? "Save bookmark"
|
||||
: clipperButtonLabelMap[clipperState]}
|
||||
</Button>
|
||||
|
||||
<Flex
|
||||
@@ -508,3 +591,11 @@ export async function clip(
|
||||
settings
|
||||
});
|
||||
}
|
||||
|
||||
function createBookmark(url: string, title: string) {
|
||||
const a = document.createElement("a");
|
||||
a.setAttribute("href", url);
|
||||
a.setAttribute("title", title);
|
||||
a.innerText = title;
|
||||
return a.outerHTML;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,8 @@ var options = {
|
||||
plugins: [
|
||||
new CleanWebpackPlugin({
|
||||
verbose: false,
|
||||
dangerouslyAllowCleanPatternsOutsideProject: true
|
||||
dangerouslyAllowCleanPatternsOutsideProject: true,
|
||||
dry: false
|
||||
}),
|
||||
new webpack.ProgressPlugin(),
|
||||
// expose and write the allowed env vars on the compiled bundle
|
||||
|
||||
3
fastlane/metadata/android/en-US/changelogs/15379.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15379.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
3
fastlane/metadata/android/en-US/changelogs/15384.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15384.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
3
fastlane/metadata/android/en-US/changelogs/15389.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15389.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
3
fastlane/metadata/android/en-US/changelogs/15394.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15394.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
@@ -104,6 +104,10 @@
|
||||
"run-ios",
|
||||
"build-ios",
|
||||
"build-android",
|
||||
"build-android-debug",
|
||||
"e2e-android-debug",
|
||||
"e2e-android",
|
||||
"e2e-ios",
|
||||
"release-android",
|
||||
"release-android-bundle",
|
||||
"release-ios",
|
||||
|
||||
@@ -60,8 +60,6 @@ for (const website of Websites) {
|
||||
|
||||
await page.addScriptTag({ content: output, type: "text/javascript" });
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// const originalScreenshot = await page.screenshot({
|
||||
// fullPage: true,
|
||||
// type: "jpeg"
|
||||
@@ -73,7 +71,12 @@ for (const website of Websites) {
|
||||
// });
|
||||
|
||||
const result = await page.evaluate(async () => {
|
||||
const html = await window.clipper.clipPage(window.document, true, false);
|
||||
const html = await window.clipper.clipPage(window.document, false, {
|
||||
corsProxy: "https://cors.notesnook.com",
|
||||
images: true,
|
||||
inlineImages: true,
|
||||
styles: true
|
||||
});
|
||||
if (html) {
|
||||
return `\ufeff${html}`;
|
||||
}
|
||||
@@ -101,97 +104,6 @@ for (const website of Websites) {
|
||||
maxDiffPixelRatio: 0.1
|
||||
});
|
||||
|
||||
rmSync(tempFilePath, { force: true });
|
||||
});
|
||||
}
|
||||
|
||||
for (const website of Websites) {
|
||||
const domain = new URL(website.url).hostname;
|
||||
test(`clip as image ${domain} (${website.title})`, async ({ page }, info) => {
|
||||
info.setTimeout(0);
|
||||
|
||||
await page.goto(website.url);
|
||||
|
||||
await page.addScriptTag({ content: output, type: "text/javascript" });
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// const originalScreenshot = await page.screenshot({
|
||||
// fullPage: true,
|
||||
// type: "jpeg"
|
||||
// });
|
||||
|
||||
// expect(originalScreenshot).toMatchSnapshot({
|
||||
// name: `${slugify(website.title)}.jpg`,
|
||||
// maxDiffPixelRatio: 0.1
|
||||
// });
|
||||
|
||||
const result = await page.evaluate(async () => {
|
||||
const data = await window.clipper.clipScreenshot(undefined, "raw");
|
||||
if (data) {
|
||||
return base64ArrayBuffer(await data.arrayBuffer());
|
||||
}
|
||||
|
||||
function base64ArrayBuffer(arrayBuffer) {
|
||||
let base64 = "";
|
||||
const encodings =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
const byteLength = bytes.byteLength;
|
||||
const byteRemainder = byteLength % 3;
|
||||
const mainLength = byteLength - byteRemainder;
|
||||
|
||||
let a, b, c, d;
|
||||
let chunk;
|
||||
|
||||
// Main loop deals with bytes in chunks of 3
|
||||
for (let i = 0; i < mainLength; i = i + 3) {
|
||||
// Combine the three bytes into a single integer
|
||||
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
||||
|
||||
// Use bitmasks to extract 6-bit segments from the triplet
|
||||
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
|
||||
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
|
||||
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
|
||||
d = chunk & 63; // 63 = 2^6 - 1
|
||||
|
||||
// Convert the raw binary segments to the appropriate ASCII encoding
|
||||
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
|
||||
}
|
||||
|
||||
// Deal with the remaining bytes and padding
|
||||
if (byteRemainder == 1) {
|
||||
chunk = bytes[mainLength];
|
||||
|
||||
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
|
||||
|
||||
// Set the 4 least significant bits to zero
|
||||
b = (chunk & 3) << 4; // 3 = 2^2 - 1
|
||||
|
||||
base64 += encodings[a] + encodings[b] + "==";
|
||||
} else if (byteRemainder == 2) {
|
||||
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
|
||||
|
||||
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
|
||||
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
|
||||
|
||||
// Set the 2 least significant bits to zero
|
||||
c = (chunk & 15) << 2; // 15 = 2^4 - 1
|
||||
|
||||
base64 += encodings[a] + encodings[b] + encodings[c] + "=";
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!result) throw new Error("Failed to clip page.");
|
||||
|
||||
expect(Buffer.from(result, "base64")).toMatchSnapshot({
|
||||
name: `${slugify(website.title)}-image.png`,
|
||||
maxDiffPixelRatio: 0.1
|
||||
});
|
||||
// rmSync(tempFilePath, { force: true });
|
||||
});
|
||||
}
|
||||
|
||||
390
packages/clipper/package-lock.json
generated
390
packages/clipper/package-lock.json
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/clipper",
|
||||
"version": "2.1.3",
|
||||
"lockfileVersion": 2,
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
@@ -11,9 +11,7 @@
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@mozilla/readability": "^0.4.2",
|
||||
"css-what": "6.1.0",
|
||||
"hyperapp": "^2.0.22",
|
||||
"specificity": "^0.4.1"
|
||||
"hyperapp": "^2.0.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.48.2",
|
||||
@@ -22,9 +20,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
|
||||
"integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -34,9 +32,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
|
||||
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz",
|
||||
"integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -115,17 +113,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mozilla/readability": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.4.2.tgz",
|
||||
"integrity": "sha512-48MJXzi4Dhy2fJ3lGjmwdEJKoMmn3oiYew9n/1OW6cZy78hAzRIyDJDBCGrg4PBFDyY4xos+H4LCFn5QVRDcfw==",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.4.4.tgz",
|
||||
"integrity": "sha512-MCgZyANpJ6msfvVMi6+A0UAsvZj//4OHREYUB9f2087uXHVoU+H+SWhuihvb1beKpM323bReQPRio0WNk2+V6g==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.6.tgz",
|
||||
"integrity": "sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
|
||||
"integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -140,6 +139,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
|
||||
"integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.48.2"
|
||||
},
|
||||
@@ -151,17 +151,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rsbuild/core": {
|
||||
"version": "1.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.5.13.tgz",
|
||||
"integrity": "sha512-P+TCvZCVpBYZ3GDdnzR/tZKicE41khJIqIRlJYnEc9dwUfX1/eqRf8lA8yrsbB5iZbSfj1iOoH1N25cCQ3hhuA==",
|
||||
"version": "1.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.5.17.tgz",
|
||||
"integrity": "sha512-tHa4puv+pEooQvSewu/K5sm270nkVPcP07Ioz1c+fbFCrFpiZWV5XumgznilS80097glUrieN+9xTbIHGXjThQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rspack/core": "1.5.8",
|
||||
"@rspack/lite-tapable": "~1.0.1",
|
||||
"@swc/helpers": "^0.5.17",
|
||||
"core-js": "~3.45.1",
|
||||
"jiti": "^2.6.0"
|
||||
"core-js": "~3.46.0",
|
||||
"jiti": "^2.6.1"
|
||||
},
|
||||
"bin": {
|
||||
"rsbuild": "bin/rsbuild.js"
|
||||
@@ -384,9 +384,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.45.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
|
||||
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
|
||||
"version": "3.46.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
|
||||
"integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -395,23 +395,13 @@
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -423,7 +413,8 @@
|
||||
"node_modules/hyperapp": {
|
||||
"version": "2.0.22",
|
||||
"resolved": "https://registry.npmjs.org/hyperapp/-/hyperapp-2.0.22.tgz",
|
||||
"integrity": "sha512-3uf9HjnjrhbfykowFNEObZewBEo4DXJIM+9FnGkiR9E4H2eh2f921SzMCMS69X5nN3A7KFfmZc9KCKh/7TQBFA=="
|
||||
"integrity": "sha512-3uf9HjnjrhbfykowFNEObZewBEo4DXJIM+9FnGkiR9E4H2eh2f921SzMCMS69X5nN3A7KFfmZc9KCKh/7TQBFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
@@ -440,6 +431,7 @@
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
|
||||
"integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.48.2"
|
||||
},
|
||||
@@ -458,6 +450,7 @@
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
|
||||
"integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
@@ -470,18 +463,11 @@
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
|
||||
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/specificity": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz",
|
||||
"integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==",
|
||||
"bin": {
|
||||
"specificity": "bin/specificity"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -489,319 +475,5 @@
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@emnapi/core": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
|
||||
"integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@emnapi/wasi-threads": "1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@emnapi/runtime": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
|
||||
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@emnapi/wasi-threads": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
|
||||
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@module-federation/error-codes": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.18.0.tgz",
|
||||
"integrity": "sha512-Woonm8ehyVIUPXChmbu80Zj6uJkC0dD9SJUZ/wOPtO8iiz/m+dkrOugAuKgoiR6qH4F+yorWila954tBz4uKsQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@module-federation/runtime": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.18.0.tgz",
|
||||
"integrity": "sha512-+C4YtoSztM7nHwNyZl6dQKGUVJdsPrUdaf3HIKReg/GQbrt9uvOlUWo2NXMZ8vDAnf/QRrpSYAwXHmWDn9Obaw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@module-federation/error-codes": "0.18.0",
|
||||
"@module-federation/runtime-core": "0.18.0",
|
||||
"@module-federation/sdk": "0.18.0"
|
||||
}
|
||||
},
|
||||
"@module-federation/runtime-core": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.18.0.tgz",
|
||||
"integrity": "sha512-ZyYhrDyVAhUzriOsVfgL6vwd+5ebYm595Y13KeMf6TKDRoUHBMTLGQ8WM4TDj8JNsy7LigncK8C03fn97of0QQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@module-federation/error-codes": "0.18.0",
|
||||
"@module-federation/sdk": "0.18.0"
|
||||
}
|
||||
},
|
||||
"@module-federation/runtime-tools": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.18.0.tgz",
|
||||
"integrity": "sha512-fSga9o4t1UfXNV/Kh6qFvRyZpPp3EHSPRISNeyT8ZoTpzDNiYzhtw0BPUSSD8m6C6XQh2s/11rI4g80UY+d+hA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@module-federation/runtime": "0.18.0",
|
||||
"@module-federation/webpack-bundler-runtime": "0.18.0"
|
||||
}
|
||||
},
|
||||
"@module-federation/sdk": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.18.0.tgz",
|
||||
"integrity": "sha512-Lo/Feq73tO2unjmpRfyyoUkTVoejhItXOk/h5C+4cistnHbTV8XHrW/13fD5e1Iu60heVdAhhelJd6F898Ve9A==",
|
||||
"dev": true
|
||||
},
|
||||
"@module-federation/webpack-bundler-runtime": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.18.0.tgz",
|
||||
"integrity": "sha512-TEvErbF+YQ+6IFimhUYKK3a5wapD90d90sLsNpcu2kB3QGT7t4nIluE25duXuZDVUKLz86tEPrza/oaaCWTpvQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@module-federation/runtime": "0.18.0",
|
||||
"@module-federation/sdk": "0.18.0"
|
||||
}
|
||||
},
|
||||
"@mozilla/readability": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.4.2.tgz",
|
||||
"integrity": "sha512-48MJXzi4Dhy2fJ3lGjmwdEJKoMmn3oiYew9n/1OW6cZy78hAzRIyDJDBCGrg4PBFDyY4xos+H4LCFn5QVRDcfw=="
|
||||
},
|
||||
"@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.6.tgz",
|
||||
"integrity": "sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@emnapi/core": "^1.5.0",
|
||||
"@emnapi/runtime": "^1.5.0",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
}
|
||||
},
|
||||
"@playwright/test": {
|
||||
"version": "1.48.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
|
||||
"integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"playwright": "1.48.2"
|
||||
}
|
||||
},
|
||||
"@rsbuild/core": {
|
||||
"version": "1.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.5.13.tgz",
|
||||
"integrity": "sha512-P+TCvZCVpBYZ3GDdnzR/tZKicE41khJIqIRlJYnEc9dwUfX1/eqRf8lA8yrsbB5iZbSfj1iOoH1N25cCQ3hhuA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@rspack/core": "1.5.8",
|
||||
"@rspack/lite-tapable": "~1.0.1",
|
||||
"@swc/helpers": "^0.5.17",
|
||||
"core-js": "~3.45.1",
|
||||
"jiti": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"@rspack/binding": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.5.8.tgz",
|
||||
"integrity": "sha512-/91CzhRl9r5BIQCgGsS7jA6MDbw1I2BQpbfcUUdkdKl2P79K3Zo/Mw/TvKzS86catwLaUQEgkGRmYawOfPg7ow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@rspack/binding-darwin-arm64": "1.5.8",
|
||||
"@rspack/binding-darwin-x64": "1.5.8",
|
||||
"@rspack/binding-linux-arm64-gnu": "1.5.8",
|
||||
"@rspack/binding-linux-arm64-musl": "1.5.8",
|
||||
"@rspack/binding-linux-x64-gnu": "1.5.8",
|
||||
"@rspack/binding-linux-x64-musl": "1.5.8",
|
||||
"@rspack/binding-wasm32-wasi": "1.5.8",
|
||||
"@rspack/binding-win32-arm64-msvc": "1.5.8",
|
||||
"@rspack/binding-win32-ia32-msvc": "1.5.8",
|
||||
"@rspack/binding-win32-x64-msvc": "1.5.8"
|
||||
}
|
||||
},
|
||||
"@rspack/binding-darwin-arm64": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.5.8.tgz",
|
||||
"integrity": "sha512-spJfpOSN3f7V90ic45/ET2NKB2ujAViCNmqb0iGurMNQtFRq+7Kd+jvVKKGXKBHBbsQrFhidSWbbqy2PBPGK8g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rspack/binding-darwin-x64": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.5.8.tgz",
|
||||
"integrity": "sha512-YFOzeL1IBknBcri8vjUp43dfUBylCeQnD+9O9p0wZmLAw7DtpN5JEOe2AkGo8kdTqJjYKI+cczJPKIw6lu1LWw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rspack/binding-linux-arm64-gnu": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.5.8.tgz",
|
||||
"integrity": "sha512-UAWCsOnpkvy8eAVRo0uipbHXDhnoDq5zmqWTMhpga0/a3yzCp2e+fnjZb/qnFNYb5MeL0O1mwMOYgn1M3oHILQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rspack/binding-linux-arm64-musl": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.5.8.tgz",
|
||||
"integrity": "sha512-GnSvGT4GjokPSD45cTtE+g7LgghuxSP1MRmvd+Vp/I8pnxTVSTsebRod4TAqyiv+l11nuS8yqNveK9qiOkBLWw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rspack/binding-linux-x64-gnu": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.5.8.tgz",
|
||||
"integrity": "sha512-XLxh5n/pzUfxsugz/8rVBv+Tx2nqEM+9rharK69kfooDsQNKyz7PANllBQ/v4svJ+W0BRHnDL4qXSGdteZeEjA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rspack/binding-linux-x64-musl": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.5.8.tgz",
|
||||
"integrity": "sha512-gE0+MZmwF+01p9/svpEESkzkLpBkVUG2o03YMpwXYC/maeRRhWvF8BJ7R3i/Ls/jFGSE87dKX5NbRLVzqksq/w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rspack/binding-wasm32-wasi": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.5.8.tgz",
|
||||
"integrity": "sha512-cfg3niNHeJuxuml1Vy9VvaJrI/5TakzoaZvKX2g5S24wfzR50Eyy4JAsZ+L2voWQQp1yMJbmPYPmnTCTxdJQBQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@napi-rs/wasm-runtime": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"@rspack/binding-win32-arm64-msvc": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.5.8.tgz",
|
||||
"integrity": "sha512-7i3ZTHFXKfU/9Jm9XhpMkrdkxO7lfeYMNVEGkuU5dyBfRMQj69dRgPL7zJwc2plXiqu9LUOl+TwDNTjap7Q36g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rspack/binding-win32-ia32-msvc": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.5.8.tgz",
|
||||
"integrity": "sha512-7ZPPWO11J+soea1+mnfaPpQt7GIodBM7A86dx6PbXgVEoZmetcWPrCF2NBfXxQWOKJ9L3RYltC4z+ZyXRgMOrw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rspack/binding-win32-x64-msvc": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.5.8.tgz",
|
||||
"integrity": "sha512-N/zXQgzIxME3YUzXT8qnyzxjqcnXudWOeDh8CAG9zqTCnCiy16SFfQ/cQgEoLlD9geQntV6jx2GbDDI5kpDGMQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rspack/core": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.5.8.tgz",
|
||||
"integrity": "sha512-sUd2LfiDhqYVfvknuoz0+/c+wSpn693xotnG5g1CSWKZArbtwiYzBIVnNlcHGmuoBRsnj/TkSq8dTQ7gwfBroQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@module-federation/runtime-tools": "0.18.0",
|
||||
"@rspack/binding": "1.5.8",
|
||||
"@rspack/lite-tapable": "1.0.1"
|
||||
}
|
||||
},
|
||||
"@rspack/lite-tapable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz",
|
||||
"integrity": "sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==",
|
||||
"dev": true
|
||||
},
|
||||
"@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "3.45.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
|
||||
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
|
||||
"dev": true
|
||||
},
|
||||
"css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"hyperapp": {
|
||||
"version": "2.0.22",
|
||||
"resolved": "https://registry.npmjs.org/hyperapp/-/hyperapp-2.0.22.tgz",
|
||||
"integrity": "sha512-3uf9HjnjrhbfykowFNEObZewBEo4DXJIM+9FnGkiR9E4H2eh2f921SzMCMS69X5nN3A7KFfmZc9KCKh/7TQBFA=="
|
||||
},
|
||||
"jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"dev": true
|
||||
},
|
||||
"playwright": {
|
||||
"version": "1.48.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
|
||||
"integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fsevents": "2.3.2",
|
||||
"playwright-core": "1.48.2"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.48.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
|
||||
"integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
|
||||
"dev": true
|
||||
},
|
||||
"slugify": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
|
||||
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
|
||||
"dev": true
|
||||
},
|
||||
"specificity": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz",
|
||||
"integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mozilla/readability": "^0.4.2",
|
||||
"css-what": "6.1.0",
|
||||
"hyperapp": "^2.0.22",
|
||||
"specificity": "^0.4.1"
|
||||
"hyperapp": "^2.0.22"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,331 +16,29 @@ 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 { createImage, FetchOptions } from "./fetch.js";
|
||||
import { Filter } from "./types.js";
|
||||
import { uid } from "./utils.js";
|
||||
|
||||
const SVGElements = [
|
||||
"altGlyph",
|
||||
"altGlyphDef",
|
||||
"altGlyphItem",
|
||||
"animate",
|
||||
"animateColor",
|
||||
"animateMotion",
|
||||
"animateTransform",
|
||||
"circle",
|
||||
"clipPath",
|
||||
"color-profile",
|
||||
"cursor",
|
||||
"defs",
|
||||
"desc",
|
||||
"ellipse",
|
||||
"feBlend",
|
||||
"feColorMatrix",
|
||||
"feComponentTransfer",
|
||||
"feComposite",
|
||||
"feConvolveMatrix",
|
||||
"feDiffuseLighting",
|
||||
"feDisplacementMap",
|
||||
"feDistantLight",
|
||||
"feFlood",
|
||||
"feFuncA",
|
||||
"feFuncB",
|
||||
"feFuncG",
|
||||
"feFuncR",
|
||||
"feGaussianBlur",
|
||||
"feImage",
|
||||
"feMerge",
|
||||
"feMergeNode",
|
||||
"feMorphology",
|
||||
"feOffset",
|
||||
"fePointLight",
|
||||
"feSpecularLighting",
|
||||
"feSpotLight",
|
||||
"feTile",
|
||||
"feTurbulence",
|
||||
"filter",
|
||||
"font-face",
|
||||
"font-face-format",
|
||||
"font-face-name",
|
||||
"font-face-src",
|
||||
"font-face-uri",
|
||||
"foreignObject",
|
||||
"g",
|
||||
"glyph",
|
||||
"glyphRef",
|
||||
"hkern",
|
||||
"image",
|
||||
"line",
|
||||
"linearGradient",
|
||||
"marker",
|
||||
"mask",
|
||||
"metadata",
|
||||
"missing-glyph",
|
||||
"mpath",
|
||||
"path",
|
||||
"pattern",
|
||||
"polygon",
|
||||
"polyline",
|
||||
"radialGradient",
|
||||
"rect",
|
||||
"set",
|
||||
"stop",
|
||||
"svg",
|
||||
"switch",
|
||||
"symbol",
|
||||
"text",
|
||||
"textPath",
|
||||
"title",
|
||||
"tref",
|
||||
"tspan",
|
||||
"use",
|
||||
"view",
|
||||
"vkern"
|
||||
].map((a) => a.toLowerCase());
|
||||
const INVALID_ELEMENTS = ["script", "noscript"];
|
||||
|
||||
const INVALID_ELEMENTS = ["script"].map((a) => a.toLowerCase());
|
||||
|
||||
type CloneProps = {
|
||||
filter?: Filter;
|
||||
root: boolean;
|
||||
vector: boolean;
|
||||
styles?: boolean;
|
||||
getElementStyles?: (element: HTMLElement) => CSSStyleDeclaration | undefined;
|
||||
getPseudoElementStyles?: (
|
||||
element: HTMLElement,
|
||||
pseudoElement: string
|
||||
) => CSSStyleDeclaration | undefined;
|
||||
fetchOptions?: FetchOptions;
|
||||
type CloneNodeOptions = {
|
||||
images?: boolean;
|
||||
styles?: boolean;
|
||||
};
|
||||
|
||||
export async function cloneNode(node: HTMLElement, options: CloneProps) {
|
||||
const { root, filter } = options;
|
||||
if (!root && filter && !filter(node)) return null;
|
||||
|
||||
let clone = await makeNodeCopy(node, options);
|
||||
|
||||
if (!clone) return null;
|
||||
clone = await cloneChildren(node, clone, options);
|
||||
|
||||
const processed = processClone(node, clone, options);
|
||||
return processed;
|
||||
}
|
||||
|
||||
function makeNodeCopy(original: HTMLElement, options?: CloneProps) {
|
||||
try {
|
||||
if (original instanceof HTMLCanvasElement && options?.images)
|
||||
return createImage(original.toDataURL(), options?.fetchOptions);
|
||||
|
||||
if (!options?.images && original instanceof HTMLImageElement) return null;
|
||||
|
||||
if (
|
||||
!options?.styles &&
|
||||
(original instanceof HTMLButtonElement ||
|
||||
original instanceof HTMLFormElement ||
|
||||
original instanceof HTMLSelectElement ||
|
||||
original instanceof HTMLInputElement ||
|
||||
original instanceof HTMLTextAreaElement)
|
||||
)
|
||||
return null;
|
||||
|
||||
if (original.nodeType === Node.COMMENT_NODE) return null;
|
||||
|
||||
if (isInvalidElement(original)) return null;
|
||||
|
||||
if (original.nodeType !== Node.TEXT_NODE && !isSVGElement(original)) {
|
||||
const { display, width, height } = window.getComputedStyle(original);
|
||||
if (display === "none" || (width === "0px" && height === "0px"))
|
||||
return null;
|
||||
|
||||
if (isCustomElement(original)) {
|
||||
const isInline = display.includes("inline");
|
||||
const element = document.createElement(isInline ? "span" : "div");
|
||||
for (const attribute of original.attributes) {
|
||||
element.setAttribute(attribute.name, attribute.value);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return original.cloneNode(false) as HTMLElement;
|
||||
} catch (e) {
|
||||
console.error("Failed to clone element", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isCustomElement(element: HTMLElement) {
|
||||
if (!element || !element.tagName) return false;
|
||||
return (
|
||||
!SVGElements.includes(element.tagName.toLowerCase()) &&
|
||||
element.tagName.includes("-")
|
||||
);
|
||||
}
|
||||
|
||||
export function isSVGElement(element: HTMLElement) {
|
||||
if (!element || !element.tagName) return false;
|
||||
return SVGElements.includes(element.tagName.toLowerCase());
|
||||
}
|
||||
|
||||
function isInvalidElement(element: HTMLElement) {
|
||||
if (!element || !element.tagName) return false;
|
||||
return INVALID_ELEMENTS.includes(element.tagName.toLowerCase());
|
||||
}
|
||||
|
||||
async function cloneChildren(
|
||||
original: HTMLElement,
|
||||
clone: HTMLElement,
|
||||
options: CloneProps
|
||||
) {
|
||||
const children = original.childNodes;
|
||||
if (children.length === 0) return clone;
|
||||
|
||||
await cloneChildrenInOrder(clone, children, options);
|
||||
return clone;
|
||||
}
|
||||
|
||||
async function cloneChildrenInOrder(
|
||||
parent: HTMLElement,
|
||||
childs: NodeListOf<ChildNode>,
|
||||
options: CloneProps
|
||||
) {
|
||||
for (const node of childs) {
|
||||
const childClone = await cloneNode(node as HTMLElement, {
|
||||
...options,
|
||||
root: false
|
||||
});
|
||||
if (childClone) parent.appendChild(childClone);
|
||||
}
|
||||
}
|
||||
|
||||
function processClone(
|
||||
original: HTMLElement,
|
||||
clone: HTMLElement,
|
||||
options: CloneProps
|
||||
) {
|
||||
if (!(clone instanceof Element)) return clone;
|
||||
|
||||
// if (clone instanceof HTMLElement) removeAttributes(clone);
|
||||
|
||||
if (options.styles) {
|
||||
copyStyle(original, clone, options);
|
||||
clonePseudoElements(original, clone, options);
|
||||
}
|
||||
fixRelativeUrl(clone);
|
||||
copyUserInput(original, clone);
|
||||
fixSvg(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
function fixRelativeUrl(node: HTMLElement) {
|
||||
const attributes = ["href", "src"];
|
||||
const baseUrl = window.location.href;
|
||||
for (const attribute of attributes) {
|
||||
const url = node.getAttribute(attribute);
|
||||
const relativeUrl = url?.startsWith("http") ? undefined : url;
|
||||
if (relativeUrl) {
|
||||
const absoluteUrl = new URL(relativeUrl, baseUrl).href;
|
||||
node.setAttribute(attribute, absoluteUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyFont(source: CSSStyleDeclaration, target: CSSStyleDeclaration) {
|
||||
target.font = source.font;
|
||||
target.fontFamily = source.fontFamily;
|
||||
target.fontFeatureSettings = source.fontFeatureSettings;
|
||||
target.fontKerning = source.fontKerning;
|
||||
target.fontSize = source.fontSize;
|
||||
target.fontStretch = source.fontStretch;
|
||||
target.fontStyle = source.fontStyle;
|
||||
target.fontVariant = source.fontVariant;
|
||||
target.fontVariantCaps = source.fontVariantCaps;
|
||||
target.fontVariantEastAsian = source.fontVariantEastAsian;
|
||||
target.fontVariantLigatures = source.fontVariantLigatures;
|
||||
target.fontVariantNumeric = source.fontVariantNumeric;
|
||||
target.fontVariationSettings = source.fontVariationSettings;
|
||||
target.fontWeight = source.fontWeight;
|
||||
}
|
||||
|
||||
function copyStyle(
|
||||
sourceElement: HTMLElement,
|
||||
targetElement: HTMLElement,
|
||||
options: CloneProps
|
||||
) {
|
||||
const { getElementStyles } = options;
|
||||
const sourceComputedStyles =
|
||||
getElementStyles && getElementStyles(sourceElement);
|
||||
if (!sourceComputedStyles) return;
|
||||
|
||||
targetElement.style.cssText = sourceComputedStyles.cssText;
|
||||
|
||||
if (sourceElement.tagName.toLowerCase() === "body") {
|
||||
copyFont(getComputedStyle(sourceElement), targetElement.style);
|
||||
export function cloneNode(node: HTMLElement, options: CloneNodeOptions) {
|
||||
node = node.cloneNode(true) as HTMLElement;
|
||||
if (!options.images) {
|
||||
const images = node.getElementsByTagName("img");
|
||||
for (const image of images) image.remove();
|
||||
}
|
||||
|
||||
const styles = targetElement.getAttribute("style");
|
||||
if (styles) targetElement.setAttribute("style", minifyStyles(styles));
|
||||
}
|
||||
|
||||
function clonePseudoElements(
|
||||
original: HTMLElement,
|
||||
clone: HTMLElement,
|
||||
options: CloneProps
|
||||
) {
|
||||
const { getPseudoElementStyles } = options;
|
||||
let hasPseudoElements = false;
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
const className = `pseudo--${uid()}`;
|
||||
|
||||
for (const element of [":before", ":after"]) {
|
||||
const style =
|
||||
(getPseudoElementStyles && getPseudoElementStyles(original, element)) ||
|
||||
getComputedStyle(original, element);
|
||||
|
||||
if (!style.cssText) continue;
|
||||
|
||||
const selector = `.${className}:${element} {
|
||||
${style.cssText}
|
||||
}`;
|
||||
|
||||
styleElement.appendChild(document.createTextNode(selector));
|
||||
hasPseudoElements = true;
|
||||
if (!options.styles) {
|
||||
const elements = node.querySelectorAll(
|
||||
`button, form, select, input, textarea`
|
||||
);
|
||||
for (const element of elements) element.remove();
|
||||
}
|
||||
|
||||
if (hasPseudoElements) {
|
||||
clone.className = className;
|
||||
clone.appendChild(styleElement);
|
||||
}
|
||||
|
||||
return hasPseudoElements;
|
||||
}
|
||||
|
||||
function copyUserInput(original: HTMLElement, clone: HTMLElement) {
|
||||
if (
|
||||
original instanceof HTMLInputElement ||
|
||||
original instanceof HTMLTextAreaElement
|
||||
)
|
||||
clone.setAttribute("value", original.value);
|
||||
}
|
||||
|
||||
function fixSvg(clone: Element) {
|
||||
if (!(clone instanceof SVGElement)) return;
|
||||
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||
|
||||
// if (!(clone instanceof SVGRectElement)) return;
|
||||
["width", "height"].forEach(function (attribute) {
|
||||
const value = clone.getAttribute(attribute);
|
||||
if (!value || !!clone.style.getPropertyValue(attribute)) return;
|
||||
|
||||
clone.style.setProperty(attribute, value);
|
||||
});
|
||||
}
|
||||
|
||||
function minifyStyles(text: string) {
|
||||
return text.replace(/(:?[:;])(:? +)/gm, (_full, sep) => {
|
||||
return sep;
|
||||
});
|
||||
const invalidElements = node.querySelectorAll(INVALID_ELEMENTS.join(","));
|
||||
for (const element of invalidElements) element.remove();
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const from = String.fromCharCode;
|
||||
|
||||
function trim(value: string): string {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function charat(value: string, index: number): number {
|
||||
return value.charCodeAt(index) | 0;
|
||||
}
|
||||
|
||||
function strlen(value: string): number {
|
||||
return value.length;
|
||||
}
|
||||
|
||||
function substr(value: string, begin: number, end: number): string {
|
||||
return value.slice(begin, end);
|
||||
}
|
||||
|
||||
function append<T>(value: T, array: T[]): T {
|
||||
array.push(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
let line = 1;
|
||||
let column = 1;
|
||||
let length = 0;
|
||||
let position = 0;
|
||||
let character = 0;
|
||||
let characters = "";
|
||||
|
||||
function next(): number {
|
||||
character = position < length ? charat(characters, position++) : 0;
|
||||
|
||||
if ((column++, character === 10)) (column = 1), line++;
|
||||
|
||||
return character;
|
||||
}
|
||||
|
||||
function peek(): number {
|
||||
return charat(characters, position);
|
||||
}
|
||||
|
||||
function slice(begin: number, end: number): string {
|
||||
return substr(characters, begin, end);
|
||||
}
|
||||
|
||||
function token(type: number): number {
|
||||
switch (type) {
|
||||
// \0 \t \n \r \s whitespace token
|
||||
case 0:
|
||||
case 9:
|
||||
case 10:
|
||||
case 13:
|
||||
case 32:
|
||||
return 5;
|
||||
|
||||
// ! + , / > @ ~ isolate token
|
||||
case 33:
|
||||
case 42:
|
||||
case 43:
|
||||
case 44:
|
||||
case 47:
|
||||
case 62:
|
||||
case 64:
|
||||
case 126:
|
||||
case 59: /* ; { } breakpoint token */
|
||||
case 123:
|
||||
case 125:
|
||||
return 4;
|
||||
// : accompanied token
|
||||
case 58:
|
||||
return 3;
|
||||
// " ' ( [ opening delimit token
|
||||
case 34:
|
||||
case 39:
|
||||
case 40:
|
||||
case 91:
|
||||
return 2;
|
||||
// ) ] closing delimit token
|
||||
case 41:
|
||||
case 93:
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function alloc(value: string): [] {
|
||||
line = column = 1;
|
||||
length = strlen((characters = value));
|
||||
position = 0;
|
||||
return [];
|
||||
}
|
||||
|
||||
function dealloc<T>(value: T): T {
|
||||
characters = "";
|
||||
return value;
|
||||
}
|
||||
|
||||
function delimit(type: number): string {
|
||||
return trim(
|
||||
slice(
|
||||
position - 1,
|
||||
delimiter(type === 91 ? type + 2 : type === 40 ? type + 1 : type)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function tokenize(value: string): string[] {
|
||||
return dealloc(tokenizer(alloc(value)));
|
||||
}
|
||||
|
||||
function tokenizer(children: string[]): string[] {
|
||||
while (next())
|
||||
switch (token(character)) {
|
||||
case 0:
|
||||
append(identifier(position - 1), children);
|
||||
break;
|
||||
case 2:
|
||||
append(delimit(character), children);
|
||||
break;
|
||||
default:
|
||||
append(from(character), children);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function delimiter(type: number): number {
|
||||
while (next())
|
||||
switch (character) {
|
||||
// ] ) " '
|
||||
case type:
|
||||
return position;
|
||||
// " '
|
||||
case 34:
|
||||
case 39:
|
||||
if (type !== 34 && type !== 39) delimiter(character);
|
||||
break;
|
||||
// (
|
||||
case 40:
|
||||
if (type === 41) delimiter(type);
|
||||
break;
|
||||
// \
|
||||
case 92:
|
||||
next();
|
||||
break;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
function identifier(index: number): string {
|
||||
while (!token(peek())) next();
|
||||
|
||||
return slice(index, position);
|
||||
}
|
||||
@@ -16,13 +16,12 @@ 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 { cloneNode, isSVGElement } from "./clone.js";
|
||||
import { createImage, FetchOptions } from "./fetch.js";
|
||||
import { resolveAll } from "./fontfaces.js";
|
||||
import { inlineAllImages } from "./images.js";
|
||||
import { Options } from "./types.js";
|
||||
import { canvasToBlob, delay, escapeXhtml, height, width } from "./utils.js";
|
||||
import { cacheStylesheets, inlineStylesheets } from "./styles.js";
|
||||
import { canvasToBlob, delay, height, width, isSVGElement } from "./utils.js";
|
||||
import { cloneNode } from "./clone.js";
|
||||
|
||||
// Default impl options
|
||||
const defaultOptions: Options = {
|
||||
@@ -33,23 +32,9 @@ async function getInlinedNode(node: HTMLElement, options: Options) {
|
||||
const { fonts, images, stylesheets, inlineImages } =
|
||||
options.inlineOptions || {};
|
||||
|
||||
if (stylesheets) await inlineStylesheets(options.fetchOptions);
|
||||
|
||||
const documentStyles = getComputedStyle(document.documentElement);
|
||||
|
||||
const styleCache = stylesheets
|
||||
? await cacheStylesheets(documentStyles)
|
||||
: undefined;
|
||||
|
||||
let clone = await cloneNode(node, {
|
||||
styles: options.styles,
|
||||
filter: options.filter,
|
||||
root: true,
|
||||
vector: !options.raster,
|
||||
fetchOptions: options.fetchOptions,
|
||||
getElementStyles: styleCache?.get,
|
||||
getPseudoElementStyles: styleCache?.getPseudo,
|
||||
images: images
|
||||
let clone = cloneNode(node, {
|
||||
images,
|
||||
styles: stylesheets
|
||||
});
|
||||
|
||||
if (!clone || clone instanceof Text) return;
|
||||
@@ -62,86 +47,72 @@ async function getInlinedNode(node: HTMLElement, options: Options) {
|
||||
return clone;
|
||||
}
|
||||
|
||||
async function toSvg(node: HTMLElement, options: Options) {
|
||||
options.inlineOptions = {
|
||||
fonts: true,
|
||||
images: true,
|
||||
stylesheets: true,
|
||||
...options.inlineOptions
|
||||
};
|
||||
|
||||
let clone = await getInlinedNode(node, options);
|
||||
if (!clone) return;
|
||||
|
||||
clone = applyOptions(clone, options);
|
||||
|
||||
return makeSvgDataUri(
|
||||
clone,
|
||||
options.width || width(node),
|
||||
options.height || height(node)
|
||||
);
|
||||
}
|
||||
|
||||
function applyOptions(clone: HTMLElement, options: Options) {
|
||||
if (options.backgroundColor)
|
||||
clone.style.backgroundColor = options.backgroundColor;
|
||||
if (options.width) clone.style.width = options.width + "px";
|
||||
if (options.height) clone.style.height = options.height + "px";
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
function toPixelData(node: HTMLElement, options: Options) {
|
||||
options = options || {};
|
||||
function toPng(body: HTMLElement, head: HTMLHeadElement, options: Options) {
|
||||
options.raster = true;
|
||||
return draw(node, options).then(function (canvas) {
|
||||
return canvas
|
||||
?.getContext("2d")
|
||||
?.getImageData(0, 0, width(node), height(node)).data;
|
||||
});
|
||||
}
|
||||
|
||||
function toPng(node: HTMLElement, options: Options) {
|
||||
options.raster = true;
|
||||
return draw(node, options).then(function (canvas) {
|
||||
return draw(body, head, options).then(function (canvas) {
|
||||
return canvas?.toDataURL();
|
||||
});
|
||||
}
|
||||
|
||||
function toJpeg(node: HTMLElement, options: Options) {
|
||||
async function toJpeg(
|
||||
body: HTMLElement,
|
||||
head: HTMLHeadElement,
|
||||
options: Options
|
||||
) {
|
||||
options.raster = true;
|
||||
return draw(node, options).then(function (canvas) {
|
||||
return canvas?.toDataURL("image/jpeg", options.quality || 1.0);
|
||||
});
|
||||
|
||||
return draw(body, head, options).then((canvas) =>
|
||||
canvas?.toDataURL("image/jpeg", options.quality || 1.0)
|
||||
);
|
||||
}
|
||||
|
||||
function toBlob(node: HTMLElement, options: Options) {
|
||||
function toBlob(body: HTMLElement, head: HTMLHeadElement, options: Options) {
|
||||
options.raster = true;
|
||||
return draw(node, options).then((canvas) => canvas && canvasToBlob(canvas));
|
||||
return draw(body, head, options).then(
|
||||
(canvas) => canvas && canvasToBlob(canvas)
|
||||
);
|
||||
}
|
||||
|
||||
function toCanvas(node: HTMLElement, options: Options) {
|
||||
options.raster = true;
|
||||
return draw(node, options);
|
||||
function toSvg(body: HTMLElement, head: HTMLHeadElement, options: Options) {
|
||||
return makeSvg(
|
||||
body,
|
||||
head,
|
||||
options.width || width(body),
|
||||
options.height || height(body)
|
||||
);
|
||||
}
|
||||
|
||||
function draw(domNode: HTMLElement, options: Options) {
|
||||
async function draw(
|
||||
body: HTMLElement,
|
||||
head: HTMLHeadElement,
|
||||
options: Options
|
||||
) {
|
||||
options = { ...defaultOptions, ...options };
|
||||
return toSvg(domNode, options)
|
||||
.then((uri) => (uri ? createImage(uri, options.fetchOptions) : null))
|
||||
const uri = makeSvgDataUri(
|
||||
makeSvg(
|
||||
body,
|
||||
head,
|
||||
options.width || width(body),
|
||||
options.height || height(body)
|
||||
)
|
||||
);
|
||||
|
||||
return createImage(uri, options.fetchOptions)
|
||||
.then(delay(0))
|
||||
.then(function (image) {
|
||||
if (!image) return null;
|
||||
|
||||
image.setAttribute("crossorigin", "anonymous");
|
||||
|
||||
const scale = typeof options.scale !== "number" ? 1 : options.scale;
|
||||
const canvas = newCanvas(domNode, scale, options);
|
||||
const canvas = newCanvas(body, scale, options);
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
// ctx.mozImageSmoothingEnabled = false;
|
||||
// ctx.msImageSmoothingEnabled = false;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
if (image) {
|
||||
ctx.scale(scale, scale);
|
||||
ctx.drawImage(image, 0, 0);
|
||||
}
|
||||
ctx.scale(scale, scale);
|
||||
ctx.drawImage(image as HTMLImageElement, 0, 0);
|
||||
return canvas;
|
||||
});
|
||||
}
|
||||
@@ -170,52 +141,60 @@ function embedFonts(node: HTMLElement, options?: FetchOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
function makeSvgDataUri(node: HTMLElement, width: number, height: number) {
|
||||
node.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
||||
const xhtml = escapeXhtml(new XMLSerializer().serializeToString(node));
|
||||
function makeSvg(
|
||||
body: HTMLElement,
|
||||
head: HTMLHeadElement,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
body.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
||||
|
||||
/**
|
||||
* We're removing all attributes that contain non-word characters
|
||||
* Sometimes a webpage could have invalid html and that causes attribute names to break
|
||||
* HTML is resilient to this but SVG is not and will throw an error, so we remove these attributes altogether
|
||||
*/
|
||||
for (const element of body.querySelectorAll("img, svg, video, iframe")) {
|
||||
const attributes = element.getAttributeNames();
|
||||
for (const attribute of attributes) {
|
||||
if (attribute.match(/\W/)) {
|
||||
element.removeAttribute(attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const xhtml = new XMLSerializer().serializeToString(body);
|
||||
|
||||
const xstyles = Array.from(head.getElementsByTagName("style"))
|
||||
.map((s) => new XMLSerializer().serializeToString(s))
|
||||
.join("\n");
|
||||
|
||||
const foreignObject =
|
||||
'<foreignObject x="0" y="0" width="100%" height="100%">' +
|
||||
xhtml +
|
||||
"</foreignObject>";
|
||||
|
||||
const svgStr =
|
||||
return (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="' +
|
||||
width +
|
||||
'" height="' +
|
||||
height +
|
||||
'">' +
|
||||
xstyles +
|
||||
foreignObject +
|
||||
"</svg>";
|
||||
|
||||
return "data:image/svg+xml;charset=utf-8," + svgStr;
|
||||
"</svg>"
|
||||
);
|
||||
}
|
||||
|
||||
export { toJpeg, toBlob, toCanvas, toPixelData, toPng, toSvg, getInlinedNode };
|
||||
function makeSvgDataUri(str: string) {
|
||||
return "data:image/svg+xml; charset=utf8, " + encodeURIComponent(str);
|
||||
}
|
||||
|
||||
const VALID_ATTRIBUTES = [
|
||||
"src",
|
||||
"href",
|
||||
"title",
|
||||
"style",
|
||||
"srcset",
|
||||
"sizes",
|
||||
"width",
|
||||
"height",
|
||||
"target",
|
||||
"rel"
|
||||
];
|
||||
export { toJpeg, toBlob, toPng, toSvg, getInlinedNode };
|
||||
|
||||
function finalize(root: HTMLElement) {
|
||||
for (const element of root.querySelectorAll("*")) {
|
||||
if (!(element instanceof HTMLElement) || isSVGElement(element)) continue;
|
||||
for (const attribute of Array.from(element.attributes)) {
|
||||
if (attribute.name === "class" && element.className.includes("pseudo--"))
|
||||
continue;
|
||||
|
||||
if (!VALID_ATTRIBUTES.includes(attribute.name)) {
|
||||
element.removeAttribute(attribute.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (element instanceof HTMLAnchorElement) {
|
||||
element.href = element.href.startsWith("http")
|
||||
|
||||
@@ -39,7 +39,10 @@ export async function fetchResource(url: string, options?: FetchOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
export function createImage(url: string, options?: FetchOptions) {
|
||||
export function createImage(
|
||||
url: string,
|
||||
options?: FetchOptions
|
||||
): Promise<HTMLImageElement | null> {
|
||||
if (url === "data:,") return Promise.resolve(null);
|
||||
return new Promise<HTMLImageElement>(function (resolve, reject) {
|
||||
const image = new Image();
|
||||
@@ -47,7 +50,7 @@ export function createImage(url: string, options?: FetchOptions) {
|
||||
image.onload = function () {
|
||||
resolve(image);
|
||||
};
|
||||
image.onerror = reject;
|
||||
image.onerror = () => reject(new Error("Failed to render image."));
|
||||
image.src = constructUrl(url, options);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
import { FetchOptions, fetchResource } from "./fetch.js";
|
||||
import { inlineAll } from "./inliner.js";
|
||||
import { isDataUrl } from "./utils.js";
|
||||
|
||||
async function inlineAllImages(root: HTMLElement, options?: FetchOptions) {
|
||||
@@ -28,15 +27,7 @@ async function inlineAllImages(root: HTMLElement, options?: FetchOptions) {
|
||||
promises.push(inlineImage(image, options));
|
||||
}
|
||||
|
||||
const backgroundImageNodes = root.querySelectorAll(
|
||||
`[style*="background-image:"],[style*="background:"]`
|
||||
);
|
||||
for (let i = 0; i < backgroundImageNodes.length; ++i) {
|
||||
const image = backgroundImageNodes[i];
|
||||
promises.push(inlineBackground(image as HTMLElement, options));
|
||||
}
|
||||
|
||||
await Promise.all(promises).catch((e) => console.error(e));
|
||||
await Promise.allSettled(promises).catch((e) => console.error(e));
|
||||
}
|
||||
export { inlineAllImages };
|
||||
|
||||
@@ -54,26 +45,10 @@ async function inlineImage(element: HTMLImageElement, options?: FetchOptions) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return new Promise<HTMLImageElement | null>(function (resolve, reject) {
|
||||
if (element.parentElement?.tagName === "PICTURE") {
|
||||
element.parentElement?.replaceWith(element);
|
||||
}
|
||||
if (element.parentElement?.tagName === "PICTURE") {
|
||||
element.parentElement?.replaceWith(element);
|
||||
}
|
||||
|
||||
element.onload = () => resolve(element);
|
||||
// for any image with invalid src(such as <img src />), just ignore it
|
||||
element.onerror = (e) => reject(e);
|
||||
element.src = dataURL;
|
||||
element.removeAttribute("srcset");
|
||||
});
|
||||
}
|
||||
|
||||
async function inlineBackground(
|
||||
backgroundNode: HTMLElement,
|
||||
options?: FetchOptions
|
||||
) {
|
||||
const background = backgroundNode.style.getPropertyValue("background-image");
|
||||
if (!background) return backgroundNode;
|
||||
const inlined = await inlineAll(background, options);
|
||||
backgroundNode.style.setProperty("background-image", inlined);
|
||||
return backgroundNode;
|
||||
element.src = dataURL;
|
||||
element.removeAttribute("srcset");
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ declare global {
|
||||
};
|
||||
}
|
||||
|
||||
global.Clipper = {
|
||||
globalThis.Clipper = {
|
||||
clipArticle,
|
||||
clipPage
|
||||
};
|
||||
|
||||
@@ -20,9 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Readability } from "@mozilla/readability";
|
||||
import { injectCss } from "./utils.js";
|
||||
import { app, h, text } from "hyperapp";
|
||||
import { getInlinedNode, toBlob, toJpeg, toPng } from "./domtoimage.js";
|
||||
import { getInlinedNode, toBlob, toJpeg, toPng, toSvg } from "./domtoimage.js";
|
||||
import { Config, InlineOptions } from "./types.js";
|
||||
import { FetchOptions } from "./fetch.js";
|
||||
import { addStylesToHead } from "./styles.js";
|
||||
|
||||
type ReadabilityEnhanced = Readability<string> & {
|
||||
PRESENTATIONAL_ATTRIBUTES: string[];
|
||||
@@ -84,36 +85,45 @@ async function clipArticle(
|
||||
}
|
||||
|
||||
async function clipScreenshot<
|
||||
TOutputFormat extends "jpeg" | "png" | "raw",
|
||||
TOutput extends TOutputFormat extends "jpeg"
|
||||
? string
|
||||
: TOutputFormat extends "png"
|
||||
TOutputFormat extends "jpeg" | "png" | "raw" | "svg",
|
||||
TOutput extends TOutputFormat extends "jpeg" | "png" | "svg"
|
||||
? string
|
||||
: Blob | undefined
|
||||
>(
|
||||
target?: HTMLElement,
|
||||
output: TOutputFormat = "jpeg" as TOutputFormat,
|
||||
config?: Config
|
||||
): Promise<TOutput> {
|
||||
const screenshotTarget = target || document.body;
|
||||
): Promise<TOutput | null> {
|
||||
const fetchOptions = resolveFetchOptions(config);
|
||||
|
||||
const func = output === "jpeg" ? toJpeg : output === "png" ? toPng : toBlob;
|
||||
const screenshot = await func(screenshotTarget, {
|
||||
const { body, head } = await getPage(document, config, false);
|
||||
if (!body || !head) return null;
|
||||
|
||||
const func =
|
||||
output === "jpeg"
|
||||
? toJpeg
|
||||
: output === "png"
|
||||
? toPng
|
||||
: output === "svg"
|
||||
? toSvg
|
||||
: toBlob;
|
||||
const screenshot = await func(body, head, {
|
||||
quality: 1,
|
||||
backgroundColor: "white",
|
||||
width: document.body.scrollWidth,
|
||||
height: document.body.scrollHeight,
|
||||
fetchOptions: resolveFetchOptions(config),
|
||||
fetchOptions,
|
||||
inlineOptions: {
|
||||
inlineImages: true,
|
||||
fonts: true,
|
||||
images: true,
|
||||
stylesheets: true
|
||||
},
|
||||
styles: true
|
||||
}
|
||||
});
|
||||
|
||||
if (output === "jpeg" || output === "png")
|
||||
return `<img width="${document.body.scrollWidth}px" height="${document.body.scrollHeight}px" src="${screenshot}" />` as TOutput;
|
||||
else if (output === "svg") return screenshot as TOutput;
|
||||
else return screenshot as TOutput;
|
||||
}
|
||||
|
||||
@@ -192,7 +202,11 @@ function enterNodeSelectionMode(doc: Document, config?: Config) {
|
||||
`.${CLASSES.nodeSelected}`
|
||||
);
|
||||
|
||||
const div = document.createElement("div");
|
||||
const { head } = await getPage(document, config, false);
|
||||
const html = document.createElement("html");
|
||||
html.append(head!);
|
||||
const body = document.createElement("body");
|
||||
html.append(body);
|
||||
for (const node of selectedNodes) {
|
||||
node.classList.remove(CLASSES.nodeSelected);
|
||||
const inlined = await getInlinedNode(node as HTMLElement, {
|
||||
@@ -205,9 +219,9 @@ function enterNodeSelectionMode(doc: Document, config?: Config) {
|
||||
}
|
||||
});
|
||||
if (!inlined) continue;
|
||||
div.appendChild(inlined);
|
||||
body.appendChild(inlined);
|
||||
}
|
||||
resolve(div?.outerHTML);
|
||||
resolve(html?.outerHTML);
|
||||
},
|
||||
() => reject("Cancelled.")
|
||||
);
|
||||
@@ -454,16 +468,16 @@ async function getPage(
|
||||
config?: Config,
|
||||
onlyVisible = false
|
||||
) {
|
||||
const fetchOptions = resolveFetchOptions(config);
|
||||
const body = await getInlinedNode(document.body, {
|
||||
raster: true,
|
||||
fetchOptions: resolveFetchOptions(config),
|
||||
fetchOptions,
|
||||
inlineOptions: {
|
||||
fonts: false,
|
||||
inlineImages: config?.inlineImages,
|
||||
images: config?.images,
|
||||
stylesheets: config?.styles
|
||||
},
|
||||
styles: config?.styles,
|
||||
filter: (node) => {
|
||||
return !onlyVisible || isElementInViewport(node);
|
||||
}
|
||||
@@ -476,6 +490,16 @@ async function getPage(
|
||||
title.innerText = document.title;
|
||||
head.appendChild(title);
|
||||
|
||||
if (config?.styles) {
|
||||
await addStylesToHead(head, fetchOptions);
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries(
|
||||
toAttributes(document.documentElement)
|
||||
)) {
|
||||
body.setAttribute(name, value);
|
||||
}
|
||||
|
||||
return {
|
||||
body,
|
||||
head
|
||||
@@ -492,3 +516,13 @@ function resolveFetchOptions(config?: Config): FetchOptions | undefined {
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function toAttributes(element: HTMLElement) {
|
||||
const attributes: Record<string, string> = {};
|
||||
for (const { name } of element.attributes) {
|
||||
const value = element.getAttribute(name);
|
||||
if (!value) continue;
|
||||
attributes[name] = value;
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { FetchOptions, fetchResource } from "./fetch.js";
|
||||
import { constructUrl, FetchOptions, fetchResource } from "./fetch.js";
|
||||
import { isDataUrl, resolveUrl, escape } from "./utils.js";
|
||||
|
||||
const URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/g;
|
||||
@@ -40,32 +40,37 @@ async function inline(
|
||||
string: string,
|
||||
url: string,
|
||||
options?: FetchOptions,
|
||||
baseUrl?: string
|
||||
baseUrl?: string,
|
||||
onlyResolve?: boolean
|
||||
) {
|
||||
url = baseUrl ? resolveUrl(url, baseUrl) : url;
|
||||
const dataUrl = await fetchResource(url, options);
|
||||
// const dataUrl = dataAsUrl(data, mimeType(url));
|
||||
return string.replace(urlAsRegex(url), "$1" + dataUrl + "$3");
|
||||
}
|
||||
|
||||
function urlAsRegex(urlValue: string) {
|
||||
return new RegExp("(url\\(['\"]?)(" + escape(urlValue) + ")(['\"]?\\))", "g");
|
||||
const resolvedUrl = baseUrl ? resolveUrl(url, baseUrl) : url;
|
||||
const dataUrl = onlyResolve
|
||||
? constructUrl(resolvedUrl, options)
|
||||
: await fetchResource(resolvedUrl, options).catch(() =>
|
||||
constructUrl(resolvedUrl, options)
|
||||
);
|
||||
return string.replace(url, dataUrl || resolvedUrl);
|
||||
}
|
||||
|
||||
async function inlineAll(
|
||||
string: string,
|
||||
options?: FetchOptions,
|
||||
baseUrl?: string
|
||||
baseUrl?: string,
|
||||
onlyResolve?: boolean
|
||||
) {
|
||||
if (!shouldProcess(string)) return string;
|
||||
|
||||
const urls = readUrls(string);
|
||||
|
||||
let prefix = string;
|
||||
for (const url of urls) {
|
||||
prefix = await inline(prefix, url, options, baseUrl);
|
||||
string = await inline(string, url, options, baseUrl, onlyResolve).catch(
|
||||
(e) => {
|
||||
console.error(e);
|
||||
return string;
|
||||
}
|
||||
);
|
||||
}
|
||||
return prefix;
|
||||
return string;
|
||||
}
|
||||
|
||||
export { shouldProcess, inlineAll, readUrls };
|
||||
|
||||
@@ -17,64 +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 { constructUrl, FetchOptions } from "./fetch.js";
|
||||
import { compare, calculate, SpecificityArray } from "specificity";
|
||||
import { tokenize } from "./css-tokenizer.js";
|
||||
import { stringify, parse, SelectorType } from "css-what";
|
||||
import { safeQuerySelectorAll } from "./utils.js";
|
||||
|
||||
const SHORTHANDS = [
|
||||
"animation",
|
||||
"background",
|
||||
"border",
|
||||
"border-block-end",
|
||||
"border-block-start",
|
||||
"border-bottom",
|
||||
"border-color",
|
||||
"border-image",
|
||||
"border-inline-end",
|
||||
"border-inline-start",
|
||||
"border-left",
|
||||
"border-radius",
|
||||
"border-right",
|
||||
"border-style",
|
||||
"border-top",
|
||||
"border-width",
|
||||
"column-rule",
|
||||
"columns",
|
||||
"contain-intrinsic-size",
|
||||
"flex",
|
||||
"flex-flow",
|
||||
"font",
|
||||
"gap",
|
||||
"grid",
|
||||
"grid-area",
|
||||
"grid-column",
|
||||
"grid-row",
|
||||
"grid-template",
|
||||
"grid-gap",
|
||||
"list-style",
|
||||
"margin",
|
||||
"mask",
|
||||
"offset",
|
||||
"outline",
|
||||
"overflow",
|
||||
"padding",
|
||||
"place-content",
|
||||
"place-items",
|
||||
"place-self",
|
||||
"scroll-margin",
|
||||
"scroll-padding",
|
||||
"text-decoration",
|
||||
"text-emphasis",
|
||||
"transition"
|
||||
];
|
||||
|
||||
export async function inlineStylesheets(options?: FetchOptions) {
|
||||
for (const sheet of document.styleSheets) {
|
||||
if (await skipStyleSheet(sheet, options)) continue;
|
||||
}
|
||||
await resolveImports(options);
|
||||
}
|
||||
import { inlineAll } from "./inliner.js";
|
||||
|
||||
async function resolveImports(options?: FetchOptions) {
|
||||
for (const sheet of document.styleSheets) {
|
||||
@@ -96,7 +39,9 @@ async function resolveImports(options?: FetchOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const ruleIndex of rulesToDelete) sheet.deleteRule(ruleIndex);
|
||||
if (sheet.cssRules.length !== 0) {
|
||||
for (const ruleIndex of rulesToDelete) sheet.deleteRule(ruleIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +50,7 @@ async function downloadStylesheet(href: string, options?: FetchOptions) {
|
||||
const style = document.createElement("style");
|
||||
const response = await fetch(constructUrl(href, options));
|
||||
if (!response.ok) return false;
|
||||
style.innerText = await response.text();
|
||||
style.innerHTML = await response.text();
|
||||
style.setAttribute("href", href);
|
||||
return style;
|
||||
} catch (e) {
|
||||
@@ -113,318 +58,129 @@ async function downloadStylesheet(href: string, options?: FetchOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
type StyleableElement = HTMLElement | SVGElement;
|
||||
type BaseStyle = {
|
||||
rule: CSSStyleDeclaration;
|
||||
href: URL | null;
|
||||
};
|
||||
type SpecifiedStyle = BaseStyle & {
|
||||
specificity: SpecificityArray;
|
||||
};
|
||||
type PseudoElementStyle = BaseStyle & {
|
||||
pseudoElement: string;
|
||||
};
|
||||
type CSSStyledElements = Map<StyleableElement, SpecifiedStyle[]>;
|
||||
type CSSPseudoElements = Map<StyleableElement, PseudoElementStyle[]>;
|
||||
|
||||
export async function cacheStylesheets(documentStyles: CSSStyleDeclaration) {
|
||||
const styledElements: CSSStyledElements = new Map();
|
||||
const styledPseudoElements: CSSPseudoElements = new Map();
|
||||
|
||||
for (const sheet of document.styleSheets) {
|
||||
if (await skipStyleSheet(sheet)) continue;
|
||||
let href = sheet.href || undefined;
|
||||
if (!href && sheet.ownerNode instanceof HTMLElement)
|
||||
href = sheet.ownerNode.getAttribute("href") || undefined;
|
||||
|
||||
walkRules(
|
||||
sheet.cssRules,
|
||||
documentStyles,
|
||||
styledElements,
|
||||
styledPseudoElements,
|
||||
href
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
getPseudo(element: StyleableElement, pseudoElement: string) {
|
||||
const styles = styledPseudoElements
|
||||
.get(element)
|
||||
?.filter((s) => s.pseudoElement.includes(pseudoElement));
|
||||
if (!styles || !styles.length) return;
|
||||
|
||||
return getElementStyles(element, styles, documentStyles);
|
||||
},
|
||||
get(element: StyleableElement) {
|
||||
const styles = styledElements.get(element);
|
||||
if (!styles) return;
|
||||
|
||||
const allStyles = styles.sort((a, b) =>
|
||||
compare(a.specificity, b.specificity)
|
||||
);
|
||||
allStyles.push({
|
||||
rule: element.style,
|
||||
specificity: [0, 0, 0, 0],
|
||||
href: null
|
||||
});
|
||||
|
||||
return getElementStyles(element, allStyles, documentStyles);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function walkRules(
|
||||
cssRules: CSSRuleList,
|
||||
documentStyles: CSSStyleDeclaration,
|
||||
styled: CSSStyledElements,
|
||||
pseudoElements: CSSPseudoElements,
|
||||
href?: string
|
||||
) {
|
||||
for (const rule of cssRules) {
|
||||
if (rule instanceof CSSStyleRule) {
|
||||
if (isPseudoSelector(rule.selectorText)) {
|
||||
const selectors = parsePseudoSelector(rule.selectorText);
|
||||
|
||||
for (const selector of selectors) {
|
||||
if (!selector || !selector.selector.trim()) continue;
|
||||
const elements = safeQuerySelectorAll(
|
||||
document,
|
||||
selector.selector
|
||||
) as NodeListOf<StyleableElement>;
|
||||
|
||||
for (const element of elements) {
|
||||
if (
|
||||
!(element instanceof HTMLElement) &&
|
||||
!(element instanceof SVGElement)
|
||||
)
|
||||
continue;
|
||||
|
||||
const styles: PseudoElementStyle[] =
|
||||
pseudoElements.get(element) || [];
|
||||
pseudoElements.set(element, styles);
|
||||
|
||||
styles.push({
|
||||
rule: rule.style,
|
||||
href: getBaseUrl(href),
|
||||
pseudoElement: selector.pseudoElement
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const elements = safeQuerySelectorAll(
|
||||
document,
|
||||
rule.selectorText
|
||||
) as NodeListOf<StyleableElement>;
|
||||
|
||||
for (const element of elements) {
|
||||
if (
|
||||
!(element instanceof HTMLElement) &&
|
||||
!(element instanceof SVGElement)
|
||||
)
|
||||
continue;
|
||||
|
||||
const parts = rule.selectorText.split(",");
|
||||
const styles: SpecifiedStyle[] = styled.get(element) || [];
|
||||
styled.set(element, styles);
|
||||
|
||||
for (const part of parts) {
|
||||
try {
|
||||
const specificity = calculate(part)[0];
|
||||
styles.push({
|
||||
specificity: specificity.specificityArray,
|
||||
rule: rule.style,
|
||||
href: getBaseUrl(href)
|
||||
});
|
||||
break;
|
||||
} catch (e) {
|
||||
console.error(e, href && getBaseUrl(href));
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
rule instanceof CSSMediaRule &&
|
||||
window.matchMedia(rule.conditionText).matches
|
||||
) {
|
||||
walkRules(rule.cssRules, documentStyles, styled, pseudoElements, href);
|
||||
} else if (
|
||||
rule instanceof CSSSupportsRule &&
|
||||
CSS.supports(rule.conditionText)
|
||||
) {
|
||||
walkRules(rule.cssRules, documentStyles, styled, pseudoElements, href);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getElementStyles(
|
||||
element: StyleableElement,
|
||||
styles: BaseStyle[],
|
||||
documentStyles: CSSStyleDeclaration
|
||||
) {
|
||||
const newStyles = newStyleDeclaration();
|
||||
const computedStyle = lazyComputedStyle(element);
|
||||
const overrides = ["display"];
|
||||
|
||||
for (const style of styles) {
|
||||
for (const property of [...style.rule, ...SHORTHANDS]) {
|
||||
let value = style.rule.getPropertyValue(property);
|
||||
if (overrides.includes(property))
|
||||
value = computedStyle.style.getPropertyValue(property);
|
||||
|
||||
if (value.trim()) {
|
||||
setStyle(
|
||||
newStyles,
|
||||
property,
|
||||
value,
|
||||
(variable) => {
|
||||
return (
|
||||
computedStyle.style.getPropertyValue(variable) ||
|
||||
documentStyles.getPropertyValue(variable)
|
||||
);
|
||||
},
|
||||
(url) => {
|
||||
console.log("resolving url", url, style.href);
|
||||
if (url.startsWith("data:") || !style.href) return url;
|
||||
console.log("resolving url", url, style.href.href);
|
||||
if (url.startsWith("/"))
|
||||
return new URL(`${style.href.origin}${url}`).href;
|
||||
|
||||
return new URL(`${style.href.href}${url}`).href;
|
||||
},
|
||||
style.rule.getPropertyPriority(property)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newStyles;
|
||||
}
|
||||
|
||||
function setStyle(
|
||||
target: CSSStyleDeclaration,
|
||||
property: string,
|
||||
value: string,
|
||||
get: (variable: string) => string,
|
||||
resolveUrl: (variable: string) => string,
|
||||
priority?: string
|
||||
) {
|
||||
value = resolveCssVariables(value, get);
|
||||
value = resolveCssUrl(value, resolveUrl);
|
||||
|
||||
target.setProperty(property, value, priority);
|
||||
}
|
||||
|
||||
function newStyleDeclaration() {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.insertRule(".dummy{}");
|
||||
return (sheet.cssRules[0] as CSSStyleRule).style;
|
||||
}
|
||||
|
||||
function lazyComputedStyle(element: StyleableElement) {
|
||||
let computedStyle: CSSStyleDeclaration | undefined;
|
||||
|
||||
return Object.defineProperty({}, "style", {
|
||||
get: () => {
|
||||
if (!computedStyle) computedStyle = getComputedStyle(element);
|
||||
return computedStyle;
|
||||
}
|
||||
}) as { style: CSSStyleDeclaration };
|
||||
}
|
||||
|
||||
async function skipStyleSheet(sheet: CSSStyleSheet, options?: FetchOptions) {
|
||||
try {
|
||||
sheet.cssRules.length;
|
||||
} catch (_e) {
|
||||
const node = sheet.ownerNode;
|
||||
if (sheet.href && node instanceof HTMLLinkElement) {
|
||||
if (isStylesheetForPrint(node)) return true;
|
||||
|
||||
const styleNode = await downloadStylesheet(node.href, options);
|
||||
if (styleNode) node.replaceWith(styleNode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return sheet.media.mediaText
|
||||
return isStylesheetForPrint(sheet);
|
||||
}
|
||||
|
||||
function isStylesheetForPrint(sheet: CSSStyleSheet | HTMLLinkElement) {
|
||||
const mediaText =
|
||||
typeof sheet.media === "string" ? sheet.media : sheet.media.mediaText;
|
||||
return mediaText
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.includes("print");
|
||||
}
|
||||
|
||||
function resolveCssVariables(css: string, get: (variable: string) => string) {
|
||||
const tokens = tokenize(css);
|
||||
const finalTokens: string[] = [];
|
||||
for (let i = 0; i < tokens.length; ++i) {
|
||||
const token = tokens[i];
|
||||
if (token === "var") {
|
||||
const args = tokenize(tokens[++i].slice(1, -1));
|
||||
const [variable, operator, space, ...restArgs] = args;
|
||||
export async function addStylesToHead(
|
||||
head: HTMLHeadElement,
|
||||
options?: FetchOptions
|
||||
) {
|
||||
await resolveImports(options);
|
||||
|
||||
const value = get(variable);
|
||||
if (value) {
|
||||
finalTokens.push(value);
|
||||
} else if (operator && restArgs.length <= 1) {
|
||||
finalTokens.push(restArgs[0] || space);
|
||||
} else if (operator && restArgs.length === 2) {
|
||||
finalTokens.push(resolveCssVariables(restArgs.join(""), get));
|
||||
for (const sheet of document.styleSheets) {
|
||||
if (isStylesheetForPrint(sheet)) continue;
|
||||
|
||||
const href =
|
||||
sheet.href && sheet.ownerNode instanceof HTMLLinkElement
|
||||
? sheet.ownerNode.href
|
||||
: sheet.ownerNode instanceof HTMLStyleElement
|
||||
? sheet.ownerNode.getAttribute("href")
|
||||
: null;
|
||||
if (href) {
|
||||
const result = await downloadStylesheet(href, options);
|
||||
if (!result) continue;
|
||||
const cssStylesheet = new CSSStyleSheet();
|
||||
await cssStylesheet.replace(result.innerHTML);
|
||||
await inlineBackgroundImages(cssStylesheet, options);
|
||||
const toAppend = rulesToStyleNode(cssStylesheet.cssRules);
|
||||
head.appendChild(toAppend);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sheet.cssRules.length > 0) {
|
||||
await inlineBackgroundImages(sheet, options);
|
||||
const styleNode = rulesToStyleNode(sheet.cssRules);
|
||||
head.appendChild(styleNode);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sheet.ownerNode instanceof HTMLStyleElement) {
|
||||
head.appendChild(sheet.ownerNode.cloneNode(true));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rulesToStyleNode(cssRules: CSSRuleList) {
|
||||
const cssText = Array.from(cssRules)
|
||||
.map((r) => r.cssText)
|
||||
.reduce((acc, text) => acc + text, "");
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = cssText;
|
||||
return style;
|
||||
}
|
||||
|
||||
async function inlineBackgroundImages(
|
||||
sheet: CSSStyleSheet,
|
||||
options?: FetchOptions
|
||||
) {
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const rule of sheet.cssRules) {
|
||||
if (rule.type === CSSRule.STYLE_RULE) {
|
||||
promises.push(processStyleRule(sheet, rule as CSSStyleRule, options));
|
||||
} else if (rule.type === CSSRule.MEDIA_RULE) {
|
||||
const mediaRule = rule as CSSMediaRule;
|
||||
const mediaMatches = window.matchMedia(mediaRule.media.mediaText).matches;
|
||||
for (const innerRule of mediaRule.cssRules) {
|
||||
if (innerRule && innerRule.type === CSSRule.STYLE_RULE) {
|
||||
promises.push(
|
||||
processStyleRule(
|
||||
sheet,
|
||||
innerRule as CSSStyleRule,
|
||||
options,
|
||||
mediaMatches
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (token.startsWith("(") && token.endsWith(")")) {
|
||||
finalTokens.push("(", resolveCssVariables(token.slice(1, -1), get), ")");
|
||||
} else finalTokens.push(token);
|
||||
}
|
||||
return finalTokens.join("");
|
||||
}
|
||||
|
||||
function resolveCssUrl(css: string, get: (url: string) => string) {
|
||||
const tokens = tokenize(css);
|
||||
const finalTokens: string[] = [];
|
||||
for (let i = 0; i < tokens.length; ++i) {
|
||||
const token = tokens[i];
|
||||
if (token === "url" && !tokens[i + 1].startsWith("(data")) {
|
||||
const url = tokens[++i].slice(2, -2);
|
||||
const resolvedUrl = get(url);
|
||||
if (resolvedUrl) {
|
||||
finalTokens.push(token);
|
||||
finalTokens.push('("');
|
||||
finalTokens.push(resolvedUrl);
|
||||
finalTokens.push('")');
|
||||
} else if (rule.type === CSSRule.SUPPORTS_RULE) {
|
||||
const supportsRule = rule as CSSSupportsRule;
|
||||
for (const innerRule of supportsRule.cssRules) {
|
||||
if (innerRule && innerRule.type === CSSRule.STYLE_RULE) {
|
||||
promises.push(
|
||||
processStyleRule(sheet, innerRule as CSSStyleRule, options, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else finalTokens.push(token);
|
||||
}
|
||||
}
|
||||
return finalTokens.join("");
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
function getBaseUrl(href?: string | null) {
|
||||
if (!href) return null;
|
||||
if (href.startsWith("/")) href = `${document.location.origin}${href}`;
|
||||
const url = new URL(href);
|
||||
const basepath = url.pathname.split("/").slice(0, -1).join("/");
|
||||
return new URL(`${url.origin}${basepath}/`);
|
||||
}
|
||||
|
||||
function isPseudoSelector(text: string) {
|
||||
return (
|
||||
text.includes(":before") ||
|
||||
text.includes(":after") ||
|
||||
text.includes("::after") ||
|
||||
text.includes("::before")
|
||||
);
|
||||
}
|
||||
|
||||
function parsePseudoSelector(selector: string) {
|
||||
const output = [];
|
||||
const selectors = parse(selector);
|
||||
for (const part of selectors) {
|
||||
const pseduoElementIndex = part.findIndex(
|
||||
(s) =>
|
||||
(s.type === SelectorType.Pseudo ||
|
||||
s.type === SelectorType.PseudoElement) &&
|
||||
(s.name === "after" || s.name === "before")
|
||||
);
|
||||
if (pseduoElementIndex <= -1) continue;
|
||||
|
||||
output.push({
|
||||
selector: stringify([part.slice(0, pseduoElementIndex)]),
|
||||
pseudoElement: stringify([part.slice(pseduoElementIndex)])
|
||||
});
|
||||
async function processStyleRule(
|
||||
sheet: CSSStyleSheet,
|
||||
rule: CSSStyleRule,
|
||||
options?: FetchOptions,
|
||||
inline = true
|
||||
) {
|
||||
const baseUrl = sheet.href || document.location.href;
|
||||
for (const property of rule.style) {
|
||||
const oldValue = rule.style.getPropertyValue(property);
|
||||
if (!oldValue) continue;
|
||||
const resolved = await inlineAll(oldValue, options, baseUrl, !inline);
|
||||
rule.style.setProperty(property, resolved);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ export type Options = {
|
||||
scale?: number;
|
||||
fetchOptions?: FetchOptions;
|
||||
inlineOptions?: InlineOptions;
|
||||
styles?: boolean;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
|
||||
@@ -24,6 +24,85 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
const WOFF = "application/font-woff";
|
||||
const JPEG = "image/jpeg";
|
||||
|
||||
const SVGElements = [
|
||||
"altGlyph",
|
||||
"altGlyphDef",
|
||||
"altGlyphItem",
|
||||
"animate",
|
||||
"animateColor",
|
||||
"animateMotion",
|
||||
"animateTransform",
|
||||
"circle",
|
||||
"clipPath",
|
||||
"color-profile",
|
||||
"cursor",
|
||||
"defs",
|
||||
"desc",
|
||||
"ellipse",
|
||||
"feBlend",
|
||||
"feColorMatrix",
|
||||
"feComponentTransfer",
|
||||
"feComposite",
|
||||
"feConvolveMatrix",
|
||||
"feDiffuseLighting",
|
||||
"feDisplacementMap",
|
||||
"feDistantLight",
|
||||
"feFlood",
|
||||
"feFuncA",
|
||||
"feFuncB",
|
||||
"feFuncG",
|
||||
"feFuncR",
|
||||
"feGaussianBlur",
|
||||
"feImage",
|
||||
"feMerge",
|
||||
"feMergeNode",
|
||||
"feMorphology",
|
||||
"feOffset",
|
||||
"fePointLight",
|
||||
"feSpecularLighting",
|
||||
"feSpotLight",
|
||||
"feTile",
|
||||
"feTurbulence",
|
||||
"filter",
|
||||
"font-face",
|
||||
"font-face-format",
|
||||
"font-face-name",
|
||||
"font-face-src",
|
||||
"font-face-uri",
|
||||
"foreignObject",
|
||||
"g",
|
||||
"glyph",
|
||||
"glyphRef",
|
||||
"hkern",
|
||||
"image",
|
||||
"line",
|
||||
"linearGradient",
|
||||
"marker",
|
||||
"mask",
|
||||
"metadata",
|
||||
"missing-glyph",
|
||||
"mpath",
|
||||
"path",
|
||||
"pattern",
|
||||
"polygon",
|
||||
"polyline",
|
||||
"radialGradient",
|
||||
"rect",
|
||||
"set",
|
||||
"stop",
|
||||
"svg",
|
||||
"switch",
|
||||
"symbol",
|
||||
"text",
|
||||
"textPath",
|
||||
"title",
|
||||
"tref",
|
||||
"tspan",
|
||||
"use",
|
||||
"view",
|
||||
"vkern"
|
||||
].map((a) => a.toLowerCase());
|
||||
|
||||
const mimes = {
|
||||
woff: WOFF,
|
||||
woff2: WOFF,
|
||||
@@ -165,12 +244,9 @@ function getRootStylesheet() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function safeQuerySelectorAll(root: Node, selector: string) {
|
||||
try {
|
||||
return (root as HTMLElement).querySelectorAll(selector);
|
||||
} catch (e) {
|
||||
return new NodeList();
|
||||
}
|
||||
function isSVGElement(element: HTMLElement) {
|
||||
if (!element || !element.tagName) return false;
|
||||
return SVGElements.includes(element.tagName.toLowerCase());
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -188,5 +264,5 @@ export {
|
||||
escapeXhtml,
|
||||
width,
|
||||
height,
|
||||
safeQuerySelectorAll
|
||||
isSVGElement
|
||||
};
|
||||
|
||||
@@ -139,3 +139,18 @@ test("search reminders", () =>
|
||||
const descriptionSearch = await db.lookup.reminders("please do").ids();
|
||||
expect(descriptionSearch).toHaveLength(1);
|
||||
}));
|
||||
|
||||
describe("notesWithHighlighting", () => {
|
||||
test("search notes with parentheses in query should load the item", () =>
|
||||
noteTest({
|
||||
title: "(with parantheses)"
|
||||
}).then(async ({ db }) => {
|
||||
await db.notes.add(TEST_NOTE);
|
||||
const filtered = await db.lookup.notesWithHighlighting(
|
||||
"(with parantheses)",
|
||||
db.notes.all
|
||||
);
|
||||
const item = await filtered.item(0);
|
||||
expect(item.item).toBeDefined();
|
||||
}));
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user