mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 11:39:21 +02:00
Compare commits
13 Commits
feature/di
...
mobile-rel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
747a2e2bf4 | ||
|
|
57e9cd3e6a | ||
|
|
0ec83fd389 | ||
|
|
d23661c0b8 | ||
|
|
11200b5c3a | ||
|
|
c4acb23164 | ||
|
|
c1d9427e64 | ||
|
|
e611261a07 | ||
|
|
7ccfba67e5 | ||
|
|
122df1bb35 | ||
|
|
73e038540a | ||
|
|
5be7c7f456 | ||
|
|
ae27b04fdb |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -11,5 +11,5 @@
|
||||
"cache": true,
|
||||
"cacheStrategy": "content"
|
||||
},
|
||||
"git.rebaseWhenSync": true
|
||||
"git.rebaseWhenSync": false
|
||||
}
|
||||
|
||||
577
apps/desktop/package-lock.json
generated
577
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.6-beta.2",
|
||||
"version": "3.3.6-beta.3",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
|
||||
@@ -125,7 +125,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3084
|
||||
versionCode 3085
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -324,7 +324,9 @@ export const Signup = ({
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
|
||||
@@ -43,7 +43,7 @@ export const Card = ({
|
||||
const fontScale = Dimensions.get("window").fontScale;
|
||||
|
||||
return !messageBoardState.visible ||
|
||||
(announcements && announcements.length) ? null : (
|
||||
(announcements && announcements.length && !customMessage) ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { SubscriptionPlan } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import dayjs from "dayjs";
|
||||
import React from "react";
|
||||
import { FlatList, View } from "react-native";
|
||||
import { DraxProvider, DraxScrollView } from "react-native-drax";
|
||||
@@ -140,19 +141,35 @@ export function SideMenuHome() {
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
{(subscriptionType === SubscriptionPlan.FREE ||
|
||||
!subscriptionType ||
|
||||
!user) &&
|
||||
!SettingsService.getProperty("serverUrls") ? (
|
||||
{dayjs().month() !== 11 ? (
|
||||
<>
|
||||
{(subscriptionType === SubscriptionPlan.FREE ||
|
||||
!subscriptionType ||
|
||||
!user) &&
|
||||
!SettingsService.getProperty("serverUrls") ? (
|
||||
<Button
|
||||
title={pro.title}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
type="accent"
|
||||
onPress={pro.onPress}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
title={pro.title}
|
||||
title={`Wrapped ${dayjs().year()} 🎉`}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
type="accent"
|
||||
onPress={pro.onPress}
|
||||
bold
|
||||
type="secondaryAccented"
|
||||
onPress={() => {
|
||||
Navigation.navigate("Wrapped");
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -273,6 +273,7 @@ let Settings: any = null;
|
||||
let ManageTags: any = null;
|
||||
let AddReminder: any = null;
|
||||
let PayWall: any = null;
|
||||
let Wrapped: any = null;
|
||||
export const RootNavigation = () => {
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
@@ -384,6 +385,14 @@ export const RootNavigation = () => {
|
||||
return PayWall;
|
||||
}}
|
||||
/>
|
||||
|
||||
<RootStack.Screen
|
||||
name="Wrapped"
|
||||
getComponent={() => {
|
||||
Wrapped = Wrapped || require("../screens/wrapped").default;
|
||||
return Wrapped;
|
||||
}}
|
||||
/>
|
||||
</RootStack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
1199
apps/mobile/app/screens/wrapped/index.tsx
Normal file
1199
apps/mobile/app/screens/wrapped/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,8 @@ const routeNames = {
|
||||
Archive: "Archive",
|
||||
ManageTags: "ManageTags",
|
||||
AddReminder: "AddReminder",
|
||||
PayWall: "PayWall"
|
||||
PayWall: "PayWall",
|
||||
Wrapped: "Wrapped"
|
||||
};
|
||||
|
||||
export type NavigationProps<T extends RouteName> = NativeStackScreenProps<
|
||||
|
||||
@@ -112,6 +112,7 @@ export interface RouteParams extends ParamListBase {
|
||||
context: "signup" | "logged-in" | "logged-out" | "subscribed";
|
||||
state?: BillingState;
|
||||
};
|
||||
Wrapped: GenericRouteParam;
|
||||
}
|
||||
|
||||
export type RouteName = keyof RouteParams;
|
||||
|
||||
@@ -1029,7 +1029,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2164;
|
||||
CURRENT_PROJECT_VERSION = 2165;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1104,7 +1104,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "3.3.10";
|
||||
MARKETING_VERSION = 3.3.10;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1135,7 +1135,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2164;
|
||||
CURRENT_PROJECT_VERSION = 2165;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1210,7 +1210,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "3.3.10";
|
||||
MARKETING_VERSION = 3.3.10;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1367,7 +1367,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2164;
|
||||
CURRENT_PROJECT_VERSION = 2165;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1379,7 +1379,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "3.3.10";
|
||||
MARKETING_VERSION = 3.3.10;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1410,7 +1410,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2164;
|
||||
CURRENT_PROJECT_VERSION = 2165;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1423,7 +1423,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "3.3.10";
|
||||
MARKETING_VERSION = 3.3.10;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1453,7 +1453,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2164;
|
||||
CURRENT_PROJECT_VERSION = 2165;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1534,7 +1534,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
|
||||
MARKETING_VERSION = "3.3.10";
|
||||
MARKETING_VERSION = 3.3.10;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1565,7 +1565,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2164;
|
||||
CURRENT_PROJECT_VERSION = 2165;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1647,7 +1647,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
|
||||
MARKETING_VERSION = "3.3.10";
|
||||
MARKETING_VERSION = 3.3.10;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -2246,6 +2246,34 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-upload (6.28.0):
|
||||
- React
|
||||
- react-native-view-shot (4.0.3):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
- fmt
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- react-native-webview (13.16.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@@ -3465,6 +3493,7 @@ DEPENDENCIES:
|
||||
- "react-native-sodium (from `../node_modules/@ammarahmed/react-native-sodium`)"
|
||||
- react-native-theme-switch-animation (from `../node_modules/react-native-theme-switch-animation`)
|
||||
- "react-native-upload (from `../node_modules/@ammarahmed/react-native-upload`)"
|
||||
- react-native-view-shot (from `../node_modules/react-native-view-shot`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
|
||||
@@ -3680,6 +3709,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-theme-switch-animation"
|
||||
react-native-upload:
|
||||
:path: "../node_modules/@ammarahmed/react-native-upload"
|
||||
react-native-view-shot:
|
||||
:path: "../node_modules/react-native-view-shot"
|
||||
react-native-webview:
|
||||
:path: "../node_modules/react-native-webview"
|
||||
React-NativeModulesApple:
|
||||
@@ -3881,6 +3912,7 @@ SPEC CHECKSUMS:
|
||||
react-native-sodium: 066f76e46c9be13e9260521e3fa994937c4cdab4
|
||||
react-native-theme-switch-animation: 449d6db7a760f55740505e7403ae8061debc9a7e
|
||||
react-native-upload: ddf12a152c62fcafa202ef0404d3d46333a6a6a6
|
||||
react-native-view-shot: 6c008e58f4720de58370848201c5d4a082c6d4ca
|
||||
react-native-webview: 654f794a7686b47491cf43aa67f7f428bea00eed
|
||||
React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0
|
||||
React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb
|
||||
|
||||
68
apps/mobile/package-lock.json
generated
68
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.10-beta.4",
|
||||
"version": "3.3.10-beta.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.10-beta.4",
|
||||
"version": "3.3.10-beta.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -123,6 +123,7 @@
|
||||
"react-native-tooltips": "^1.0.3",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-vector-icons": "10.3.0",
|
||||
"react-native-view-shot": "^4.0.3",
|
||||
"react-native-webview": "^13.13.5",
|
||||
"react-native-wheel-color-picker": "^1.3.1",
|
||||
"react-native-worklets": "^0.7.1",
|
||||
@@ -228,6 +229,7 @@
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@streetwriters/showdown": "^3.0.9-alpha",
|
||||
"@types/mime-db": "^1.43.5",
|
||||
"alfaaz": "^1.1.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"dayjs": "1.11.13",
|
||||
"dom-serializer": "^2.0.0",
|
||||
@@ -7498,6 +7500,15 @@
|
||||
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
|
||||
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -8554,6 +8565,15 @@
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||
@@ -11634,6 +11654,19 @@
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||
@@ -18066,6 +18099,19 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-view-shot": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-4.0.3.tgz",
|
||||
"integrity": "sha512-USNjYmED7C0me02c1DxKA0074Hw+y/nxo+xJKlffMvfUWWzL5ELh/TJA/pTnVqFurIrzthZDPtDM7aBFJuhrHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"html2canvas": "^1.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webview": {
|
||||
"version": "13.16.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.0.tgz",
|
||||
@@ -20279,6 +20325,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/thingies": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz",
|
||||
@@ -21090,6 +21145,15 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.10-beta.5",
|
||||
"version": "3.3.10-beta.6",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
@@ -139,6 +139,7 @@
|
||||
"react-native-tooltips": "^1.0.3",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-vector-icons": "10.3.0",
|
||||
"react-native-view-shot": "^4.0.3",
|
||||
"react-native-webview": "^13.13.5",
|
||||
"react-native-wheel-color-picker": "^1.3.1",
|
||||
"react-native-worklets": "^0.7.1",
|
||||
|
||||
1452
apps/web/package-lock.json
generated
1452
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.6-beta.2",
|
||||
"version": "3.3.6-beta.3",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -57,6 +57,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"hash-wasm": "4.12.0",
|
||||
"hotkeys-js": "^3.8.3",
|
||||
"htmlparser2": "^10.0.0",
|
||||
"katex": "0.16.11",
|
||||
"mac-scrollbar": "0.13.6",
|
||||
"mutative": "^1.1.0",
|
||||
|
||||
@@ -48,6 +48,7 @@ import { getFontSizes } from "@notesnook/theme/theme/font/fontsize.js";
|
||||
import { useWindowControls } from "./hooks/use-window-controls";
|
||||
import { STATUS_BAR_HEIGHT } from "./common/constants";
|
||||
import { NavigationEvents } from "./navigation";
|
||||
import { db } from "./common/db";
|
||||
|
||||
new WebExtensionRelay();
|
||||
|
||||
@@ -143,6 +144,10 @@ function DesktopAppContents() {
|
||||
const isTablet = useTablet();
|
||||
const navPane = useRef<SplitPaneImperativeHandle>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTablet) navPane.current?.collapse(0);
|
||||
else if (navPane.current?.isCollapsed(0)) navPane.current?.expand(0);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
isFeatureSupported
|
||||
} from "./utils/feature-check";
|
||||
import { initializeLogger } from "./utils/logger";
|
||||
import { shouldShowWrapped } from "./utils/should-show-wrapped";
|
||||
|
||||
type Route<TProps = null> = {
|
||||
component: () => Promise<{
|
||||
@@ -55,6 +56,9 @@ const routes = {
|
||||
"/plans": {
|
||||
component: () => import("./views/plans")
|
||||
},
|
||||
"/wrapped": {
|
||||
component: () => import("./views/wrapped")
|
||||
},
|
||||
"/checkout": {
|
||||
component: () => import("./views/checkout")
|
||||
},
|
||||
@@ -122,6 +126,12 @@ function getRoute(): RouteWithPath<AuthProps> | RouteWithPath {
|
||||
routes[path] ? { route: routes[path], path } : null
|
||||
) as RouteWithPath<AuthProps> | null;
|
||||
|
||||
if (route?.path === "/wrapped" && !shouldShowWrapped())
|
||||
return {
|
||||
route: routes.default,
|
||||
path: "default"
|
||||
};
|
||||
|
||||
return signup || sessionExpired || route || fallback;
|
||||
}
|
||||
|
||||
|
||||
@@ -850,17 +850,27 @@ function useDragOverlay() {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
dropElement.addEventListener("dragenter", showOverlay);
|
||||
overlay.addEventListener("drop", hideOverlay);
|
||||
overlay.addEventListener("dragenter", allowDrag);
|
||||
overlay.addEventListener("dragover", allowDrag);
|
||||
overlay.addEventListener("dragleave", hideOverlay);
|
||||
overlay.addEventListener("click", hideOverlay);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
dropElement.removeEventListener("dragenter", showOverlay);
|
||||
overlay.removeEventListener("drop", hideOverlay);
|
||||
overlay.removeEventListener("dragenter", allowDrag);
|
||||
overlay.removeEventListener("dragover", allowDrag);
|
||||
overlay.removeEventListener("dragleave", hideOverlay);
|
||||
overlay.removeEventListener("click", hideOverlay);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
143
apps/web/src/components/monthly-activity-heatmap/index.tsx
Normal file
143
apps/web/src/components/monthly-activity-heatmap/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Box, Flex, Text } from "@theme-ui/components";
|
||||
import { SxProp } from "@theme-ui/core";
|
||||
|
||||
type MonthlyActivityHeatmapProps = {
|
||||
monthlyStats: Record<string, number>;
|
||||
year?: number;
|
||||
} & SxProp;
|
||||
|
||||
const MONTHS = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December"
|
||||
];
|
||||
|
||||
const MONTHS_SHORT = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec"
|
||||
];
|
||||
|
||||
export function MonthlyActivityHeatmap({
|
||||
monthlyStats
|
||||
}: MonthlyActivityHeatmapProps) {
|
||||
// Find max count for bar height calculation
|
||||
const maxCount = Math.max(...Object.values(monthlyStats), 1);
|
||||
|
||||
// Create month data with counts
|
||||
const monthData = MONTHS.map((month) => ({
|
||||
month,
|
||||
count: monthlyStats[month] || 0
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Bar chart */}
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "space-between",
|
||||
gap: 2,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
pb: 2
|
||||
}}
|
||||
>
|
||||
{monthData.map((data, index) => {
|
||||
const barHeight = maxCount > 0 ? (data.count / maxCount) * 100 : 0;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={data.month}
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
cursor: "pointer",
|
||||
"&:hover .tooltip": { opacity: 1 },
|
||||
"&:hover .bar": { bg: "accent" },
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
className="tooltip"
|
||||
sx={{ opacity: 0, bg: "background", zIndex: 1000 }}
|
||||
>
|
||||
<Text variant="body" sx={{ fontWeight: "bold" }}>
|
||||
{data.count}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box
|
||||
className="bar"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: `${barHeight}%`,
|
||||
minHeight: data.count > 0 ? "10px" : "5px",
|
||||
bg: maxCount === data.count ? "accent" : "paragraph",
|
||||
borderRadius: "4px 4px 0 0",
|
||||
transition: "all 0.3s ease",
|
||||
position: "relative",
|
||||
"&:hover": {
|
||||
transform: "scaleY(1.05)",
|
||||
transformOrigin: "bottom"
|
||||
}
|
||||
}}
|
||||
></Box>
|
||||
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "11px",
|
||||
color: "paragraph-secondary",
|
||||
fontWeight: "normal",
|
||||
transition: "all 0.3s ease"
|
||||
}}
|
||||
>
|
||||
{MONTHS_SHORT[index]}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -97,7 +97,6 @@ import { strings } from "@notesnook/intl";
|
||||
import Tags from "../../views/tags";
|
||||
import { Notebooks } from "../../views/notebooks";
|
||||
import { UserProfile } from "../../dialogs/settings/components/user-profile";
|
||||
import { SUBSCRIPTION_STATUS } from "../../common/constants";
|
||||
import {
|
||||
checkFeature,
|
||||
createSetDefaultHomepageMenuItem,
|
||||
@@ -105,7 +104,6 @@ import {
|
||||
withFeatureCheck
|
||||
} from "../../common";
|
||||
import { TabItem } from "./tab-item";
|
||||
import Notice from "../notice";
|
||||
import { Freeze } from "react-freeze";
|
||||
import { CREATE_BUTTON_MAP } from "../../common";
|
||||
import { useStore as useNotebookStore } from "../../stores/notebook-store";
|
||||
@@ -118,6 +116,7 @@ import {
|
||||
useIsFeatureAvailable
|
||||
} from "@notesnook/common";
|
||||
import { isUserSubscribed } from "../../hooks/use-is-user-premium";
|
||||
import { shouldShowWrapped } from "../../utils/should-show-wrapped";
|
||||
|
||||
type Route = {
|
||||
id: "notes" | "favorites" | "reminders" | "monographs" | "trash" | "archive";
|
||||
@@ -449,7 +448,17 @@ function NavigationMenu({ onExpand }: { onExpand?: () => void }) {
|
||||
</FlexScrollContainer>
|
||||
</Freeze>
|
||||
</Flex>
|
||||
{currentTab.id === "home" && !isCollapsed ? <Notice /> : null}
|
||||
{currentTab.id === "home" && !isCollapsed && shouldShowWrapped() ? (
|
||||
<Button
|
||||
variant="accent"
|
||||
sx={{ m: 2 }}
|
||||
onClick={() => {
|
||||
hardNavigate("/wrapped");
|
||||
}}
|
||||
>
|
||||
🎉 Wrapped {new Date().getFullYear()}
|
||||
</Button>
|
||||
) : null}
|
||||
</ScopedThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
23
apps/web/src/utils/should-show-wrapped.ts
Normal file
23
apps/web/src/utils/should-show-wrapped.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
export const shouldShowWrapped = () => {
|
||||
const now = new Date();
|
||||
return now.getMonth() === 11;
|
||||
};
|
||||
776
apps/web/src/views/wrapped.tsx
Normal file
776
apps/web/src/views/wrapped.tsx
Normal file
@@ -0,0 +1,776 @@
|
||||
/*
|
||||
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 { Button, Flex, Text, Box, FlexProps } from "@theme-ui/components";
|
||||
import { db } from "../common/db";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { NoteStats, WrappedStats } from "@notesnook/core";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { ArrowDown, ArrowLeft, Loading } from "../components/icons";
|
||||
import { hardNavigate } from "../navigation";
|
||||
import { MonthlyActivityHeatmap } from "../components/monthly-activity-heatmap";
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return num.toLocaleString();
|
||||
}
|
||||
function formatCount(num: number) {
|
||||
return num > 1000 ? `${(num / 1000).toFixed(0)}k` : num.toString();
|
||||
}
|
||||
|
||||
interface SlideProps {
|
||||
children: React.ReactNode;
|
||||
pattern?: "dots" | "grid" | "diagonal" | "none";
|
||||
}
|
||||
|
||||
function Slide({
|
||||
children,
|
||||
pattern = "none",
|
||||
sx,
|
||||
...flexProps
|
||||
}: SlideProps & FlexProps) {
|
||||
const slideRef = useRef<HTMLDivElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
);
|
||||
|
||||
if (slideRef.current) {
|
||||
observer.observe(slideRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (slideRef.current) {
|
||||
observer.unobserve(slideRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getBackgroundPattern = () => {
|
||||
switch (pattern) {
|
||||
case "dots":
|
||||
return "radial-gradient(circle, var(--border) 1px, transparent 1px)";
|
||||
case "grid":
|
||||
return "linear-gradient(var(--border) 1px, transparent 1px), linear-gradient(90deg, var(--border) 1px, transparent 1px)";
|
||||
case "diagonal":
|
||||
return "repeating-linear-gradient(-45deg, transparent, transparent 20px, var(--border) 20px, var(--border) 21px)";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
};
|
||||
|
||||
const getBackgroundSize = () => {
|
||||
switch (pattern) {
|
||||
case "dots":
|
||||
return "20px 20px";
|
||||
case "grid":
|
||||
return "30px 30px";
|
||||
case "diagonal":
|
||||
default:
|
||||
return "auto";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={slideRef}
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
minWidth: "100%",
|
||||
scrollSnapAlign: "start",
|
||||
scrollSnapStop: "always",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
p: 4,
|
||||
position: "relative",
|
||||
backgroundImage: getBackgroundPattern(),
|
||||
backgroundSize: getBackgroundSize(),
|
||||
backgroundPosition: "center"
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
{...flexProps}
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
opacity: isVisible ? 1 : 0,
|
||||
transform: isVisible ? "translateY(0)" : "translateY(50px)",
|
||||
transition: "opacity 0.8s ease-out, transform 0.8s ease-out",
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function WelcomeSlide({ loading }: { loading: boolean }) {
|
||||
return (
|
||||
<Slide pattern="dots">
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "6rem",
|
||||
fontWeight: "bold",
|
||||
mb: 3,
|
||||
textAlign: "center",
|
||||
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
animation: "slideUp 1s ease-out"
|
||||
}}
|
||||
>
|
||||
🎉
|
||||
</Text>
|
||||
<Text
|
||||
variant="heading"
|
||||
sx={{
|
||||
fontSize: ["2rem", "3rem", "4rem"],
|
||||
fontWeight: "bold",
|
||||
mb: 2,
|
||||
textAlign: "center",
|
||||
animation: "slideUp 1s ease-out 0.2s both"
|
||||
}}
|
||||
>
|
||||
Your {new Date().getFullYear()} Wrapped
|
||||
</Text>
|
||||
|
||||
{loading ? (
|
||||
<Loading
|
||||
sx={{
|
||||
animation: "fadeIn 1s ease-out 0.4s both"
|
||||
}}
|
||||
size={30}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: ["1rem", "1.2rem"],
|
||||
textAlign: "center",
|
||||
animation: "fadeIn 1s ease-out 0.4s both"
|
||||
}}
|
||||
>
|
||||
Let's look back at your year in Notesnook
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
mt: 4,
|
||||
fontSize: "title",
|
||||
color: "paragraph-secondary",
|
||||
textAlign: "center",
|
||||
animation: "fadeIn 1s ease-out 0.4s both"
|
||||
}}
|
||||
>
|
||||
Scroll down to explore
|
||||
</Text>
|
||||
<ArrowDown sx={{ mt: 2 }} color="paragraph-secondary" />
|
||||
</>
|
||||
)}
|
||||
</Slide>
|
||||
);
|
||||
}
|
||||
|
||||
function TotalNotesSlide({ count }: { count: number }) {
|
||||
return (
|
||||
<Slide pattern="dots">
|
||||
<Flex>
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: ["1.2rem", "1.5rem"],
|
||||
textAlign: "center",
|
||||
animation: "fadeIn 0.8s ease-out"
|
||||
}}
|
||||
>
|
||||
You created
|
||||
</Text>
|
||||
<Text
|
||||
variant="heading"
|
||||
sx={{
|
||||
fontSize: ["5rem", "7rem", "8rem"],
|
||||
textAlign: "center",
|
||||
color: "accent",
|
||||
animation: "scaleIn 0.8s ease-out 0.2s both"
|
||||
}}
|
||||
>
|
||||
{formatNumber(count)}
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: ["1.2rem", "1.5rem"],
|
||||
textAlign: "center",
|
||||
animation: "fadeIn 0.8s ease-out 0.4s both"
|
||||
}}
|
||||
>
|
||||
notes this year
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
ml: [0, 5],
|
||||
mt: [4, 0],
|
||||
borderLeft: ["none", "1px solid var(--border)"],
|
||||
pl: [0, 5],
|
||||
borderTop: ["1px solid var(--border)", "none"]
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
color="paragraph-secondary"
|
||||
sx={{
|
||||
fontSize: ["1.2rem", "1.2rem"],
|
||||
animation: "fadeIn 0.8s ease-out",
|
||||
lineHeight: 1.8
|
||||
}}
|
||||
>
|
||||
That's <strong>{formatNumber(count)}</strong>
|
||||
<br />
|
||||
ideas
|
||||
<br />
|
||||
thoughts
|
||||
<br />
|
||||
memories.
|
||||
<br />
|
||||
100% encrypted.
|
||||
<br />
|
||||
100% yours.
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Slide>
|
||||
);
|
||||
}
|
||||
|
||||
const TOTAL_WORDS_TAGLINES = {
|
||||
1000: "That's longer than the average blog post on Medium!",
|
||||
10000: "That's real commitment to your mindspace.",
|
||||
25000: "That's almost the length of a short novel!",
|
||||
50000:
|
||||
"That's the length of The Great Gatsby if you were F. Scott Fitzgerald.",
|
||||
100000: "You wrote more than many published authors this year.",
|
||||
250000: "Woah! Your notes could fill a small library.",
|
||||
500000: "Your vault is becoming a chronicle.",
|
||||
1000000: "That's longer than the entire Harry Potter series!"
|
||||
};
|
||||
function TotalWordsSlide({ count }: { count: number }) {
|
||||
const tagline = Object.entries(TOTAL_WORDS_TAGLINES)
|
||||
.sort((a, b) => Number(b[0]) - Number(a[0]))
|
||||
.find(([threshold]) => count >= Number(threshold))?.[1];
|
||||
return (
|
||||
<Slide pattern="dots">
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: ["1.2rem", "1.5rem"],
|
||||
textAlign: "center",
|
||||
animation: "fadeIn 0.8s ease-out"
|
||||
}}
|
||||
>
|
||||
You wrote a total of
|
||||
</Text>
|
||||
<Text
|
||||
variant="heading"
|
||||
sx={{
|
||||
fontSize: ["5rem", "7rem"],
|
||||
textAlign: "center",
|
||||
color: "accent",
|
||||
animation: "scaleIn 0.8s ease-out 0.2s both"
|
||||
}}
|
||||
>
|
||||
{formatNumber(count)}
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: ["1.2rem", "1.5rem"],
|
||||
textAlign: "center",
|
||||
animation: "fadeIn 0.8s ease-out 0.4s both"
|
||||
}}
|
||||
>
|
||||
words this year
|
||||
</Text>
|
||||
{tagline ? (
|
||||
<Text
|
||||
variant="body"
|
||||
color="paragraph-secondary"
|
||||
sx={{
|
||||
fontSize: ["1.2rem", "1.2rem"],
|
||||
animation: "fadeIn 0.8s ease-out",
|
||||
borderTop: "1px solid var(--border)",
|
||||
mt: 3,
|
||||
pt: 3
|
||||
}}
|
||||
>
|
||||
{tagline}
|
||||
</Text>
|
||||
) : null}
|
||||
</Slide>
|
||||
);
|
||||
}
|
||||
|
||||
type ActivityStatsSlideProps = {
|
||||
mostNotesCreatedInMonth: NoteStats["mostNotesCreatedInMonth"];
|
||||
mostNotesCreatedInDay: NoteStats["mostNotesCreatedInDay"];
|
||||
};
|
||||
|
||||
function ActivityStatsSlide({
|
||||
mostNotesCreatedInMonth,
|
||||
mostNotesCreatedInDay
|
||||
}: ActivityStatsSlideProps) {
|
||||
if (!mostNotesCreatedInMonth && !mostNotesCreatedInDay) return null;
|
||||
|
||||
return (
|
||||
<Slide pattern="diagonal" sx={{ alignItems: "start" }}>
|
||||
{mostNotesCreatedInMonth && (
|
||||
<>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: ["1.2rem", "1.5rem"]
|
||||
}}
|
||||
>
|
||||
Your most productive month was
|
||||
</Text>
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="heading"
|
||||
sx={{
|
||||
fontSize: ["5rem", "7rem", "3rem"]
|
||||
}}
|
||||
>
|
||||
{mostNotesCreatedInMonth.month}
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ fontSize: "1rem" }}
|
||||
color="paragraph-secondary"
|
||||
>
|
||||
{formatNumber(mostNotesCreatedInMonth.count)} notes
|
||||
</Text>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
{mostNotesCreatedInDay && (
|
||||
<>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: ["1.2rem", "1.5rem"],
|
||||
mt: 5
|
||||
}}
|
||||
>
|
||||
Your favorite day to write was
|
||||
</Text>
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="heading"
|
||||
sx={{
|
||||
fontSize: ["5rem", "7rem", "3rem"]
|
||||
}}
|
||||
>
|
||||
{mostNotesCreatedInDay.day}
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ fontSize: "1rem" }}
|
||||
color="paragraph-secondary"
|
||||
>
|
||||
{formatNumber(mostNotesCreatedInDay.count)} notes
|
||||
</Text>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Slide>
|
||||
);
|
||||
}
|
||||
|
||||
function SummarySlide({ stats }: { stats: WrappedStats }) {
|
||||
return (
|
||||
<Slide
|
||||
pattern="dots"
|
||||
sx={{
|
||||
bg: "background-secondary",
|
||||
border: "1px solid var(--accent)",
|
||||
borderRadius: "dialog",
|
||||
boxShadow: "-15px 15px 0px 0px var(--accent)",
|
||||
p: 5
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
height: 20,
|
||||
width: 20,
|
||||
position: "absolute",
|
||||
bottom: "20px",
|
||||
right: "20px"
|
||||
}}
|
||||
>
|
||||
<use href="#themed-logo" />
|
||||
</svg>
|
||||
<Text
|
||||
variant="heading"
|
||||
sx={{
|
||||
fontSize: "1.2rem",
|
||||
fontWeight: 800,
|
||||
textAlign: "center",
|
||||
transform: "skew(-10deg) scaleX(1.5)",
|
||||
letterSpacing: "-1px",
|
||||
mb: "25px",
|
||||
textDecorationLine: "underline",
|
||||
textDecorationColor: "border"
|
||||
}}
|
||||
>
|
||||
NOTESNOOK WRAPPED {new Date().getFullYear()}
|
||||
</Text>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: ["column", "row"],
|
||||
gap: "30px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr 1fr",
|
||||
gridTemplateRows: "1fr 1fr",
|
||||
gap: 4,
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
icon: "📝",
|
||||
count: stats.totalNotes,
|
||||
label: "Notes"
|
||||
},
|
||||
{
|
||||
icon: "🎨",
|
||||
count: stats.totalColors,
|
||||
label: "Colors"
|
||||
},
|
||||
{
|
||||
icon: "📚",
|
||||
count: stats.totalNotebooks,
|
||||
label: "Notebooks"
|
||||
},
|
||||
{
|
||||
icon: "🏷️",
|
||||
count: stats.totalTags,
|
||||
label: "Tags"
|
||||
},
|
||||
{
|
||||
icon: "📂",
|
||||
count: stats.totalAttachments,
|
||||
label: "Files"
|
||||
},
|
||||
{
|
||||
icon: "☁️",
|
||||
count: stats.totalMonographs,
|
||||
label: "Monographs"
|
||||
}
|
||||
].map(({ icon, count, label }) => (
|
||||
<Flex
|
||||
key={label}
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<Text sx={{ fontSize: "1.5rem" }}>{icon}</Text>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "2rem",
|
||||
fontWeight: "bold"
|
||||
}}
|
||||
>
|
||||
{formatCount(count)}
|
||||
</Text>
|
||||
<Text sx={{ fontSize: "0.85rem", color: "fontTertiary" }}>
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
borderTop: "1px solid var(--border)",
|
||||
mt: 6,
|
||||
pt: 3,
|
||||
"& strong": {
|
||||
color: "accent"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text sx={{ fontSize: "1rem", fontWeight: "bold" }}>
|
||||
Fun facts of the year
|
||||
</Text>
|
||||
{stats.mostNotesCreatedInMonth && (
|
||||
<Text sx={{ fontSize: "0.9rem", color: "fontTertiary" }}>
|
||||
📅 Your most productive month was{" "}
|
||||
<Text as="strong">{stats.mostNotesCreatedInMonth.month}</Text>
|
||||
</Text>
|
||||
)}
|
||||
{stats.mostNotesCreatedInDay && (
|
||||
<Text sx={{ fontSize: "0.9rem", color: "fontTertiary" }}>
|
||||
🗓️ Your favorite day to write was{" "}
|
||||
<Text as="strong">{stats.mostNotesCreatedInDay.day}</Text>
|
||||
</Text>
|
||||
)}
|
||||
{stats.largestNote && (
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "0.9rem",
|
||||
color: "fontTertiary",
|
||||
maxWidth: 340
|
||||
}}
|
||||
>
|
||||
📝 Your longest note was{" "}
|
||||
<Text as="strong">
|
||||
{formatNumber(stats.largestNote.length)}
|
||||
{" words"}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
{stats.largestAttachment && (
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "0.9rem",
|
||||
color: "fontTertiary",
|
||||
maxWidth: 340
|
||||
}}
|
||||
>
|
||||
🔗 Your largest attachment was{" "}
|
||||
<strong>{formatBytes(stats.largestAttachment.size)}</strong>
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
justifyContent: "stretch",
|
||||
height: "100%",
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Text sx={{ fontSize: "2.5rem" }}>✍️</Text>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "3rem",
|
||||
fontWeight: "bold",
|
||||
color: "accent"
|
||||
}}
|
||||
>
|
||||
{formatNumber(stats.totalWords)}
|
||||
</Text>
|
||||
<Text sx={{ fontSize: "1rem", color: "fontTertiary" }}>
|
||||
Words Written
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
borderTop: "1px solid var(--border)",
|
||||
mt: 6,
|
||||
pt: 2,
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<MonthlyActivityHeatmap monthlyStats={stats.monthlyStats} />
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
color: "fontTertiary",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Notes per month
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Text
|
||||
variant="body"
|
||||
color="paragraph-secondary"
|
||||
sx={{ mt: 2, borderTop: "1px solid var(--border)", pt: 2 }}
|
||||
>
|
||||
Generated 100% locally on your device.
|
||||
</Text>
|
||||
</Slide>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Wrapped() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [stats, setStats] = useState<WrappedStats>();
|
||||
|
||||
useEffect(() => {
|
||||
async function loadWrapped() {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.time("wrapped - getting from core");
|
||||
const wrapped = await db.wrapped.get();
|
||||
console.timeEnd("wrapped - getting from core");
|
||||
setStats(wrapped);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
loadWrapped();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
`}
|
||||
</style>
|
||||
<Flex
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
flexDirection: "column",
|
||||
overflowY: "scroll",
|
||||
overflowX: "hidden",
|
||||
scrollSnapType: "y mandatory",
|
||||
scrollBehavior: "smooth",
|
||||
"&::-webkit-scrollbar": {
|
||||
display: "none"
|
||||
},
|
||||
msOverflowStyle: "none",
|
||||
scrollbarWidth: "none"
|
||||
}}
|
||||
>
|
||||
<WelcomeSlide loading={loading} />
|
||||
{stats ? (
|
||||
<>
|
||||
<TotalNotesSlide count={stats.totalNotes} />
|
||||
{stats.totalWords > 0 && (
|
||||
<TotalWordsSlide count={stats.totalWords} />
|
||||
)}
|
||||
{(stats.mostNotesCreatedInMonth || stats.mostNotesCreatedInDay) && (
|
||||
<ActivityStatsSlide
|
||||
mostNotesCreatedInMonth={stats.mostNotesCreatedInMonth}
|
||||
mostNotesCreatedInDay={stats.mostNotesCreatedInDay}
|
||||
/>
|
||||
)}
|
||||
<SummarySlide stats={stats} />
|
||||
</>
|
||||
) : null}
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
onClick={() => hardNavigate("/")}
|
||||
variant="secondary"
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 3,
|
||||
left: 3,
|
||||
zIndex: 1000
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ alignItems: "center", gap: 1, justifyContent: "center" }}>
|
||||
<ArrowLeft size={16} />
|
||||
<Text variant="body">Go back to app</Text>
|
||||
</Flex>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -67,6 +67,7 @@ The following keyboard shortcuts will help you navigate Notesnook faster.
|
||||
| Toggle outline list | Ctrl ⇧ O | Ctrl ⇧ O | ⌘ ⇧ O |
|
||||
| Toggle outline list expand | Ctrl Space | Ctrl Space | ⌘ Space |
|
||||
| Open search | Ctrl F | Ctrl F | ⌘ F |
|
||||
| Open search and replace | Ctrl Alt F | Ctrl Alt F | ⌘ ⌥ F |
|
||||
| Toggle strike | Ctrl ⇧ S | Ctrl ⇧ S | ⌘ ⇧ S |
|
||||
| Toggle subscript | Ctrl , | Ctrl , | ⌘ , |
|
||||
| Toggle superscript | Ctrl . | Ctrl . | ⌘ . |
|
||||
|
||||
1
packages/common/package-lock.json
generated
1
packages/common/package-lock.json
generated
@@ -41,6 +41,7 @@
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@streetwriters/showdown": "^3.0.9-alpha",
|
||||
"@types/mime-db": "^1.43.5",
|
||||
"alfaaz": "^1.1.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"dayjs": "1.11.13",
|
||||
"dom-serializer": "^2.0.0",
|
||||
|
||||
@@ -335,6 +335,12 @@ export const tiptapKeys = {
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
openSearchAndReplace: {
|
||||
keys: "Mod-Alt-f",
|
||||
description: "Open search and replace",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleStrike: {
|
||||
keys: "Mod-Shift-S",
|
||||
description: "Toggle strike",
|
||||
|
||||
7
packages/core/package-lock.json
generated
7
packages/core/package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@streetwriters/showdown": "^3.0.9-alpha",
|
||||
"@types/mime-db": "^1.43.5",
|
||||
"alfaaz": "^1.1.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"dayjs": "1.11.13",
|
||||
"dom-serializer": "^2.0.0",
|
||||
@@ -1391,6 +1392,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/alfaaz": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/alfaaz/-/alfaaz-1.1.0.tgz",
|
||||
"integrity": "sha512-J/P07R41APslK7NmD5303bwStN8jpRA4DdvtLeAr1Jhfj6XWGrASUWI0G6jbWjJAZyw3Lu1Pb4J8rsM/cb+xDQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@streetwriters/showdown": "^3.0.9-alpha",
|
||||
"@types/mime-db": "^1.43.5",
|
||||
"alfaaz": "^1.1.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"dayjs": "1.11.13",
|
||||
"dom-serializer": "^2.0.0",
|
||||
|
||||
@@ -83,6 +83,7 @@ import { ConfigStorage } from "../database/config.js";
|
||||
import { LazyPromise } from "../utils/lazy-promise.js";
|
||||
import { InboxApiKeys } from "./inbox-api-keys.js";
|
||||
import { Circle } from "./circle.js";
|
||||
import { Wrapped } from "./wrapped.js";
|
||||
|
||||
type EventSourceConstructor = new (
|
||||
uri: string,
|
||||
@@ -224,6 +225,8 @@ class Database {
|
||||
|
||||
inboxApiKeys = new InboxApiKeys(this, this.tokenManager);
|
||||
|
||||
wrapped = new Wrapped(this);
|
||||
|
||||
/**
|
||||
* @deprecated only kept here for migration purposes
|
||||
*/
|
||||
|
||||
342
packages/core/src/api/wrapped.ts
Normal file
342
packages/core/src/api/wrapped.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
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 { Parser } from "htmlparser2";
|
||||
import Database from "./index.js";
|
||||
import { countWords } from "alfaaz";
|
||||
import { DatabaseSchema, isFalse } from "../database/index.js";
|
||||
import { SelectQueryBuilder } from "@streetwriters/kysely";
|
||||
|
||||
const dayNames = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday"
|
||||
];
|
||||
|
||||
const monthNames = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December"
|
||||
];
|
||||
|
||||
export type NoteStats = {
|
||||
totalNotes: number;
|
||||
totalWords: number;
|
||||
totalMonographs: number;
|
||||
mostNotesCreatedInMonth: { month: string; count: number } | null;
|
||||
mostNotesCreatedInDay: { day: string; count: number } | null;
|
||||
monthlyStats: Record<string, number>;
|
||||
dayOfWeekStats: Record<string, number>;
|
||||
largestNote: { title: string; length: number } | null;
|
||||
};
|
||||
|
||||
export type OrganizationStats = {
|
||||
totalNotebooks: number;
|
||||
totalTags: number;
|
||||
mostUsedTags: { id: string; title: string; noteCount: number }[];
|
||||
mostActiveNotebooks: { id: string; title: string; noteCount: number }[];
|
||||
totalColors: number;
|
||||
};
|
||||
|
||||
export type AttachmentStats = {
|
||||
totalAttachments: number;
|
||||
totalStorageUsed: number;
|
||||
largestAttachment: { id: string; filename: string; size: number } | null;
|
||||
mostCommonFileType: string | null;
|
||||
};
|
||||
|
||||
export type WrappedStats = NoteStats & OrganizationStats & AttachmentStats;
|
||||
|
||||
export class Wrapped {
|
||||
constructor(private readonly db: Database) {}
|
||||
|
||||
async get(): Promise<WrappedStats> {
|
||||
const { startDate, endDate } = this.getYearRange();
|
||||
|
||||
const [noteStats, organizationStats, attachmentStats] = await Promise.all([
|
||||
this.getNoteStats(startDate, endDate),
|
||||
this.getOrganizationStats(startDate, endDate),
|
||||
this.getAttachmentStats(startDate, endDate)
|
||||
]);
|
||||
|
||||
return {
|
||||
...noteStats,
|
||||
...organizationStats,
|
||||
...attachmentStats
|
||||
};
|
||||
}
|
||||
|
||||
private getYearRange(): { startDate: number; endDate: number } {
|
||||
const year = new Date().getFullYear();
|
||||
const startDate = new Date(year, 0, 1, 0, 0, 0, 0).getTime();
|
||||
const endDate = new Date(year + 1, 0, 1, 0, 0, 0, 0).getTime();
|
||||
return { startDate, endDate };
|
||||
}
|
||||
|
||||
private async getNoteStats(
|
||||
startDate: number,
|
||||
endDate: number
|
||||
): Promise<NoteStats> {
|
||||
const notesSelector = this.db
|
||||
.sql()
|
||||
.selectFrom("notes")
|
||||
.where((eb) => eb("dateCreated", ">=", startDate))
|
||||
.where((eb) => eb("dateCreated", "<", endDate))
|
||||
.where(isFalse("deleted"))
|
||||
.where(isFalse("dateDeleted"));
|
||||
const notes = await notesSelector.select(["notes.dateCreated"]).execute();
|
||||
|
||||
const monthlyStats: Map<string, number> = new Map();
|
||||
const dayOfWeekStats: Map<string, number> = new Map();
|
||||
let totalNotes = 0;
|
||||
|
||||
for (const note of notes) {
|
||||
if (!note.dateCreated) continue;
|
||||
|
||||
totalNotes++;
|
||||
const month = new Date(note.dateCreated).getMonth();
|
||||
const monthName = monthNames[month];
|
||||
monthlyStats.set(monthName, (monthlyStats.get(monthName) || 0) + 1);
|
||||
const dayOfWeek = new Date(note.dateCreated).getDay();
|
||||
const dayName = dayNames[dayOfWeek];
|
||||
dayOfWeekStats.set(dayName, (dayOfWeekStats.get(dayName) || 0) + 1);
|
||||
}
|
||||
|
||||
let mostNotesCreatedInMonth: NoteStats["mostNotesCreatedInMonth"] = null;
|
||||
for (const [month, count] of monthlyStats.entries()) {
|
||||
if (!mostNotesCreatedInMonth || count > mostNotesCreatedInMonth.count) {
|
||||
mostNotesCreatedInMonth = { month, count };
|
||||
}
|
||||
}
|
||||
|
||||
let mostNotesCreatedInDay: NoteStats["mostNotesCreatedInDay"] = null;
|
||||
let maxDayCount = 0;
|
||||
for (const [day, count] of dayOfWeekStats.entries()) {
|
||||
if (count > maxDayCount) {
|
||||
maxDayCount = count;
|
||||
mostNotesCreatedInDay = { day, count };
|
||||
}
|
||||
}
|
||||
|
||||
const totalMonographs = await this.db.monographs.all
|
||||
.where((eb) =>
|
||||
eb.and([
|
||||
eb("dateCreated", ">=", startDate),
|
||||
eb("dateCreated", "<", endDate)
|
||||
])
|
||||
)
|
||||
.count();
|
||||
|
||||
const { largestNote, totalWords } = await this.countTotalWords(
|
||||
notesSelector
|
||||
);
|
||||
|
||||
return {
|
||||
totalNotes,
|
||||
totalWords,
|
||||
totalMonographs,
|
||||
largestNote: largestNote
|
||||
? {
|
||||
title: (await this.db.notes.note(largestNote.id))?.title || "",
|
||||
length: largestNote.wordCount
|
||||
}
|
||||
: null,
|
||||
monthlyStats: Object.fromEntries(monthlyStats),
|
||||
dayOfWeekStats: Object.fromEntries(dayOfWeekStats),
|
||||
mostNotesCreatedInMonth,
|
||||
mostNotesCreatedInDay
|
||||
};
|
||||
}
|
||||
|
||||
private async countItemNotes<T extends { id: string; title: string }>(
|
||||
items: T[],
|
||||
itemType: "tag" | "notebook"
|
||||
): Promise<Array<T & { noteCount: number }>> {
|
||||
const allRelations = await this.db.relations
|
||||
.from({ ids: items.map((item) => item.id), type: itemType }, "note")
|
||||
.get();
|
||||
|
||||
const noteCounts: Map<string, number> = new Map();
|
||||
for (const relation of allRelations) {
|
||||
const itemId = relation.fromId;
|
||||
noteCounts.set(itemId, (noteCounts.get(itemId) || 0) + 1);
|
||||
}
|
||||
|
||||
return items
|
||||
.map((item) => ({
|
||||
...item,
|
||||
noteCount: noteCounts.get(item.id) || 0
|
||||
}))
|
||||
.filter((item) => item.noteCount > 0)
|
||||
.sort((a, b) => b.noteCount - a.noteCount);
|
||||
}
|
||||
|
||||
private async getOrganizationStats(
|
||||
startDate: number,
|
||||
endDate: number
|
||||
): Promise<OrganizationStats> {
|
||||
const notebookSelector = this.db.notebooks.all
|
||||
.where((eb) => eb("dateCreated", ">=", startDate))
|
||||
.where((eb) => eb("dateCreated", "<", endDate));
|
||||
const tagSelector = this.db.tags.all
|
||||
.where((eb) => eb("dateCreated", ">=", startDate))
|
||||
.where((eb) => eb("dateCreated", "<", endDate));
|
||||
|
||||
const [totalNotebooks, totalTags, tags, notebooks, totalColors] =
|
||||
await Promise.all([
|
||||
notebookSelector.count(),
|
||||
tagSelector.count(),
|
||||
tagSelector.fields(["tags.id", "tags.title"]).items(),
|
||||
notebookSelector.fields(["notebooks.id", "notebooks.title"]).items(),
|
||||
this.db.colors.all
|
||||
.where((eb) => eb("dateCreated", ">=", startDate))
|
||||
.where((eb) => eb("dateCreated", "<", endDate))
|
||||
.count()
|
||||
]);
|
||||
|
||||
const tagNotes = await this.countItemNotes(tags, "tag");
|
||||
const mostUsedTags = tagNotes.slice(0, 3);
|
||||
|
||||
const notebookNotes = await this.countItemNotes(notebooks, "notebook");
|
||||
const mostActiveNotebooks = notebookNotes.slice(0, 3);
|
||||
|
||||
return {
|
||||
totalNotebooks,
|
||||
totalTags,
|
||||
mostUsedTags:
|
||||
mostUsedTags.length > 0
|
||||
? mostUsedTags
|
||||
: tags.slice(0, 3).map((tag) => ({ ...tag, noteCount: 0 })),
|
||||
mostActiveNotebooks:
|
||||
mostActiveNotebooks.length > 0
|
||||
? mostActiveNotebooks
|
||||
: notebooks.slice(0, 3).map((n) => ({ ...n, noteCount: 0 })),
|
||||
totalColors
|
||||
};
|
||||
}
|
||||
|
||||
private async getAttachmentStats(
|
||||
startDate: number,
|
||||
endDate: number
|
||||
): Promise<AttachmentStats> {
|
||||
const attachmentsSelector = this.db.attachments.all
|
||||
.where((eb) => eb("dateCreated", ">=", startDate))
|
||||
.where((eb) => eb("dateCreated", "<", endDate));
|
||||
const totalAttachments = await attachmentsSelector.count();
|
||||
|
||||
if (totalAttachments === 0) {
|
||||
return {
|
||||
totalAttachments: 0,
|
||||
totalStorageUsed: 0,
|
||||
largestAttachment: null,
|
||||
mostCommonFileType: null
|
||||
};
|
||||
}
|
||||
|
||||
const totalStorageUsed =
|
||||
(await this.db.attachments.totalSize(attachmentsSelector)) || 0;
|
||||
const attachments = await attachmentsSelector.items();
|
||||
let largestAttachment: AttachmentStats["largestAttachment"] = null;
|
||||
const mimeTypeCounts: Map<string, number> = new Map();
|
||||
|
||||
for (const attachment of attachments) {
|
||||
if (!largestAttachment || attachment.size > largestAttachment.size) {
|
||||
largestAttachment = {
|
||||
id: attachment.id,
|
||||
filename: attachment.filename,
|
||||
size: attachment.size
|
||||
};
|
||||
}
|
||||
|
||||
const mimeType = attachment.mimeType.split("/")[0] || attachment.mimeType;
|
||||
mimeTypeCounts.set(mimeType, (mimeTypeCounts.get(mimeType) || 0) + 1);
|
||||
}
|
||||
|
||||
let mostCommonFileType: string | null = null;
|
||||
let maxCount = 0;
|
||||
for (const [mimeType, count] of mimeTypeCounts.entries()) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
mostCommonFileType = mimeType;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalAttachments,
|
||||
totalStorageUsed,
|
||||
largestAttachment,
|
||||
mostCommonFileType
|
||||
};
|
||||
}
|
||||
|
||||
private async countTotalWords(
|
||||
selector: SelectQueryBuilder<DatabaseSchema, "notes", unknown>
|
||||
) {
|
||||
let words = 0;
|
||||
let largestNote = { id: "", wordCount: 0 };
|
||||
const contents = await this.db
|
||||
.sql()
|
||||
.selectFrom("content")
|
||||
.where("noteId", "in", selector.select("id"))
|
||||
.where(isFalse("locked"))
|
||||
.where(isFalse("deleted"))
|
||||
.select(["content.data", "content.noteId"])
|
||||
.execute();
|
||||
|
||||
for (const content of contents) {
|
||||
if (typeof content?.data !== "string") continue;
|
||||
const counted = countWords(toTextContent(content.data));
|
||||
words += counted;
|
||||
if (content.noteId && counted > largestNote.wordCount) {
|
||||
largestNote = { id: content.noteId, wordCount: counted };
|
||||
}
|
||||
}
|
||||
|
||||
return { totalWords: words, largestNote };
|
||||
}
|
||||
}
|
||||
|
||||
function toTextContent(html: string) {
|
||||
let text = "";
|
||||
|
||||
const parser = new Parser({
|
||||
ontext: (data) => {
|
||||
text += data;
|
||||
},
|
||||
onclosetag() {
|
||||
text += " ";
|
||||
}
|
||||
});
|
||||
parser.write(html);
|
||||
parser.end();
|
||||
return text;
|
||||
}
|
||||
@@ -45,3 +45,4 @@ export type { SyncOptions } from "./api/sync/index.js";
|
||||
export { sanitizeTag } from "./collections/tags.js";
|
||||
export { default as DataURL } from "./utils/dataurl.js";
|
||||
export { type ResolveInternalLink } from "./content-types/tiptap.js";
|
||||
export type * from "./api/wrapped.js";
|
||||
|
||||
@@ -23,11 +23,9 @@ import {
|
||||
textblockTypeInputRule
|
||||
} from "@tiptap/core";
|
||||
import { Heading as TiptapHeading } from "@tiptap/extension-heading";
|
||||
import { isClickWithinBounds } from "../../utils/prosemirror.js";
|
||||
import { Plugin, PluginKey, Selection, Transaction } from "@tiptap/pm/state";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import { Plugin, PluginKey, Selection, Transaction } from "@tiptap/pm/state";
|
||||
import { Callout } from "../callout/callout.js";
|
||||
|
||||
const COLLAPSIBLE_BLOCK_TYPES = [
|
||||
"paragraph",
|
||||
@@ -44,7 +42,8 @@ const COLLAPSIBLE_BLOCK_TYPES = [
|
||||
"outlineList",
|
||||
"mathBlock",
|
||||
"webclip",
|
||||
"embed"
|
||||
"embed",
|
||||
"horizontalRule"
|
||||
];
|
||||
|
||||
const HEADING_REGEX = /^(#{1,6})\s$/;
|
||||
@@ -187,6 +186,17 @@ export const Heading = TiptapHeading.extend({
|
||||
if (typeof getPos === "boolean") return;
|
||||
|
||||
const pos = getPos();
|
||||
const resolvedPos = editor.state.doc.resolve(pos);
|
||||
|
||||
const callout = findParentNodeClosestToPos(
|
||||
resolvedPos,
|
||||
(node) => node.type.name === Callout.name
|
||||
);
|
||||
// the first callout heading's collapsibility is handled by callout itself
|
||||
if (callout?.node.firstChild === node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clientX =
|
||||
e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
|
||||
const clientY =
|
||||
@@ -194,7 +204,7 @@ export const Heading = TiptapHeading.extend({
|
||||
const isRtl =
|
||||
e.target.dir === "rtl" ||
|
||||
findParentNodeClosestToPos(
|
||||
editor.state.doc.resolve(pos),
|
||||
resolvedPos,
|
||||
(node) => !!node.attrs.textDirection
|
||||
)?.node.attrs.textDirection === "rtl";
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ type DispatchFn = (tr: Transaction) => void;
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
searchreplace: {
|
||||
startSearch: () => ReturnType;
|
||||
startSearch: (isReplacing?: boolean) => ReturnType;
|
||||
endSearch: () => ReturnType;
|
||||
search: (term: string, options?: SearchSettings) => ReturnType;
|
||||
moveToNextResult: () => ReturnType;
|
||||
@@ -51,7 +51,7 @@ interface Result {
|
||||
|
||||
interface SearchOptions {
|
||||
searchResultClass: string;
|
||||
onStartSearch: (term?: string) => boolean;
|
||||
onStartSearch: (term?: string, isReplacing?: boolean) => boolean;
|
||||
onEndSearch: () => boolean;
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
|
||||
addCommands() {
|
||||
return {
|
||||
startSearch:
|
||||
() =>
|
||||
(isReplacing) =>
|
||||
({ state, commands }) => {
|
||||
const term = !state.selection.empty
|
||||
? state.doc.textBetween(
|
||||
@@ -252,7 +252,7 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
|
||||
: undefined;
|
||||
if (term) commands.search(term);
|
||||
|
||||
return this.options.onStartSearch(term);
|
||||
return this.options.onStartSearch(term, isReplacing);
|
||||
},
|
||||
endSearch:
|
||||
() =>
|
||||
@@ -357,6 +357,8 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
|
||||
return {
|
||||
[tiptapKeys.openSearch.keys]: ({ editor }) =>
|
||||
editor.commands.startSearch(),
|
||||
[tiptapKeys.openSearchAndReplace.keys]: ({ editor }) =>
|
||||
editor.commands.startSearch(true),
|
||||
Escape: ({ editor }) => editor.commands.endSearch()
|
||||
};
|
||||
},
|
||||
|
||||
@@ -191,11 +191,12 @@ const useTiptap = (
|
||||
extensions: [
|
||||
...CoreExtensions,
|
||||
SearchReplace.configure({
|
||||
onStartSearch: (term) => {
|
||||
onStartSearch: (term, isReplacing) => {
|
||||
useEditorSearchStore.setState({
|
||||
isSearching: true,
|
||||
searchTerm: term,
|
||||
focusNonce: Math.random()
|
||||
focusNonce: Math.random(),
|
||||
isReplacing: isReplacing
|
||||
});
|
||||
return true;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user