mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
1 Commits
fix-file-e
...
fix-precen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
610e0d8b81 |
@@ -10,7 +10,7 @@ const authors = readFileSync("AUTHORS", "utf-8");
|
||||
const isAuthor = authors.includes(`<${authorEmail}>`);
|
||||
|
||||
const SCOPES = [
|
||||
// for full list of scopes + details see: https://github.com/streetwriters/notesnook/blob/master/CONTRIBUTING.md#commit-guidelines
|
||||
// for full list of scopes + details see: https://github.com/streetwriters/notesnook-private/blob/master/CONTRIBUTING.md#commit-guidelines
|
||||
|
||||
"mobile",
|
||||
"web",
|
||||
@@ -36,8 +36,7 @@ const SCOPES = [
|
||||
"global",
|
||||
"docs",
|
||||
"themebuilder",
|
||||
"intl",
|
||||
"webclipper"
|
||||
"intl"
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -7,15 +7,13 @@ runs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 20.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
apps/mobile/package-lock.json
|
||||
apps/desktop/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
apps/monograph/package-lock.json
|
||||
apps/theme-builder/package-lock.json
|
||||
extensions/web-clipper/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
@@ -26,4 +24,3 @@ runs:
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
packages/intl/package-lock.json
|
||||
|
||||
11
.github/workflows/android.e2e.yml
vendored
11
.github/workflows/android.e2e.yml
vendored
@@ -47,21 +47,16 @@ 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
|
||||
ls apps/mobile/android/app/build/outputs/apk
|
||||
ls apps/mobile/android/app/build/outputs/apk/release
|
||||
ls apps/mobile/native/android/app/build/outputs/apk
|
||||
ls apps/mobile/native/android/app/build/outputs/apk/release
|
||||
|
||||
- name: Get device name
|
||||
id: device
|
||||
run: |
|
||||
AVD_NAME=$(node -p "require('./apps/mobile/.detoxrc.js').devices.emulator.device.avdName")
|
||||
AVD_NAME=$(node -p "require('./apps/mobile/native/.detoxrc.js').devices.emulator.device.avdName")
|
||||
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: AVD cache
|
||||
|
||||
63
.github/workflows/android.publish.internal.yml
vendored
63
.github/workflows/android.publish.internal.yml
vendored
@@ -4,7 +4,8 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
||||
@@ -21,7 +22,7 @@ jobs:
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
java-version: "11"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install node modules
|
||||
@@ -30,23 +31,7 @@ jobs:
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/android && chmod +x ./gradlew
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||
|
||||
- name: Install CCache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.11
|
||||
@@ -73,7 +58,7 @@ jobs:
|
||||
ccache -p
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -84,19 +69,12 @@ jobs:
|
||||
run: |
|
||||
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
|
||||
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
|
||||
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
|
||||
- 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: Remove debug keystore
|
||||
run: rm -rf apps/mobile/android/app/debug.keystore
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
@@ -107,7 +85,7 @@ jobs:
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@master
|
||||
with:
|
||||
releaseDirectory: apps/mobile/android/app/build/outputs/bundle/release
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
@@ -122,7 +100,7 @@ jobs:
|
||||
id: sign_apk
|
||||
uses: r0adkll/sign-android-release@master
|
||||
with:
|
||||
releaseDirectory: apps/mobile/android/app/build/outputs/apk/release
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/apk/release
|
||||
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
|
||||
alias: ${{ secrets.PUBLIC_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
@@ -132,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Rename apk files
|
||||
run: |
|
||||
cd apps/mobile/android/app/build/outputs/apk/release/
|
||||
cd apps/mobile/native/android/app/build/outputs/apk/release/
|
||||
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
|
||||
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
|
||||
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
|
||||
@@ -140,7 +118,7 @@ jobs:
|
||||
|
||||
- name: Get app version
|
||||
id: package-version
|
||||
uses: saionaro/extract-package-version@master
|
||||
uses: martinbeentjes/npm-get-version-action@master
|
||||
with:
|
||||
path: apps/mobile
|
||||
|
||||
@@ -151,29 +129,28 @@ jobs:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: beta
|
||||
track: alpha
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/android/releasenotes/
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
|
||||
- name: Create release draft on Github
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ steps.package-version.outputs.version}}-beta-android
|
||||
name: Notesnook Android v${{ steps.package-version.outputs.version}}
|
||||
tag_name: ${{ steps.package-version.outputs.current-version}}-beta-android
|
||||
name: Notesnook Android v${{ steps.package-version.outputs.current-version}} Beta
|
||||
repository: streetwriters/notesnook
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
apps/mobile/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
|
||||
apps/mobile/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/android/app/build/**/*.map
|
||||
apps/mobile/native/android/app/build/**/*.map
|
||||
packages/editor-mobile/sourcemaps/*.map
|
||||
|
||||
58
.github/workflows/android.publish.yml
vendored
58
.github/workflows/android.publish.yml
vendored
@@ -4,7 +4,8 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
||||
@@ -24,29 +25,13 @@ jobs:
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/android && chmod +x ./gradlew
|
||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||
|
||||
- name: Install CCache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.11
|
||||
@@ -73,7 +58,7 @@ jobs:
|
||||
ccache -p
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -84,19 +69,12 @@ jobs:
|
||||
run: |
|
||||
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
|
||||
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
|
||||
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||
|
||||
- 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: Remove debug keystore
|
||||
run: rm -rf apps/mobile/android/app/debug.keystore
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
@@ -107,7 +85,7 @@ jobs:
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@master
|
||||
with:
|
||||
releaseDirectory: apps/mobile/android/app/build/outputs/bundle/release
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
@@ -122,7 +100,7 @@ jobs:
|
||||
id: sign_apk
|
||||
uses: r0adkll/sign-android-release@master
|
||||
with:
|
||||
releaseDirectory: apps/mobile/android/app/build/outputs/apk/release
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/apk/release
|
||||
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
|
||||
alias: ${{ secrets.PUBLIC_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
@@ -132,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Rename apk files
|
||||
run: |
|
||||
cd apps/mobile/android/app/build/outputs/apk/release/
|
||||
cd apps/mobile/native/android/app/build/outputs/apk/release/
|
||||
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
|
||||
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
|
||||
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
|
||||
@@ -140,7 +118,7 @@ jobs:
|
||||
|
||||
- name: Get app version
|
||||
id: package-version
|
||||
uses: saionaro/extract-package-version@master
|
||||
uses: martinbeentjes/npm-get-version-action@master
|
||||
with:
|
||||
path: apps/mobile
|
||||
|
||||
@@ -153,26 +131,26 @@ jobs:
|
||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: production
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/android/releasenotes/
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
|
||||
- name: Create release draft on Github
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ steps.package-version.outputs.version}}-android
|
||||
name: Notesnook Android v${{ steps.package-version.outputs.version}}
|
||||
tag_name: ${{ steps.package-version.outputs.current-version}}-android
|
||||
name: Notesnook Android v${{ steps.package-version.outputs.current-version}}
|
||||
repository: streetwriters/notesnook
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
apps/mobile/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
|
||||
apps/mobile/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/android/app/build/**/*.map
|
||||
apps/mobile/native/android/app/build/**/*.map
|
||||
packages/editor-mobile/sourcemaps/*.map
|
||||
|
||||
25
.github/workflows/core.tests.yml
vendored
25
.github/workflows/core.tests.yml
vendored
@@ -9,13 +9,7 @@ on:
|
||||
- "packages/core/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/core.tests.yml"
|
||||
pull_request_target:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "packages/core/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/core.tests.yml"
|
||||
pull_request:
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
@@ -23,21 +17,11 @@ on:
|
||||
- "reopened"
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
environment: ${{ github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
'external' || 'internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo true
|
||||
|
||||
test:
|
||||
needs: authorize
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -52,6 +36,7 @@ jobs:
|
||||
echo "USER_PASSWORD=${{ secrets.USER_PASSWORD }}" >> $GITHUB_ENV
|
||||
echo "USER_TOTP_SECRET=${{ secrets.USER_TOTP_SECRET }}" >> $GITHUB_ENV
|
||||
echo "USER_HASHED_PASSWORD=${{ secrets.USER_HASHED_PASSWORD }}" >> $GITHUB_ENV
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Run all @notesnook/core tests
|
||||
run: npm run tx @notesnook/core:test:e2e
|
||||
run: npx nx test:e2e @notesnook/core
|
||||
|
||||
163
.github/workflows/desktop.publish.yml
vendored
163
.github/workflows/desktop.publish.yml
vendored
@@ -33,19 +33,11 @@ on:
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for macOS?"
|
||||
release-track:
|
||||
type: choice
|
||||
required: true
|
||||
default: stable
|
||||
description: "Select the release track"
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -57,25 +49,13 @@ jobs:
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
- name: Setup environment
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate desktop build (stable)
|
||||
if: ${{ inputs.release-track == 'stable' }}
|
||||
run: npm run tx @notesnook/web:build:desktop
|
||||
|
||||
- name: Generate desktop build (beta)
|
||||
if: ${{ inputs.release-track == 'beta' }}
|
||||
run: BETA=true npm run tx @notesnook/web:build:desktop
|
||||
|
||||
- name: Build desktop bundle
|
||||
working-directory: ./apps/desktop
|
||||
run: npm run bundle
|
||||
- name: Generate desktop build
|
||||
run: npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -91,8 +71,7 @@ jobs:
|
||||
|
||||
- name: Package desktop build
|
||||
run: |
|
||||
cp -r ./apps/web/build ./apps/desktop/
|
||||
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/desktop/build/
|
||||
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/web/build/
|
||||
echo "Build folder archived to ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip"
|
||||
|
||||
- name: Upload desktop build
|
||||
@@ -103,14 +82,12 @@ jobs:
|
||||
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
|
||||
tag_name: v${{ steps.app_metadata.outputs.app_version }}
|
||||
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
|
||||
prerelease: ${{ inputs.release-track == 'beta' }}
|
||||
target_commitish: ${{ github.ref }}
|
||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||
|
||||
- name: Generate flatpak sources
|
||||
if: inputs.publish-github && inputs.build-linux
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
|
||||
sudo apt-get install -y flatpak flatpak-builder pipx
|
||||
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
|
||||
cd /tmp/flatpak-builder-tools/node
|
||||
pipx install .
|
||||
@@ -195,9 +172,9 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.mac_certs }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
run: |
|
||||
npm run tx @notesnook/desktop:release -- --variant=mas
|
||||
cd apps/desktop
|
||||
yarn electron-builder --config=electron-builder.config.js --mac mas --universal -p never
|
||||
npx nx run release --project @notesnook/desktop -- --variant=mas
|
||||
yarn electron-builder --mac mas --universal -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build zip and dmg
|
||||
env:
|
||||
@@ -208,13 +185,13 @@ jobs:
|
||||
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
|
||||
run: |
|
||||
npm run tx @notesnook/desktop:release
|
||||
cd apps/desktop
|
||||
npx nx run release --project @notesnook/desktop
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p always
|
||||
yarn electron-builder --mac zip dmg --arm64 --x64 -p always
|
||||
else
|
||||
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p never
|
||||
yarn electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Deploy to Testflight
|
||||
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
|
||||
@@ -229,8 +206,8 @@ jobs:
|
||||
xcrun altool --upload-package $package -t osx --apiKey $API_KEY_ID --apiIssuer $API_KEY_ISSUER_ID --apple-id ${{ steps.app_metadata.outputs.apple_app_id }} --bundle-id ${{ steps.app_metadata.outputs.app_bundle_id }} --bundle-short-version-string ${{ steps.app_metadata.outputs.app_version }} --bundle-version ${{ steps.app_metadata.outputs.bundle_version }}
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
build-linux-x64:
|
||||
name: Build for Linux x64
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
needs: build
|
||||
if: inputs.build-linux
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -247,76 +224,9 @@ jobs:
|
||||
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Snapcraft
|
||||
uses: samuelmeuli/action-snapcraft@v3
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
if: inputs.publish-snap
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --linux snap:x64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build AppImage
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p always
|
||||
else
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Publish on Snapcraft
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
if [ ${{ inputs.release-track }} == 'beta' ]; then
|
||||
snapcraft upload --release=beta ./output/notesnook_linux_amd64.snap
|
||||
else
|
||||
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
build-linux-arm64:
|
||||
name: Build for Linux arm64
|
||||
needs: build
|
||||
if: inputs.build-linux
|
||||
runs-on: ubuntu-22.04-arm
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
# - name: Setup Snapcraft Auth
|
||||
# if: inputs.publish-snap
|
||||
# run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
|
||||
|
||||
# - name: Install Snapcraft
|
||||
# uses: samuelmeuli/action-snapcraft@v1
|
||||
# if: inputs.publish-snap
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -331,33 +241,35 @@ jobs:
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
# - name: Build snap
|
||||
# if: inputs.publish-snap
|
||||
# run: |
|
||||
# yarn electron-builder --config=electron-builder.config.js --linux snap:arm64 -p never
|
||||
# working-directory: ./apps/desktop
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
yarn electron-builder --linux snap:x64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build AppImage
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p always
|
||||
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p always
|
||||
else
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
|
||||
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
# - name: Publish on Snapcraft
|
||||
# if: inputs.publish-snap
|
||||
# run: |
|
||||
# snapcraft upload --release=stable ./output/notesnook_linux_arm64.snap
|
||||
# working-directory: ./apps/desktop
|
||||
- name: Publish on Snapcraft
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
@@ -396,13 +308,10 @@ jobs:
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build
|
||||
run: node scripts/execute.mjs @notesnook/desktop:release
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
@@ -412,8 +321,8 @@ jobs:
|
||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
run: |
|
||||
if ($${{ inputs.publish-github }} -eq $true) {
|
||||
yarn electron-builder --config=electron-builder.config.js --win --publish always
|
||||
yarn electron-builder --win --publish always
|
||||
} else {
|
||||
yarn electron-builder --config=electron-builder.config.js --win --publish never
|
||||
yarn electron-builder --win --publish never
|
||||
}
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
55
.github/workflows/desktop.tests.yml
vendored
55
.github/workflows/desktop.tests.yml
vendored
@@ -6,21 +6,15 @@ on:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/desktop/**"
|
||||
- "app/desktop/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/desktop/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -33,8 +27,12 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate desktop build
|
||||
run: npm run tx @notesnook/web:build:desktop
|
||||
run: npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -45,7 +43,7 @@ jobs:
|
||||
test-macos-x64:
|
||||
name: Test macOS x64
|
||||
needs: build
|
||||
runs-on: macos-15-intel
|
||||
runs-on: macos-13
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
@@ -66,15 +64,17 @@ jobs:
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --x64
|
||||
yarn electron-builder --mac --dir --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests x64
|
||||
run: npm run test
|
||||
run: |
|
||||
EXECUTABLE_PATH=output/mac/Notesnook.app/Contents/MacOS/Notesnook npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
@@ -109,15 +109,17 @@ jobs:
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
|
||||
yarn electron-builder --mac --dir --arm64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests arm64
|
||||
run: npm run test
|
||||
run: |
|
||||
EXECUTABLE_PATH=output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
@@ -131,7 +133,7 @@ jobs:
|
||||
test-linux:
|
||||
name: Test for Linux
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
@@ -158,15 +160,17 @@ jobs:
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --linux --dir --arm64 --x64
|
||||
yarn electron-builder --linux --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
|
||||
run: |
|
||||
EXECUTABLE_PATH=output/linux-unpacked/notesnook xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
@@ -204,21 +208,20 @@ jobs:
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: node scripts/execute.mjs @notesnook/desktop:release
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --config=electron-builder.config.js --win --dir --arm64 --x64
|
||||
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --win --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
run: |
|
||||
npx cross-env EXECUTABLE_PATH=output/win-unpacked/Notesnook.exe npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
|
||||
16
.github/workflows/editor.tests.yml
vendored
16
.github/workflows/editor.tests.yml
vendored
@@ -10,12 +10,6 @@ on:
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/editor.tests.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "packages/editor/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/editor.tests.yml"
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
@@ -25,7 +19,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -38,8 +32,12 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=editor
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build editor
|
||||
run: npm run tx @notesnook/editor:build
|
||||
run: npx nx build @notesnook/editor
|
||||
|
||||
- name: Run all @notesnook/editor tests
|
||||
run: npm run tx @notesnook/editor:test
|
||||
run: npx nx test @notesnook/editor
|
||||
|
||||
2
.github/workflows/help.publish.yml
vendored
2
.github/workflows/help.publish.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
37
.github/workflows/ios.publish.yml
vendored
37
.github/workflows/ios.publish.yml
vendored
@@ -4,33 +4,28 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-26
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "26.1.1"
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Build packages
|
||||
run: npm run tx @notesnook/mobile:build
|
||||
run: npx nx run @notesnook/mobile:build
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.11
|
||||
with:
|
||||
key: ${{ runner.os }}-ccache-${{ hashFiles(format('apps/mobile/ios/Podfile.lock')) }}
|
||||
key: ${{ runner.os }}-ccache-${{ hashFiles(format('apps/mobile/native/ios/Podfile.lock')) }}
|
||||
max-size: 1500M
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ccache-
|
||||
@@ -52,22 +47,14 @@ jobs:
|
||||
ccache -p
|
||||
|
||||
- name: Cache Pods
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v2
|
||||
id: pods-cache
|
||||
with:
|
||||
path: apps/mobile/ios/Pods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }}
|
||||
path: apps/mobile/native/ios/Pods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/native/ios/Podfile.lock') }}
|
||||
|
||||
- name: Install Pods
|
||||
run: |
|
||||
cd apps/mobile/ios
|
||||
pod install
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
run: npm run prepare:ios
|
||||
|
||||
- name: CCache Stats Before Build
|
||||
run: ccache -sv
|
||||
@@ -78,9 +65,9 @@ jobs:
|
||||
bundle-identifier: org.streetwriters.notesnook
|
||||
scheme: Notesnook
|
||||
configuration: "Release"
|
||||
export-options: apps/mobile/ios/ExportOptions.plist
|
||||
project-path: apps/mobile/ios/Notesnook.xcodeproj
|
||||
workspace-path: apps/mobile/ios/Notesnook.xcworkspace
|
||||
export-options: apps/mobile/native/ios/ExportOptions.plist
|
||||
project-path: apps/mobile/native/ios/Notesnook.xcodeproj
|
||||
workspace-path: apps/mobile/native/ios/Notesnook.xcworkspace
|
||||
update-targets: |
|
||||
Notesnook
|
||||
Make Note
|
||||
@@ -116,5 +103,5 @@ jobs:
|
||||
name: Notesnook.zip
|
||||
path: |
|
||||
Notesnook.ipa
|
||||
apps/mobile/ios/**/*.map
|
||||
apps/mobile/native/ios/**/*.map
|
||||
packages/editor-mobile/sourcemaps/*.map
|
||||
|
||||
8
.github/workflows/monograph.publish.yml
vendored
8
.github/workflows/monograph.publish.yml
vendored
@@ -14,7 +14,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
@@ -39,13 +39,13 @@ jobs:
|
||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
||||
|
||||
- name: Generate build
|
||||
run: npm run tx @notesnook/monograph:build
|
||||
run: npx nx build @notesnook/monograph
|
||||
|
||||
# Setup Buildx
|
||||
- name: Docker Setup Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
context: apps/monograph
|
||||
file: apps/monograph/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
||||
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
|
||||
cache-from: streetwriters/monograph:latest
|
||||
|
||||
|
||||
3
.github/workflows/theme-builder.publish.yml
vendored
3
.github/workflows/theme-builder.publish.yml
vendored
@@ -5,7 +5,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -20,6 +20,7 @@ jobs:
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
|
||||
3
.github/workflows/vericrypt.publish.yml
vendored
3
.github/workflows/vericrypt.publish.yml
vendored
@@ -5,7 +5,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -20,6 +20,7 @@ jobs:
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
|
||||
6
.github/workflows/web.benchmarks.yml
vendored
6
.github/workflows/web.benchmarks.yml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
bench:
|
||||
name: Bench
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -27,6 +27,10 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
run: npm run build:web
|
||||
|
||||
|
||||
33
.github/workflows/web.publish.yml
vendored
33
.github/workflows/web.publish.yml
vendored
@@ -1,21 +1,11 @@
|
||||
name: Publish @notesnook/web
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-track:
|
||||
type: choice
|
||||
required: true
|
||||
default: stable
|
||||
description: "Select the release track"
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -30,23 +20,12 @@ jobs:
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate build (stable)
|
||||
if: ${{ inputs.release-track == 'stable' }}
|
||||
- name: Generate build
|
||||
run: npm run build:web
|
||||
|
||||
- name: Publish to Cloudflare Pages (stable)
|
||||
if: ${{ inputs.release-track == 'stable' }}
|
||||
working-directory: ./apps/web
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./build/
|
||||
|
||||
- name: Generate build (beta)
|
||||
if: ${{ inputs.release-track == 'beta' }}
|
||||
run: npm run build:beta:web
|
||||
|
||||
- name: Publish to Cloudflare Pages (beta)
|
||||
if: ${{ inputs.release-track == 'beta' }}
|
||||
working-directory: ./apps/web
|
||||
run: npx --yes wrangler pages deploy --branch=beta --project-name=notesnook-app-beta ./build/
|
||||
- name: Publish to Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./apps/web/build/
|
||||
|
||||
29
.github/workflows/web.tests.yml
vendored
29
.github/workflows/web.tests.yml
vendored
@@ -9,13 +9,7 @@ on:
|
||||
- "apps/web/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/web.tests.yml"
|
||||
pull_request_target:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/web.tests.yml"
|
||||
pull_request:
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
@@ -23,23 +17,12 @@ on:
|
||||
- "reopened"
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
environment: ${{ github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
'external' || 'internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo true
|
||||
|
||||
build:
|
||||
needs: authorize
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -49,6 +32,10 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate test build
|
||||
run: npm run build:test:web
|
||||
|
||||
@@ -62,13 +49,11 @@ jobs:
|
||||
name: 🧪 Test (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
shard: [1, 2, 3, 4]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,5 +11,3 @@ nx-cloud.env
|
||||
site
|
||||
node_modules.backup
|
||||
.nx
|
||||
/*.patch
|
||||
.taskcache
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -11,5 +11,5 @@
|
||||
"cache": true,
|
||||
"cacheStrategy": "content"
|
||||
},
|
||||
"git.rebaseWhenSync": false
|
||||
"git.rebaseWhenSync": true
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ Notesnook is built using the following technologies:
|
||||
3. React Native — For mobile apps we are using React Native
|
||||
4. Electron — For desktop app
|
||||
5. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
|
||||
6. Nx — maintaining monorepos is hard but Nx makes it easier.
|
||||
|
||||
> **Note: Each project in the monorepo contains its own architecture details which you can refer to.**
|
||||
|
||||
@@ -65,7 +66,7 @@ We take all queries, issues and bug reports that you might have. Feel free to as
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Migrating & Importing your data from other apps — Importer](https://help.notesnook.com/importing-notes)
|
||||
- [Migrating & Importing your data from other apps — Importer](https://importer.notesnook.com/)
|
||||
- [Privacy policy](https://notesnook.com/privacy) & [Terms of service](https://notesnook.com/terms)
|
||||
- [Verify Notesnook encryption claims yourself — Vericrypt](https://vericrypt.notesnook.com/)
|
||||
- [Why Notesnook requires an email address?](https://blog.notesnook.com/why-notesnook-requires-an-email-address/)
|
||||
|
||||
4
apps/desktop/.gitignore
vendored
4
apps/desktop/.gitignore
vendored
@@ -2,6 +2,4 @@ node_modules
|
||||
build
|
||||
output
|
||||
dist
|
||||
_catalog
|
||||
test-results
|
||||
test-artifacts
|
||||
_catalog
|
||||
@@ -24,7 +24,7 @@ Requirements:
|
||||
|
||||
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
|
||||
|
||||
1. `clone` the monorepo:
|
||||
Once you have completed the setup, the first step is to `clone` the monorepo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/streetwriters/notesnook.git
|
||||
@@ -33,28 +33,19 @@ git clone https://github.com/streetwriters/notesnook.git
|
||||
cd notesnook
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
|
||||
```bash
|
||||
# this might take a while to complete
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Run the webapp for desktop environment:
|
||||
|
||||
```bash
|
||||
cd apps/web
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
4. In a separate terminal session, run the desktop app from the root of the project:
|
||||
Now you can finally start the desktop app for development:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
### Release mode
|
||||
|
||||
To run the app in release mode:
|
||||
|
||||
```bash
|
||||
@@ -67,13 +58,13 @@ This will compile and run the app in production mode but it won't generate any p
|
||||
npm run release -- --rebuild
|
||||
|
||||
# For macOS
|
||||
npx electron-builder --config=electron-builder.config.js --mac dmg --arm64 --x64 --publish never
|
||||
npx electron-builder --mac dmg --arm64 --x64 -publish never
|
||||
|
||||
# For Linux (AppImage)
|
||||
npx electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 --publish never
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -publish never
|
||||
|
||||
# For Windows
|
||||
npx electron-builder --config=electron-builder.config.js --win --publish never
|
||||
npx electron-builder --win --publish never
|
||||
```
|
||||
|
||||
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.
|
||||
|
||||
@@ -1,242 +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 { testCleanup, test } from "./test-override.js";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { Page } from "playwright";
|
||||
import { gt, lt } from "semver";
|
||||
import { describe } from "vitest";
|
||||
|
||||
test("update starts downloading if version is outdated", async ({
|
||||
ctx: { page },
|
||||
expect,
|
||||
onTestFinished
|
||||
}) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /updating/i })
|
||||
.waitFor({ state: "attached" });
|
||||
});
|
||||
|
||||
test("update is only shown if version is outdated and auto updates are disabled", async ({
|
||||
ctx,
|
||||
expect,
|
||||
onTestFinished
|
||||
}) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i })
|
||||
.waitFor({ state: "attached" });
|
||||
});
|
||||
|
||||
describe("update to stable if it is newer", () => {
|
||||
test.scoped({ options: { version: "3.0.0-beta.0" } });
|
||||
test("test", async ({ ctx, expect, onTestFinished }) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
const updateButton = page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i });
|
||||
await updateButton.waitFor({ state: "visible" });
|
||||
const content = await updateButton.textContent();
|
||||
const version = content?.split(" ")?.[0] || "";
|
||||
expect(gt(version, "3.0.0-beta.0")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update is not available if it latest stable version is older", () => {
|
||||
test.scoped({ options: { version: "99.0.0-beta.0" } });
|
||||
test("test", async ({ ctx, expect, onTestFinished }) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
expect(
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i })
|
||||
.isHidden()
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("downgrade to stable on switching to stable release track", () => {
|
||||
test.scoped({ options: { version: "99.0.0-beta.0" } });
|
||||
test("test", async ({ ctx, expect, onTestFinished }) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "stable"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
const updateButton = page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i });
|
||||
await updateButton.waitFor({ state: "visible" });
|
||||
const content = await updateButton.textContent();
|
||||
const version = content?.split(" ")?.[0] || "";
|
||||
expect(lt(version, "99.0.0-beta.0")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
async function skipDialog(page: Page) {
|
||||
try {
|
||||
const dialog = page.locator(".ReactModal__Content");
|
||||
const positiveButton = dialog.locator(
|
||||
"button[data-role='positive-button']"
|
||||
);
|
||||
const negativeButton = dialog.locator(
|
||||
"button[data-role='negative-button']"
|
||||
);
|
||||
if (await positiveButton.isVisible())
|
||||
await positiveButton.click({ timeout: 1000 });
|
||||
else if (await negativeButton.isVisible())
|
||||
await negativeButton.click({ timeout: 1000 });
|
||||
} catch (e) {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
@@ -1,24 +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 { buildApp } from "./utils";
|
||||
|
||||
export default async function setup() {
|
||||
await buildApp();
|
||||
}
|
||||
53
apps/desktop/__tests__/launch.test.mjs
Normal file
53
apps/desktop/__tests__/launch.test.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import { launchApp } from "./utils.mjs";
|
||||
import assert from "assert";
|
||||
import slugify from "slugify";
|
||||
import path from "path";
|
||||
import { mkdir } from "fs/promises";
|
||||
|
||||
test("make sure app loads", async (t) => {
|
||||
const { app, page } = await launchApp();
|
||||
try {
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
assert.ok(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
} catch (e) {
|
||||
await mkdir("test-results", { recursive: true });
|
||||
await page.screenshot({
|
||||
path: path.join(
|
||||
"test-results",
|
||||
`${slugify(t.name)}-${process.platform}-${process.arch}-error.png`
|
||||
)
|
||||
});
|
||||
throw e;
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
@@ -1,40 +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 { testCleanup, test } from "./test-override.js";
|
||||
|
||||
test("make sure app loads", async ({
|
||||
ctx: { page },
|
||||
expect,
|
||||
onTestFinished
|
||||
}) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
});
|
||||
@@ -1,64 +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 { test as vitestTest, TestContext } from "vitest";
|
||||
import { buildAndLaunchApp, Fixtures, TestOptions } from "./utils";
|
||||
import { mkdir, rm } from "fs/promises";
|
||||
import path from "path";
|
||||
import slugify from "slugify";
|
||||
|
||||
export const test = vitestTest.extend<Fixtures>({
|
||||
options: { version: "3.0.0" } as TestOptions,
|
||||
ctx: async ({ options }, use) => {
|
||||
const ctx = await buildAndLaunchApp(options);
|
||||
await use(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
export async function testCleanup(context: TestContext) {
|
||||
const ctx = (context.task.context as unknown as Fixtures).ctx;
|
||||
if (context.task.result?.state === "fail") {
|
||||
await mkdir("test-results", { recursive: true });
|
||||
await ctx.page.screenshot({
|
||||
path: path.join(
|
||||
"test-results",
|
||||
`${slugify(context.task.name)}-${process.platform}-${
|
||||
process.arch
|
||||
}-error.png`
|
||||
)
|
||||
});
|
||||
}
|
||||
await ctx.app.close();
|
||||
await rm(ctx.userDataDir, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
maxRetries: 3,
|
||||
retryDelay: 5000
|
||||
}).catch(() => {
|
||||
/*ignore */
|
||||
});
|
||||
await rm(ctx.outputDir, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
maxRetries: 3,
|
||||
retryDelay: 5000
|
||||
}).catch(() => {
|
||||
/*ignore */
|
||||
});
|
||||
}
|
||||
@@ -17,14 +17,22 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { db } from "../common/database";
|
||||
import createDBCollectionStore from "./create-db-collection-store";
|
||||
import { _electron as electron } from "playwright";
|
||||
|
||||
const { useStore: useArchivedStore, useCollection: useArchived } =
|
||||
createDBCollectionStore({
|
||||
getCollection: () =>
|
||||
db.notes.archived.grouped(db.settings.getGroupOptions("archive")),
|
||||
eagerlyFetchFirstBatch: true
|
||||
const executablePath = process.env.EXECUTABLE_PATH;
|
||||
|
||||
// process.platform === "linux"
|
||||
// ? "output/linux-unpacked/Notesnook"
|
||||
// : process.platform === "darwin"
|
||||
// ? "output/mac/Notesnook.app/Contents/MacOS/Notesnook"
|
||||
// : "output/win-unpacked/Notesnook.exe";
|
||||
|
||||
export async function launchApp() {
|
||||
const app = await electron.launch({
|
||||
executablePath
|
||||
});
|
||||
|
||||
export { useArchivedStore, useArchived };
|
||||
const page = await app.firstWindow();
|
||||
|
||||
return { app, page };
|
||||
}
|
||||
@@ -1,226 +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 { execSync } from "child_process";
|
||||
import { cp } from "fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path, { join, resolve } from "path";
|
||||
import { _electron as electron } from "playwright";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true";
|
||||
const productName = `NotesnookTestHarness`;
|
||||
const SOURCE_DIR = resolve("output", productName);
|
||||
|
||||
export interface AppContext {
|
||||
app: import("playwright").ElectronApplication;
|
||||
page: import("playwright").Page;
|
||||
configPath: string;
|
||||
userDataDir: string;
|
||||
outputDir: string;
|
||||
relaunch: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface TestOptions {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface Fixtures {
|
||||
options: TestOptions;
|
||||
ctx: AppContext;
|
||||
}
|
||||
|
||||
export async function buildAndLaunchApp(
|
||||
options?: TestOptions
|
||||
): Promise<AppContext> {
|
||||
const productName = `notesnooktest${makeid(10)}`;
|
||||
const outputDir = path.join("test-artifacts", `${productName}-output`);
|
||||
const executablePath = await copyBuild({
|
||||
...options,
|
||||
outputDir
|
||||
});
|
||||
const { app, page, configPath, userDataDir } = await launchApp(
|
||||
executablePath,
|
||||
productName,
|
||||
options?.version
|
||||
);
|
||||
const ctx: AppContext = {
|
||||
app,
|
||||
page,
|
||||
configPath,
|
||||
userDataDir,
|
||||
outputDir,
|
||||
relaunch: async () => {
|
||||
const { app, page, configPath, userDataDir } = await launchApp(
|
||||
executablePath,
|
||||
productName,
|
||||
options?.version
|
||||
);
|
||||
ctx.app = app;
|
||||
ctx.page = page;
|
||||
ctx.userDataDir = userDataDir;
|
||||
ctx.configPath = configPath;
|
||||
}
|
||||
};
|
||||
return ctx;
|
||||
}
|
||||
|
||||
async function launchApp(
|
||||
executablePath: string,
|
||||
packageName: string,
|
||||
version?: string
|
||||
) {
|
||||
const userDataDir = resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"test-artifacts",
|
||||
"user_data_dirs",
|
||||
packageName
|
||||
);
|
||||
const app = await electron.launch({
|
||||
executablePath,
|
||||
args: IS_DEBUG ? [] : ["--hidden"],
|
||||
env: {
|
||||
...(process.platform === "linux"
|
||||
? {
|
||||
...(process.env as Record<string, string>),
|
||||
APPIMAGE: "true"
|
||||
}
|
||||
: (process.env as Record<string, string>)),
|
||||
CUSTOM_USER_DATA_DIR: userDataDir,
|
||||
...(version
|
||||
? {
|
||||
CUSTOM_APP_VERSION: version
|
||||
}
|
||||
: {})
|
||||
}
|
||||
});
|
||||
|
||||
const page = await app.firstWindow();
|
||||
|
||||
const configPath = path.join(userDataDir, "UserData", "config.json");
|
||||
return {
|
||||
app,
|
||||
page,
|
||||
configPath,
|
||||
userDataDir
|
||||
};
|
||||
}
|
||||
|
||||
let MAX_RETRIES = 3;
|
||||
export async function buildApp(version?: string) {
|
||||
if (!existsSync(SOURCE_DIR)) {
|
||||
const args = [
|
||||
"electron-builder",
|
||||
"--dir",
|
||||
`--${process.arch}`,
|
||||
`--config electron-builder.config.js`,
|
||||
`--c.extraMetadata.productName=${productName}`,
|
||||
`--c.compression=store`,
|
||||
"--publish=never"
|
||||
];
|
||||
if (version) args.push(`--c.extraMetadata.version=${version}`);
|
||||
try {
|
||||
execSync(`npx ${args.join(" ")}`, {
|
||||
stdio: IS_DEBUG ? "inherit" : "ignore",
|
||||
env: {
|
||||
...process.env,
|
||||
NOTESNOOK_STAGING: "true",
|
||||
NN_PRODUCT_NAME: productName,
|
||||
NN_APP_ID: `com.notesnook.test.${productName}`,
|
||||
NN_OUTPUT_DIR: SOURCE_DIR
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (--MAX_RETRIES) {
|
||||
console.log("retrying...");
|
||||
return await buildApp(version);
|
||||
} else throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyBuild({ outputDir }: { outputDir: string }) {
|
||||
return process.platform === "win32"
|
||||
? await makeBuildCopyWindows(outputDir, productName)
|
||||
: process.platform === "darwin"
|
||||
? await makeBuildCopyMacOS(outputDir, productName)
|
||||
: await makeBuildCopyLinux(outputDir, productName);
|
||||
}
|
||||
|
||||
async function makeBuildCopyLinux(outputDir: string, productName: string) {
|
||||
const platformDir =
|
||||
process.arch === "arm64" ? "linux-arm64-unpacked" : "linux-unpacked";
|
||||
const appDir = await makeBuildCopy(outputDir, platformDir);
|
||||
return resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
appDir,
|
||||
productName.toLowerCase().replace(/\s+/g, "-")
|
||||
);
|
||||
}
|
||||
|
||||
async function makeBuildCopyWindows(outputDir: string, productName: string) {
|
||||
const platformDir =
|
||||
process.arch === "arm64" ? "win-arm64-unpacked" : "win-unpacked";
|
||||
const appDir = await makeBuildCopy(outputDir, platformDir);
|
||||
return resolve(__dirname, "..", appDir, `${productName}.exe`);
|
||||
}
|
||||
|
||||
async function makeBuildCopyMacOS(outputDir: string, productName: string) {
|
||||
const platformDir = process.arch === "arm64" ? "mac-arm64" : "mac";
|
||||
const appDir = await makeBuildCopy(outputDir, platformDir);
|
||||
return resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
appDir,
|
||||
`${productName}.app`,
|
||||
"Contents",
|
||||
"MacOS",
|
||||
productName
|
||||
);
|
||||
}
|
||||
|
||||
async function makeBuildCopy(outputDir: string, platformDir: string) {
|
||||
const appDir = outputDir;
|
||||
await cp(join(SOURCE_DIR, platformDir), outputDir, {
|
||||
recursive: true,
|
||||
preserveTimestamps: true,
|
||||
verbatimSymlinks: true,
|
||||
dereference: false,
|
||||
force: true
|
||||
});
|
||||
|
||||
return appDir;
|
||||
}
|
||||
|
||||
function makeid(length: number) {
|
||||
let result = "";
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const charactersLength = characters.length;
|
||||
let counter = 0;
|
||||
while (counter < length) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
counter += 1;
|
||||
}
|
||||
return result.toLowerCase();
|
||||
}
|
||||
Binary file not shown.
@@ -1,223 +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 path = require("path");
|
||||
const pkg = require("./package.json");
|
||||
|
||||
const buildRoot = process.env.NN_BUILD_ROOT || ".";
|
||||
const buildFiles = [
|
||||
`${buildRoot}/build/`,
|
||||
`!${buildRoot}/build/screenshots\${/*}`,
|
||||
`!${buildRoot}/build/banner.jpg`,
|
||||
`!${buildRoot}/build/*.ico`,
|
||||
`!${buildRoot}/build/*.png`
|
||||
];
|
||||
|
||||
const productName = process.env.NN_PRODUCT_NAME || "Notesnook";
|
||||
const appId = process.env.NN_APP_ID || "org.streetwriters.notesnook";
|
||||
const outputDir = process.env.NN_OUTPUT_DIR || "output";
|
||||
const linuxExecutableName = process.env.NN_PRODUCT_NAME
|
||||
? process.env.NN_PRODUCT_NAME.toLowerCase().replace(/\s+/g, "-")
|
||||
: "notesnook";
|
||||
const year = new Date().getFullYear();
|
||||
const isBeta = pkg.version.includes("-beta");
|
||||
|
||||
/**
|
||||
* @type {import("app-builder-lib").Configuration}
|
||||
*/
|
||||
module.exports = {
|
||||
appId: appId,
|
||||
productName: productName,
|
||||
copyright: `Copyright © ${year} Streetwriters (Private) Limited`,
|
||||
artifactName: "notesnook_${os}_${arch}.${ext}",
|
||||
generateUpdatesFilesForAllChannels: true,
|
||||
asar: true,
|
||||
asarUnpack: [
|
||||
"node_modules/sqlite-better-trigram-@(linux|darwin|windows)-${arch}/**/*",
|
||||
"node_modules/sqlite3-fts5-html-@(linux|darwin|windows)-${arch}/**/*"
|
||||
],
|
||||
files: [
|
||||
"!*.chunk.js.map",
|
||||
"!*.chunk.js.LICENSE.txt",
|
||||
...buildFiles,
|
||||
"!node_modules${/*}",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/lib",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/package.json",
|
||||
"node_modules/file-uri-to-path",
|
||||
"node_modules/bindings",
|
||||
"node_modules/node-gyp-build",
|
||||
"node_modules/sqlite-better-trigram",
|
||||
"node_modules/sqlite3-fts5-html",
|
||||
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
|
||||
{
|
||||
from: "node_modules/sqlite-better-trigram-linux-${arch}",
|
||||
to: "node_modules/sqlite-better-trigram-linux-${arch}"
|
||||
},
|
||||
{
|
||||
from: "node_modules/sqlite-better-trigram-darwin-${arch}",
|
||||
to: "node_modules/sqlite-better-trigram-darwin-${arch}"
|
||||
},
|
||||
{
|
||||
from: "node_modules/sqlite-better-trigram-windows-${arch}",
|
||||
to: "node_modules/sqlite-better-trigram-windows-${arch}"
|
||||
},
|
||||
|
||||
{
|
||||
from: "node_modules/sqlite3-fts5-html-linux-${arch}",
|
||||
to: "node_modules/sqlite3-fts5-html-linux-${arch}"
|
||||
},
|
||||
{
|
||||
from: "node_modules/sqlite3-fts5-html-darwin-${arch}",
|
||||
to: "node_modules/sqlite3-fts5-html-darwin-${arch}"
|
||||
},
|
||||
{
|
||||
from: "node_modules/sqlite3-fts5-html-windows-${arch}",
|
||||
to: "node_modules/sqlite3-fts5-html-windows-${arch}"
|
||||
},
|
||||
|
||||
"node_modules/sodium-native/index.js",
|
||||
"node_modules/sodium-native/package.json"
|
||||
],
|
||||
afterPack: "./scripts/removeLocales.js",
|
||||
mac: {
|
||||
bundleVersion: "240",
|
||||
minimumSystemVersion: "10.12.0",
|
||||
target: [
|
||||
{
|
||||
target: "dmg",
|
||||
arch: ["arm64", "x64"]
|
||||
},
|
||||
{
|
||||
target: "zip",
|
||||
arch: ["arm64", "x64"]
|
||||
}
|
||||
],
|
||||
category: "public.app-category.productivity",
|
||||
darkModeSupport: true,
|
||||
type: "distribution",
|
||||
hardenedRuntime: true,
|
||||
entitlements: "assets/entitlements.mac.plist",
|
||||
entitlementsInherit: "assets/entitlements.mac.plist",
|
||||
gatekeeperAssess: false,
|
||||
icon: "assets/icons/app.icns",
|
||||
notarize: true
|
||||
},
|
||||
dmg: {
|
||||
contents: [
|
||||
{
|
||||
x: 130,
|
||||
y: 220
|
||||
},
|
||||
{
|
||||
x: 410,
|
||||
y: 220,
|
||||
type: "link",
|
||||
path: "/Applications"
|
||||
}
|
||||
],
|
||||
icon: "assets/icons/app.icns",
|
||||
title: "Install Notesnook"
|
||||
},
|
||||
mas: {
|
||||
entitlements: "assets/entitlements.mas.plist",
|
||||
entitlementsInherit: "assets/entitlements.mas.inherit.plist",
|
||||
entitlementsLoginHelper: "assets/entitlements.mas.loginhelper.plist",
|
||||
hardenedRuntime: true
|
||||
},
|
||||
win: {
|
||||
target: [
|
||||
{
|
||||
target: "nsis",
|
||||
arch: ["x64", "arm64"]
|
||||
},
|
||||
{
|
||||
target: "portable",
|
||||
arch: ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
signtoolOptions: {
|
||||
signingHashAlgorithms: ["sha256"],
|
||||
sign: "./scripts/sign.js"
|
||||
},
|
||||
icon: "assets/icons/app.ico"
|
||||
},
|
||||
portable: {
|
||||
artifactName: "notesnook_${os}_${arch}_portable.${ext}"
|
||||
},
|
||||
nsis: {
|
||||
oneClick: true,
|
||||
createDesktopShortcut: "always",
|
||||
deleteAppDataOnUninstall: true
|
||||
},
|
||||
linux: {
|
||||
target: [
|
||||
{
|
||||
target: "AppImage",
|
||||
arch: ["x64", "arm64"]
|
||||
},
|
||||
{
|
||||
target: "snap",
|
||||
arch: ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
category: "Office",
|
||||
icon: "assets/icons/app.icns",
|
||||
description: "Your private note taking space",
|
||||
executableName: linuxExecutableName,
|
||||
desktop: {
|
||||
desktopActions: {
|
||||
"new-note": {
|
||||
Name: "New note",
|
||||
Exec: `${linuxExecutableName} new note`
|
||||
},
|
||||
"new-notebook": {
|
||||
Name: "New notebook",
|
||||
Exec: `${linuxExecutableName} new notebook`
|
||||
},
|
||||
"new-reminder": {
|
||||
Name: "New reminder",
|
||||
Exec: `${linuxExecutableName} new reminder`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
snap: {
|
||||
autoStart: false,
|
||||
confinement: "strict",
|
||||
allowNativeWayland: true,
|
||||
base: "core22"
|
||||
},
|
||||
extraResources: ["app-update.yml", "./assets/**"],
|
||||
extraMetadata: {
|
||||
main: path.join(buildRoot, "build", "electron.js")
|
||||
},
|
||||
directories: {
|
||||
buildResources: "assets",
|
||||
output: outputDir
|
||||
},
|
||||
publish: [
|
||||
{
|
||||
provider: "github",
|
||||
repo: "notesnook",
|
||||
owner: "streetwriters",
|
||||
channel: isBeta ? "beta" : "latest"
|
||||
}
|
||||
]
|
||||
};
|
||||
3231
apps/desktop/package-lock.json
generated
3231
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,15 +2,13 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.7",
|
||||
"version": "3.0.21",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"sideEffects": [
|
||||
"src/overrides.ts"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"require": {
|
||||
@@ -27,57 +25,241 @@
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@lingui/core": "5.1.2",
|
||||
"@lingui/core": "4.11.4",
|
||||
"@notesnook/intl": "file:../../packages/intl",
|
||||
"@notesnook/ui": "file:../../packages/ui",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"better-sqlite3-multiple-ciphers": "^12.4.1",
|
||||
"electron-trpc": "0.7.1",
|
||||
"electron-updater": "^6.6.2",
|
||||
"icojs": "^0.19.5",
|
||||
"sqlite-better-trigram": "0.0.5",
|
||||
"sqlite3-fts5-html": "^0.0.6",
|
||||
"better-sqlite3-multiple-ciphers": "11.5.0",
|
||||
"electron-trpc": "0.6.1",
|
||||
"electron-updater": "^6.3.4",
|
||||
"icojs": "^0.19.4",
|
||||
"sqlite-better-trigram": "^0.0.2",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "3.24.3"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@types/node": "22.15.3",
|
||||
"@types/node": "22.7.0",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^37.0.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"esbuild": "0.21.5",
|
||||
"node-abi": "^4.5.0",
|
||||
"node-gyp-build": "^4.8.4",
|
||||
"playwright": "1.48.2",
|
||||
"chokidar": "^4.0.1",
|
||||
"electron": "^31.7.4",
|
||||
"electron-builder": "^25.1.8",
|
||||
"esbuild": "^0.24.0",
|
||||
"node-abi": "^3.68.0",
|
||||
"node-gyp-build": "^4.8.2",
|
||||
"playwright": "^1.48.2",
|
||||
"prebuildify": "^6.0.1",
|
||||
"slugify": "1.6.6",
|
||||
"slugify": "^1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^7.8.0",
|
||||
"vitest": "^3.2.4"
|
||||
"undici": "^6.19.8",
|
||||
"vitest": "^2.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
},
|
||||
"overrides": {
|
||||
"@electron/node-gyp": "^10.2.0-electron.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/dev.mjs",
|
||||
"staging": "node scripts/build.mjs --run",
|
||||
"release": "node scripts/build.mjs",
|
||||
"build": "node ../../scripts/build.mjs",
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||
"postinstall": "patch-package",
|
||||
"test": "vitest run"
|
||||
"test": "node --test --test-timeout 30000 --test-force-exit"
|
||||
},
|
||||
"author": {
|
||||
"name": "Streetwriters (Private) Limited",
|
||||
"email": "support@streetwriters.co",
|
||||
"url": "https://streetwriters.co"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.streetwriters.notesnook",
|
||||
"productName": "Notesnook",
|
||||
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
||||
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"asar": false,
|
||||
"files": [
|
||||
"!*.chunk.js.map",
|
||||
"!*.chunk.js.LICENSE.txt",
|
||||
"build/",
|
||||
"!build/screenshots${/*}",
|
||||
"!build/banner.jpg",
|
||||
"!build/*.ico",
|
||||
"!build/*.png",
|
||||
"!node_modules${/*}",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/lib",
|
||||
"node_modules/better-sqlite3-multiple-ciphers/package.json",
|
||||
"node_modules/file-uri-to-path",
|
||||
"node_modules/bindings",
|
||||
"node_modules/node-gyp-build",
|
||||
"node_modules/sqlite-better-trigram",
|
||||
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
|
||||
{
|
||||
"from": "node_modules/sqlite-better-trigram-linux-${arch}",
|
||||
"to": "node_modules/sqlite-better-trigram-linux-${arch}"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/sqlite-better-trigram-darwin-${arch}",
|
||||
"to": "node_modules/sqlite-better-trigram-darwin-${arch}"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/sqlite-better-trigram-windows-${arch}",
|
||||
"to": "node_modules/sqlite-better-trigram-windows-${arch}"
|
||||
},
|
||||
"node_modules/sodium-native/index.js",
|
||||
"node_modules/sodium-native/package.json"
|
||||
],
|
||||
"afterPack": "./scripts/removeLocales.js",
|
||||
"mac": {
|
||||
"bundleVersion": "240",
|
||||
"minimumSystemVersion": "10.12.0",
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"category": "public.app-category.productivity",
|
||||
"darkModeSupport": true,
|
||||
"type": "distribution",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "assets/entitlements.mac.plist",
|
||||
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||
"gatekeeperAssess": false,
|
||||
"icon": "assets/icons/app.icns",
|
||||
"notarize": true
|
||||
},
|
||||
"dmg": {
|
||||
"contents": [
|
||||
{
|
||||
"x": 130,
|
||||
"y": 220
|
||||
},
|
||||
{
|
||||
"x": 410,
|
||||
"y": 220,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"icon": "assets/icons/app.icns",
|
||||
"title": "Install Notesnook"
|
||||
},
|
||||
"mas": {
|
||||
"entitlements": "assets/entitlements.mas.plist",
|
||||
"entitlementsInherit": "assets/entitlements.mas.inherit.plist",
|
||||
"entitlementsLoginHelper": "assets/entitlements.mas.loginhelper.plist",
|
||||
"hardenedRuntime": true
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"signtoolOptions": {
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"sign": "./scripts/sign.js"
|
||||
},
|
||||
"icon": "assets/icons/app.ico"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "notesnook_${os}_${arch}_portable.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": true,
|
||||
"createDesktopShortcut": "always",
|
||||
"deleteAppDataOnUninstall": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "snap",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"category": "Office",
|
||||
"icon": "assets/icons/app.icns",
|
||||
"description": "Your private note taking space",
|
||||
"executableName": "notesnook",
|
||||
"desktop": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "new-note",
|
||||
"name": "New note",
|
||||
"args": "new note"
|
||||
},
|
||||
{
|
||||
"id": "new-notebook",
|
||||
"name": "New notebook",
|
||||
"args": "new notebook"
|
||||
},
|
||||
{
|
||||
"id": "new-reminder",
|
||||
"name": "New reminder",
|
||||
"args": "new reminder"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"snap": {
|
||||
"autoStart": false,
|
||||
"confinement": "strict",
|
||||
"allowNativeWayland": true
|
||||
},
|
||||
"extraResources": [
|
||||
"app-update.yml",
|
||||
"./assets/**"
|
||||
],
|
||||
"extraMetadata": {
|
||||
"main": "./build/electron.js"
|
||||
},
|
||||
"directories": {
|
||||
"buildResources": "assets",
|
||||
"output": "./output/"
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"repo": "notesnook",
|
||||
"owner": "streetwriters"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
36
apps/desktop/patches/app-builder-lib+25.0.5.patch
Normal file
36
apps/desktop/patches/app-builder-lib+25.0.5.patch
Normal file
@@ -0,0 +1,36 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
index 739a48c..7a7b4af 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
@@ -117,6 +117,7 @@ class LinuxTargetHelper {
|
||||
StartupWMClass: appInfo.productName,
|
||||
...extra,
|
||||
...targetSpecificOptions.desktop,
|
||||
+ actions: undefined
|
||||
};
|
||||
const description = this.getDescription(targetSpecificOptions);
|
||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||
@@ -160,6 +161,23 @@ class LinuxTargetHelper {
|
||||
data += `\n${name}=${desktopMeta[name]}`;
|
||||
}
|
||||
data += "\n";
|
||||
+
|
||||
+ if (targetSpecificOptions.desktop.actions) {
|
||||
+ let actionsData = "";
|
||||
+ const validActions = [];
|
||||
+ for (const action of targetSpecificOptions.desktop.actions) {
|
||||
+ if (!action.id || !action.name || !action.args) continue;
|
||||
+ actionsData += "\n";
|
||||
+ actionsData += `[Desktop Action ${action.id}]
|
||||
+Name=${action.name}
|
||||
+Exec=${desktopMeta.Exec} ${action.args}`;
|
||||
+ actionsData += "\n";
|
||||
+
|
||||
+ validActions.push(action.id);
|
||||
+ }
|
||||
+
|
||||
+ data += `Actions=${validActions.join(";")};\n${actionsData}`
|
||||
+ }
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/targets/snap.js b/node_modules/app-builder-lib/out/targets/snap.js
|
||||
index 0b5405e..753f6c6 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/snap.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/snap.js
|
||||
@@ -115,7 +115,7 @@ class SnapTarget extends core_1.Target {
|
||||
else {
|
||||
const archTriplet = archNameToTriplet(arch);
|
||||
appDescriptor.environment = {
|
||||
- DISABLE_WAYLAND: options.allowNativeWayland ? "" : "1",
|
||||
+ DISABLE_WAYLAND: options.allowNativeWayland ? "0" : "1",
|
||||
PATH: "$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH",
|
||||
SNAP_DESKTOP_RUNTIME: "$SNAP/gnome-platform",
|
||||
LD_LIBRARY_PATH: [
|
||||
@@ -1,11 +0,0 @@
|
||||
diff --git a/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi b/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
|
||||
index 1a14ecd..ff938f1 100644
|
||||
--- a/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
|
||||
+++ b/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
|
||||
@@ -38,5 +38,6 @@
|
||||
'SQLITE_TRACE_SIZE_LIMIT=32',
|
||||
'SQLITE_USER_AUTHENTICATION=0',
|
||||
'SQLITE_USE_URI=0',
|
||||
+ 'SQLITE_ENABLE_REGEXP',
|
||||
],
|
||||
}
|
||||
39
apps/desktop/patches/electron-trpc+0.6.1.patch
Normal file
39
apps/desktop/patches/electron-trpc+0.6.1.patch
Normal file
@@ -0,0 +1,39 @@
|
||||
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
||||
index 07971db..b67a3cf 100644
|
||||
--- a/node_modules/electron-trpc/dist/main.mjs
|
||||
+++ b/node_modules/electron-trpc/dist/main.mjs
|
||||
@@ -537,8 +537,15 @@ class be {
|
||||
attachWindow(r) {
|
||||
F(this, w).includes(r) || (y("Attaching window", r.id), F(this, w).push(r), v(this, N, Z).call(this, r));
|
||||
}
|
||||
- detachWindow(r) {
|
||||
- y("Detaching window", r.id), L(this, w, F(this, w).filter((e) => e !== r)), v(this, R, M).call(this, { webContentsId: r.webContents.id });
|
||||
+ detachWindow(r, webContentsId) {
|
||||
+ y("Detaching window", r.id);
|
||||
+
|
||||
+ if (r.isDestroyed() && webContentsId === undefined) {
|
||||
+ throw new Error('webContentsId is required when calling detachWindow on a destroyed window');
|
||||
+ }
|
||||
+
|
||||
+ L(this, w, F(this, w).filter((e) => e !== r));
|
||||
+ v(this, R, M).call(this, { webContentsId: webContentsId ?? r.webContents.id });
|
||||
}
|
||||
}
|
||||
w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
@@ -548,6 +555,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
for (const [i, a] of F(this, E).entries())
|
||||
i.startsWith(`${r}-${e ?? ""}`) && (y("Closing subscription", i), a.unsubscribe(), F(this, E).delete(i));
|
||||
}, N = new WeakSet(), Z = function(r) {
|
||||
+ const webContentsId = r.webContents.id;
|
||||
r.webContents.on("did-start-navigation", ({ frame: e }) => {
|
||||
y(
|
||||
"Handling webContents `did-start-navigation` event",
|
||||
@@ -558,7 +566,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
frameRoutingId: e.routingId
|
||||
});
|
||||
}), r.webContents.on("destroyed", () => {
|
||||
- y("Handling webContents `destroyed` event"), this.detachWindow(r);
|
||||
+ y("Handling webContents `destroyed` event"), this.detachWindow(r, webContentsId);
|
||||
});
|
||||
};
|
||||
const me = ({
|
||||
@@ -17,72 +17,60 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
console.log("starting build...");
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "fs";
|
||||
import fs, { readFile, writeFile } from "fs/promises";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import yargs from "yargs-parser";
|
||||
import os from "os";
|
||||
import * as childProcess from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
|
||||
|
||||
console.log("imports done...");
|
||||
const args = yargs(process.argv);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const root = args.root || path.join(__dirname, "..");
|
||||
const skipTscBuild = args.skipTscBuild || false;
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
|
||||
);
|
||||
|
||||
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
||||
|
||||
console.log("loaded args", {
|
||||
args,
|
||||
__filename,
|
||||
__dirname,
|
||||
root,
|
||||
skipTscBuild,
|
||||
webAppPath
|
||||
});
|
||||
await fs.rm(path.join(root, "build"), { force: true, recursive: true });
|
||||
|
||||
console.log("removed build folder");
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
console.log("rebuilding...");
|
||||
await exec(
|
||||
"node scripts/execute.mjs @notesnook/web:build:desktop",
|
||||
"yarn nx build:desktop @notesnook/web",
|
||||
path.join(__dirname, "..", "..", "..")
|
||||
);
|
||||
}
|
||||
|
||||
await patchBetterSQLite3();
|
||||
// temporary until there's support for prebuilt binaries for linux ARM
|
||||
if (os.platform() === "linux") await patchBetterSQLite3();
|
||||
|
||||
await fs.cp(path.join(webAppPath, "build"), path.join(root, "build"), {
|
||||
// if (os.platform() === "win32")
|
||||
// await exec(
|
||||
// `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
// path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||
// );
|
||||
|
||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
if (args.variant === "mas") {
|
||||
await exec(`yarn run bundle:mas --outdir=${path.join(root, "build")}`);
|
||||
await exec(`yarn run bundle:mas`);
|
||||
} else {
|
||||
await exec(`yarn run bundle --outdir=${path.join(root, "build")}`);
|
||||
await exec(`yarn run bundle`);
|
||||
}
|
||||
|
||||
if (!skipTscBuild) {
|
||||
await exec(`yarn run build`);
|
||||
}
|
||||
await exec(`yarn run build`);
|
||||
|
||||
if (args.run) {
|
||||
await exec(
|
||||
`yarn electron-builder --dir --${process.arch} --config=electron-builder.config.js`
|
||||
);
|
||||
await exec(`yarn electron-builder --dir --x64`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
if (process.arch === "arm64")
|
||||
await exec(`./output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
else await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
} else {
|
||||
await exec(`./output/linux-unpacked/Notesnook`);
|
||||
}
|
||||
@@ -95,3 +83,21 @@ async function exec(cmd, cwd) {
|
||||
cwd: cwd || process.cwd()
|
||||
});
|
||||
}
|
||||
|
||||
async function patchBetterSQLite3() {
|
||||
const jsonPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"better-sqlite3-multiple-ciphers",
|
||||
"package.json"
|
||||
);
|
||||
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
||||
|
||||
json.version = "11.5.1";
|
||||
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
|
||||
json.repository.url =
|
||||
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
|
||||
|
||||
await writeFile(jsonPath, JSON.stringify(json));
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ async function onChange(first) {
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["npm", "run", "start:desktop"],
|
||||
path.join(__dirname, "..", "..", "web"),
|
||||
["yarn", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,58 +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 { readFile, rm, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export async function patchBetterSQLite3() {
|
||||
console.log("Patching better-sqlite3");
|
||||
|
||||
const jsonPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"better-sqlite3-multiple-ciphers",
|
||||
"package.json"
|
||||
);
|
||||
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
||||
|
||||
delete json.homepage;
|
||||
delete json.repository;
|
||||
|
||||
await writeFile(jsonPath, JSON.stringify(json));
|
||||
|
||||
await rm(
|
||||
path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"better-sqlite3-multiple-ciphers",
|
||||
"build"
|
||||
),
|
||||
{ force: true, recursive: true }
|
||||
);
|
||||
}
|
||||
|
||||
if (process.argv[1] === __filename) {
|
||||
patchBetterSQLite3();
|
||||
}
|
||||
@@ -19,16 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
app,
|
||||
dialog,
|
||||
Menu,
|
||||
MenuItem,
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
Notification,
|
||||
shell
|
||||
} from "electron";
|
||||
import { app, dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { AutoLaunch } from "../utils/autolaunch";
|
||||
import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
@@ -38,11 +29,10 @@ import { dirname } from "path";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak, isSnap } from "../utils";
|
||||
import { isFlatpak } from "../utils";
|
||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||
import { rm } from "fs/promises";
|
||||
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
|
||||
import type { MenuItem as NNMenuItem } from "@notesnook/ui";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
@@ -59,7 +49,6 @@ const NotificationOptions = z.object({
|
||||
|
||||
export const osIntegrationRouter = t.router({
|
||||
isFlatpak: t.procedure.query(() => isFlatpak()),
|
||||
isSnap: t.procedure.query(() => isSnap()),
|
||||
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
@@ -235,63 +224,5 @@ export const osIntegrationRouter = t.router({
|
||||
nativeTheme.off("updated", updated);
|
||||
};
|
||||
})
|
||||
),
|
||||
|
||||
showMenu: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
menuItems: z.array(z.any())
|
||||
})
|
||||
)
|
||||
.subscription(({ input: { menuItems } }) =>
|
||||
observable<string[]>((emit) => {
|
||||
const items = menuItems as NNMenuItem[];
|
||||
const menu = new Menu();
|
||||
for (const item of items) {
|
||||
const menuItem = toMenuItem(item, (id) => emit.next(id));
|
||||
if (menuItem) menu.append(menuItem);
|
||||
}
|
||||
if (menu.items.length > 0) menu.popup();
|
||||
return () => {
|
||||
menu.removeAllListeners();
|
||||
menu.closePopup();
|
||||
};
|
||||
})
|
||||
)
|
||||
)
|
||||
});
|
||||
|
||||
function toMenuItem(
|
||||
item: NNMenuItem,
|
||||
onClick: (id: string[]) => void,
|
||||
parentKey?: string
|
||||
): MenuItem | undefined {
|
||||
switch (item.type) {
|
||||
case "lazy-loader":
|
||||
return undefined;
|
||||
case "separator":
|
||||
return new MenuItem({ type: "separator" });
|
||||
case "button": {
|
||||
const submenu = item.menu ? new Menu() : undefined;
|
||||
if (submenu && item.menu) {
|
||||
for (const subitem of item.menu.items) {
|
||||
const subMenuItem = toMenuItem(subitem, onClick, item.key);
|
||||
if (subMenuItem) submenu.append(subMenuItem);
|
||||
}
|
||||
}
|
||||
|
||||
return new MenuItem({
|
||||
label: item.title,
|
||||
enabled: !item.isDisabled,
|
||||
visible: !item.isHidden,
|
||||
toolTip: item.tooltip,
|
||||
sublabel: item.tooltip,
|
||||
checked: item.isChecked,
|
||||
type: submenu ? "submenu" : item.isChecked ? "checkbox" : "normal",
|
||||
id: item.key,
|
||||
submenu,
|
||||
click: () => onClick(parentKey ? [parentKey, item.key] : [item.key]),
|
||||
accelerator: item.modifier?.replace("Mod", "CommandOrControl")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@ export class SQLite {
|
||||
initialized = false;
|
||||
preparedStatements: Map<string, Statement<unknown[]>> = new Map();
|
||||
retryCounter: Record<string, number> = {};
|
||||
extensionsLoaded = false;
|
||||
|
||||
constructor() {
|
||||
console.log("new sqlite worker");
|
||||
}
|
||||
@@ -48,6 +46,8 @@ export class SQLite {
|
||||
this.sqlite = require("better-sqlite3-multiple-ciphers")(
|
||||
filePath
|
||||
).unsafeMode(true);
|
||||
const betterTrigram = require("sqlite-better-trigram");
|
||||
betterTrigram.load(this.sqlite);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,29 +115,9 @@ export class SQLite {
|
||||
} catch (e) {
|
||||
if (e instanceof Error) e.message += ` (query: ${sql})`;
|
||||
throw e;
|
||||
} finally {
|
||||
// Since SQLite 3.48.0 (SQLite3MC v2.0.2) it's not possible to load fts5
|
||||
// extensions before database has been decrypting. This is because
|
||||
// executing a `SELECT` now accesses the underlying databases resulting in
|
||||
// an error. Since FTS5 extensions depend on `SELECT fts5` to load the
|
||||
// fts5 API, we must wait decrypt the database before we can load
|
||||
// the extensions.
|
||||
if (!this.extensionsLoaded && (await this.isDatabaseReady())) {
|
||||
this.loadExtensions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadExtensions() {
|
||||
this.sqlite?.loadExtension(
|
||||
getExtensionPath("sqlite-better-trigram", "better-trigram")
|
||||
);
|
||||
this.sqlite?.loadExtension(
|
||||
getExtensionPath("sqlite3-fts5-html", "fts5-html")
|
||||
);
|
||||
this.extensionsLoaded = true;
|
||||
}
|
||||
|
||||
async run<R>(
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
@@ -162,53 +142,4 @@ export class SQLite {
|
||||
retryDelay: 500
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This just executes `SELECT 1` on the database to make sure its ready.
|
||||
* On an encrypted database, this will fail until `PRAGMA key` has been
|
||||
* called.
|
||||
*/
|
||||
private async isDatabaseReady() {
|
||||
// return this.exec(`SELECT 1;`)
|
||||
// .then(() => true)
|
||||
// .catch(() => false);
|
||||
if (!this.sqlite) return false;
|
||||
try {
|
||||
this.sqlite.prepare(`SELECT 1;`).run();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExtensionPath(extensionName: string, entryPoint: string) {
|
||||
const path = require("path");
|
||||
const { statSync } = require("fs");
|
||||
|
||||
const os = process.platform === "win32" ? "windows" : process.platform;
|
||||
const packageName = `${extensionName}-${os}-${process.arch}`;
|
||||
const extensionSuffix =
|
||||
process.platform === "win32"
|
||||
? "dll"
|
||||
: process.platform === "darwin"
|
||||
? "dylib"
|
||||
: "so";
|
||||
let loadablePath = path.join(
|
||||
require.resolve(extensionName),
|
||||
"..",
|
||||
"..",
|
||||
packageName,
|
||||
`${entryPoint}.${extensionSuffix}`
|
||||
);
|
||||
|
||||
if (loadablePath.includes(".asar"))
|
||||
loadablePath = loadablePath
|
||||
.replace("electron.asar", "app.asar")
|
||||
.replace(".asar", ".asar.unpacked");
|
||||
|
||||
if (!statSync(loadablePath, { throwIfNoEntry: false })) {
|
||||
throw new Error(`${extensionName} not found at ${loadablePath}.`);
|
||||
}
|
||||
return loadablePath;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import { CancellationToken, autoUpdater } from "electron-updater";
|
||||
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
|
||||
import { z } from "zod";
|
||||
import { config } from "../utils/config";
|
||||
import { app } from "electron";
|
||||
|
||||
type UpdateInfo = { version: string };
|
||||
type Progress = { percent: number };
|
||||
@@ -34,7 +33,6 @@ let downloadTimeout: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
export const updaterRouter = t.router({
|
||||
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
||||
releaseTrack: t.procedure.query(() => config.releaseTrack),
|
||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||
download: t.procedure.query(async () => {
|
||||
if (cancellationToken) return;
|
||||
@@ -69,13 +67,7 @@ export const updaterRouter = t.router({
|
||||
.mutation(({ input: { enabled } }) => {
|
||||
config.automaticUpdates = enabled;
|
||||
}),
|
||||
changeReleaseTrack: t.procedure
|
||||
.input(z.object({ track: z.string() }))
|
||||
.mutation(({ input: { track } }) => {
|
||||
config.releaseTrack = track;
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
}),
|
||||
|
||||
onChecking: createSubscription("checking-for-update"),
|
||||
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
|
||||
"update-downloaded"
|
||||
|
||||
@@ -17,8 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./overrides";
|
||||
import { app, BrowserWindow, nativeTheme, shell, dialog } from "electron";
|
||||
import { app, BrowserWindow, nativeTheme, shell } from "electron";
|
||||
import { isDevelopment } from "./utils";
|
||||
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
|
||||
import { configureAutoUpdater } from "./utils/autoupdater";
|
||||
@@ -86,8 +85,6 @@ async function createWindow() {
|
||||
const mainWindowState = new WindowState({});
|
||||
const mainWindow = new BrowserWindow({
|
||||
show: !cliOptions.hidden,
|
||||
paintWhenInitiallyHidden: cliOptions.hidden,
|
||||
skipTaskbar: cliOptions.hidden,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
@@ -104,10 +101,7 @@ async function createWindow() {
|
||||
...(config.desktopSettings.nativeTitlebar
|
||||
? {}
|
||||
: {
|
||||
titleBarStyle:
|
||||
process.platform === "win32" || process.platform === "darwin"
|
||||
? "hidden"
|
||||
: "default",
|
||||
titleBarStyle: process.platform === "win32" || process.platform === "darwin" ? "hidden" : "default",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
@@ -148,11 +142,6 @@ async function createWindow() {
|
||||
await AssetManager.loadIcons();
|
||||
setupDesktopIntegration(config.desktopSettings);
|
||||
|
||||
mainWindow.webContents.session.setPermissionRequestHandler(
|
||||
(webContents, permission, callback) => {
|
||||
callback(permission === "geolocation" ? false : true);
|
||||
}
|
||||
);
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
);
|
||||
@@ -182,17 +171,6 @@ 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();
|
||||
|
||||
|
||||
@@ -1,41 +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 { app } from "electron";
|
||||
import path from "path";
|
||||
|
||||
const customVersion = process.env.CUSTOM_APP_VERSION;
|
||||
if (customVersion) {
|
||||
app.getVersion = () => customVersion;
|
||||
console.log("setting custom version:", customVersion);
|
||||
}
|
||||
|
||||
if (process.env.CUSTOM_USER_DATA_DIR) {
|
||||
app.setPath(
|
||||
"appData",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "AppData")
|
||||
);
|
||||
app.setPath(
|
||||
"userData",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "UserData")
|
||||
);
|
||||
app.setPath(
|
||||
"documents",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "Documents")
|
||||
);
|
||||
}
|
||||
@@ -20,22 +20,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { config } from "./config";
|
||||
|
||||
const CHANNEL = autoUpdater.currentVersion.raw.endsWith("-beta")
|
||||
? "beta"
|
||||
: "latest";
|
||||
async function configureAutoUpdater() {
|
||||
const releaseTrack =
|
||||
config.releaseTrack === "stable" ? "latest" : config.releaseTrack;
|
||||
autoUpdater.setFeedURL({
|
||||
provider: "generic",
|
||||
url: `https://notesnook.com/api/v1/releases/${process.platform}/${releaseTrack}`,
|
||||
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
|
||||
useMultipleRangeRequest: false,
|
||||
channel: releaseTrack
|
||||
channel: CHANNEL
|
||||
});
|
||||
|
||||
autoUpdater.autoDownload = config.automaticUpdates;
|
||||
autoUpdater.allowDowngrade =
|
||||
// only allow downgrade if the current version is a prerelease
|
||||
// and the user has changed the release track to stable
|
||||
config.releaseTrack === "stable" &&
|
||||
autoUpdater.currentVersion.prerelease.length > 0;
|
||||
autoUpdater.allowDowngrade = false;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
|
||||
@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { nativeTheme } from "electron";
|
||||
import { JSONStorage } from "./json-storage";
|
||||
import { z } from "zod";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
@@ -47,9 +46,6 @@ export const config = {
|
||||
automaticUpdates: true,
|
||||
proxyRules: "",
|
||||
customDns: true,
|
||||
releaseTrack: autoUpdater.currentVersion.raw.includes("-beta")
|
||||
? "beta"
|
||||
: "stable",
|
||||
|
||||
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
|
||||
windowControlsIconColor:
|
||||
|
||||
@@ -29,7 +29,3 @@ export function isDevelopment() {
|
||||
export function isFlatpak() {
|
||||
return existsSync("/.flatpak-info");
|
||||
}
|
||||
|
||||
export function isSnap() {
|
||||
return process.env.SNAP !== undefined;
|
||||
}
|
||||
|
||||
@@ -143,44 +143,16 @@ function setupMenu() {
|
||||
})
|
||||
);
|
||||
|
||||
if (params.isEditable) {
|
||||
if (params.isEditable)
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: strings.paste(),
|
||||
role: "paste",
|
||||
role: "pasteAndMatchStyle",
|
||||
enabled: clipboard.readText("clipboard").length > 0,
|
||||
accelerator: "CommandOrControl+V"
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label:
|
||||
process.platform === "darwin"
|
||||
? strings.pasteAndMatchStyle()
|
||||
: strings.pasteWithoutFormatting(),
|
||||
role: "pasteAndMatchStyle",
|
||||
enabled: clipboard.readText("clipboard").length > 0,
|
||||
accelerator:
|
||||
process.platform === "darwin"
|
||||
? "Option+Shift+Command+V"
|
||||
: "Shift+CommandOrControl+V"
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
type: "separator"
|
||||
})
|
||||
);
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: strings.spellCheck(),
|
||||
role: "toggleSpellChecker"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (menu.items.length > 0) menu.popup();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,42 +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 { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120 * 1000,
|
||||
hookTimeout: 120 * 1000,
|
||||
sequence: {
|
||||
concurrent: true,
|
||||
shuffle: true
|
||||
},
|
||||
globalSetup: "./__tests__/global-setup.ts",
|
||||
dir: "./__tests__/",
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/cypress/**",
|
||||
"**/.{idea,git,cache,output,temp}/**",
|
||||
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*",
|
||||
"**/test-results/**",
|
||||
"**/test-artifacts/**"
|
||||
]
|
||||
}
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
/** @type {Detox.DetoxConfig} */
|
||||
module.exports = {
|
||||
testRunner: {
|
||||
args: {
|
||||
$0: "jest",
|
||||
config: "e2e/jest.config.js"
|
||||
},
|
||||
jest: {
|
||||
setupTimeout: 120000
|
||||
}
|
||||
},
|
||||
apps: {
|
||||
"ios.debug": {
|
||||
type: "ios.app",
|
||||
binaryPath:
|
||||
"ios/build/Build/Products/Debug-iphonesimulator/Notesnook.app",
|
||||
build:
|
||||
"xcodebuild -workspace ios/Notesnook.xcworkspace -scheme YOUR_APP -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build"
|
||||
},
|
||||
"ios.release": {
|
||||
type: "ios.app",
|
||||
binaryPath:
|
||||
"ios/build/Build/Products/Release-iphonesimulator/Notesnook.app",
|
||||
build:
|
||||
"xcodebuild -workspace ios/Notesnook.xcworkspace -scheme YOUR_APP -configuration Release -sdk iphonesimulator -derivedDataPath ios/build"
|
||||
},
|
||||
"android.debug": {
|
||||
type: "android.apk",
|
||||
binaryPath: "android/app/build/outputs/apk/debug/app-arm64-v8a-debug.apk",
|
||||
testBinaryPath:
|
||||
"android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk",
|
||||
build:
|
||||
"cd android ; ENVFILE=.env.test ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..",
|
||||
reversePorts: [8081]
|
||||
},
|
||||
"android.release": {
|
||||
type: "android.apk",
|
||||
binaryPath:
|
||||
"android/app/build/outputs/apk/release/app-arm64-v8a-release.apk",
|
||||
testBinaryPath:
|
||||
"android/app/build/outputs/apk/androidTest/release/app-release-androidTest.apk",
|
||||
build:
|
||||
"cd android ; ENVFILE=.env.test ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd .."
|
||||
}
|
||||
},
|
||||
devices: {
|
||||
simulator: {
|
||||
type: "ios.simulator",
|
||||
device: {
|
||||
type: "iPhone 17 Pro Max"
|
||||
}
|
||||
},
|
||||
attached: {
|
||||
type: "android.attached",
|
||||
device: {
|
||||
adbName: ".*"
|
||||
}
|
||||
},
|
||||
emulator: {
|
||||
type: "android.emulator",
|
||||
device: {
|
||||
avdName: "Pixel_5_API_36"
|
||||
}
|
||||
}
|
||||
},
|
||||
configurations: {
|
||||
"ios.sim.debug": {
|
||||
device: "simulator",
|
||||
app: "ios.debug"
|
||||
},
|
||||
"ios.sim.release": {
|
||||
device: "simulator",
|
||||
app: "ios.release"
|
||||
},
|
||||
"android.att.debug": {
|
||||
device: "attached",
|
||||
app: "android.debug"
|
||||
},
|
||||
"android.att.release": {
|
||||
device: "attached",
|
||||
app: "android.release"
|
||||
},
|
||||
"android.emu.debug": {
|
||||
device: "emulator",
|
||||
app: "android.debug"
|
||||
},
|
||||
"android.emu.release": {
|
||||
device: "emulator",
|
||||
app: "android.release"
|
||||
}
|
||||
}
|
||||
};
|
||||
12
apps/mobile/.gitignore
vendored
12
apps/mobile/.gitignore
vendored
@@ -4,7 +4,7 @@ artifacts/
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
android/app/src/main/assets/
|
||||
native/android/app/src/main/assets/
|
||||
*Issues.md
|
||||
build_cache/
|
||||
#
|
||||
@@ -16,12 +16,11 @@ build_cache/
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
.kotlin/
|
||||
|
||||
# Xcode
|
||||
#
|
||||
ios/Pods
|
||||
ios/DerivedData
|
||||
native/ios/Pods
|
||||
native/ios/DerivedData
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
@@ -40,7 +39,7 @@ DerivedData
|
||||
*.xcuserstate
|
||||
*.hprof
|
||||
**/.xcode.env.local
|
||||
cache
|
||||
native/cache
|
||||
# Android/IntelliJ
|
||||
#
|
||||
rn-build-deps/
|
||||
@@ -58,7 +57,7 @@ yarn-error.log
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
|
||||
*.keystore
|
||||
|
||||
# fastlane
|
||||
#
|
||||
@@ -70,7 +69,6 @@ buck-out/
|
||||
*/fastlane/report.xml
|
||||
*/fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
vendor
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby ">= 2.6.10"
|
||||
|
||||
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
|
||||
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||
gem 'xcodeproj', '< 1.26.0'
|
||||
gem 'concurrent-ruby', '< 1.3.4'
|
||||
|
||||
# Ruby 3.4.0 has removed some libraries from the standard library.
|
||||
gem 'bigdecimal'
|
||||
gem 'logger'
|
||||
gem 'benchmark'
|
||||
gem 'mutex_m'
|
||||
Binary file not shown.
@@ -1,264 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.streetwriters.notesnook">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="com.android.vending.BILLING" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:node="remove" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.USE_FULL_SCREEN_INTENT"
|
||||
tools:node="remove" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<!-- If you don't know the MIME type in advance, set "mimeType" to "*/*". -->
|
||||
<data android:mimeType="application/pdf" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<!-- If you don't know the MIME type in advance, set "mimeType" to "*/*". -->
|
||||
<data android:mimeType="text/*" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<!-- If you don't know the MIME type in advance, set "mimeType" to "*/*". -->
|
||||
<data android:mimeType="image/*" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<!-- If you don't know the MIME type in advance, set "mimeType" to "*/*". -->
|
||||
|
||||
<data android:mimeType="video/*" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<!-- If you don't know the MIME type in advance, set "mimeType" to "*/*". -->
|
||||
<data android:mimeType="audio/*" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/BootTheme">
|
||||
|
||||
<receiver
|
||||
android:name=".NoteWidget"
|
||||
android:exported="false"
|
||||
android:label="@string/quick_note">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/new_note_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".NotePreviewWidget"
|
||||
android:exported="false"
|
||||
android:label="@string/note">
|
||||
<intent-filter>"
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/note_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".ReminderWidgetProvider"
|
||||
android:exported="false"
|
||||
android:label="@string/reminders_title">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_reminders_info" />
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".NotePreviewConfigureActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:exported="true"
|
||||
android:label="NotePreviewConfigure"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Notesnook">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="app.notesnook.com"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Notesnook">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="notesnook" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.facebook.react.devsupport.DevSettingsActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_share"
|
||||
android:noHistory="true"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/AppThemeB"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<intent-filter android:label="Make Note">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Make Note">
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Make Note">
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".OnClearFromRecentService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:stopWithTask="false" />
|
||||
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
|
||||
|
||||
<service
|
||||
android:name="com.streetwriters.notesnook.BootTaskService"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="The service is required by the app to restore notifications of pinned notes, restore notification with reply input for creating notes and restore data in note preview widgets on device reboot." />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".NotesnookTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/add_note"
|
||||
android:label="New note"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".ReminderViewsService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_viewer_provider_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="com.vinzscam.reactnativefileviewer.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_viewer_provider_paths" />
|
||||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name=".BootRecieverService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.streetwriters.notesnook
|
||||
|
||||
import android.app.Application
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import org.wonday.orientation.OrientationActivityLifecycle;
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
override val reactHost: ReactHost by lazy {
|
||||
getDefaultReactHost(
|
||||
context = applicationContext,
|
||||
packageList =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
add(NNativeModulePackage());
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
registerActivityLifecycleCallbacks(OrientationActivityLifecycle.getInstance());
|
||||
loadReactNative(this)
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
import com.google.gson.Gson;
|
||||
import com.streetwriters.notesnook.datatypes.Note;
|
||||
|
||||
public class NotePreviewConfigureActivity extends ReactActivity {
|
||||
|
||||
static int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
|
||||
static NotePreviewConfigureActivity activity;
|
||||
|
||||
/**
|
||||
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
|
||||
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
|
||||
* (aka React 18) with two boolean flags.
|
||||
*/
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new DefaultReactActivityDelegate(
|
||||
this,
|
||||
getMainComponentName(),
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
|
||||
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
|
||||
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
Intent intent = getIntent();
|
||||
Bundle extras = intent.getExtras();
|
||||
int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID);
|
||||
NotePreviewConfigureActivity.appWidgetId = appWidgetId;
|
||||
}
|
||||
Intent resultValue = new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
setResult(Activity.RESULT_CANCELED, resultValue);
|
||||
activity = this;
|
||||
}
|
||||
|
||||
public static void saveAndFinish(Context context) {
|
||||
if (NotePreviewConfigureActivity.activity == null || appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return;
|
||||
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
|
||||
NotePreviewWidget.updateAppWidget(context, appWidgetManager, appWidgetId);
|
||||
Intent resultValue = new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
NotePreviewConfigureActivity.activity.setResult(RESULT_OK, resultValue);
|
||||
NotePreviewConfigureActivity.activity.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "NotePreviewConfigure";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.widget.RemoteViews;
|
||||
import com.google.gson.Gson;
|
||||
import com.streetwriters.notesnook.datatypes.Note;
|
||||
|
||||
|
||||
public class NotePreviewWidget extends AppWidgetProvider {
|
||||
static String OpenNoteId = "com.streetwriters.notesnook.OpenNoteId";
|
||||
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
|
||||
int appWidgetId) {
|
||||
String data = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), "");
|
||||
if (data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Gson gson = new Gson();
|
||||
Note note = gson.fromJson(data, Note.class);
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget);
|
||||
views.setTextViewText(R.id.widget_title, note.getTitle());
|
||||
views.setTextViewText(R.id.widget_body, note.getHeadline());
|
||||
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
intent.putExtra(OpenNoteId, note.getId());
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(RCTNNativeModule.IntentType, "OpenNote");
|
||||
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);
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
|
||||
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
|
||||
}
|
||||
|
||||
private static Bundle getActivityOptionsBundle() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
ActivityOptions activityOptions = ActivityOptions.makeBasic();
|
||||
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
|
||||
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
|
||||
return activityOptions.toBundle();
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleted(Context context, int[] appWidgetIds) {
|
||||
super.onDeleted(context, appWidgetIds);
|
||||
SharedPreferences.Editor edit = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE).edit();
|
||||
for (int id: appWidgetIds) {
|
||||
edit.remove(String.valueOf(id));
|
||||
}
|
||||
edit.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
|
||||
// There may be multiple widgets active, so update all of them
|
||||
for (int appWidgetId : appWidgetIds) {
|
||||
updateAppWidget(context, appWidgetManager, appWidgetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
/**
|
||||
* Implementation of App Widget functionality.
|
||||
*/
|
||||
public class NoteWidget extends AppWidgetProvider {
|
||||
|
||||
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
|
||||
int appWidgetId) {
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_note_widget);
|
||||
setClickIntent(context, views);
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views);
|
||||
}
|
||||
|
||||
static void setClickIntent(Context context, RemoteViews views) {
|
||||
Intent intent = new Intent(context, ShareActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
|
||||
views.setOnClickPendingIntent(R.id.new_note, pendingIntent);
|
||||
}
|
||||
|
||||
private static Bundle getActivityOptionsBundle() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
ActivityOptions activityOptions = ActivityOptions.makeBasic();
|
||||
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
|
||||
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
|
||||
return activityOptions.toBundle();
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
|
||||
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
|
||||
updateAppWidget(context, appWidgetManager, appWidgetId, newOptions);
|
||||
}
|
||||
|
||||
private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle options) {
|
||||
int minWidth = options != null ? options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) : 0;
|
||||
int minHeight = options != null ? options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) : 0;
|
||||
|
||||
int layoutId = (minWidth < 100) ? R.layout.new_note_widget_icon : R.layout.new_note_widget;
|
||||
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), layoutId);
|
||||
setClickIntent(context, views);
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
|
||||
for (int appWidgetId : appWidgetIds) {
|
||||
updateAppWidget(context, appWidgetManager, appWidgetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.google.gson.Gson;
|
||||
import com.streetwriters.notesnook.datatypes.Note;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
Intent lastIntent;
|
||||
ReactContext mContext;
|
||||
static String IntentType = "com.streetwriters.notesnook.IntentType";
|
||||
|
||||
public RCTNNativeModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mContext = reactContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "NNativeModule";
|
||||
}
|
||||
|
||||
|
||||
@ReactMethod
|
||||
public void setBackgroundColor(final String color) {
|
||||
try {
|
||||
getCurrentActivity().getWindow().getDecorView().setBackgroundColor(Color.parseColor(color));
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getActivityName(Promise promise) {
|
||||
try {
|
||||
promise.resolve(getCurrentActivity().getClass().getSimpleName());
|
||||
} catch (Exception e) {
|
||||
promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setSecureMode(final boolean mode) {
|
||||
try {
|
||||
|
||||
getCurrentActivity().runOnUiThread(() -> {
|
||||
|
||||
try {
|
||||
if (mode)
|
||||
getCurrentActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
else
|
||||
getCurrentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
});
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setAppState(final String appState) {
|
||||
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor edit = appStateDetails.edit();
|
||||
edit.putString("appState", appState);
|
||||
edit.apply();
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public String getAppState() {
|
||||
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
|
||||
String appStateValue = appStateDetails.getString("appState", "");
|
||||
return appStateValue.isEmpty() ? null : appStateValue;
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public int getWidgetId() {
|
||||
return NotePreviewConfigureActivity.appWidgetId;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void setString(final String storeName, final String key, final String value) {
|
||||
SharedPreferences details = getReactApplicationContext().getSharedPreferences(storeName, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor edit = details.edit();
|
||||
edit.putString(key, value);
|
||||
edit.apply();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeString(final String storeName, final String key) {
|
||||
SharedPreferences details = getReactApplicationContext().getSharedPreferences(storeName, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor edit = details.edit();
|
||||
edit.remove(key);
|
||||
edit.apply();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getString(final String storeName, final String key, Promise promise) {
|
||||
SharedPreferences details = getReactApplicationContext().getSharedPreferences(storeName, Context.MODE_PRIVATE);
|
||||
String value = details.getString(key, "");
|
||||
promise.resolve(value.isEmpty() ? null : value);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void saveAndFinish() {
|
||||
NotePreviewConfigureActivity.saveAndFinish(mContext);
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public WritableMap getIntent() {
|
||||
WritableMap map = Arguments.createMap();
|
||||
if (getCurrentActivity() != null) {
|
||||
Intent intent = getCurrentActivity().getIntent();
|
||||
Bundle extras = getCurrentActivity().getIntent().getExtras();
|
||||
|
||||
if (extras != null && intent != lastIntent) {
|
||||
lastIntent = intent;
|
||||
if (Objects.equals(extras.getString(IntentType), "NewReminder")) {
|
||||
map.putString(ReminderWidgetProvider.NewReminder, extras.getString(ReminderWidgetProvider.NewReminder));
|
||||
} else if (Objects.equals(extras.getString(IntentType), "OpenReminder")) {
|
||||
map.putString(ReminderViewsService.OpenReminderId, extras.getString(ReminderViewsService.OpenReminderId));
|
||||
} else if (Objects.equals(extras.getString(IntentType), "OpenNote")) {
|
||||
map.putString(NotePreviewWidget.OpenNoteId, extras.getString(NotePreviewWidget.OpenNoteId));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void cancelAndFinish() {
|
||||
NotePreviewConfigureActivity.activity.setResult(Activity.RESULT_CANCELED);
|
||||
NotePreviewConfigureActivity.activity.finish();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getWidgetNotes(Promise promise) {
|
||||
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
Map<String, ?> map = pref.getAll();
|
||||
WritableArray arr = Arguments.createArray();
|
||||
for(Map.Entry<String,?> entry : map.entrySet()){
|
||||
if (entry.getKey().equals("remindersList")) continue;
|
||||
String value = (String) entry.getValue();
|
||||
Gson gson = new Gson();
|
||||
Note note = gson.fromJson(value, Note.class);
|
||||
arr.pushString(note.getId());
|
||||
}
|
||||
promise.resolve(arr);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void hasWidgetNote(final String noteId, Promise promise) {
|
||||
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
Map<String, ?> map = pref.getAll();
|
||||
boolean found = false;
|
||||
for(Map.Entry<String,?> entry : map.entrySet()){
|
||||
String value = (String) entry.getValue();
|
||||
if (value.contains(noteId)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
promise.resolve(found);
|
||||
}
|
||||
@ReactMethod
|
||||
public void updateWidgetNote(final String noteId, final String data) {
|
||||
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
Map<String, ?> map = pref.getAll();
|
||||
SharedPreferences.Editor edit = pref.edit();
|
||||
ArrayList<String> ids = new ArrayList<>();
|
||||
for(Map.Entry<String,?> entry : map.entrySet()) {
|
||||
String value = (String) entry.getValue();
|
||||
if (value.contains(noteId)) {
|
||||
edit.putString(entry.getKey(), data);
|
||||
ids.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
edit.apply();
|
||||
for (String id: ids) {
|
||||
NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), Integer.parseInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void updateReminderWidget() {
|
||||
AppWidgetManager wm = AppWidgetManager.getInstance(mContext);
|
||||
int[] ids = wm.getAppWidgetIds(ComponentName.createRelative(mContext.getPackageName(), ReminderWidgetProvider.class.getName()));
|
||||
for (int id: ids) {
|
||||
Log.d("Reminders", "Updating" + id);
|
||||
RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.widget_reminders);
|
||||
ReminderWidgetProvider.updateAppWidget(mContext, wm, id, views);
|
||||
wm.notifyAppWidgetViewDataChanged(id, R.id.widget_list_view);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public boolean isGestureNavigationEnabled() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
try {
|
||||
String navBarMode = Settings.Secure.getString(
|
||||
mContext.getContentResolver(),
|
||||
"navigation_mode"
|
||||
);
|
||||
return "2".equals(navBarMode);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViewsService;
|
||||
import android.content.Context;
|
||||
import android.widget.RemoteViews;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.streetwriters.notesnook.datatypes.Reminder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ReminderViewsService extends RemoteViewsService {
|
||||
static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId";
|
||||
@Override
|
||||
public RemoteViewsFactory onGetViewFactory(Intent intent) {
|
||||
return new ReminderRemoteViewsFactory(this.getApplicationContext(), intent);
|
||||
}
|
||||
}
|
||||
|
||||
class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
|
||||
private Context context;
|
||||
private List<Reminder> reminders;
|
||||
|
||||
public ReminderRemoteViewsFactory(Context context, Intent intent) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Initialize reminders list
|
||||
reminders = new ArrayList<Reminder>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataSetChanged() {
|
||||
reminders.clear();
|
||||
SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
Gson gson = new Gson();
|
||||
reminders = gson.fromJson(preferences.getString("remindersList","[]"), new TypeToken<List<Reminder>>(){}.getType());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
reminders.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return reminders.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteViews getViewAt(int position) {
|
||||
Reminder reminder = reminders.get(position);
|
||||
|
||||
boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty();
|
||||
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout);
|
||||
|
||||
views.setTextViewText(R.id.reminder_title, reminder.getTitle());
|
||||
if (!useMiniLayout) {
|
||||
views.setTextViewText(R.id.reminder_description, reminder.getDescription());
|
||||
}
|
||||
views.setTextViewText(R.id.reminder_time, reminder.getFormattedTime());
|
||||
final Intent fillInIntent = new Intent();
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString(ReminderViewsService.OpenReminderId, 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);
|
||||
return views;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RemoteViews getLoadingView() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
public class ReminderWidgetProvider extends AppWidgetProvider {
|
||||
static String NewReminder = "com.streetwriters.notesnook.NewReminder";
|
||||
|
||||
@Override
|
||||
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
|
||||
for (int appWidgetId : appWidgetIds) {
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders);
|
||||
updateAppWidget(context, appWidgetManager, appWidgetId, views);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static Bundle getActivityOptionsBundle() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
ActivityOptions activityOptions = ActivityOptions.makeBasic();
|
||||
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
|
||||
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
|
||||
return activityOptions.toBundle();
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, RemoteViews views) {
|
||||
Intent listview_intent_template = new Intent(context, MainActivity.class);
|
||||
listview_intent_template.setAction(Intent.ACTION_VIEW);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, getActivityOptionsBundle());
|
||||
views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent);
|
||||
|
||||
Intent new_reminder_intent = new Intent(context, MainActivity.class);
|
||||
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://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);
|
||||
|
||||
Intent list_remote_adapter_intent = new Intent(context, ReminderViewsService.class);
|
||||
list_remote_adapter_intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
||||
views.setRemoteAdapter(R.id.widget_list_view, list_remote_adapter_intent);
|
||||
views.setEmptyView(R.id.widget_list_view, R.id.empty_view);
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package com.streetwriters.notesnook.datatypes;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
public class BaseItem {
|
||||
private String id;
|
||||
private String type;
|
||||
private long dateModified;
|
||||
private long dateCreated;
|
||||
private Boolean migrated;
|
||||
private Boolean remote;
|
||||
private Boolean synced;
|
||||
private Boolean deleted;
|
||||
/**
|
||||
* @deprecated only kept here for migration purposes
|
||||
*/
|
||||
private Object deleteReason; // Assuming never can be represented as Object
|
||||
|
||||
// Getters and Setters
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public long getDateModified() {
|
||||
return dateModified;
|
||||
}
|
||||
|
||||
public void setDateModified(long dateModified) {
|
||||
this.dateModified = dateModified;
|
||||
}
|
||||
|
||||
public long getDateCreated() {
|
||||
return dateCreated;
|
||||
}
|
||||
|
||||
public void setDateCreated(long dateCreated) {
|
||||
this.dateCreated = dateCreated;
|
||||
}
|
||||
|
||||
public Boolean getMigrated() {
|
||||
return migrated;
|
||||
}
|
||||
|
||||
public void setMigrated(Boolean migrated) {
|
||||
this.migrated = migrated;
|
||||
}
|
||||
|
||||
public Boolean getRemote() {
|
||||
return remote;
|
||||
}
|
||||
|
||||
public void setRemote(Boolean remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
public Boolean getSynced() {
|
||||
return synced;
|
||||
}
|
||||
|
||||
public void setSynced(Boolean synced) {
|
||||
this.synced = synced;
|
||||
}
|
||||
|
||||
public Boolean getDeleted() {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public void setDeleted(Boolean deleted) {
|
||||
this.deleted = deleted;
|
||||
}
|
||||
|
||||
public Object getDeleteReason() {
|
||||
return deleteReason;
|
||||
}
|
||||
|
||||
public void setDeleteReason(Object deleteReason) {
|
||||
this.deleteReason = deleteReason;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package com.streetwriters.notesnook.datatypes;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
public class Note extends BaseItem {
|
||||
private String title;
|
||||
private String headline;
|
||||
private String contentId;
|
||||
private Boolean locked;
|
||||
private boolean pinned;
|
||||
private boolean favorite;
|
||||
private boolean localOnly;
|
||||
private boolean conflicted;
|
||||
private boolean readonly;
|
||||
private long dateEdited;
|
||||
private Object dateDeleted; // Assuming null can be represented as Object
|
||||
private Object itemType; // Assuming null can be represented as Object
|
||||
private Object deletedBy; // Assuming null can be represented as Object
|
||||
|
||||
// Getters and Setters
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getHeadline() {
|
||||
return headline;
|
||||
}
|
||||
|
||||
public void setHeadline(String headline) {
|
||||
this.headline = headline;
|
||||
}
|
||||
|
||||
public String getContentId() {
|
||||
return contentId;
|
||||
}
|
||||
|
||||
public void setContentId(String contentId) {
|
||||
this.contentId = contentId;
|
||||
}
|
||||
|
||||
public Boolean getLocked() {
|
||||
return locked;
|
||||
}
|
||||
|
||||
public void setLocked(Boolean locked) {
|
||||
this.locked = locked;
|
||||
}
|
||||
|
||||
public boolean isPinned() {
|
||||
return pinned;
|
||||
}
|
||||
|
||||
public void setPinned(boolean pinned) {
|
||||
this.pinned = pinned;
|
||||
}
|
||||
|
||||
public boolean isFavorite() {
|
||||
return favorite;
|
||||
}
|
||||
|
||||
public void setFavorite(boolean favorite) {
|
||||
this.favorite = favorite;
|
||||
}
|
||||
|
||||
public boolean isLocalOnly() {
|
||||
return localOnly;
|
||||
}
|
||||
|
||||
public void setLocalOnly(boolean localOnly) {
|
||||
this.localOnly = localOnly;
|
||||
}
|
||||
|
||||
public boolean isConflicted() {
|
||||
return conflicted;
|
||||
}
|
||||
|
||||
public void setConflicted(boolean conflicted) {
|
||||
this.conflicted = conflicted;
|
||||
}
|
||||
|
||||
public boolean isReadonly() {
|
||||
return readonly;
|
||||
}
|
||||
|
||||
public void setReadonly(boolean readonly) {
|
||||
this.readonly = readonly;
|
||||
}
|
||||
|
||||
public long getDateEdited() {
|
||||
return dateEdited;
|
||||
}
|
||||
|
||||
public void setDateEdited(long dateEdited) {
|
||||
this.dateEdited = dateEdited;
|
||||
}
|
||||
|
||||
public Object getDateDeleted() {
|
||||
return dateDeleted;
|
||||
}
|
||||
|
||||
public void setDateDeleted(Object dateDeleted) {
|
||||
this.dateDeleted = dateDeleted;
|
||||
}
|
||||
|
||||
public Object getItemType() {
|
||||
return itemType;
|
||||
}
|
||||
|
||||
public void setItemType(Object itemType) {
|
||||
this.itemType = itemType;
|
||||
}
|
||||
|
||||
public Object getDeletedBy() {
|
||||
return deletedBy;
|
||||
}
|
||||
|
||||
public void setDeletedBy(Object deletedBy) {
|
||||
this.deletedBy = deletedBy;
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package com.streetwriters.notesnook.datatypes;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Keep
|
||||
public class Reminder extends BaseItem {
|
||||
private String title;
|
||||
private String description;
|
||||
private String formattedTime;
|
||||
private String priority; // "silent", "vibrate", "urgent"
|
||||
private long date;
|
||||
private String mode; // "repeat", "once", "permanent"
|
||||
private String recurringMode; // "week", "month", "day", "year"
|
||||
private int[] selectedDays;
|
||||
private boolean localOnly;
|
||||
private boolean disabled;
|
||||
private long snoozeUntil;
|
||||
|
||||
// Getters and Setters
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setPriority(String priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public long getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(long date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public String getRecurringMode() {
|
||||
return recurringMode;
|
||||
}
|
||||
|
||||
public void setRecurringMode(String recurringMode) {
|
||||
this.recurringMode = recurringMode;
|
||||
}
|
||||
|
||||
public int[] getSelectedDays() {
|
||||
return selectedDays;
|
||||
}
|
||||
|
||||
public void setSelectedDays(int[] selectedDays) {
|
||||
this.selectedDays = selectedDays;
|
||||
}
|
||||
|
||||
public boolean isLocalOnly() {
|
||||
return localOnly;
|
||||
}
|
||||
|
||||
public void setLocalOnly(boolean localOnly) {
|
||||
this.localOnly = localOnly;
|
||||
}
|
||||
|
||||
public boolean isDisabled() {
|
||||
return disabled;
|
||||
}
|
||||
|
||||
public void setDisabled(boolean disabled) {
|
||||
this.disabled = disabled;
|
||||
}
|
||||
|
||||
public long getSnoozeUntil() {
|
||||
return snoozeUntil;
|
||||
}
|
||||
|
||||
public void setSnoozeUntil(long snoozeUntil) {
|
||||
this.snoozeUntil = snoozeUntil;
|
||||
}
|
||||
|
||||
public String getFormattedTime() {
|
||||
return formattedTime;
|
||||
}
|
||||
|
||||
public void setFormattedTime(String formattedTime) {
|
||||
this.formattedTime = formattedTime;
|
||||
}
|
||||
|
||||
public String formatTime(long timeInMillis) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long diff = timeInMillis - currentTime;
|
||||
|
||||
if (diff < TimeUnit.MINUTES.toMillis(1)) {
|
||||
return "in " + (diff / 1000) + " seconds";
|
||||
} else if (diff < TimeUnit.HOURS.toMillis(1)) {
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(diff);
|
||||
return "in " + minutes + " minute" + (minutes > 1 ? "s" : "");
|
||||
} else if (diff < TimeUnit.DAYS.toMillis(1)) {
|
||||
long hours = TimeUnit.MILLISECONDS.toHours(diff);
|
||||
return "in " + hours + " hour" + (hours > 1 ? "s" : "");
|
||||
} else if (diff < TimeUnit.DAYS.toMillis(2)) {
|
||||
return "tomorrow";
|
||||
} else if (diff < TimeUnit.DAYS.toMillis(7)) {
|
||||
long days = TimeUnit.MILLISECONDS.toDays(diff);
|
||||
return "in " + days + " day" + (days > 1 ? "s" : "");
|
||||
} else if (diff < TimeUnit.DAYS.toMillis(30)) {
|
||||
long weeks = TimeUnit.MILLISECONDS.toDays(diff) / 7;
|
||||
return "in " + weeks + " week" + (weeks > 1 ? "s" : "");
|
||||
} else if (diff < TimeUnit.DAYS.toMillis(365)) {
|
||||
long months = TimeUnit.MILLISECONDS.toDays(diff) / 30;
|
||||
return "in " + months + " month" + (months > 1 ? "s" : "");
|
||||
} else {
|
||||
long years = TimeUnit.MILLISECONDS.toDays(diff) / 365;
|
||||
return "in " + years + " year" + (years > 1 ? "s" : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.3 KiB |
@@ -1,14 +0,0 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:id="@+id/new_note"
|
||||
android:background="@drawable/layout_bg"
|
||||
android:padding="@dimen/widget_margin">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/add_note" />
|
||||
</FrameLayout>
|
||||
@@ -1,40 +0,0 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/open_note"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="@drawable/layout_bg"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="10dp"
|
||||
android:elevation="5dp"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:id="@+id/widget_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:text="Widget unconfigured" />
|
||||
<TextView
|
||||
android:id="@+id/widget_body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="14sp"
|
||||
android:text="Configure this widget to show a note here." />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -1,8 +0,0 @@
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="No upcoming reminders"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:gravity="center" />
|
||||
@@ -1,29 +0,0 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="10dp"
|
||||
android:id="@+id/reminder_item_btn">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reminder_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reminder_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reminder_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text" />
|
||||
</LinearLayout>
|
||||
@@ -1,22 +0,0 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="10dp"
|
||||
android:id="@+id/reminder_item_btn">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reminder_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reminder_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text" />
|
||||
</LinearLayout>
|
||||
@@ -1,64 +0,0 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/layout_bg"
|
||||
>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:layout_gravity="center"
|
||||
android:paddingTop="12dp"
|
||||
>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Upcoming Reminders"/>
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="35dp"
|
||||
android:id="@+id/add_button"
|
||||
android:layout_height="35dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:background="@drawable/ic_newnote" />
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@color/border" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/widget_list_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:divider="@color/border"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:dividerHeight="1dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:textAlignment="center"
|
||||
android:gravity="center"
|
||||
android:text="Tap on + to add reminder"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,12 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Notesnook</string>
|
||||
<string name="title_activity_share">NotesnookShare</string>
|
||||
<string name="appwidget_text">EXAMPLE</string>
|
||||
<string name="add_widget">Add widget</string>
|
||||
<string name="take_a_quick_note">Take a quick note.</string>
|
||||
<string name="reminders">Quick overview of upcoming reminders</string>
|
||||
<string name="reminders_title">Reminders</string>
|
||||
<string name="note">Note</string>
|
||||
<string name="note_description">Add a note to home screen</string>
|
||||
<string name="quick_note">Quick note</string>
|
||||
</resources>
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialKeyguardLayout="@layout/new_note_widget"
|
||||
android:initialLayout="@layout/new_note_widget"
|
||||
android:minWidth="400dp"
|
||||
android:minHeight="50dp"
|
||||
android:minResizeWidth="50dp"
|
||||
android:minResizeHeight="50dp"
|
||||
android:description="@string/take_a_quick_note"
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="1"
|
||||
android:previewImage="@drawable/widget_preview"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="86400000"
|
||||
android:widgetCategory="home_screen"/>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialKeyguardLayout="@layout/note_widget"
|
||||
android:initialLayout="@layout/note_widget"
|
||||
android:configure="com.streetwriters.notesnook.NotePreviewConfigureActivity"
|
||||
android:widgetFeatures="reconfigurable"
|
||||
android:minResizeWidth="100dp"
|
||||
android:minResizeHeight="50dp"
|
||||
android:minWidth="400dp"
|
||||
android:description="@string/note_description"
|
||||
android:minHeight="50dp"
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="1"
|
||||
android:previewImage="@drawable/note_widget_preview"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="86400000"
|
||||
android:widgetCategory="home_screen"/>
|
||||
@@ -1,14 +0,0 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/widget_reminders"
|
||||
android:minWidth="400dp"
|
||||
android:minHeight="100dp"
|
||||
android:minResizeWidth="400dp"
|
||||
android:minResizeHeight="50dp"
|
||||
android:description="@string/reminders"
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="2"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:previewImage="@drawable/reminder_preview"
|
||||
android:updatePeriodMillis="1024"
|
||||
android:widgetCategory="home_screen"
|
||||
/>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application android:networkSecurityConfig="@xml/network_security_config">
|
||||
</application>
|
||||
</manifest>
|
||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
- Get an overview of your year with Notesnook with Wrapped 2025
|
||||
- Cell selection and column resizing in tables
|
||||
- Added support for uploading large files in background
|
||||
- Bug fixes and minor improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -1,11 +0,0 @@
|
||||
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
||||
plugins { id("com.facebook.react.settings") }
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
||||
rootProject.name = 'Notesnook'
|
||||
include ':app'
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
|
||||
include ":react-native-config"
|
||||
project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android')
|
||||
include ":lazysodium-android"
|
||||
project(':lazysodium-android').projectDir = new File(rootProject.projectDir, '../node_modules/@ammarahmed/react-native-sodium/android/lazysodium-android/app')
|
||||
@@ -19,30 +19,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import {
|
||||
ScopedThemeProvider,
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { Appearance, I18nManager, StatusBar } from "react-native";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import DialogProvider from "./components/dialog-provider";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
import { Toast } from "./components/toast";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { NotePreviewConfigure } from "./screens/note-preview-configure";
|
||||
import { RootNavigation } from "./navigation/navigation-stack";
|
||||
import { ApplicationHolder } from "./navigation";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
import Notifications from "./services/notifications";
|
||||
import SettingsService from "./services/settings";
|
||||
import { TipManager } from "./services/tip-manager";
|
||||
import { changeSystemBarColors, useThemeStore } from "./stores/use-theme-store";
|
||||
import { useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import RNBootSplash from "react-native-bootsplash";
|
||||
import AppLocked from "./components/app-lock";
|
||||
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
@@ -51,47 +46,54 @@ const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
RNBootSplash.hide({
|
||||
fade: true
|
||||
});
|
||||
const App = (props: { configureMode: "note-preview" }) => {
|
||||
|
||||
const App = () => {
|
||||
useAppEvents();
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
useEffect(() => {
|
||||
changeSystemBarColors();
|
||||
SettingsService.onFirstLaunch();
|
||||
setTimeout(async () => {
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote();
|
||||
Notifications.pinQuickNote(true);
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: -1
|
||||
}}
|
||||
pointerEvents="none"
|
||||
>
|
||||
<SafeAreaProvider>
|
||||
<GlobalSafeAreaProvider />
|
||||
</SafeAreaProvider>
|
||||
</View>
|
||||
|
||||
<GestureHandlerRootView
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<GlobalSafeAreaProvider />
|
||||
{props.configureMode === "note-preview" ? (
|
||||
<NotePreviewConfigure />
|
||||
) : (
|
||||
<RootNavigation />
|
||||
)}
|
||||
<ScopedThemeProvider value="dialog">
|
||||
<Toast />
|
||||
</ScopedThemeProvider>
|
||||
<DialogProvider />
|
||||
<AppLocked />
|
||||
<ApplicationHolder />
|
||||
</GestureHandlerRootView>
|
||||
</SafeAreaProvider>
|
||||
<AppLockedOverlay />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -101,8 +103,8 @@ let currTheme =
|
||||
: SettingsService.getProperty("lighTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
return function AppWithThemeProvider(props: any) {
|
||||
export const withTheme = (Element: () => JSX.Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
state.colorScheme,
|
||||
state.darkTheme,
|
||||
@@ -121,42 +123,28 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
})
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
console.log(theme.version, "theme updated");
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.getState().setDarkTheme(theme)
|
||||
: useThemeStore.getState().setLightTheme(theme);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
}, 1000);
|
||||
|
||||
const listener = Appearance.addChangeListener(({ colorScheme }) => {
|
||||
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
|
||||
useThemeStore
|
||||
.getState()
|
||||
.setColorScheme(colorScheme as "light" | "dark");
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
listener.remove();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
}, [colorScheme, darkTheme, lightTheme]);
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<Element {...props} />
|
||||
<Element />
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
385
apps/mobile/app/common/database/encryption.js
Normal file
385
apps/mobile/app/common/database/encryption.js
Normal file
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
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 Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { MMKV } from "./mmkv";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
// app lock with password enabled.
|
||||
export const CipherStorage = new MMKVLoader()
|
||||
.withInstanceID("cipher_storage")
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
const APPLOCK_CIPHER = "applockCipher";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
"Crypto API is not supported. Please upgrade your web browser"
|
||||
);
|
||||
}
|
||||
const charset =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
|
||||
const indexes = crypto.getRandomValues(new Uint32Array(length));
|
||||
let secret = "";
|
||||
for (const index of indexes) {
|
||||
secret += charset[index % charset.length];
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
|
||||
const key = getDatabaseKey();
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const databaseKeyCipher = await encrypt(appLockCredentials, key);
|
||||
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
databaseKey,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.removeItem(DB_KEY_CIPHER);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
try {
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
);
|
||||
return typeof decrypted === "string";
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let DB_KEY;
|
||||
export function clearDatabaseKey() {
|
||||
DB_KEY = undefined;
|
||||
DatabaseLogger.info("Cleared database key");
|
||||
}
|
||||
|
||||
export async function getDatabaseKey(appLockPassword) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
try {
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
DatabaseLogger.info("Getting database key from cipher");
|
||||
DB_KEY = databaseKey;
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
const hasKey = await Keychain.hasInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
);
|
||||
if (hasKey) {
|
||||
let credentials = await Keychain.getInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
|
||||
DatabaseLogger.info("Getting database key from Keychain");
|
||||
DB_KEY = credentials.password;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
DatabaseLogger.info("Generating new database key");
|
||||
const password = generatePassword();
|
||||
const derivedDatabaseKey = await Sodium.deriveKey(
|
||||
password,
|
||||
NOTESNOOK_DB_KEY_SALT
|
||||
);
|
||||
|
||||
DB_KEY = derivedDatabaseKey.key;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
DB_KEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
}
|
||||
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
}
|
||||
DatabaseLogger.info("Migrated user credentials to cipher storage");
|
||||
}
|
||||
|
||||
return DB_KEY;
|
||||
} catch (e) {
|
||||
ToastManager.error(e, "Error getting database key");
|
||||
console.log(e, "error");
|
||||
DatabaseLogger.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveCryptoKey(data) {
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
try {
|
||||
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
console.log("getCryptoKey", e);
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCryptoKey(_name) {
|
||||
try {
|
||||
MMKV.removeItem(USER_KEY_CIPHER);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
return true;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRandomBytes(length) {
|
||||
return await generateSecureRandom(length);
|
||||
}
|
||||
|
||||
export async function hash(password, email) {
|
||||
let result = await Sodium.hashPassword(password, email);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function generateCryptoKey(password, salt) {
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(password, salt || null);
|
||||
return credentials;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAlgorithm(base64Variant) {
|
||||
return `xcha-argon2i13-${base64Variant}`;
|
||||
}
|
||||
|
||||
export async function decrypt(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
let _data = { ...data };
|
||||
_data.output = "plain";
|
||||
|
||||
if (!password.salt) password.salt = data.salt;
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
export async function decryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
data = data.map((d) => {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
|
||||
if (data.length && !password.salt) {
|
||||
password.salt = data[0].salt;
|
||||
}
|
||||
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
export function parseAlgorithm(alg) {
|
||||
if (!alg) return {};
|
||||
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
||||
return {
|
||||
encryptionAlgorithm: enc,
|
||||
kdfAlgorithm: kdf,
|
||||
compressionAlgorithm: compressionAlg,
|
||||
isCompress: compressed === "1",
|
||||
base64_variant: base64variant
|
||||
};
|
||||
}
|
||||
|
||||
export async function encrypt(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
let message = {
|
||||
type: "plain",
|
||||
data: data
|
||||
};
|
||||
let result = await Sodium.encrypt(password, message);
|
||||
|
||||
return {
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
let results = await Sodium.encryptMulti(
|
||||
password,
|
||||
data.map((item) => ({
|
||||
type: "plain",
|
||||
data: item
|
||||
}))
|
||||
);
|
||||
|
||||
return !results
|
||||
? []
|
||||
: results.map((result) => ({
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
}));
|
||||
}
|
||||
@@ -1,476 +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 Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
|
||||
import { SerializedKey } from "@notesnook/crypto";
|
||||
import { Platform } from "react-native";
|
||||
import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
// app lock with password enabled.
|
||||
export const CipherStorage = new MMKVLoader()
|
||||
.withInstanceID("cipher_storage")
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
const APPLOCK_CIPHER = "applockCipher";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
//@ts-ignore
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
"Crypto API is not supported. Please upgrade your web browser"
|
||||
);
|
||||
}
|
||||
const charset =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
|
||||
const indexes = crypto.getRandomValues(new Uint32Array(length));
|
||||
let secret = "";
|
||||
for (const index of indexes) {
|
||||
secret += charset[index % charset.length];
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
|
||||
const key = (await getDatabaseKey()) as string;
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
|
||||
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
|
||||
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER) as Cipher;
|
||||
const databaseKey = (await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
)) as string;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
databaseKey,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.removeItem(DB_KEY_CIPHER);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = (await encrypt(
|
||||
appLockCredentials,
|
||||
generatePassword()
|
||||
)) as Cipher;
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword: string) {
|
||||
try {
|
||||
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);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
);
|
||||
return typeof decrypted === "string";
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let DB_KEY: string | undefined;
|
||||
export function clearDatabaseKey() {
|
||||
DB_KEY = undefined;
|
||||
DatabaseLogger.info("Cleared database key");
|
||||
}
|
||||
|
||||
export async function getDatabaseKey(appLockPassword?: string) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(
|
||||
"databaseKeyCipher"
|
||||
) as Cipher;
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
DatabaseLogger.info("Getting database key from cipher");
|
||||
DB_KEY = databaseKey;
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
if (hasKey) {
|
||||
const credentials = await Keychain.getInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
);
|
||||
|
||||
DatabaseLogger.info("Getting database key from Keychain");
|
||||
DB_KEY = (credentials as Keychain.UserCredentials).password;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
DatabaseLogger.info("Generating new database key");
|
||||
const password = generatePassword();
|
||||
const derivedDatabaseKey = await Sodium.deriveKey(
|
||||
password,
|
||||
NOTESNOOK_DB_KEY_SALT
|
||||
);
|
||||
|
||||
DB_KEY = derivedDatabaseKey.key as string;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
DB_KEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
}
|
||||
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
const userKeyCredentials =
|
||||
await Keychain.getInternetCredentials("notesnook");
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher: Cipher = (await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
)) as Cipher;
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
}
|
||||
DatabaseLogger.info("Migrated user credentials to cipher storage");
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
throw new Error(
|
||||
`Failed to get database key, ${await Keychain.hasInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return DB_KEY;
|
||||
}
|
||||
|
||||
export async function deriveCryptoKeyFallback(data: SerializedKey) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
try {
|
||||
if (!data.password || !data.salt)
|
||||
throw new Error(
|
||||
"Invalid password and salt provided to deriveCryptoKeyFallback"
|
||||
);
|
||||
|
||||
const credentials = await Sodium.deriveKeyFallback?.(
|
||||
data.password,
|
||||
data.salt
|
||||
);
|
||||
|
||||
if (!credentials) return;
|
||||
|
||||
const userKeyCipher = (await encrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key as string
|
||||
)) as Cipher<"base64">;
|
||||
DatabaseLogger.info("User key fallback stored: ", {
|
||||
userKeyCipher: !!userKeyCipher
|
||||
});
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveCryptoKey(data: SerializedKey) {
|
||||
try {
|
||||
if (!data.password || !data.salt)
|
||||
throw new Error("Invalid password and salt provided to deriveCryptoKey");
|
||||
|
||||
const credentials = (await Sodium.deriveKey(
|
||||
data.password,
|
||||
data.salt
|
||||
)) as Password;
|
||||
const userKeyCipher = (await encrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key as string
|
||||
)) as Cipher<"base64">;
|
||||
DatabaseLogger.info("User key stored: ", {
|
||||
userKeyCipher: !!userKeyCipher
|
||||
});
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey() {
|
||||
try {
|
||||
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER) as Cipher;
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCryptoKey() {
|
||||
try {
|
||||
MMKV.removeItem(USER_KEY_CIPHER);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
return true;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRandomBytes(length: number) {
|
||||
return await generateSecureRandom(length);
|
||||
}
|
||||
|
||||
export async function hash(
|
||||
password: string,
|
||||
email: string,
|
||||
options?: { usesFallback?: boolean }
|
||||
) {
|
||||
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
|
||||
|
||||
if (options?.usesFallback && Platform.OS !== "ios") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
options?.usesFallback
|
||||
? await Sodium.hashPasswordFallback?.(password, email)
|
||||
: await Sodium.hashPassword(password, email)
|
||||
) as string;
|
||||
}
|
||||
|
||||
export async function generateCryptoKey(password: string, salt?: string) {
|
||||
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export async function generateCryptoKeyFallback(
|
||||
password: string,
|
||||
salt?: string
|
||||
): Promise<SerializedKey> {
|
||||
return Sodium.deriveKeyFallback?.(
|
||||
password,
|
||||
salt as string
|
||||
) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export function getAlgorithm(base64Variant: number) {
|
||||
return `xcha-argon2i13-${base64Variant}`;
|
||||
}
|
||||
|
||||
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
|
||||
const _data = { ...data };
|
||||
_data.output = "plain";
|
||||
|
||||
if (!password.salt) password.salt = data.salt;
|
||||
|
||||
if (Platform.OS === "ios" && !password.key && password.password) {
|
||||
const key = await Sodium.deriveKey(password.password, password.salt);
|
||||
try {
|
||||
return await Sodium.decrypt(key, _data);
|
||||
} catch (e) {
|
||||
const fallbackKey = await Sodium.deriveKeyFallback?.(
|
||||
password.password,
|
||||
password.salt
|
||||
);
|
||||
if (Platform.OS === "ios" && fallbackKey) {
|
||||
DatabaseLogger.info("Using fallback key for decryption");
|
||||
}
|
||||
if (fallbackKey) {
|
||||
return await Sodium.decrypt(fallbackKey, _data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
export async function decryptMulti(
|
||||
password: Password,
|
||||
data: Cipher<"base64">[]
|
||||
) {
|
||||
data = data.map((d) => {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
|
||||
if (data.length && !password.salt) {
|
||||
password.salt = data[0].salt;
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios" && !password.key && password.password) {
|
||||
const key = await Sodium.deriveKey(password.password, password.salt);
|
||||
try {
|
||||
return await Sodium.decryptMulti(key, data);
|
||||
} catch (e) {
|
||||
const fallbackKey = await Sodium.deriveKeyFallback?.(
|
||||
password.password,
|
||||
password.salt as string
|
||||
);
|
||||
if (Platform.OS === "ios" && fallbackKey) {
|
||||
DatabaseLogger.info("Using fallback key for decryption");
|
||||
}
|
||||
if (fallbackKey) {
|
||||
return await Sodium.decryptMulti(fallbackKey, data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
export function parseAlgorithm(alg: string) {
|
||||
if (!alg) return {};
|
||||
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
||||
return {
|
||||
encryptionAlgorithm: enc,
|
||||
kdfAlgorithm: kdf,
|
||||
compressionAlgorithm: compressionAlg,
|
||||
isCompress: compressed === "1",
|
||||
base64_variant: base64variant
|
||||
};
|
||||
}
|
||||
|
||||
export async function encrypt(password: SerializedKey, plainText: string) {
|
||||
const result = await Sodium.encrypt<"base64">(password, {
|
||||
type: "plain",
|
||||
data: plainText
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptMulti(
|
||||
password: SerializedKey,
|
||||
plainText: string[]
|
||||
) {
|
||||
const results = await Sodium.encryptMulti<"base64">(
|
||||
password,
|
||||
plainText.map((item) => ({
|
||||
type: "plain",
|
||||
data: item
|
||||
}))
|
||||
);
|
||||
|
||||
return !results
|
||||
? []
|
||||
: results.map((result) => ({
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
}));
|
||||
}
|
||||
@@ -16,40 +16,30 @@ 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 { database, getFeature, getFeatureLimit } from "@notesnook/common";
|
||||
import { logger as dbLogger, ICompressor } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import "./logger";
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { FileStorage } from "../filesystem";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import "./logger";
|
||||
import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { Storage } from "./storage";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export async function setupDatabase(password?: string) {
|
||||
export async function setupDatabase(password) {
|
||||
const key = await getDatabaseKey(password);
|
||||
if (!key) throw new Error(strings.databaseSetupFailed());
|
||||
|
||||
// const base = `http://192.168.100.92`;
|
||||
|
||||
// database.host({
|
||||
// API_HOST: `${base}:5264`,
|
||||
// AUTH_HOST: `${base}:8264`,
|
||||
// SSE_HOST: `${base}:7264`,
|
||||
// ISSUES_HOST: `${base}:2624`,
|
||||
// SUBSCRIPTIONS_HOST: `${base}:9264`,
|
||||
// MONOGRAPH_HOST: `${base}:6264`,
|
||||
// NOTESNOOK_HOST: `${base}:8788`
|
||||
// });
|
||||
console.log("Opening database with key:", !!key);
|
||||
|
||||
database.host({
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
@@ -57,23 +47,18 @@ export async function setupDatabase(password?: string) {
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
MONOGRAPH_HOST: "https://monogr.ph",
|
||||
NOTESNOOK_HOST: "https://notesnook.com",
|
||||
...(SettingsService.getProperty("serverUrls") || {})
|
||||
});
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: (Platform.OS === "ios"
|
||||
? EventSource
|
||||
: AndroidEventSource) as any,
|
||||
fs: FileStorage,
|
||||
compressor: async () =>
|
||||
({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}) as ICompressor,
|
||||
batchSize: 50,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: () => ({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}),
|
||||
batchSize: 100,
|
||||
sqliteOptions: {
|
||||
dialect: (name) => ({
|
||||
createDriver: () => {
|
||||
@@ -86,10 +71,6 @@ export async function setupDatabase(password?: string) {
|
||||
tempStore: "memory",
|
||||
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL",
|
||||
password: key
|
||||
},
|
||||
maxNoteVersions: async () => {
|
||||
const limit = await getFeatureLimit(getFeature("maxNoteVersions"));
|
||||
return typeof limit.caption === "number" ? limit.caption : undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import { CompiledQuery } from "@streetwriters/kysely";
|
||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type Config = { dbName: string; async: boolean; location?: string };
|
||||
type Config = { dbName: string; async: boolean; location: string };
|
||||
|
||||
export class RNSqliteDriver implements Driver {
|
||||
private connection?: DatabaseConnection;
|
||||
|
||||
@@ -16,52 +16,58 @@ 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 { IStorage } from "@notesnook/core";
|
||||
import { MMKVInstance } from "react-native-mmkv-storage";
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import {
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
deriveCryptoKey,
|
||||
deriveCryptoKeyFallback,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
generateCryptoKey,
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
generateCryptoKeyFallback
|
||||
removeCryptoKey
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
export class KV {
|
||||
storage: MMKVInstance;
|
||||
constructor(storage: MMKVInstance) {
|
||||
/**
|
||||
* @type {typeof MMKV}
|
||||
*/
|
||||
storage = null;
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
async read<T>(key: string, isArray?: boolean) {
|
||||
if (!key) return undefined;
|
||||
const data = this.storage.getString(key);
|
||||
if (!data) return undefined;
|
||||
async read(key) {
|
||||
if (!key) return null;
|
||||
let data = this.storage.getString(key);
|
||||
if (!data) return null;
|
||||
try {
|
||||
return JSON.parse(data) as T;
|
||||
let parse = JSON.parse(data);
|
||||
return parse;
|
||||
} catch (e) {
|
||||
return data as T;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
async write<T>(key: string, data: T) {
|
||||
async write(key, data) {
|
||||
this.storage.setString(
|
||||
key,
|
||||
typeof data === "string" ? data : JSON.stringify(data)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async readMulti<T>(keys: string[]) {
|
||||
async readMulti(keys) {
|
||||
if (keys.length <= 0) {
|
||||
return [];
|
||||
} else {
|
||||
try {
|
||||
const data = await this.storage.getMultipleItemsAsync<any>(
|
||||
let data = await this.storage.getMultipleItemsAsync(
|
||||
keys.slice(),
|
||||
"string"
|
||||
);
|
||||
@@ -73,24 +79,24 @@ export class KV {
|
||||
obj = value;
|
||||
}
|
||||
return [key, obj];
|
||||
}) as [string, T][];
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key: string) {
|
||||
this.storage.removeItem(key);
|
||||
async remove(key) {
|
||||
return this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
async removeMulti(keys: string[]) {
|
||||
if (!keys) return;
|
||||
this.storage.removeItems(keys);
|
||||
async removeMulti(keys) {
|
||||
if (!keys) return true;
|
||||
return this.storage.removeItems(keys);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
this.storage.clearStore();
|
||||
return this.storage.clearStore();
|
||||
}
|
||||
|
||||
async getAllKeys() {
|
||||
@@ -107,52 +113,54 @@ export class KV {
|
||||
return keys;
|
||||
}
|
||||
|
||||
async writeMulti(items: [string, any][]) {
|
||||
await this.storage.setMultipleItemsAsync(items, "object");
|
||||
async writeMulti(items) {
|
||||
return this.storage.setMultipleItemsAsync(items, "object");
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultStorage = new KV(MMKV);
|
||||
|
||||
export const Storage: IStorage = {
|
||||
write<T>(key: string, data: T): Promise<void> {
|
||||
return DefaultStorage.write(key, data);
|
||||
},
|
||||
writeMulti<T>(entries: [string, T][]): Promise<void> {
|
||||
return DefaultStorage.writeMulti(entries);
|
||||
},
|
||||
readMulti<T>(keys: string[]): Promise<[string, T][]> {
|
||||
return DefaultStorage.readMulti(keys);
|
||||
},
|
||||
read<T>(key: string, isArray?: boolean): Promise<T | undefined> {
|
||||
return DefaultStorage.read(key, isArray);
|
||||
},
|
||||
remove(key: string): Promise<void> {
|
||||
return DefaultStorage.remove(key);
|
||||
},
|
||||
removeMulti(keys: string[]): Promise<void> {
|
||||
return DefaultStorage.removeMulti(keys);
|
||||
},
|
||||
clear(): Promise<void> {
|
||||
return DefaultStorage.clear();
|
||||
},
|
||||
generateCryptoKeyPair() {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
decryptAsymmetric() {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
getAllKeys(): Promise<string[]> {
|
||||
return DefaultStorage.getAllKeys();
|
||||
},
|
||||
hash,
|
||||
getCryptoKey,
|
||||
async function requestPermission() {
|
||||
if (Platform.OS === "ios") return true;
|
||||
return true;
|
||||
}
|
||||
async function checkAndCreateDir(path) {
|
||||
let dir =
|
||||
Platform.OS === "ios"
|
||||
? RNFetchBlob.fs.dirs.DocumentDir + path
|
||||
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
|
||||
|
||||
try {
|
||||
let exists = await RNFetchBlob.fs.exists(dir);
|
||||
let isDir = await RNFetchBlob.fs.isDir(dir);
|
||||
if (!exists || !isDir) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
} catch (e) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
export default {
|
||||
read: (key) => DefaultStorage.read(key),
|
||||
write: (key, value) => DefaultStorage.write(key, value),
|
||||
readMulti: (keys) => DefaultStorage.readMulti(keys),
|
||||
remove: (key) => DefaultStorage.remove(key),
|
||||
clear: () => DefaultStorage.clear(),
|
||||
getAllKeys: () => DefaultStorage.getAllKeys(),
|
||||
writeMulti: (items) => DefaultStorage.writeMulti(items),
|
||||
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
getRandomBytes,
|
||||
checkAndCreateDir,
|
||||
requestPermission,
|
||||
deriveCryptoKey,
|
||||
getCryptoKey,
|
||||
removeCryptoKey,
|
||||
hash,
|
||||
generateCryptoKey,
|
||||
generateCryptoKeyFallback,
|
||||
deriveCryptoKeyFallback
|
||||
encryptMulti
|
||||
};
|
||||
@@ -27,9 +27,13 @@ import RNFetchBlob from "react-native-blob-util";
|
||||
*/
|
||||
export async function compressToBase64(path, type) {
|
||||
const { width: screenWidth, scale } = Dimensions.get("window");
|
||||
|
||||
console.log("COMPRESSING TO BASE64...");
|
||||
return new Promise((resolve) => {
|
||||
console.log(path, "image path...");
|
||||
|
||||
Image.getSize(path, async (width) => {
|
||||
console.log("image width", width);
|
||||
|
||||
const response = await ImageResizer.createResizedImage(
|
||||
path,
|
||||
screenWidth * scale,
|
||||
@@ -51,12 +55,10 @@ export async function compressToBase64(path, type) {
|
||||
: response.uri,
|
||||
"base64"
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||
RNFetchBlob.fs
|
||||
.unlink(response.uri.replace("file://", ""))
|
||||
.catch(console.log);
|
||||
|
||||
resolve(base64);
|
||||
});
|
||||
@@ -78,8 +80,6 @@ export async function compressToFile(path, type) {
|
||||
onlyScaleDown: true
|
||||
}
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||
return response.uri;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { getFileNameWithExtension } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
@@ -24,12 +25,12 @@ import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import filesystem from ".";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
|
||||
@@ -50,23 +51,23 @@ export async function downloadAllAttachments() {
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param {string[]} attachmentIds
|
||||
* @param {string[]} attachments
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(attachmentIds: string[]) {
|
||||
export async function downloadAttachments(attachments) {
|
||||
await createCacheDir();
|
||||
if (!attachmentIds || !attachmentIds.length) return;
|
||||
if (!attachments || !attachments.length) return;
|
||||
const groupId = `download-all-${Date.now()}`;
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
// Ask the user to select a directory to store the file
|
||||
const file = await ScopedStorage.openDocumentTree(true);
|
||||
let file = await ScopedStorage.openDocumentTree(true);
|
||||
outputFolder = file.uri;
|
||||
if (!outputFolder) return;
|
||||
} else {
|
||||
outputFolder = await filesystem.checkAndCreateDir("/downloads/");
|
||||
outputFolder = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
// Create the folder to zip;
|
||||
@@ -82,10 +83,8 @@ export async function downloadAttachments(attachmentIds: string[]) {
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
const isCancelled = () => {
|
||||
if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
if (useAttachmentStore.getState().downloading[groupId]?.canceled) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId,
|
||||
current: 0,
|
||||
@@ -98,21 +97,19 @@ export async function downloadAttachments(attachmentIds: string[]) {
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < attachmentIds.length; i++) {
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
if (isCancelled()) return;
|
||||
const attachment = await db.attachments.attachment(attachmentIds[i]);
|
||||
if (!attachment) continue;
|
||||
|
||||
let attachment = await db.attachments.attachment(attachments[i]);
|
||||
const hash = attachment.hash;
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: i + 1,
|
||||
total: attachmentIds.length,
|
||||
total: attachments.length,
|
||||
filename: attachment.hash
|
||||
});
|
||||
// Download to cache
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
@@ -187,85 +184,59 @@ export async function downloadAttachments(attachmentIds: string[]) {
|
||||
});
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e as Error, "Error zipping attachments");
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
hashOrId: string,
|
||||
hash,
|
||||
global = true,
|
||||
options?: {
|
||||
silent?: boolean;
|
||||
cache?: boolean;
|
||||
throwError?: boolean;
|
||||
groupId?: string;
|
||||
base64?: boolean;
|
||||
text?: boolean;
|
||||
options = {
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined,
|
||||
base64: false,
|
||||
text: false
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
|
||||
const attachment = await db.attachments.attachment(hashOrId);
|
||||
let attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
DatabaseLogger.log("Attachment not found", {
|
||||
hash: hashOrId
|
||||
});
|
||||
DatabaseLogger.log("Attachment not found");
|
||||
return;
|
||||
}
|
||||
|
||||
let folder: {
|
||||
uri: string;
|
||||
} | null = null;
|
||||
if (!options?.cache) {
|
||||
let folder = {};
|
||||
if (!options.cache) {
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder = {
|
||||
uri: await filesystem.checkAndCreateDir("/downloads/")
|
||||
};
|
||||
folder.uri = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
current: 0,
|
||||
total: 1,
|
||||
filename: attachment.filename
|
||||
});
|
||||
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(
|
||||
options?.groupId || attachment.hash,
|
||||
options.groupId || attachment.hash,
|
||||
attachment.hash,
|
||||
attachment.chunkSize
|
||||
);
|
||||
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
current: 1,
|
||||
total: 1,
|
||||
filename: attachment.filename,
|
||||
success: true
|
||||
});
|
||||
|
||||
if (!(await exists(attachment.hash))) {
|
||||
DatabaseLogger.log("Attachment does not exist after download.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options?.base64 || options?.text) {
|
||||
if (options.base64 || options.text) {
|
||||
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
|
||||
return await db.attachments.read(
|
||||
attachment.hash,
|
||||
@@ -273,16 +244,14 @@ export default async function downloadAttachment(
|
||||
);
|
||||
}
|
||||
|
||||
const filename = await getFileNameWithExtension(
|
||||
let filename = await getFileNameWithExtension(
|
||||
attachment.filename,
|
||||
attachment.mimeType
|
||||
);
|
||||
|
||||
const key = await db.attachments.decryptKey(attachment.key);
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
|
||||
if (!key) return;
|
||||
|
||||
const info = {
|
||||
let info = {
|
||||
iv: attachment.iv,
|
||||
salt: attachment.salt,
|
||||
length: attachment.size,
|
||||
@@ -290,18 +259,18 @@ export default async function downloadAttachment(
|
||||
hash: attachment.hash,
|
||||
hashType: attachment.hashType,
|
||||
mime: attachment.mimeType,
|
||||
fileName: options?.cache ? undefined : filename,
|
||||
uri: options?.cache ? undefined : folder?.uri,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
};
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
key,
|
||||
info,
|
||||
options?.cache ? "cache" : "file"
|
||||
options.cache ? "cache" : "file"
|
||||
);
|
||||
|
||||
if (!options?.silent) {
|
||||
if (!options.silent) {
|
||||
ToastManager.show({
|
||||
heading: strings.network.downloadSuccess(),
|
||||
message: strings.network.fileDownloaded(filename),
|
||||
@@ -309,15 +278,15 @@ export default async function downloadAttachment(
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios" && !options?.cache) {
|
||||
fileUri = folder?.uri + `/${filename}`;
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
fileUri = folder.uri + `/${filename}`;
|
||||
}
|
||||
if (!options?.silent) {
|
||||
if (!options.silent) {
|
||||
presentSheet({
|
||||
title: strings.network.fileDownloaded(),
|
||||
paragraph: strings.fileSaved(filename, Platform.OS),
|
||||
icon: "download",
|
||||
context: global ? "global" : attachment.hash,
|
||||
context: global ? null : attachment.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
});
|
||||
}
|
||||
@@ -327,26 +296,14 @@ export default async function downloadAttachment(
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
current: 0,
|
||||
total: 0,
|
||||
filename: attachment.filename,
|
||||
success: false
|
||||
});
|
||||
DatabaseLogger.error(e);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
if (options?.throwError) {
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -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 { RequestOptions } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
@@ -27,13 +26,7 @@ import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
|
||||
|
||||
export async function downloadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
||||
@@ -43,11 +36,9 @@ export async function downloadFile(
|
||||
|
||||
DatabaseLogger.log(`Downloading ${filename}`);
|
||||
await createCacheDir();
|
||||
|
||||
const { url, headers, chunkSize } = requestOptions;
|
||||
const tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
const originalFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
let { url, headers, chunkSize } = requestOptions;
|
||||
let tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
let originalFilePath = `${cacheDir}/${filename}`;
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
DatabaseLogger.log(`File Exists already: ${filename}`);
|
||||
@@ -55,8 +46,6 @@ export async function downloadFile(
|
||||
}
|
||||
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
|
||||
const size = await getUploadedFileSize(filename);
|
||||
|
||||
if (size === -1) {
|
||||
@@ -70,7 +59,7 @@ export async function downloadFile(
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(size / (chunkSize + ABYTES));
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
|
||||
if (attachment && attachment.size !== decryptedLength) {
|
||||
@@ -82,21 +71,21 @@ export async function downloadFile(
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const resolveUrlResponse = await fetch(url, {
|
||||
let res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers
|
||||
});
|
||||
|
||||
if (!resolveUrlResponse.ok) {
|
||||
if (!res.ok) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, ${resolveUrlResponse.status}, ${resolveUrlResponse.statusText}, reason: Unable to resolve download url`
|
||||
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
|
||||
);
|
||||
throw new Error(
|
||||
`${resolveUrlResponse.status}: ${strings.failedToResolvedDownloadUrl()}`
|
||||
`${res.status}: ${strings.failedToResolvedDownloadUrl()}`
|
||||
);
|
||||
}
|
||||
|
||||
const downloadUrl = await resolveUrlResponse.text();
|
||||
const downloadUrl = await res.text();
|
||||
|
||||
if (!downloadUrl) {
|
||||
DatabaseLogger.log(
|
||||
@@ -106,36 +95,29 @@ export async function downloadFile(
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
const request = RNFetchBlob.config({
|
||||
let request = RNFetchBlob.config({
|
||||
path: tempFilePath,
|
||||
IOSBackgroundTask: true,
|
||||
overwrite: true
|
||||
})
|
||||
.fetch("GET", downloadUrl)
|
||||
.fetch("GET", downloadUrl, null)
|
||||
.progress(async (recieved, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(
|
||||
0,
|
||||
parseInt(total),
|
||||
filename,
|
||||
parseInt(recieved),
|
||||
"download"
|
||||
);
|
||||
.setProgress(0, total, filename, recieved, "download");
|
||||
|
||||
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
|
||||
});
|
||||
|
||||
cancelToken.cancel = async (reason) => {
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
DatabaseLogger.log(`Download cancelled: ${filename}`);
|
||||
};
|
||||
|
||||
const response = await request;
|
||||
let response = await request;
|
||||
console.log(response.info().headers);
|
||||
|
||||
const contentType =
|
||||
response.info().headers?.["content-type"] ||
|
||||
@@ -146,18 +128,14 @@ export async function downloadFile(
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
|
||||
const status = response.info().status;
|
||||
let status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
if (await exists(originalFilePath)) {
|
||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
if (exists(originalFilePath)) {
|
||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
|
||||
|
||||
if (!(await exists(filename))) {
|
||||
throw new Error("File size mismatch");
|
||||
@@ -165,14 +143,11 @@ export async function downloadFile(
|
||||
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (
|
||||
(e as Error).message !== "canceled" &&
|
||||
!(e as Error).message.includes("NoSuchKey")
|
||||
) {
|
||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
||||
const toast = {
|
||||
heading: strings.downloadError((e as Error).message),
|
||||
message: (e as Error).message,
|
||||
type: "error" as const,
|
||||
heading: strings.downloadError(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
};
|
||||
ToastManager.show(toast);
|
||||
@@ -181,20 +156,17 @@ export async function downloadFile(
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
DatabaseLogger.error(e, "Download failed: ", {
|
||||
url
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
DatabaseLogger.error(e, {
|
||||
url,
|
||||
headers
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash: string) {
|
||||
export async function checkAttachment(hash) {
|
||||
const internetState = await NetInfo.fetch();
|
||||
const isInternetReachable =
|
||||
internetState.isConnected && internetState.isInternetReachable;
|
||||
@@ -204,7 +176,7 @@ export async function checkAttachment(hash: string) {
|
||||
|
||||
try {
|
||||
const size = await getUploadedFileSize(hash);
|
||||
|
||||
console.log("File Size", size);
|
||||
if (size === -1) return { success: true };
|
||||
|
||||
if (size === 0)
|
||||
@@ -212,7 +184,7 @@ export async function checkAttachment(hash: string) {
|
||||
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
|
||||
};
|
||||
} catch (e) {
|
||||
return { failed: (e as Error)?.message };
|
||||
return { failed: e?.message };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
@@ -17,41 +17,24 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { IFileStorage } from "@notesnook/core";
|
||||
import { checkAttachment, downloadFile } from "./download";
|
||||
import {
|
||||
bulkExists,
|
||||
clearCache,
|
||||
clearFileStorage,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
deleteFile,
|
||||
exists,
|
||||
getCacheSize,
|
||||
hashBase64,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import {
|
||||
cancelable,
|
||||
checkAndCreateDir,
|
||||
getUploadedFileSize,
|
||||
requestPermission
|
||||
} from "./utils";
|
||||
|
||||
export default {
|
||||
checkAttachment,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
getCacheSize,
|
||||
requestPermission,
|
||||
checkAndCreateDir,
|
||||
getUploadedFileSize
|
||||
};
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable, getUploadedFileSize } from "./utils";
|
||||
|
||||
export const FileStorage: IFileStorage = {
|
||||
export default {
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
@@ -61,5 +44,10 @@ export const FileStorage: IFileStorage = {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
bulkExists
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
};
|
||||
@@ -18,13 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
FileEncryptionMetadataWithHash,
|
||||
FileEncryptionMetadataWithOutputType,
|
||||
Output,
|
||||
RequestOptions
|
||||
} from "@notesnook/core";
|
||||
import { DataFormat, SerializedKey } from "@notesnook/crypto";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
@@ -32,21 +25,17 @@ import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted<TOutputFormat extends DataFormat>(
|
||||
filename: string,
|
||||
key: SerializedKey,
|
||||
cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
|
||||
) {
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
DatabaseLogger.log("Read encrypted file...");
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (!(await exists(filename))) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const output = await Sodium.decryptFile(
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
...cipherData,
|
||||
@@ -58,16 +47,15 @@ export async function readEncrypted<TOutputFormat extends DataFormat>(
|
||||
|
||||
DatabaseLogger.log("File decrypted...");
|
||||
|
||||
return output as Output<TOutputFormat>;
|
||||
return output;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashBase64(data: string) {
|
||||
export async function hashBase64(data) {
|
||||
const hash = await Sodium.hashFile({
|
||||
type: "base64",
|
||||
data,
|
||||
@@ -79,80 +67,61 @@ export async function hashBase64(data: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64(
|
||||
data: string,
|
||||
encryptionKey: SerializedKey,
|
||||
mimeType: string
|
||||
): Promise<FileEncryptionMetadataWithHash> {
|
||||
export async function writeEncryptedBase64(data, key) {
|
||||
await createCacheDir();
|
||||
const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
const output = await Sodium.encryptFile(encryptionKey, {
|
||||
let output = await Sodium.encryptFile(key, {
|
||||
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
|
||||
type: "url"
|
||||
});
|
||||
|
||||
RNFetchBlob.fs.unlink(filepath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
|
||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||
console.log("encrypted file output: ", output);
|
||||
output.size = output.length;
|
||||
delete output.length;
|
||||
return {
|
||||
...output,
|
||||
alg: "xcha-stream"
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteFile(
|
||||
filename: string,
|
||||
requestOptions?: RequestOptions
|
||||
): Promise<boolean> {
|
||||
export async function deleteFile(filename, data) {
|
||||
await createCacheDir();
|
||||
const localFilePath = cacheDir + `/${filename}`;
|
||||
if (!requestOptions) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
let delFilePath = cacheDir + `/${filename}`;
|
||||
if (!data) {
|
||||
if (!filename) return;
|
||||
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
|
||||
return true;
|
||||
}
|
||||
|
||||
const { url, headers } = requestOptions;
|
||||
|
||||
let { url, headers } = data;
|
||||
try {
|
||||
const response = await RNFetchBlob.fetch("DELETE", url, headers);
|
||||
const status = response.info().status;
|
||||
const ok = status >= 200 && status < 300;
|
||||
let response = await RNFetchBlob.fetch("DELETE", url, headers);
|
||||
let status = response.info().status;
|
||||
let ok = status >= 200 && status < 300;
|
||||
if (ok) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
|
||||
}
|
||||
return ok;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e, "Delete file", {
|
||||
url: url
|
||||
});
|
||||
console.log("delete file: ", e, url, headers);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
for (const file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
for (let file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
|
||||
}
|
||||
for (const file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
for (let file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
|
||||
}
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e, "clearFileStorage");
|
||||
console.log("clearFileStorage", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,98 +142,62 @@ export async function migrateFilesFromCache() {
|
||||
const migratedFilesPath = cacheDir + "/.migrated_1";
|
||||
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
|
||||
if (migrated) {
|
||||
console.log("Files migrated already");
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
console.log("Files to migrate:", files.join(","));
|
||||
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (const file of oldCache) {
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (let file of oldCache) {
|
||||
if (file.startsWith("org.") || file.startsWith("com.")) continue;
|
||||
RNFetchBlob.fs
|
||||
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
console.log("Moved", file);
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e, "migrateFilesFromCache");
|
||||
console.log("migrateFilesFromCache", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearCache() {
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(console.log);
|
||||
await createCacheDir();
|
||||
eSendEvent("cache-cleared");
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByPath(path: string) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
export async function deleteCacheFileByPath(path) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByName(name: string) {
|
||||
export async function deleteCacheFileByName(name) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${name}`;
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (
|
||||
file.includes("_dcache") ||
|
||||
file.startsWith("NN_") ||
|
||||
file.endsWith(".pdf")
|
||||
) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export async function getCachePathForFile(filename: string) {
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
const exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
// Check if file is present in app group path.
|
||||
let existsInAppGroup = false;
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
|
||||
}
|
||||
|
||||
return existsInAppGroup ? appGroupPath : path;
|
||||
}
|
||||
|
||||
export async function exists(filename: string) {
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
@@ -278,7 +211,6 @@ export async function exists(filename: string) {
|
||||
|
||||
if (exists || existsInAppGroup) {
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
|
||||
const totalAbytes = totalChunks * ABYTES;
|
||||
const expectedFileSize = attachment.size + totalAbytes;
|
||||
@@ -293,9 +225,7 @@ export async function exists(filename: string) {
|
||||
);
|
||||
RNFetchBlob.fs
|
||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
.catch(console.log);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -304,7 +234,7 @@ export async function exists(filename: string) {
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function bulkExists(files: string[]) {
|
||||
export async function bulkExists(files) {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
@@ -313,7 +243,7 @@ export async function bulkExists(files: string[]) {
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter(
|
||||
@@ -331,9 +261,9 @@ export async function getCacheSize() {
|
||||
await createCacheDir();
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
|
||||
stat.forEach((file) => {
|
||||
total += parseInt(file.size as unknown as string);
|
||||
console.log("Total files", stat.length);
|
||||
stat.forEach((s) => {
|
||||
total += parseInt(s.size);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
132
apps/mobile/app/common/filesystem/upload.js
Normal file
132
apps/mobile/app/common/filesystem/upload.js
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
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 { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir } from "./io";
|
||||
import { cacheDir, checkUpload, getUploadedFileSize } from "./utils";
|
||||
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) return false;
|
||||
let { url, headers } = requestOptions;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
let filePath = `${cacheDir}/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(filePath);
|
||||
// Check for file in appGroupPath if it doesn't exist in cacheDir
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
filePath = appGroupPath;
|
||||
exists = await RNFetchBlob.fs.exists(filePath);
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
throw new Error(
|
||||
`Trying to upload file at path ${filePath} that doest not exist.`
|
||||
);
|
||||
}
|
||||
|
||||
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
|
||||
|
||||
let remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === -1) return false;
|
||||
if (remoteFileSize > 0 && remoteFileSize === fileSize) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
let uploadUrlResponse = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
|
||||
const uploadUrl = uploadUrlResponse.ok
|
||||
? await uploadUrlResponse.text()
|
||||
: await uploadUrlResponse.json();
|
||||
|
||||
if (typeof uploadUrl !== "string") {
|
||||
throw new Error(
|
||||
uploadUrl.error || "Unable to resolve attachment upload url."
|
||||
);
|
||||
}
|
||||
|
||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||
|
||||
let uploadRequest = RNFetchBlob.config({
|
||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||
})
|
||||
.fetch(
|
||||
"PUT",
|
||||
uploadUrl,
|
||||
{
|
||||
"content-type": ""
|
||||
},
|
||||
RNFetchBlob.wrap(filePath)
|
||||
)
|
||||
.uploadProgress((sent, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(sent, total, filename, 0, "upload");
|
||||
DatabaseLogger.info(
|
||||
`File upload progress: ${filename}, ${sent}/${total}`
|
||||
);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
uploadRequest.cancel();
|
||||
};
|
||||
|
||||
let uploadResponse = await uploadRequest;
|
||||
let status = uploadResponse.info().status;
|
||||
let uploaded = status >= 200 && status < 300;
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
if (!uploaded) {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(filePath);
|
||||
throw new Error(
|
||||
`${status}, name: ${fileInfo.filename}, length: ${
|
||||
fileInfo.size
|
||||
}, info: ${JSON.stringify(uploadResponse.info())}`
|
||||
);
|
||||
}
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
|
||||
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
|
||||
return uploaded;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
ToastManager.error(e, "File upload failed");
|
||||
DatabaseLogger.error(e, "File upload failed", {
|
||||
filename
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,174 +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 { isImage, RequestOptions } from "@notesnook/core";
|
||||
import { PermissionsAndroid, Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir } from "./io";
|
||||
import {
|
||||
cacheDir,
|
||||
checkUpload,
|
||||
FileSizeResult,
|
||||
getUploadedFileSize
|
||||
} from "./utils";
|
||||
import Upload from "@ammarahmed/react-native-upload";
|
||||
|
||||
export async function uploadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
if (!requestOptions) return false;
|
||||
const { url, headers } = requestOptions;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
let filePath = `${cacheDir}/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(filePath);
|
||||
// Check for file in appGroupPath if it doesn't exist in cacheDir
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
filePath = appGroupPath;
|
||||
exists = await RNFetchBlob.fs.exists(filePath);
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
throw new Error(
|
||||
`Trying to upload file at path ${filePath} that doest not exist.`
|
||||
);
|
||||
}
|
||||
|
||||
const fileInfo = await RNFetchBlob.fs.stat(filePath);
|
||||
|
||||
const remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === FileSizeResult.Error) return false;
|
||||
|
||||
if (
|
||||
remoteFileSize > FileSizeResult.Empty &&
|
||||
remoteFileSize === fileInfo.size
|
||||
) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
let attachmentInfo = await db.attachments.attachment(filename);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`Starting upload of ${filename} at path: ${fileInfo.path} ${fileInfo.size}`
|
||||
);
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const status = await PermissionsAndroid.request(
|
||||
"android.permission.POST_NOTIFICATIONS"
|
||||
);
|
||||
if (status !== "granted") {
|
||||
ToastManager.show({
|
||||
message: `The permission to show file upload notification was disallowed by the user.`,
|
||||
type: "info"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const upload = Upload.create({
|
||||
customUploadId: filename,
|
||||
path: Platform.OS === "ios" ? "file://" + fileInfo.path : fileInfo.path,
|
||||
url: url,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
...headers,
|
||||
"content-type": "application/octet-stream"
|
||||
},
|
||||
appGroup: IOS_APPGROUPID,
|
||||
notification: {
|
||||
filename:
|
||||
attachmentInfo && isImage(attachmentInfo?.mimeType)
|
||||
? "image"
|
||||
: attachmentInfo?.filename || "file",
|
||||
enabled: true,
|
||||
enableRingTone: true,
|
||||
autoClear: true
|
||||
}
|
||||
}).onChange((event) => {
|
||||
switch (event.status) {
|
||||
case "running":
|
||||
case "pending":
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(
|
||||
event.uploadedBytes || 0,
|
||||
event.totalBytes || fileInfo.size,
|
||||
filename,
|
||||
0,
|
||||
"upload"
|
||||
);
|
||||
DatabaseLogger.info(
|
||||
`File upload progress: ${filename}, ${event.uploadedBytes}/${
|
||||
event.totalBytes || fileInfo.size
|
||||
}`
|
||||
);
|
||||
break;
|
||||
case "completed":
|
||||
console.log("Upload completed");
|
||||
break;
|
||||
}
|
||||
});
|
||||
const result = await upload.start();
|
||||
cancelToken.cancel = async () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
upload.cancel();
|
||||
};
|
||||
|
||||
const status = result.responseCode || 0;
|
||||
const uploaded = status >= 200 && status < 300;
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
if (!uploaded) {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(filePath);
|
||||
throw new Error(
|
||||
`${status}, name: ${fileInfo.filename}, length: ${
|
||||
fileInfo.size
|
||||
}, info: ${JSON.stringify(result.error)}`
|
||||
);
|
||||
}
|
||||
attachmentInfo = await db.attachments.attachment(filename);
|
||||
if (!attachmentInfo) return false;
|
||||
await checkUpload(filename, requestOptions.chunkSize, attachmentInfo.size);
|
||||
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
|
||||
return uploaded;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
ToastManager.error(e as Error, "File upload failed");
|
||||
DatabaseLogger.error(e, "File upload failed", {
|
||||
filename
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { hosts, RequestOptions } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { hosts } from "@notesnook/core";
|
||||
|
||||
export const ABYTES = 17;
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
@@ -31,13 +31,18 @@ export const cacheDir =
|
||||
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
|
||||
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
|
||||
|
||||
export function getRandomId(prefix: string) {
|
||||
export function getRandomId(prefix) {
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
export function parseS3Error(data?: string) {
|
||||
/**
|
||||
*
|
||||
* @param {string | undefined} data
|
||||
* @returns
|
||||
*/
|
||||
export function parseS3Error(data) {
|
||||
const xml = typeof data === "string" ? data : null;
|
||||
|
||||
const error = {
|
||||
@@ -65,19 +70,11 @@ export function parseS3Error(data?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelable(
|
||||
operation: (
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) => Promise<boolean>
|
||||
) {
|
||||
export function cancelable(operation) {
|
||||
const cancelToken = {
|
||||
cancel: async (reason?: string) => {}
|
||||
cancel: () => {}
|
||||
};
|
||||
return (filename: string, requestOptions: RequestOptions) => {
|
||||
return (filename, requestOptions) => {
|
||||
return {
|
||||
execute: () => operation(filename, requestOptions, cancelToken),
|
||||
cancel: async () => {
|
||||
@@ -87,35 +84,29 @@ export function cancelable(
|
||||
};
|
||||
}
|
||||
|
||||
export function copyFileAsync(source: string, dest: string) {
|
||||
export function copyFileAsync(source, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
//@ts-ignore
|
||||
ScopedStorage.copyFile(source, dest, (e: any, r: any) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function releasePermissions(path: string) {
|
||||
export async function releasePermissions(path) {
|
||||
if (Platform.OS === "ios") return;
|
||||
const uris = await ScopedStorage.getPersistedUriPermissions();
|
||||
for (const uri of uris) {
|
||||
for (let uri of uris) {
|
||||
if (path.startsWith(uri)) {
|
||||
await ScopedStorage.releasePersistableUriPermission(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const FileSizeResult = {
|
||||
Empty: 0,
|
||||
Error: -1
|
||||
};
|
||||
|
||||
export async function getUploadedFileSize(hash: string, retry = 0) {
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
@@ -123,73 +114,27 @@ export async function getUploadedFileSize(hash: string, retry = 0) {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (
|
||||
!attachmentInfo.ok ||
|
||||
attachmentInfo.headers?.get("content-length") === null
|
||||
) {
|
||||
if (retry < 3) {
|
||||
DatabaseLogger.log(`Retrying file size check: ${hash}, ${retry}`);
|
||||
return getUploadedFileSize(hash, retry + 1);
|
||||
}
|
||||
throw new Error(
|
||||
`File size check failed: ${hash}, ${
|
||||
attachmentInfo.status
|
||||
}, ${attachmentInfo.headers?.get("content-length")}`
|
||||
);
|
||||
}
|
||||
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length") as string
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return FileSizeResult.Error;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUpload(
|
||||
filename: string,
|
||||
chunkSize: number,
|
||||
expectedSize: number
|
||||
) {
|
||||
export async function checkUpload(filename, chunkSize, expectedSize) {
|
||||
const size = await getUploadedFileSize(filename);
|
||||
const totalChunks = Math.ceil(size / (chunkSize + ABYTES));
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
const error =
|
||||
size === 0
|
||||
? `File size is 0.`
|
||||
: size === -1
|
||||
? `File verification check failed.`
|
||||
: expectedSize !== decryptedLength
|
||||
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
|
||||
: undefined;
|
||||
? `File verification check failed.`
|
||||
: expectedSize !== decryptedLength
|
||||
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
|
||||
: undefined;
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
|
||||
export async function requestPermission() {
|
||||
if (Platform.OS === "ios") return true;
|
||||
return true;
|
||||
}
|
||||
export async function checkAndCreateDir(path: string) {
|
||||
const dir =
|
||||
Platform.OS === "ios"
|
||||
? RNFetchBlob.fs.dirs.DocumentDir + path
|
||||
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
|
||||
|
||||
try {
|
||||
const exists = await RNFetchBlob.fs.exists(dir);
|
||||
const isDir = await RNFetchBlob.fs.isDir(dir);
|
||||
if (!exists || !isDir) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
} catch (e) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
export const santizeUri = (uri: string) => {
|
||||
return Platform.OS === "ios" ? decodeURI(uri).replace("file:///", "/") : uri;
|
||||
};
|
||||
@@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function info(context: string, ...logs: unknown[]) {}
|
||||
function info(context: string, ...logs: unknown[]) {
|
||||
console.log(`${new Date().toLocaleDateString()}::info::${context}:`, ...logs);
|
||||
}
|
||||
|
||||
function error(context: string, ...logs: unknown[]) {
|
||||
console.log(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user