├── .node-version ├── .watchmanconfig ├── .ruby-version ├── .npmrc ├── app.json ├── src ├── lib │ ├── app-settings.json │ ├── files │ │ ├── cache.js │ │ ├── zip.js │ │ ├── constant.js │ │ ├── note.js │ │ ├── file.js │ │ └── helpers.js │ ├── money.js │ ├── device.js │ ├── openpgp │ │ ├── constant.js │ │ ├── decryptFile.js │ │ ├── decryptSmallFile.js │ │ ├── encryptSmallFile.js │ │ ├── decryptLargeFile.js │ │ ├── helpers.js │ │ ├── encryptLargeFile.js │ │ └── encryptFiles.js │ ├── constants.js │ ├── toast.js │ ├── style.js │ ├── password.js │ ├── array.js │ ├── localstorage.js │ └── keychain.js ├── assets │ ├── logo.png │ └── xiangcai.jpeg ├── router │ ├── navigationRef.js │ ├── routes.js │ ├── Router.js │ └── BottomTab.js ├── hooks │ ├── useColors.js │ ├── useTakePhotoInTabs.js │ └── useInAppPurchase.js ├── components │ ├── Icon.js │ ├── PlatformToggle.js │ ├── ContentWrapper.js │ ├── FoldersList.js │ ├── FabButton.js │ ├── FoldersEmptyState.js │ ├── RenameButton.js │ ├── ScreenWrapper.js │ ├── DonationCard.js │ ├── Confirm.js │ ├── Collapsible.js │ ├── PickFilesButton.js │ ├── TakePhotoButton.js │ ├── DownloadButton.js │ ├── AddNoteButton.js │ ├── ShareButton.js │ ├── DeleteButton.js │ ├── Caches.js │ ├── OpenFileButton.js │ ├── PickImagesButton.js │ ├── FolderItem.js │ ├── RootFolders.js │ ├── PasswordAlert.js │ ├── MoveToButton.js │ ├── FolderPickerItem.js │ ├── NoteItem.js │ ├── AppBar.js │ ├── PickNotesButton.js │ ├── FolderNotes.js │ ├── FolderPicker.js │ ├── DownloadRemoteFileButton.js │ ├── DonateBanner.js │ ├── NoteItemActions.js │ └── FolderActions.js ├── views │ ├── TakePhoto.js │ ├── Folders.js │ ├── RenameFileForm.js │ ├── FolderForm.js │ ├── PasswordGenerator.js │ ├── Passwords.js │ ├── PasswordForm.js │ ├── Donation.js │ └── NoteDetails.js └── App.js ├── .bundle └── config ├── ios ├── fonts │ ├── Entypo.ttf │ ├── Feather.ttf │ ├── Fontisto.ttf │ ├── Ionicons.ttf │ ├── Octicons.ttf │ ├── Zocial.ttf │ ├── AntDesign.ttf │ ├── EvilIcons.ttf │ ├── Foundation.ttf │ ├── FontAwesome.ttf │ ├── MaterialIcons.ttf │ ├── SimpleLineIcons.ttf │ ├── FontAwesome5_Solid.ttf │ ├── FontAwesome5_Brands.ttf │ ├── FontAwesome5_Regular.ttf │ └── MaterialCommunityIcons.ttf ├── PreCloud │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── logo.imageset │ │ │ ├── logo.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── logo-1024.png │ │ │ ├── logo-20@2x.png │ │ │ ├── logo-20@3x.png │ │ │ ├── logo-29@2x.png │ │ │ ├── logo-29@3x.png │ │ │ ├── logo-40@2x.png │ │ │ ├── logo-40@3x.png │ │ │ ├── logo-60@2x.png │ │ │ ├── logo-60@3x.png │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── PreCloud.entitlements │ ├── Info.plist │ └── LaunchScreen.storyboard ├── PreCloud.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── .xcode.env ├── PreCloudTests │ ├── Info.plist │ └── PreCloudTests.m ├── Podfile └── PreCloud.xcodeproj │ └── xcshareddata │ └── xcschemes │ └── PreCloud.xcscheme ├── android ├── app │ ├── debug.keystore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ ├── colors.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── drawable-hdpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── screen.png │ │ │ │ ├── drawable-ldpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── screen.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── screen.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── screen.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── screen.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ ├── icon.png │ │ │ │ │ └── screen.png │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ ├── ic_launcher_adaptive_back.png │ │ │ │ │ └── ic_launcher_adaptive_fore.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ ├── ic_launcher_adaptive_back.png │ │ │ │ │ └── ic_launcher_adaptive_fore.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ ├── ic_launcher_adaptive_back.png │ │ │ │ │ └── ic_launcher_adaptive_fore.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ ├── ic_launcher_adaptive_back.png │ │ │ │ │ └── ic_launcher_adaptive_fore.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ ├── ic_launcher_adaptive_back.png │ │ │ │ │ └── ic_launcher_adaptive_fore.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ ├── drawable │ │ │ │ │ ├── background_splash.xml │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ └── layout │ │ │ │ │ └── launch_screen.xml │ │ │ ├── assets │ │ │ │ └── fonts │ │ │ │ │ └── Ionicons.ttf │ │ │ ├── jni │ │ │ │ ├── CMakeLists.txt │ │ │ │ ├── MainApplicationModuleProvider.h │ │ │ │ ├── OnLoad.cpp │ │ │ │ ├── MainComponentsRegistry.h │ │ │ │ ├── MainApplicationModuleProvider.cpp │ │ │ │ ├── MainApplicationTurboModuleManagerDelegate.h │ │ │ │ ├── MainApplicationTurboModuleManagerDelegate.cpp │ │ │ │ └── MainComponentsRegistry.cpp │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── precloud │ │ │ │ │ ├── SplashActivity.java │ │ │ │ │ ├── newarchitecture │ │ │ │ │ ├── components │ │ │ │ │ │ └── MainComponentsRegistry.java │ │ │ │ │ └── modules │ │ │ │ │ │ └── MainApplicationTurboModuleManagerDelegate.java │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ └── AndroidManifest.xml │ │ └── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── com │ │ │ └── precloud │ │ │ └── ReactNativeFlipper.java │ ├── proguard-rules.pro │ ├── build_defs.bzl │ └── _BUCK ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle ├── gradle.properties ├── build.gradle └── gradlew.bat ├── babel.config.js ├── .buckconfig ├── .prettierrc.js ├── Gemfile ├── metro.config.js ├── __tests__ └── App-test.js ├── index.js ├── .eslintrc.js ├── .github └── FUNDING.yml ├── PRIVACYPOLICY.md ├── .gitignore ├── README.md ├── scripts ├── bump-build-numbers.js └── bump-version.js ├── patches ├── react-native-pell-rich-editor+1.8.8.patch └── react-native-toast-message+2.1.5.patch ├── Gemfile.lock └── package.json /.node-version: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.5 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PreCloud", 3 | "displayName": "PreCloud" 4 | } -------------------------------------------------------------------------------- /src/lib/app-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildDate": 1668281488901 3 | } 4 | -------------------------------------------------------------------------------- /.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /ios/fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/Entypo.ttf -------------------------------------------------------------------------------- /ios/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/Feather.ttf -------------------------------------------------------------------------------- /ios/fonts/Fontisto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/Fontisto.ttf -------------------------------------------------------------------------------- /ios/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /ios/fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/Octicons.ttf -------------------------------------------------------------------------------- /ios/fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/Zocial.ttf -------------------------------------------------------------------------------- /ios/fonts/AntDesign.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/AntDesign.ttf -------------------------------------------------------------------------------- /ios/fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /ios/fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/Foundation.ttf -------------------------------------------------------------------------------- /src/assets/xiangcai.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/src/assets/xiangcai.jpeg -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /ios/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /ios/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /ios/fonts/SimpleLineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/SimpleLineIcons.ttf -------------------------------------------------------------------------------- /ios/fonts/FontAwesome5_Solid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/FontAwesome5_Solid.ttf -------------------------------------------------------------------------------- /ios/fonts/FontAwesome5_Brands.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/FontAwesome5_Brands.ttf -------------------------------------------------------------------------------- /ios/fonts/FontAwesome5_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/FontAwesome5_Regular.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PreCloud 3 | 4 | -------------------------------------------------------------------------------- /ios/fonts/MaterialCommunityIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/fonts/MaterialCommunityIcons.ttf -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/assets/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-hdpi/screen.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-ldpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-ldpi/screen.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-mdpi/screen.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-xhdpi/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-xxhdpi/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-xhdpi/screen.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-xxhdpi/screen.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-xxxhdpi/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/drawable-xxxhdpi/screen.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/logo.imageset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/PreCloud/Images.xcassets/logo.imageset/logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src/router/navigationRef.js: -------------------------------------------------------------------------------- 1 | import { createNavigationContainerRef } from '@react-navigation/native'; 2 | 3 | export const navigationRef = createNavigationContainerRef(); -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | trailingComma: 'es5', 4 | singleQuote: true, 5 | printWidth: 100, 6 | bracketSpacing: true, 7 | }; 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-1024.png -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-20@2x.png -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-20@3x.png -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-29@2x.png -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-29@3x.png -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-40@2x.png -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-40@3x.png -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-60@2x.png -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/ios/PreCloud/Images.xcassets/AppIcon.appiconset/logo-60@3x.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penghuili/PreCloud/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby '2.7.5' 5 | 6 | gem 'cocoapods', '~> 1.11', '>= 1.11.2' 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #39DAA2 4 | #00170c 5 | -------------------------------------------------------------------------------- /ios/PreCloud/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transformer: { 3 | getTransformOptions: async () => ({ 4 | transform: { 5 | experimentalImportSupport: false, 6 | inlineRequires: true, 7 | }, 8 | }), 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ios/PreCloud/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/files/cache.js: -------------------------------------------------------------------------------- 1 | import { CachesDirectoryPath } from 'react-native-fs'; 2 | 3 | import { emptyFolder } from './helpers'; 4 | 5 | export const cachePath = CachesDirectoryPath; 6 | 7 | export async function emptyCache() { 8 | await emptyFolder(cachePath); 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/money.js: -------------------------------------------------------------------------------- 1 | import { isAndroid } from './device'; 2 | 3 | const { buildDate } = require('../lib/app-settings.json'); 4 | 5 | export function showDonate() { 6 | // eslint-disable-next-line no-undef 7 | return isAndroid() || Date.now() > buildDate + 3 * 24 * 60 * 60 * 1000 || !!__DEV__; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/device.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import { appStoreLink } from './constants'; 3 | 4 | export function isAndroid() { 5 | return Platform.OS === 'android'; 6 | } 7 | 8 | export function getStoreLink() { 9 | return isAndroid() ? appStoreLink.android : appStoreLink.ios; 10 | } 11 | -------------------------------------------------------------------------------- /ios/PreCloud.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/PreCloud.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | # Define the library name here. 4 | project(rndiffapp_appmodules) 5 | 6 | # This file includes all the necessary to let you build your application with the New Architecture. 7 | include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake) 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /src/hooks/useColors.js: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'native-base'; 2 | 3 | function useColors() { 4 | const { colors } = useTheme(); 5 | 6 | return { 7 | text: colors.text[900], 8 | primary: colors.primary[400], 9 | orange: colors.orange[600], 10 | white: colors.white, 11 | }; 12 | } 13 | 14 | export default useColors; 15 | -------------------------------------------------------------------------------- /ios/PreCloud/PreCloud.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.com.precloud 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry, LogBox } from 'react-native'; 2 | 3 | import App from './src/App'; 4 | import { name as appName } from './app.json'; 5 | 6 | LogBox.ignoreLogs([ 7 | 'new NativeEventEmitter', 8 | 'When server rendering', 9 | 'Failed prop type: Invalid prop `color`', 10 | 'EventEmitter.removeListener' 11 | ]); 12 | 13 | AppRegistry.registerComponent(appName, () => App); 14 | -------------------------------------------------------------------------------- /src/lib/openpgp/constant.js: -------------------------------------------------------------------------------- 1 | export const openpgpStatus = { 2 | encrypted: 'openpgpStatus/encrypted', 3 | notLargeFile: 'openpgpStatus/notLargeFile', 4 | error: 'openpgpStatus/error', 5 | }; 6 | 7 | export const ENCRYPTION_LIMIT_IN_GIGABYTES = 1; 8 | export const ENCRYPTION_LIMIT_IN_BYTES = ENCRYPTION_LIMIT_IN_GIGABYTES * 1024 * 1024 * 1024; 9 | export const LARGE_FILE_SIZE_IN_BYTES = 100 * 1024 * 1024; 10 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainApplicationModuleProvider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | std::shared_ptr MainApplicationModuleProvider( 12 | const std::string &moduleName, 13 | const JavaTurboModule::InitParams ¶ms); 14 | 15 | } // namespace react 16 | } // namespace facebook 17 | -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android/app/src/main/jni/OnLoad.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "MainApplicationTurboModuleManagerDelegate.h" 3 | #include "MainComponentsRegistry.h" 4 | 5 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { 6 | return facebook::jni::initialize(vm, [] { 7 | facebook::react::MainApplicationTurboModuleManagerDelegate:: 8 | registerNatives(); 9 | facebook::react::MainComponentsRegistry::registerNatives(); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Ionicon from 'react-native-vector-icons/Ionicons'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | 6 | function Icon({ name, size, color, onPress }) { 7 | const colors = useColors(); 8 | const innerSize = !size || typeof size !== 'number' ? 24 : size; 9 | return ; 10 | } 11 | 12 | export default Icon; 13 | -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | export const routeNames = { 2 | folders: 'folders', 3 | folder: 'folder', 4 | folderForm: 'folderForm', 5 | renameFileForm: 'renameFileForm', 6 | takePhoto: 'takePhoto', 7 | noteDetails: 'noteDetails', 8 | passwords: 'passwords', 9 | passwordForm: 'passwordForm', 10 | settings: 'settings', 11 | plainText: 'plainText', 12 | passwordGenerator: 'passwordGenerator', 13 | backup: 'backup', 14 | donation: 'donation', 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/PlatformToggle.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import { platforms } from '../lib/constants'; 3 | 4 | function PlatformToggle({ children, for: forPlatform }) { 5 | if ( 6 | (Platform.OS === platforms.android && forPlatform === platforms.android) || 7 | (Platform.OS === platforms.ios && forPlatform === platforms.ios) 8 | ) { 9 | return children; 10 | } 11 | 12 | return null; 13 | } 14 | 15 | export default PlatformToggle; 16 | -------------------------------------------------------------------------------- /src/lib/openpgp/decryptFile.js: -------------------------------------------------------------------------------- 1 | import { isLargeFile } from '../files/helpers'; 2 | import { decryptLargeFile } from './decryptLargeFile'; 3 | import { decryptSmallFile } from './decryptSmallFile'; 4 | 5 | export async function decryptFile(file, password) { 6 | let decrypted; 7 | if (isLargeFile(file)) { 8 | decrypted = await decryptLargeFile(file, password); 9 | } else { 10 | decrypted = await decryptSmallFile(file, password); 11 | } 12 | 13 | return decrypted; 14 | } 15 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /src/components/ContentWrapper.js: -------------------------------------------------------------------------------- 1 | import { Box, KeyboardAvoidingView, ScrollView } from 'native-base'; 2 | import React from 'react'; 3 | 4 | function ContentWrapper({ children, hasPX = true }) { 5 | return ( 6 | 7 | 8 | {children} 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default ContentWrapper; 16 | -------------------------------------------------------------------------------- /src/lib/constants.js: -------------------------------------------------------------------------------- 1 | export const myEmail = 'peng@duck.com'; 2 | 3 | export const appStoreLink = { 4 | ios: 'https://apps.apple.com/us/app/precloud/id1638793841', 5 | android: 'https://play.google.com/store/apps/details?id=com.precloud', 6 | }; 7 | 8 | export const platforms = { 9 | ios: 'ios', 10 | android: 'android', 11 | }; 12 | 13 | export const heights = { 14 | appBar: 40, 15 | editorToolBar: 44, 16 | }; 17 | 18 | export const imageLimitInNote = 100; 19 | 20 | export const tabbarHeight = 60; -------------------------------------------------------------------------------- /src/lib/toast.js: -------------------------------------------------------------------------------- 1 | import Toast from 'react-native-toast-message'; 2 | import { heights } from './constants'; 3 | 4 | export function showToast(message, type = 'success', durationInSeconds = 4) { 5 | Toast.show({ 6 | type, 7 | text1: message, 8 | position: 'top', 9 | autoHide: true, 10 | visibilityTime: durationInSeconds * 1000, 11 | topOffset: heights.appBar / 2, 12 | onPress: () => Toast.hide(), 13 | }); 14 | } 15 | 16 | export function hideToast() { 17 | Toast.hide(); 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/style.js: -------------------------------------------------------------------------------- 1 | import { extendTheme } from 'native-base'; 2 | 3 | // primary color: #39DAA2 4 | 5 | export function getTheme() { 6 | return extendTheme({ 7 | colors: { 8 | primary: { 9 | 50: '#defef3', 10 | 100: '#b9f4e0', 11 | 200: '#90eccc', 12 | 300: '#67e3b8', 13 | 400: '#39DAA2', 14 | 500: '#24c18b', 15 | 600: '#17966c', 16 | 700: '#0b6b4c', 17 | 800: '#00412d', 18 | 900: '#00170c', 19 | }, 20 | }, 21 | }); 22 | } -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) -------------------------------------------------------------------------------- /android/app/src/main/java/com/precloud/SplashActivity.java: -------------------------------------------------------------------------------- 1 | package com.precloud; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import androidx.appcompat.app.AppCompatActivity; 6 | 7 | public class SplashActivity extends AppCompatActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | 12 | Intent intent = new Intent(this, MainActivity.class); 13 | startActivity(intent); 14 | finish(); 15 | } 16 | } -------------------------------------------------------------------------------- /android/app/src/main/res/layout/launch_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/FoldersList.js: -------------------------------------------------------------------------------- 1 | import { HStack } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import FolderItem from './FolderItem'; 5 | 6 | function FoldersList({ folders, navigate }) { 7 | if (!folders?.length) { 8 | return null; 9 | } 10 | 11 | return ( 12 | 13 | {folders.map(folder => ( 14 | 15 | ))} 16 | 17 | ); 18 | } 19 | 20 | export default FoldersList; 21 | -------------------------------------------------------------------------------- /src/lib/password.js: -------------------------------------------------------------------------------- 1 | const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 2 | const integers = '0123456789'; 3 | const specialCharacters = '!@#$%^&*_-=+'; 4 | 5 | export function generatePassword(length, hasSpecialCharacters) { 6 | let chars = `${letters}${integers}`; 7 | if (hasSpecialCharacters) { 8 | chars = `${chars}${specialCharacters}`; 9 | } 10 | 11 | let password = ''; 12 | for (let i = 0; i < length; i++) { 13 | password += chars.charAt(Math.floor(Math.random() * chars.length)); 14 | } 15 | 16 | return password; 17 | } 18 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | es2021: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:react/recommended', 10 | 'plugin:react-hooks/recommended', 11 | 'plugin:import/recommended', 12 | ], 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | ecmaVersion: 'latest', 18 | sourceType: 'module', 19 | }, 20 | plugins: ['react'], 21 | rules: { 22 | 'import/no-named-as-default-member': 0, 23 | 'react/prop-types': 0, 24 | 'no-empty': 0, 25 | 'import/namespace': 0, 26 | 'react/display-name': 0, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/openpgp/decryptSmallFile.js: -------------------------------------------------------------------------------- 1 | import { cachePath } from '../files/cache'; 2 | import { getOriginalFileName, statFile } from '../files/helpers'; 3 | import { decryptFile } from './helpers'; 4 | 5 | export async function decryptSmallFile(file, password) { 6 | const originalFileName = getOriginalFileName(file.name); 7 | const decryptedPath = `${cachePath}/${originalFileName}`; 8 | 9 | const success = await decryptFile(file.path, decryptedPath, password); 10 | 11 | if (success) { 12 | const { size: decryptedSize } = await statFile(decryptedPath); 13 | return { path: decryptedPath, name: originalFileName, size: decryptedSize }; 14 | } 15 | 16 | return null; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/FabButton.js: -------------------------------------------------------------------------------- 1 | import { Fab } from 'native-base'; 2 | import React from 'react'; 3 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 4 | 5 | import useColors from '../hooks/useColors'; 6 | import Icon from './Icon'; 7 | 8 | function FabButton({ icon, onPress }) { 9 | const colors = useColors(); 10 | const { bottom } = useSafeAreaInsets(); 11 | 12 | return ( 13 | } 19 | onPress={onPress} 20 | /> 21 | ); 22 | } 23 | 24 | export default FabButton; 25 | -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /src/components/FoldersEmptyState.js: -------------------------------------------------------------------------------- 1 | import { Button, Text, VStack } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import { routeNames } from '../router/routes'; 5 | import { useStore } from '../store/store'; 6 | 7 | function FoldersEmptyState({ navigate }) { 8 | const password = useStore(state => state.activePassword); 9 | 10 | function handleAddFolder() { 11 | navigate(routeNames.folderForm, { folder: null }); 12 | } 13 | 14 | return ( 15 | 16 | Create your first folder, then you can encrypt and save files to it. 17 | 20 | 21 | ); 22 | } 23 | 24 | export default FoldersEmptyState; 25 | -------------------------------------------------------------------------------- /src/components/RenameButton.js: -------------------------------------------------------------------------------- 1 | import { IconButton } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { routeNames } from '../router/routes'; 6 | import Icon from './Icon'; 7 | 8 | function RenameButton({ file, navigate, onRename }) { 9 | const colors = useColors(); 10 | 11 | async function handlePress() { 12 | onRename(); 13 | navigate(routeNames.renameFileForm, { 14 | file: { name: file.name, path: file.path }, 15 | }); 16 | } 17 | 18 | return ( 19 | } 21 | size="sm" 22 | variant="subtle" 23 | mr="2" 24 | onPress={handlePress} 25 | /> 26 | ); 27 | } 28 | 29 | export default RenameButton; 30 | -------------------------------------------------------------------------------- /src/views/TakePhoto.js: -------------------------------------------------------------------------------- 1 | import { Text } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import AppBar from '../components/AppBar'; 5 | import ContentWrapper from '../components/ContentWrapper'; 6 | import Icon from '../components/Icon'; 7 | import ScreenWrapper from '../components/ScreenWrapper'; 8 | import useColors from '../hooks/useColors'; 9 | 10 | function TakePhoto() { 11 | const colors = useColors(); 12 | return ( 13 | 14 | 15 | 16 | 17 | Press the below again. 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default TakePhoto; 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: penghuili 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /src/components/ScreenWrapper.js: -------------------------------------------------------------------------------- 1 | import { StatusBar } from 'native-base'; 2 | import React from 'react'; 3 | import { View } from 'react-native'; 4 | import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; 5 | 6 | import useColors from '../hooks/useColors'; 7 | 8 | function ScreenWrapper({ children }) { 9 | const colors = useColors(); 10 | const { top } = useSafeAreaInsets(); 11 | return ( 12 | 16 | 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | 23 | export default ScreenWrapper; 24 | -------------------------------------------------------------------------------- /src/lib/openpgp/encryptSmallFile.js: -------------------------------------------------------------------------------- 1 | import { precloudExtension } from '../files/constant'; 2 | import { statFile } from '../files/helpers'; 3 | import { encryptFile } from './helpers'; 4 | 5 | export async function encryptSmallFile(file, { folder, password }) { 6 | const { name, path } = file; 7 | 8 | const inputPath = path; 9 | const encryptedName = `${name}.${precloudExtension}`; 10 | const outputPath = `${folder.path}/${encryptedName}`; 11 | const success = await encryptFile(inputPath, outputPath, password); 12 | 13 | if (success) { 14 | const { size: newSize } = await statFile(outputPath); 15 | return { 16 | name: encryptedName, 17 | path: outputPath, 18 | size: newSize, 19 | isDirectory: () => false, 20 | isFile: () => true, 21 | }; 22 | } 23 | 24 | return null; 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useTakePhotoInTabs.js: -------------------------------------------------------------------------------- 1 | import { takePhoto } from '../lib/files/actions'; 2 | import { navigationRef } from '../router/navigationRef'; 3 | import { routeNames } from '../router/routes'; 4 | import { useStore } from '../store/store'; 5 | 6 | function useTakePhotoInTabs() { 7 | const defaultFolder = useStore(state => state.defaultFolder); 8 | const rootFolders = useStore(state => state.rootFolders); 9 | 10 | function handleTakePhoto() { 11 | const folder = rootFolders.find(f => f.name === defaultFolder); 12 | if (folder) { 13 | takePhoto().then(photo => { 14 | if (photo) { 15 | navigationRef.navigate(routeNames.folder, { selectedFiles: [photo], path: folder.path }); 16 | } 17 | }); 18 | } 19 | } 20 | 21 | return handleTakePhoto; 22 | } 23 | 24 | export default useTakePhotoInTabs; 25 | -------------------------------------------------------------------------------- /ios/PreCloudTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/views/Folders.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AppBar from '../components/AppBar'; 4 | import ContentWrapper from '../components/ContentWrapper'; 5 | import PasswordAlert from '../components/PasswordAlert'; 6 | import RootFolders from '../components/RootFolders'; 7 | import ScreenWrapper from '../components/ScreenWrapper'; 8 | import { useStore } from '../store/store'; 9 | 10 | function Folders({ navigation }) { 11 | const rootFolders = useStore(state => state.rootFolders); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default Folders; 26 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'PreCloud' 2 | include ':react-native-splash-screen' 3 | project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android') 4 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 5 | include ':app' 6 | includeBuild('../node_modules/react-native-gradle-plugin') 7 | 8 | if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") { 9 | include(":ReactAndroid") 10 | project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid') 11 | 12 | include(":ReactAndroid:hermes-engine") 13 | project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine') 14 | } 15 | -------------------------------------------------------------------------------- /PRIVACYPOLICY.md: -------------------------------------------------------------------------------- 1 | # PreCloud - Encrypt before upload 2 | 3 | PreCloud has no tracking, it's open sourced, you can check the code [here](https://github.com/penghuili/PreCloud). 4 | 5 | **PreCloud has no server, everything happens on your device**: Your master password, the encryption process, the encrypted texts and files. 6 | 7 | You can safely upload the encrypted texts or files to wherever you want, making any cloud provider an encrypted storage. 8 | 9 | All your texts and files are encrypted with your master password via [openpgpjs](https://github.com/openpgpjs/openpgpjs). 10 | 11 | Your master password is saved in the secure storage on your phone ([Keychain](https://developer.apple.com/documentation/security/keychain_services) on iOS and [Keystore](https://developer.android.com/training/articles/keystore) on Android). 12 | 13 | Contact me for anything: peng@duck.com 14 | -------------------------------------------------------------------------------- /src/components/DonationCard.js: -------------------------------------------------------------------------------- 1 | import { Button, Text, VStack } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | 6 | function DonationCard({ product, onPress }) { 7 | const colors = useColors(); 8 | 9 | const price = product?.localizedPrice || product?.oneTimePurchaseOfferDetails?.formattedPrice; 10 | 11 | if (!price) { 12 | return null; 13 | } 14 | 15 | return ( 16 | 25 | 26 | Donate {price} to PreCloud 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export default DonationCard; 35 | -------------------------------------------------------------------------------- /src/components/Confirm.js: -------------------------------------------------------------------------------- 1 | import { AlertDialog, Button } from 'native-base'; 2 | import React from 'react'; 3 | 4 | function Confirm({ isOpen, title, message, onClose, onConfirm, isDanger }) { 5 | return ( 6 | 7 | 8 | {!!title && Delete password} 9 | 10 | {message} 11 | 12 | 13 | 16 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | export default Confirm; 27 | -------------------------------------------------------------------------------- /src/components/Collapsible.js: -------------------------------------------------------------------------------- 1 | import { Box, Collapse, Heading, HStack, Pressable } from 'native-base'; 2 | import React, { useState } from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import Icon from './Icon'; 6 | 7 | function Collapsible({ title, children, defaultValue = true }) { 8 | const colors = useColors(); 9 | 10 | const [isOpen, setIsOpen] = useState(defaultValue); 11 | 12 | return ( 13 | 14 | setIsOpen(!isOpen)}> 15 | 16 | {title} 17 | 18 | 19 | 20 | 21 | 22 | {children} 23 | 24 | 25 | ); 26 | } 27 | 28 | export default Collapsible; 29 | -------------------------------------------------------------------------------- /src/components/PickFilesButton.js: -------------------------------------------------------------------------------- 1 | import { Actionsheet } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { pickFiles } from '../lib/files/actions'; 6 | import Icon from './Icon'; 7 | 8 | function PickFilesButton({ isDisabled, isLoading, onClose, onStart, onSelected }) { 9 | const colors = useColors(); 10 | 11 | async function handlePress() { 12 | onStart(true); 13 | const pickedFiles = await pickFiles(); 14 | 15 | await onSelected(pickedFiles); 16 | 17 | onStart(false); 18 | onClose(); 19 | } 20 | 21 | return ( 22 | } 26 | onPress={handlePress} 27 | > 28 | Pick files 29 | 30 | ); 31 | } 32 | 33 | export default PickFilesButton; 34 | -------------------------------------------------------------------------------- /src/components/TakePhotoButton.js: -------------------------------------------------------------------------------- 1 | import { Actionsheet } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { takePhoto } from '../lib/files/actions'; 6 | import Icon from './Icon'; 7 | 8 | function TakePhotoButton({ isDisabled, isLoading, onClose, onSelected }) { 9 | const colors = useColors(); 10 | 11 | async function handlePress() { 12 | try { 13 | const photo = await takePhoto(); 14 | if (photo) { 15 | onSelected(photo); 16 | } 17 | } catch (e) { 18 | console.log('Take photo failed', e); 19 | } 20 | 21 | onClose(); 22 | } 23 | 24 | return ( 25 | } 29 | onPress={handlePress} 30 | > 31 | Take photo 32 | 33 | ); 34 | } 35 | 36 | export default TakePhotoButton; 37 | -------------------------------------------------------------------------------- /src/components/DownloadButton.js: -------------------------------------------------------------------------------- 1 | import { IconButton } from 'native-base'; 2 | import React, { useState } from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { downloadFile } from '../lib/files/actions'; 6 | import { showToast } from '../lib/toast'; 7 | import Icon from './Icon'; 8 | 9 | function DownloadButton({ file }) { 10 | const colors = useColors(); 11 | 12 | const [isPending, setIsPending] = useState(false); 13 | 14 | async function handlePress() { 15 | setIsPending(true); 16 | 17 | const message = await downloadFile({ path: file.path, name: file.name }); 18 | if (message) { 19 | showToast(message); 20 | } 21 | 22 | setIsPending(false); 23 | } 24 | 25 | return ( 26 | } 28 | size="sm" 29 | variant="subtle" 30 | mr="2" 31 | isDisabled={isPending} 32 | onPress={handlePress} 33 | /> 34 | ); 35 | } 36 | 37 | export default DownloadButton; 38 | -------------------------------------------------------------------------------- /src/components/AddNoteButton.js: -------------------------------------------------------------------------------- 1 | import { Actionsheet } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { routeNames } from '../router/routes'; 6 | import { useStore } from '../store/store'; 7 | import Icon from './Icon'; 8 | 9 | function AddNoteButton({ folder, isDisabled, onClose, navigate }) { 10 | const colors = useColors(); 11 | const setNoteContent = useStore(state => state.setNoteContent); 12 | const setActiveNote = useStore(state => state.setActiveNote); 13 | 14 | async function handlePress() { 15 | setActiveNote(null); 16 | setNoteContent(''); 17 | onClose(); 18 | navigate(routeNames.noteDetails, { folder: { name: folder.name, path: folder.path } }); 19 | } 20 | 21 | return ( 22 | } 25 | onPress={handlePress} 26 | > 27 | Add note 28 | 29 | ); 30 | } 31 | 32 | export default AddNoteButton; 33 | -------------------------------------------------------------------------------- /src/components/ShareButton.js: -------------------------------------------------------------------------------- 1 | import { IconButton } from 'native-base'; 2 | import React, { useState } from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { shareFile } from '../lib/files/actions'; 6 | import { showToast } from '../lib/toast'; 7 | import Icon from './Icon'; 8 | 9 | function ShareButton({ file }) { 10 | const colors = useColors(); 11 | 12 | const [isPending, setIsPending] = useState(false); 13 | 14 | async function handlePress() { 15 | setIsPending(true); 16 | 17 | const success = await shareFile({ 18 | name: file.name, 19 | path: file.path, 20 | saveToFiles: false, 21 | }); 22 | if (success) { 23 | showToast('Shared!'); 24 | } 25 | 26 | setIsPending(false); 27 | } 28 | 29 | return ( 30 | } 32 | size="sm" 33 | variant="subtle" 34 | mr="2" 35 | isDisabled={isPending} 36 | onPress={handlePress} 37 | /> 38 | ); 39 | } 40 | 41 | export default ShareButton; 42 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainComponentsRegistry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | class MainComponentsRegistry 12 | : public facebook::jni::HybridClass { 13 | public: 14 | // Adapt it to the package you used for your Java class. 15 | constexpr static auto kJavaDescriptor = 16 | "Lcom/precloud/newarchitecture/components/MainComponentsRegistry;"; 17 | 18 | static void registerNatives(); 19 | 20 | MainComponentsRegistry(ComponentFactory *delegate); 21 | 22 | private: 23 | static std::shared_ptr 24 | sharedProviderRegistry(); 25 | 26 | static jni::local_ref initHybrid( 27 | jni::alias_ref, 28 | ComponentFactory *delegate); 29 | }; 30 | 31 | } // namespace react 32 | } // namespace facebook 33 | -------------------------------------------------------------------------------- /src/components/DeleteButton.js: -------------------------------------------------------------------------------- 1 | import { IconButton } from 'native-base'; 2 | import React, { useState } from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { deleteFile } from '../lib/files/actions'; 6 | import { showToast } from '../lib/toast'; 7 | import Icon from './Icon'; 8 | 9 | function DeleteButton({ file, onDelete }) { 10 | const colors = useColors(); 11 | 12 | const [isPending, setIsPending] = useState(false); 13 | 14 | async function handlePress() { 15 | setIsPending(true); 16 | 17 | try { 18 | await deleteFile(file.path); 19 | 20 | onDelete(); 21 | showToast('Deleted!'); 22 | } catch (error) { 23 | console.log('Delete file failed:', error); 24 | } 25 | 26 | setIsPending(false); 27 | } 28 | 29 | return ( 30 | } 32 | size="sm" 33 | variant="subtle" 34 | mr="2" 35 | isDisabled={isPending} 36 | onPress={handlePress} 37 | /> 38 | ); 39 | } 40 | 41 | export default DeleteButton; 42 | -------------------------------------------------------------------------------- /src/lib/array.js: -------------------------------------------------------------------------------- 1 | export async function asyncForEach(arr, callback) { 2 | if (!arr || !arr.length) { 3 | return; 4 | } 5 | 6 | for (let i = 0; i < arr.length; i += 1) { 7 | await callback(arr[i], i); 8 | } 9 | } 10 | 11 | export function randomiseArray(arr) { 12 | const newArr = arr.slice(); 13 | 14 | for (let i = newArr.length - 1; i > 0; i--) { 15 | let j = Math.floor(Math.random() * i); 16 | const temp = newArr[i]; 17 | newArr[i] = newArr[j]; 18 | newArr[j] = temp; 19 | } 20 | 21 | return newArr; 22 | } 23 | 24 | export const sortKeys = { mtime: 'mtime', name: 'name', random: 'random' }; 25 | 26 | export function sortWith(arr, sortKey) { 27 | if (!arr?.length) { 28 | return arr; 29 | } 30 | 31 | switch (sortKey) { 32 | case sortKeys.mtime: 33 | return arr.sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime()); 34 | case sortKeys.name: 35 | return arr.sort((a, b) => (a.name > b.name ? 1 : -1)); 36 | case sortKeys.random: 37 | return randomiseArray(arr); 38 | default: 39 | return arr; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/localstorage.js: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | 3 | export const LocalStorageKeys = { 4 | activePassword: 'activePassword', 5 | donateBannerCheckDate: 'donateBannerCheckDate', 6 | donateBannerDonateDate: 'donateBannerDonateDate', 7 | defaultFileFolder: 'defaultFileFolder', 8 | }; 9 | 10 | export const LocalStorage = { 11 | async set(key, value) { 12 | try { 13 | await AsyncStorage.setItem(key, JSON.stringify(value)); 14 | } catch (e) {} 15 | }, 16 | async get(key) { 17 | try { 18 | const value = await AsyncStorage.getItem(key); 19 | return JSON.parse(value); 20 | } catch (e) { 21 | return null; 22 | } 23 | }, 24 | async remove(key) { 25 | try { 26 | await AsyncStorage.removeItem(key); 27 | } catch (e) {} 28 | }, 29 | async getKeys() { 30 | try { 31 | return AsyncStorage.getAllKeys(); 32 | } catch (e) { 33 | return []; 34 | } 35 | }, 36 | async batchRemove(keys) { 37 | try { 38 | await AsyncStorage.multiRemove(keys); 39 | } catch (e) {} 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainApplicationModuleProvider.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationModuleProvider.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace facebook { 7 | namespace react { 8 | 9 | std::shared_ptr MainApplicationModuleProvider( 10 | const std::string &moduleName, 11 | const JavaTurboModule::InitParams ¶ms) { 12 | // Here you can provide your own module provider for TurboModules coming from 13 | // either your application or from external libraries. The approach to follow 14 | // is similar to the following (for a library called `samplelibrary`: 15 | // 16 | // auto module = samplelibrary_ModuleProvider(moduleName, params); 17 | // if (module != nullptr) { 18 | // return module; 19 | // } 20 | // return rncore_ModuleProvider(moduleName, params); 21 | 22 | // Module providers autolinked by RN CLI 23 | auto rncli_module = rncli_ModuleProvider(moduleName, params); 24 | if (rncli_module != nullptr) { 25 | return rncli_module; 26 | } 27 | return rncore_ModuleProvider(moduleName, params); 28 | } 29 | 30 | } // namespace react 31 | } // namespace facebook 32 | -------------------------------------------------------------------------------- /src/components/Caches.js: -------------------------------------------------------------------------------- 1 | import { Button, HStack, Text } from 'native-base'; 2 | import React, { useEffect, useState } from 'react'; 3 | 4 | import { cachePath, emptyCache } from '../lib/files/cache'; 5 | import { getFolderSize, getSizeText } from '../lib/files/helpers'; 6 | import { routeNames } from '../router/routes'; 7 | 8 | function Caches({ route }) { 9 | const [cacheSize, setCacheSize] = useState(''); 10 | 11 | useEffect(() => { 12 | if (route === routeNames.settings) { 13 | readFilesInCache(); 14 | } 15 | }, [route]); 16 | 17 | async function readFilesInCache() { 18 | const size = await getFolderSize(cachePath); 19 | setCacheSize(getSizeText(size)); 20 | } 21 | 22 | async function handleClearCache() { 23 | await emptyCache(); 24 | await readFilesInCache(); 25 | } 26 | 27 | return ( 28 | 29 | Cache: {cacheSize} 30 | 31 | {cacheSize !== '0KB' && ( 32 | 35 | )} 36 | 37 | ); 38 | } 39 | 40 | export default Caches; 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | ios/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | 35 | # node.js 36 | # 37 | node_modules/ 38 | npm-debug.log 39 | yarn-error.log 40 | 41 | # BUCK 42 | buck-out/ 43 | \.buckd/ 44 | *.keystore 45 | !debug.keystore 46 | 47 | # fastlane 48 | # 49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 50 | # screenshots whenever they are needed. 51 | # For more information about the recommended setup visit: 52 | # https://docs.fastlane.tools/best-practices/source-control/ 53 | 54 | */fastlane/report.xml 55 | */fastlane/Preview.html 56 | */fastlane/screenshots 57 | 58 | # Bundle artifact 59 | *.jsbundle 60 | 61 | # Ruby / CocoaPods 62 | /ios/Pods/ 63 | /vendor/bundle/ 64 | -------------------------------------------------------------------------------- /src/lib/files/zip.js: -------------------------------------------------------------------------------- 1 | import { unzip, zip } from 'react-native-zip-archive'; 2 | 3 | import { deleteFile } from './actions'; 4 | import { cachePath } from './cache'; 5 | import { getOriginalFileName, getParentPath } from './helpers'; 6 | 7 | export async function zipFolder(name, paths) { 8 | try { 9 | const zippedName = `${name}.zip`; 10 | const targetPath = `${cachePath}/${zippedName}`; 11 | await deleteFile(targetPath); 12 | await zip(paths, targetPath); 13 | return { name: zippedName, path: targetPath }; 14 | } catch (e) { 15 | console.log('zip folder failed', e); 16 | return null; 17 | } 18 | } 19 | 20 | export async function unzipFolder(name, path) { 21 | try { 22 | const originalFileName = getOriginalFileName(name); 23 | const parentPath = getParentPath(path); 24 | const targetPath = `${parentPath}/${originalFileName}`; 25 | await unzip(path, targetPath); 26 | return { 27 | name: originalFileName, 28 | path: targetPath, 29 | isDirectory: () => true, 30 | isFile: () => false, 31 | }; 32 | } catch (e) { 33 | console.log('unzip folder failed', e); 34 | return null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/OpenFileButton.js: -------------------------------------------------------------------------------- 1 | import { IconButton } from 'native-base'; 2 | import React, { useMemo } from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { viewableFileExtensions } from '../lib/files/constant'; 6 | import { viewFile } from '../lib/files/file'; 7 | import { extractFileNameAndExtension } from '../lib/files/helpers'; 8 | import Icon from './Icon'; 9 | 10 | function OpenFileButton({ file }) { 11 | const colors = useColors(); 12 | 13 | const canBeOpened = useMemo(() => { 14 | if (!file?.name) { 15 | return false; 16 | } 17 | 18 | const { extensionWithoutDot } = extractFileNameAndExtension(file.name); 19 | return viewableFileExtensions.includes(extensionWithoutDot); 20 | }, [file]); 21 | 22 | async function handlePress() { 23 | await viewFile(file.path) 24 | } 25 | 26 | if (!canBeOpened) { 27 | return null; 28 | } 29 | 30 | return ( 31 | } 33 | size="sm" 34 | variant="subtle" 35 | mr="2" 36 | onPress={handlePress} 37 | /> 38 | ); 39 | } 40 | 41 | export default OpenFileButton; 42 | -------------------------------------------------------------------------------- /src/lib/keychain.js: -------------------------------------------------------------------------------- 1 | import * as Keychain from 'react-native-keychain'; 2 | 3 | const passwordSeparator = 'YE7VJ6WBUGGV6D4RZRN36Q'; 4 | const labelPasswordSeparator = 'QR62XVUHJU7TBCNHTXF9TM'; 5 | const defaultId = '1663012537092'; 6 | 7 | export async function savePasswords(passwords) { 8 | const value = passwords 9 | .map( 10 | p => `${p.id}${labelPasswordSeparator}${p.label.trim()}${labelPasswordSeparator}${p.password}` 11 | ) 12 | .join(passwordSeparator); 13 | await Keychain.setGenericPassword('precloud', value); 14 | } 15 | 16 | export async function getPasswords() { 17 | try { 18 | const result = await Keychain.getGenericPassword(); 19 | if (!result?.password) { 20 | return []; 21 | } 22 | const password = result.password; 23 | return password.split(passwordSeparator).map(p => { 24 | const arr = p.split(labelPasswordSeparator); 25 | return arr.length === 3 26 | ? { id: arr[0], label: arr[1], password: arr[2] } 27 | : { id: defaultId, label: 'Primary password', password: arr[0] }; 28 | }); 29 | } catch (e) { 30 | console.log('read keychain failed', e); 31 | return []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/PickImagesButton.js: -------------------------------------------------------------------------------- 1 | import { Actionsheet } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { pickImages } from '../lib/files/actions'; 6 | import { hideToast, showToast } from '../lib/toast'; 7 | import Icon from './Icon'; 8 | 9 | function PickImagesButton({ isDisabled, isLoading, onClose, onStart, onSelected }) { 10 | const colors = useColors(); 11 | 12 | async function handlePress() { 13 | onStart(true); 14 | try { 15 | showToast('Copying files ...', 'info', 300); 16 | 17 | const images = await pickImages(); 18 | hideToast(); 19 | 20 | await onSelected(images); 21 | } catch (e) { 22 | hideToast(); 23 | console.log('Pick images failed', e); 24 | } 25 | onStart(false); 26 | onClose(); 27 | } 28 | 29 | return ( 30 | } 34 | onPress={handlePress} 35 | > 36 | Pick images 37 | 38 | ); 39 | } 40 | 41 | export default PickImagesButton; 42 | -------------------------------------------------------------------------------- /src/components/FolderItem.js: -------------------------------------------------------------------------------- 1 | import { Pressable, Text } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { routeNames } from '../router/routes'; 6 | import { useStore } from '../store/store'; 7 | 8 | function FolderItem({ folder, navigate }) { 9 | const colors = useColors(); 10 | const password = useStore(state => state.activePassword); 11 | const defaultFolder = useStore(state => state.defaultFolder); 12 | 13 | async function handleOpenFolder() { 14 | navigate(routeNames.folder, { path: folder.path }); 15 | } 16 | 17 | return ( 18 | { 27 | if (password) { 28 | handleOpenFolder(); 29 | } 30 | }} 31 | > 32 | 33 | {folder.name} 34 | 35 | {folder.name === defaultFolder && ( 36 | 37 | Default 38 | 39 | )} 40 | 41 | ); 42 | } 43 | 44 | export default FolderItem; 45 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | namespace facebook { 8 | namespace react { 9 | 10 | class MainApplicationTurboModuleManagerDelegate 11 | : public jni::HybridClass< 12 | MainApplicationTurboModuleManagerDelegate, 13 | TurboModuleManagerDelegate> { 14 | public: 15 | // Adapt it to the package you used for your Java class. 16 | static constexpr auto kJavaDescriptor = 17 | "Lcom/precloud/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;"; 18 | 19 | static jni::local_ref initHybrid(jni::alias_ref); 20 | 21 | static void registerNatives(); 22 | 23 | std::shared_ptr getTurboModule( 24 | const std::string &name, 25 | const std::shared_ptr &jsInvoker) override; 26 | std::shared_ptr getTurboModule( 27 | const std::string &name, 28 | const JavaTurboModule::InitParams ¶ms) override; 29 | 30 | /** 31 | * Test-only method. Allows user to verify whether a TurboModule can be 32 | * created by instances of this class. 33 | */ 34 | bool canCreateTurboModule(const std::string &name); 35 | }; 36 | 37 | } // namespace react 38 | } // namespace facebook 39 | -------------------------------------------------------------------------------- /src/lib/files/constant.js: -------------------------------------------------------------------------------- 1 | import FS from 'react-native-fs'; 2 | 3 | export const imageExtensions = ['gif', 'heic', 'jpg', 'jpeg', 'png', 'psd', 'webp']; 4 | export const videoExtensions = [ 5 | '3gp', 6 | 'avi', 7 | 'flv', 8 | 'm4v', 9 | 'mkv', 10 | 'mov', 11 | 'mp4', 12 | 'mpeg', 13 | 'mpg', 14 | 'ogv', 15 | 'webm', 16 | ]; 17 | const audioExtensions = ['mp3', 'wav', 'ogg', 'm4a', 'aac', 'flac', 'midi']; 18 | export const viewableFileExtensions = [ 19 | ...imageExtensions, 20 | ...videoExtensions, 21 | ...audioExtensions, 22 | 'css', 23 | 'csv', 24 | 'html', 25 | 'json', 26 | 'pdf', 27 | 'txt', 28 | ]; 29 | 30 | export const androidDownloadFolder = FS.DownloadDirectoryPath; 31 | export const documentPath = FS.DocumentDirectoryPath; 32 | export const precloudFolder = `${documentPath}/PRECLOUD`; 33 | export const notesFolderName = 'notes'; 34 | export const filesFolderName = 'files'; 35 | export const notesFolder = `${precloudFolder}/${notesFolderName}`; 36 | export const filesFolder = `${precloudFolder}/${filesFolderName}`; 37 | export const legacyNotesFolder = `${documentPath}/${notesFolderName}`; 38 | export const legacyFilesFolder = `${documentPath}/${filesFolderName}`; 39 | 40 | export const largeFileExtension = 'precloudlarge'; 41 | export const precloudExtension = 'precloud'; 42 | export const noteExtension = 'precloudnote'; 43 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/precloud/newarchitecture/components/MainComponentsRegistry.java: -------------------------------------------------------------------------------- 1 | package com.precloud.newarchitecture.components; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.proguard.annotations.DoNotStrip; 5 | import com.facebook.react.fabric.ComponentFactory; 6 | import com.facebook.soloader.SoLoader; 7 | 8 | /** 9 | * Class responsible to load the custom Fabric Components. This class has native methods and needs a 10 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 11 | * folder for you). 12 | * 13 | *

Please note that this class is used ONLY if you opt-in for the New Architecture (see the 14 | * `newArchEnabled` property). Is ignored otherwise. 15 | */ 16 | @DoNotStrip 17 | public class MainComponentsRegistry { 18 | static { 19 | SoLoader.loadLibrary("fabricjni"); 20 | } 21 | 22 | @DoNotStrip private final HybridData mHybridData; 23 | 24 | @DoNotStrip 25 | private native HybridData initHybrid(ComponentFactory componentFactory); 26 | 27 | @DoNotStrip 28 | private MainComponentsRegistry(ComponentFactory componentFactory) { 29 | mHybridData = initHybrid(componentFactory); 30 | } 31 | 32 | @DoNotStrip 33 | public static MainComponentsRegistry register(ComponentFactory componentFactory) { 34 | return new MainComponentsRegistry(componentFactory); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, '12.4' 5 | install! 'cocoapods', :deterministic_uuids => false 6 | 7 | target 'PreCloud' do 8 | config = use_native_modules! 9 | 10 | # Flags change depending on the env values. 11 | flags = get_default_flags() 12 | 13 | use_react_native!( 14 | :path => config[:reactNativePath], 15 | # Hermes is now enabled by default. Disable by setting this flag to false. 16 | # Upcoming versions of React Native may rely on get_default_flags(), but 17 | # we make it explicit here to aid in the React Native upgrade process. 18 | :hermes_enabled => true, 19 | :fabric_enabled => flags[:fabric_enabled], 20 | # An absolute path to your application root. 21 | :app_path => "#{Pod::Config.instance.installation_root}/.." 22 | ) 23 | 24 | target 'PreCloudTests' do 25 | inherit! :complete 26 | # Pods for testing 27 | end 28 | 29 | post_install do |installer| 30 | react_native_post_install( 31 | installer, 32 | # Set `mac_catalyst_enabled` to `true` in order to apply patches 33 | # necessary for Mac Catalyst builds 34 | :mac_catalyst_enabled => false 35 | ) 36 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 37 | end 38 | end 39 | 40 | -------------------------------------------------------------------------------- /src/components/RootFolders.js: -------------------------------------------------------------------------------- 1 | import { Button, VStack } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { routeNames } from '../router/routes'; 6 | import { useStore } from '../store/store'; 7 | import FoldersEmptyState from './FoldersEmptyState'; 8 | import FoldersList from './FoldersList'; 9 | import Icon from './Icon'; 10 | 11 | function RootFolders({ navigation, rootFolders }) { 12 | const colors = useColors(); 13 | const password = useStore(state => state.activePassword); 14 | 15 | function handleAddFolder() { 16 | navigation.navigate(routeNames.folderForm, { folder: null }); 17 | } 18 | 19 | function renderFolders() { 20 | if (!rootFolders.length) { 21 | return ; 22 | } 23 | 24 | return ( 25 | 26 | 35 | 36 | 37 | ); 38 | } 39 | 40 | return {renderFolders()}; 41 | } 42 | 43 | export default RootFolders; 44 | -------------------------------------------------------------------------------- /src/components/PasswordAlert.js: -------------------------------------------------------------------------------- 1 | import { Alert, Box, Button, Text, VStack } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import { routeNames } from '../router/routes'; 5 | import { useStore } from '../store/store'; 6 | 7 | function PasswordAlert({ navigate }) { 8 | const isLoadingPasswords = useStore(state => state.isLoadingPasswords); 9 | const activePasswordLabel = useStore(state => state.activePasswordLabel); 10 | 11 | if (isLoadingPasswords) { 12 | return null; 13 | } 14 | 15 | if (!activePasswordLabel) { 16 | return ( 17 | 18 | 19 | 20 | Setup your password first 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | return ( 29 | 30 | 31 | Active password: {activePasswordLabel} {' '} 32 | { 35 | navigate(routeNames.passwords); 36 | }} 37 | > 38 | Change 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default PasswordAlert; 46 | -------------------------------------------------------------------------------- /src/components/MoveToButton.js: -------------------------------------------------------------------------------- 1 | import { IconButton } from 'native-base'; 2 | import React, { useState } from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { moveFile } from '../lib/files/actions'; 6 | import { showToast } from '../lib/toast'; 7 | import FolderPicker from './FolderPicker'; 8 | import Icon from './Icon'; 9 | 10 | function MoveToButton({ file, folder, onMove, navigate }) { 11 | const colors = useColors(); 12 | 13 | const [isPending, setIsPending] = useState(false); 14 | const [showFolderPicker, setShowFolderPicker] = useState(false); 15 | 16 | async function handlePress(newFolder) { 17 | setIsPending(true); 18 | await moveFile(file.path, `${newFolder.path}/${file.name}`); 19 | setShowFolderPicker(false); 20 | onMove(); 21 | showToast('Moved!'); 22 | 23 | setIsPending(false); 24 | } 25 | 26 | return ( 27 | <> 28 | setShowFolderPicker(false)} 31 | onSave={handlePress} 32 | navigate={navigate} 33 | currentFolder={folder} 34 | /> 35 | } 37 | size="sm" 38 | variant="subtle" 39 | mr="2" 40 | isDisabled={isPending} 41 | onPress={() => { 42 | setShowFolderPicker(true); 43 | }} 44 | /> 45 | 46 | ); 47 | } 48 | 49 | export default MoveToButton; 50 | -------------------------------------------------------------------------------- /ios/PreCloud/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo-20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "logo-20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "logo-29@2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "logo-29@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "logo-40@2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "logo-40@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "logo-60@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "logo-60@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "logo-1024.png", 53 | "idiom" : "ios-marketing", 54 | "scale" : "1x", 55 | "size" : "1024x1024" 56 | } 57 | ], 58 | "info" : { 59 | "author" : "xcode", 60 | "version" : 1 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/views/RenameFileForm.js: -------------------------------------------------------------------------------- 1 | import { Button, FormControl, Input, Text } from 'native-base'; 2 | import React, { useState } from 'react'; 3 | 4 | import AppBar from '../components/AppBar'; 5 | import ContentWrapper from '../components/ContentWrapper'; 6 | import ScreenWrapper from '../components/ScreenWrapper'; 7 | import { renameFile } from '../lib/files/actions'; 8 | import { extractFileNameAndExtension } from '../lib/files/helpers'; 9 | 10 | function RenameFileForm({ 11 | navigation, 12 | route: { 13 | params: { file }, 14 | }, 15 | }) { 16 | const { fileName, extension } = extractFileNameAndExtension(file?.name); 17 | const [innerFileName, setInnerFileName] = useState(''); 18 | 19 | async function handleSave() { 20 | await renameFile(file, innerFileName); 21 | navigation.goBack(); 22 | } 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | Current name: 31 | {fileName} 32 | New name: 33 | {extension}} 37 | /> 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default RenameFileForm; 46 | -------------------------------------------------------------------------------- /android/app/_BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.precloud", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.precloud", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /src/components/FolderPickerItem.js: -------------------------------------------------------------------------------- 1 | import { Box, HStack, Text, VStack } from 'native-base'; 2 | import React, { useEffect, useState } from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { readFiles } from '../lib/files/file'; 6 | import Icon from './Icon'; 7 | 8 | function FolderPickerItem({ folder, selectedPath, onPress }) { 9 | const colors = useColors(); 10 | 11 | const [subfolders, setSubfolders] = useState([]); 12 | 13 | useEffect(() => { 14 | readFiles(folder.path).then(result => { 15 | setSubfolders(result.folders); 16 | }); 17 | }, [folder]); 18 | 19 | return ( 20 | 21 | 22 | onPress(folder)} 26 | flex="1" 27 | numberOfLines={1} 28 | > 29 | {folder.name} 30 | 31 | {subfolders.length > 0 && ( 32 | 33 | )} 34 | 35 | {subfolders.length > 0 && ( 36 | 37 | {subfolders.map(sf => ( 38 | 44 | ))} 45 | 46 | )} 47 | 48 | ); 49 | } 50 | 51 | export default FolderPickerItem; 52 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | // react-native-fast-openpgp 2 | import 'fast-text-encoding'; 3 | 4 | import { NavigationContainer } from '@react-navigation/native'; 5 | import { NativeBaseProvider } from 'native-base'; 6 | import React, { useEffect } from 'react'; 7 | import { withIAPContext } from 'react-native-iap'; 8 | import SplashScreen from 'react-native-splash-screen'; 9 | import Toast from 'react-native-toast-message'; 10 | 11 | import DonateBanner from './components/DonateBanner'; 12 | import useInAppPurchase from './hooks/useInAppPurchase'; 13 | import { migrateNotesToFolders } from './lib/files/note'; 14 | import { getTheme } from './lib/style'; 15 | import { navigationRef } from './router/navigationRef'; 16 | import Router from './router/Router'; 17 | import { useStore } from './store/store'; 18 | 19 | function App() { 20 | const theme = getTheme(); 21 | useInAppPurchase(); 22 | 23 | const getPasswords = useStore(state => state.getPasswords); 24 | const loadRootFolders = useStore(state => state.loadRootFolders); 25 | 26 | useEffect(() => { 27 | SplashScreen.hide(); 28 | getPasswords(); 29 | migrateNotesToFolders().then(() => { 30 | loadRootFolders(); 31 | }); 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, []); 34 | 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | export default withIAPContext(App); 48 | -------------------------------------------------------------------------------- /src/lib/files/note.js: -------------------------------------------------------------------------------- 1 | import FS from 'react-native-fs'; 2 | 3 | import { asyncForEach } from '../array'; 4 | import { deleteFile, moveFile, readFolder } from './actions'; 5 | import { filesFolder, legacyNotesFolder, noteExtension, notesFolder } from './constant'; 6 | import { extractFileNameAndExtension } from './helpers'; 7 | 8 | export function getNoteTitle(note) { 9 | return extractFileNameAndExtension(note?.name)?.fileName; 10 | } 11 | 12 | export async function migrateNotesToFolders() { 13 | let notesPath = null; 14 | 15 | const notesExists = await FS.exists(notesFolder); 16 | const legacyExists = await FS.exists(legacyNotesFolder); 17 | if (notesExists) { 18 | notesPath = notesFolder; 19 | } else if (legacyExists) { 20 | notesPath = legacyNotesFolder; 21 | } 22 | 23 | if (notesPath) { 24 | const result = await readFolder(notesPath); 25 | const notebooks = result.filter(n => n.isDirectory()); 26 | await asyncForEach(notebooks, async notebook => { 27 | const newPath = `${filesFolder}/${notebook.name}`; 28 | const notebookExists = await FS.exists(newPath); 29 | if (notebookExists) { 30 | const notes = await readFolder(notebook.path); 31 | const filtered = notes.filter(n => n.isFile() && n.name.endsWith(noteExtension)); 32 | await asyncForEach(filtered, async note => { 33 | await moveFile(note.path, `${newPath}/${note.name}`); 34 | }); 35 | await deleteFile(notebook.path); 36 | } else { 37 | await moveFile(notebook.path, newPath); 38 | } 39 | }); 40 | 41 | await deleteFile(notesPath); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationTurboModuleManagerDelegate.h" 2 | #include "MainApplicationModuleProvider.h" 3 | 4 | namespace facebook { 5 | namespace react { 6 | 7 | jni::local_ref 8 | MainApplicationTurboModuleManagerDelegate::initHybrid( 9 | jni::alias_ref) { 10 | return makeCxxInstance(); 11 | } 12 | 13 | void MainApplicationTurboModuleManagerDelegate::registerNatives() { 14 | registerHybrid({ 15 | makeNativeMethod( 16 | "initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid), 17 | makeNativeMethod( 18 | "canCreateTurboModule", 19 | MainApplicationTurboModuleManagerDelegate::canCreateTurboModule), 20 | }); 21 | } 22 | 23 | std::shared_ptr 24 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 25 | const std::string &name, 26 | const std::shared_ptr &jsInvoker) { 27 | // Not implemented yet: provide pure-C++ NativeModules here. 28 | return nullptr; 29 | } 30 | 31 | std::shared_ptr 32 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 33 | const std::string &name, 34 | const JavaTurboModule::InitParams ¶ms) { 35 | return MainApplicationModuleProvider(name, params); 36 | } 37 | 38 | bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule( 39 | const std::string &name) { 40 | return getTurboModule(name, nullptr) != nullptr || 41 | getTurboModule(name, {.moduleName = name}) != nullptr; 42 | } 43 | 44 | } // namespace react 45 | } // namespace facebook 46 | -------------------------------------------------------------------------------- /src/components/NoteItem.js: -------------------------------------------------------------------------------- 1 | import { HStack, IconButton, Pressable, Text } from 'native-base'; 2 | import React, { useMemo, useState } from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { getNoteTitle } from '../lib/files/note'; 6 | import { useStore } from '../store/store'; 7 | import Icon from './Icon'; 8 | import NoteItemActions from './NoteItemActions'; 9 | 10 | function NoteItem({ folder, note, onOpen, onMoved, navigation }) { 11 | const colors = useColors(); 12 | const password = useStore(state => state.activePassword); 13 | const fileName = useMemo(() => getNoteTitle(note), [note]); 14 | 15 | const [showActions, setShowActions] = useState(false); 16 | 17 | function handleOpen() { 18 | if (password) { 19 | onOpen(note); 20 | setShowActions(false); 21 | } 22 | } 23 | 24 | return ( 25 | <> 26 | 27 | 28 | 29 | {fileName} 30 | 31 | } 33 | onPress={() => setShowActions(true)} 34 | /> 35 | 36 | 37 | setShowActions(false)} 42 | isNoteDetails={false} 43 | onView={handleOpen} 44 | navigation={navigation} 45 | onMoved={onMoved} 46 | /> 47 | 48 | ); 49 | } 50 | 51 | export default NoteItem; 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PreCloud - Encrypt before upload 2 | 3 | Encrypt your texts and files before uploading them to cloud. 4 | 5 | **Open source, no tracking and free forever.** 6 | 7 | **PreCloud has no server, everything happens on your device**: Your master password, the encryption process, the encrypted texts and files. 8 | 9 | You can safely upload the encrypted texts or files to wherever you want, making any cloud provider an encrypted storage. 10 | 11 | **Everything is encrypted by the very well established algorithm PGP** (https://en.wikipedia.org/wiki/Pretty_Good_Privacy). The algorithm is used by [Proton](https://proton.me/), [Mailvelope](https://mailvelope.com/), [Encrypt.to](https://encrypt.to/) and many others. 12 | 13 | **NOTE**: Please keep your master password in a safe place, like a password manager. If you lose it, you can't decrypt your texts and files! 14 | 15 | --- 16 | 17 | ## Get the App: 18 | 19 | 20 | 21 | 22 | 23 | --- 24 | 25 | ## What's coming next: 26 | - [x] Release Android version; 27 | - [x] Release iOS version; 28 | - [ ] Buy the precloud.me domain, if there are 1000 app downloads; 29 | - [ ] Create web version, if there are 1000 app downloads; 30 | - [x] Encrypt large files. Now you can encrypt files that are up to 1GB; 31 | 32 | --- 33 | 34 | Write to me, I reply to all emails: peng@duck.com 35 | 36 | --- 37 | 38 | ## Donate 39 | 40 | ❤️ Consider donating to this free and open source app: [Ko-Fi](https://ko-fi.com/penghuili) or [PayPal](https://paypal.me/penghuili/) 41 | -------------------------------------------------------------------------------- /src/components/AppBar.js: -------------------------------------------------------------------------------- 1 | import { Avatar, HStack, IconButton, Text } from 'native-base'; 2 | import React from 'react'; 3 | import { useWindowDimensions } from 'react-native'; 4 | 5 | import logo from '../assets/logo.png'; 6 | import useColors from '../hooks/useColors'; 7 | import { navigationRef } from '../router/navigationRef'; 8 | import { routeNames } from '../router/routes'; 9 | import Icon from './Icon'; 10 | 11 | function AppBar({ title, hasBack, rightIconName, onRightIconPress }) { 12 | const colors = useColors(); 13 | const { width } = useWindowDimensions(); 14 | 15 | function handleGoBack() { 16 | if (navigationRef.canGoBack()) { 17 | navigationRef.goBack(); 18 | } else { 19 | navigationRef.navigate('BottomTab', { screen: routeNames.folders }); 20 | } 21 | } 22 | 23 | return ( 24 | 32 | 33 | {hasBack ? ( 34 | } 37 | onPress={handleGoBack} 38 | /> 39 | ) : ( 40 | 41 | )} 42 | 43 | 50 | {title} 51 | 52 | 53 | {!!rightIconName && ( 54 | } 57 | onPress={onRightIconPress} 58 | /> 59 | )} 60 | 61 | ); 62 | } 63 | 64 | export default AppBar; 65 | -------------------------------------------------------------------------------- /scripts/bump-build-numbers.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { writeFileSync, readFileSync } = require('fs'); 3 | const { execSync } = require('child_process'); 4 | 5 | // bump android build number 6 | const properties = require('properties-parser'); 7 | const gradlePath = join(__dirname, '../android/gradle.properties'); 8 | const gradlePropsEditor = properties.createEditor(gradlePath); 9 | const versionCode = gradlePropsEditor.get('releaseVersionCode'); 10 | const newAndroidNumber = (+versionCode + 1).toString(); 11 | gradlePropsEditor.set('releaseVersionCode', newAndroidNumber); 12 | gradlePropsEditor.save(gradlePath); 13 | console.log(`Android build number ${newAndroidNumber} is written to ${gradlePath}`); 14 | 15 | // bump ios build number 16 | const plist = require('simple-plist'); 17 | const plistPath = join(__dirname, '../ios/PreCloud/Info.plist'); 18 | const infoPlist = plist.parse(String(readFileSync(plistPath))); 19 | const newIOSNumber = ((+infoPlist.CFBundleVersion || 0) + 1).toString(); 20 | const content = plist.stringify({ 21 | ...infoPlist, 22 | CFBundleVersion: newIOSNumber, 23 | }); 24 | const contentTabbed = content.replace(/^\s+/gm, spaces => { 25 | const indent = spaces.length / 2; 26 | return new Array(indent).join('\t'); 27 | }); 28 | writeFileSync(plistPath, `${contentTabbed}\n`); 29 | console.log(`iOS build number ${newIOSNumber} is written to ${plistPath}`); 30 | 31 | // bump build date 32 | const appSettings = require('../src/lib/app-settings.json'); 33 | const appSettingsPath = join(__dirname, '../src/lib/app-settings.json'); 34 | const appSettingsContent = JSON.stringify({ ...appSettings, buildDate: Date.now() }, null, 2); 35 | writeFileSync(appSettingsPath, `${appSettingsContent}\n`); 36 | console.log(`Build date written to ${appSettingsContent}`); 37 | 38 | execSync(`git cc -am "build-number: Android ${newAndroidNumber}, iOS ${newIOSNumber}"`, { 39 | stdio: 'inherit', 40 | }); 41 | -------------------------------------------------------------------------------- /src/router/Router.js: -------------------------------------------------------------------------------- 1 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 2 | import React from 'react'; 3 | 4 | import Backup from '../views/Backup'; 5 | import Donation from '../views/Donation'; 6 | import Folder from '../views/Folder'; 7 | import FolderForm from '../views/FolderForm'; 8 | import NoteDetails from '../views/NoteDetails'; 9 | import PasswordForm from '../views/PasswordForm'; 10 | import PasswordGenerator from '../views/PasswordGenerator'; 11 | import Passwords from '../views/Passwords'; 12 | import PlainText from '../views/PlainText'; 13 | import RenameFileForm from '../views/RenameFileForm'; 14 | import BottomTab from './BottomTab'; 15 | import { routeNames } from './routes'; 16 | 17 | const NavStack = createNativeStackNavigator(); 18 | 19 | function Router() { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export default Router; 41 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.125.0 29 | 30 | # Use this property to specify which architecture you want to build. 31 | # You can also override it from the CLI using 32 | # ./gradlew -PreactNativeArchitectures=x86_64 33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 34 | 35 | # Use this property to enable support to the new architecture. 36 | # This will allow you to use TurboModules and the Fabric render in 37 | # your application. You should enable this flag either if you want 38 | # to write custom TurboModules/Fabric components OR use libraries that 39 | # are providing them. 40 | newArchEnabled=false 41 | 42 | releaseVersionCode=184 43 | 44 | releaseVersionName=1.15.0 45 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/precloud/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java: -------------------------------------------------------------------------------- 1 | package com.precloud.newarchitecture.modules; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.soloader.SoLoader; 8 | import java.util.List; 9 | 10 | /** 11 | * Class responsible to load the TurboModules. This class has native methods and needs a 12 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 13 | * folder for you). 14 | * 15 | *

Please note that this class is used ONLY if you opt-in for the New Architecture (see the 16 | * `newArchEnabled` property). Is ignored otherwise. 17 | */ 18 | public class MainApplicationTurboModuleManagerDelegate 19 | extends ReactPackageTurboModuleManagerDelegate { 20 | 21 | private static volatile boolean sIsSoLibraryLoaded; 22 | 23 | protected MainApplicationTurboModuleManagerDelegate( 24 | ReactApplicationContext reactApplicationContext, List packages) { 25 | super(reactApplicationContext, packages); 26 | } 27 | 28 | protected native HybridData initHybrid(); 29 | 30 | native boolean canCreateTurboModule(String moduleName); 31 | 32 | public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder { 33 | protected MainApplicationTurboModuleManagerDelegate build( 34 | ReactApplicationContext context, List packages) { 35 | return new MainApplicationTurboModuleManagerDelegate(context, packages); 36 | } 37 | } 38 | 39 | @Override 40 | protected synchronized void maybeLoadOtherSoLibraries() { 41 | if (!sIsSoLibraryLoaded) { 42 | // If you change the name of your application .so file in the Android.mk file, 43 | // make sure you update the name here as well. 44 | SoLoader.loadLibrary("precloud_appmodules"); 45 | sIsSoLibraryLoaded = true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "31.0.0" 6 | compileSdkVersion = 31 7 | targetSdkVersion = 31 8 | androidXAnnotation = "1.1.0" 9 | androidXBrowser = "1.0.0" 10 | minSdkVersion = 24 11 | kotlinVersion = "1.6.0" 12 | 13 | if (System.properties['os.arch'] == "aarch64") { 14 | // For M1 Users we need to use the NDK 24 which added support for aarch64 15 | ndkVersion = "24.0.8215888" 16 | } else { 17 | // Otherwise we default to the side-by-side NDK version from AGP. 18 | ndkVersion = "21.4.7075529" 19 | } 20 | } 21 | repositories { 22 | google() 23 | mavenCentral() 24 | } 25 | dependencies { 26 | classpath("com.android.tools.build:gradle:7.2.1") 27 | classpath("com.facebook.react:react-native-gradle-plugin") 28 | classpath("de.undercouch:gradle-download-task:5.0.1") 29 | // NOTE: Do not place your application dependencies here; they belong 30 | // in the individual module build.gradle files 31 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 32 | } 33 | } 34 | 35 | allprojects { 36 | repositories { 37 | maven { 38 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 39 | url("$rootDir/../node_modules/react-native/android") 40 | } 41 | maven { 42 | // Android JSC is installed from npm 43 | url("$rootDir/../node_modules/jsc-android/dist") 44 | } 45 | mavenCentral { 46 | // We don't want to fetch react-native from Maven Central as there are 47 | // older versions over there. 48 | content { 49 | excludeGroup "com.facebook.react" 50 | } 51 | } 52 | google() 53 | maven { url 'https://www.jitpack.io' } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/lib/openpgp/decryptLargeFile.js: -------------------------------------------------------------------------------- 1 | import { appendFile, readDir, readFile } from 'react-native-fs'; 2 | 3 | import { asyncForEach } from '../array'; 4 | import { deleteFile } from '../files/actions'; 5 | import { cachePath } from '../files/cache'; 6 | import { precloudExtension } from '../files/constant'; 7 | import { getOriginalFileName, isLargeFile, statFile } from '../files/helpers'; 8 | import { openpgpStatus } from './constant'; 9 | import { decryptFile } from './helpers'; 10 | 11 | export async function decryptLargeFile(file, password) { 12 | try { 13 | const { path } = file; 14 | if (!isLargeFile(file)) { 15 | console.log('it should be a folder for large files'); 16 | throw new Error(openpgpStatus.notLargeFile); 17 | } 18 | 19 | const files = await readDir(path); 20 | const sorted = files.sort((a, b) => (a.name > b.name ? 1 : -1)); 21 | const originalFileName = getOriginalFileName(file.name); 22 | const decryptedPath = `${cachePath}/${originalFileName}`; 23 | const tempPath = `${cachePath}/large-file-chunk.${precloudExtension}`; 24 | await deleteFile(decryptedPath); 25 | await deleteFile(tempPath); 26 | 27 | await asyncForEach(sorted, async (encryptedChunk, index) => { 28 | if (index === 0) { 29 | const success = await decryptFile(encryptedChunk.path, decryptedPath, password); 30 | if (!success) { 31 | throw new Error(openpgpStatus.error); 32 | } 33 | } else { 34 | const success = await decryptFile(encryptedChunk.path, tempPath, password); 35 | if (!success) { 36 | throw new Error(openpgpStatus.error); 37 | } else { 38 | const content = await readFile(tempPath, 'base64'); 39 | await appendFile(decryptedPath, content, 'base64'); 40 | } 41 | await deleteFile(tempPath); 42 | } 43 | }); 44 | 45 | const { size: decryptedSize } = await statFile(decryptedPath); 46 | return { name: originalFileName, path: decryptedPath, size: decryptedSize }; 47 | } catch (e) { 48 | console.log('decrypt large file failed', e); 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/hooks/useInAppPurchase.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { 3 | endConnection, 4 | finishTransaction, 5 | flushFailedPurchasesCachedAsPendingAndroid, 6 | initConnection, 7 | purchaseErrorListener, 8 | purchaseUpdatedListener, 9 | } from 'react-native-iap'; 10 | 11 | import { isAndroid } from '../lib/device'; 12 | import { showToast } from '../lib/toast'; 13 | 14 | let purchaseUpdateSubscription = null; 15 | let purchaseErrorSubscription = null; 16 | 17 | async function flushAndroid() { 18 | if (isAndroid()) { 19 | return flushFailedPurchasesCachedAsPendingAndroid().catch(e => { 20 | console.log('flushFailedPurchasesCachedAsPendingAndroid failed', e); 21 | }); 22 | } 23 | 24 | return Promise.resolve(); 25 | } 26 | 27 | function useInAppPurchase() { 28 | useEffect(() => { 29 | initConnection() 30 | .then(flushAndroid) 31 | .then(() => { 32 | purchaseUpdateSubscription = purchaseUpdatedListener(async purchase => { 33 | console.log('purchaseUpdatedListener', purchase); 34 | const receipt = purchase.transactionReceipt; 35 | if (receipt) { 36 | await finishTransaction({ purchase, isConsumable: true }); 37 | console.log('purchase succeeded', receipt); 38 | showToast('Thank you very much for your support!') 39 | } else { 40 | console.log('purchase without receipt', purchase); 41 | } 42 | }); 43 | 44 | purchaseErrorSubscription = purchaseErrorListener(error => { 45 | console.warn('purchaseErrorListener', error); 46 | }); 47 | }) 48 | .catch(e => { 49 | console.log('init purchase failed', e); 50 | }); 51 | 52 | return () => { 53 | if (purchaseUpdateSubscription) { 54 | purchaseUpdateSubscription.remove(); 55 | purchaseUpdateSubscription = null; 56 | } 57 | 58 | if (purchaseErrorSubscription) { 59 | purchaseErrorSubscription.remove(); 60 | purchaseErrorSubscription = null; 61 | } 62 | 63 | endConnection(); 64 | }; 65 | }, []); 66 | } 67 | 68 | export default useInAppPurchase; 69 | -------------------------------------------------------------------------------- /src/lib/openpgp/helpers.js: -------------------------------------------------------------------------------- 1 | import OpenPGP from 'react-native-fast-openpgp'; 2 | 3 | export async function encryptFile(inputPath, outputPath, password) { 4 | try { 5 | await OpenPGP.encryptSymmetricFile(inputPath, outputPath, password); 6 | return true; 7 | } catch (e) { 8 | console.log('encrypt file failed', e); 9 | return false; 10 | } 11 | } 12 | 13 | export async function decryptFile(inputPath, outputPath, password) { 14 | try { 15 | await OpenPGP.decryptSymmetricFile(inputPath, outputPath, password); 16 | return true; 17 | } catch (e) { 18 | console.log('decrypt file failed', e); 19 | return false; 20 | } 21 | } 22 | 23 | const messageStart = `-----BEGIN PGP MESSAGE----- 24 | Version: openpgp-mobile`; 25 | const messageEnd = '----END PGP MESSAGE----'; 26 | 27 | function extractTextContent(encryptedText) { 28 | const startIndex = 53; 29 | const endIndex = encryptedText.indexOf(messageEnd) - 2; 30 | return `PreCloud:${encryptedText.slice(startIndex, endIndex)}`; 31 | } 32 | 33 | function removePreCloudPrefix(input) { 34 | if (!input) { 35 | return ''; 36 | } 37 | 38 | const splitted = input.split(':'); 39 | if (splitted.length > 1) { 40 | return splitted[1]; 41 | } 42 | 43 | return splitted[0]; 44 | } 45 | 46 | function fillText(extractedText) { 47 | const withoutPrefix = removePreCloudPrefix(extractedText); 48 | 49 | return `${messageStart} 50 | 51 | ${withoutPrefix} 52 | ${messageEnd}`; 53 | } 54 | 55 | // TODO: wait that this lib has unarmor and armor 56 | export async function encryptText(text, password) { 57 | try { 58 | const encryptedText = await OpenPGP.encryptSymmetric(text, password); 59 | return extractTextContent(encryptedText); 60 | } catch (e) { 61 | console.log('encrypt text failed', e); 62 | return null; 63 | } 64 | } 65 | 66 | export async function decryptText(encryptedText, password) { 67 | try { 68 | const fullText = fillText(encryptedText); 69 | const text = await OpenPGP.decryptSymmetric(fullText, password); 70 | return text; 71 | } catch (e) { 72 | console.log('decrypt text failed', e); 73 | return null; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/PickNotesButton.js: -------------------------------------------------------------------------------- 1 | import { Actionsheet, Text } from 'native-base'; 2 | import React, { useState } from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { asyncForEach } from '../lib/array'; 6 | import { moveFile, pickFiles } from '../lib/files/actions'; 7 | import { noteExtension } from '../lib/files/constant'; 8 | import { showToast } from '../lib/toast'; 9 | import Confirm from './Confirm'; 10 | import Icon from './Icon'; 11 | 12 | function PickNotesButton({ folder, onStart, isDisabled, isLoading, onClose, onSelected }) { 13 | const colors = useColors(); 14 | 15 | const [showConfirm, setShowConfirm] = useState(false); 16 | 17 | async function handlePress() { 18 | onStart(true); 19 | const picked = await pickFiles(); 20 | const filtered = picked.filter(f => f.name.endsWith(noteExtension)); 21 | await asyncForEach(filtered, async file => { 22 | await moveFile(file.path, `${folder.path}/${file.name}`); 23 | }); 24 | 25 | await onSelected(filtered); 26 | 27 | if (filtered.length) { 28 | showToast(`Selected ${filtered.length} ${filtered.length === 1 ? 'note' : 'notes'}.`); 29 | } else { 30 | showToast('No notes selected.'); 31 | } 32 | 33 | onStart(false); 34 | } 35 | 36 | return ( 37 | <> 38 | } 42 | onPress={() => { 43 | setShowConfirm(true); 44 | onClose(); 45 | }} 46 | > 47 | Pick notes 48 | 49 | 50 | 54 | Only select files ending with .{noteExtension} 55 | 56 | } 57 | onClose={() => { 58 | setShowConfirm(false); 59 | }} 60 | onConfirm={async () => { 61 | setShowConfirm(false); 62 | handlePress(); 63 | }} 64 | /> 65 | 66 | ); 67 | } 68 | 69 | export default PickNotesButton; 70 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/precloud/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.precloud; 2 | 3 | import com.facebook.react.ReactActivity; 4 | import com.facebook.react.ReactActivityDelegate; 5 | import com.facebook.react.ReactRootView; 6 | import android.os.Bundle; 7 | // react-native-splash-screen 8 | import org.devio.rn.splashscreen.SplashScreen; 9 | 10 | public class MainActivity extends ReactActivity { 11 | 12 | /** 13 | * Returns the name of the main component registered from JavaScript. This is used to schedule 14 | * rendering of the component. 15 | */ 16 | @Override 17 | protected String getMainComponentName() { 18 | return "PreCloud"; 19 | } 20 | 21 | // react-native-splash-screen and react-navigation 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | SplashScreen.show(this); 25 | super.onCreate(savedInstanceState); 26 | } 27 | 28 | /** 29 | * Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and 30 | * you can specify the renderer you wish to use - the new renderer (Fabric) or the old renderer 31 | */ 32 | @Override 33 | protected ReactActivityDelegate createReactActivityDelegate() { 34 | return new MainActivityDelegate(this, getMainComponentName()); 35 | } 36 | 37 | public static class MainActivityDelegate extends ReactActivityDelegate { 38 | public MainActivityDelegate(ReactActivity activity, String mainComponentName) { 39 | super(activity, mainComponentName); 40 | } 41 | 42 | @Override 43 | protected ReactRootView createRootView() { 44 | ReactRootView reactRootView = new ReactRootView(getContext()); 45 | // If you opted-in for the New Architecture, we enable the Fabric Renderer. 46 | reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED); 47 | return reactRootView; 48 | } 49 | 50 | @Override 51 | protected boolean isConcurrentRootEnabled() { 52 | // If you opted-in for the New Architecture, we enable Concurrent Root (i.e. React 18). 53 | // More on this on https://reactjs.org/blog/2022/03/29/react-v18.html 54 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/FolderNotes.js: -------------------------------------------------------------------------------- 1 | import { Text, VStack } from 'native-base'; 2 | import React from 'react'; 3 | import FS from 'react-native-fs'; 4 | 5 | import NoteItem from '../components/NoteItem'; 6 | import { deleteFile } from '../lib/files/actions'; 7 | import { cachePath } from '../lib/files/cache'; 8 | import { decryptFile } from '../lib/openpgp/helpers'; 9 | import { showToast } from '../lib/toast'; 10 | import { routeNames } from '../router/routes'; 11 | import { useStore } from '../store/store'; 12 | import Collapsible from './Collapsible'; 13 | 14 | function FolderNotes({ folder, notes, onMoved, navigation }) { 15 | const password = useStore(state => state.activePassword); 16 | const setNoteContent = useStore(state => state.setNoteContent); 17 | const setActiveNote = useStore(state => state.setActiveNote); 18 | 19 | async function handleOpenNote(note) { 20 | setActiveNote(note); 21 | const outputPath = `${cachePath}/${note.fileName}.txt`; 22 | const success = await decryptFile(note.path, outputPath, password); 23 | 24 | if (success) { 25 | const decryptedNote = await FS.readFile(outputPath, 'utf8'); 26 | await deleteFile(outputPath); 27 | setNoteContent(decryptedNote || ''); 28 | showToast('Note is decrypted.'); 29 | 30 | navigation.navigate(routeNames.noteDetails, { 31 | folder: { name: folder.name, path: folder.path }, 32 | }); 33 | } else { 34 | showToast('Decrypt note failed.', 'error'); 35 | } 36 | } 37 | 38 | function renderNotes() { 39 | if (!notes.length) { 40 | return ( 41 | 42 | No notes yet. 43 | 44 | ); 45 | } 46 | 47 | return ( 48 | <> 49 | {notes.map(note => ( 50 | 58 | ))} 59 | 60 | ); 61 | } 62 | 63 | return ( 64 | 65 | {renderNotes()} 66 | 67 | ); 68 | } 69 | 70 | export default FolderNotes; 71 | -------------------------------------------------------------------------------- /ios/PreCloudTests/PreCloudTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface PreCloudTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation PreCloudTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /src/lib/files/file.js: -------------------------------------------------------------------------------- 1 | import FileViewer from 'react-native-file-viewer'; 2 | import FS from 'react-native-fs'; 3 | import { moveFile, readFolder } from './actions'; 4 | 5 | import { filesFolder, largeFileExtension, legacyFilesFolder, noteExtension, precloudFolder } from './constant'; 6 | import { statFile } from './helpers'; 7 | 8 | export async function makeFolders() { 9 | const preCloudExists = await FS.exists(precloudFolder); 10 | if (!preCloudExists) { 11 | await FS.mkdir(precloudFolder); 12 | } 13 | 14 | const filesExists = await FS.exists(filesFolder); 15 | if (!filesExists) { 16 | await FS.mkdir(filesFolder); 17 | } 18 | 19 | const legacyExists = await FS.exists(legacyFilesFolder); 20 | if (legacyExists) { 21 | await moveFile(legacyFilesFolder, filesFolder); 22 | } 23 | } 24 | 25 | export async function createFolder(label, parentPath) { 26 | if (!label) { 27 | return; 28 | } 29 | 30 | await makeFolders(); 31 | 32 | const fullPath = parentPath ? `${parentPath}/${label}` : `${filesFolder}/${label}`; 33 | const exists = await FS.exists(fullPath); 34 | if (!exists) { 35 | await FS.mkdir(fullPath); 36 | } 37 | 38 | const info = await statFile(fullPath); 39 | return info; 40 | } 41 | 42 | export async function readFolders() { 43 | await makeFolders(); 44 | const folders = await FS.readDir(filesFolder); 45 | return folders 46 | .filter(n => n.isDirectory()) 47 | .sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime()); 48 | } 49 | 50 | export async function readFiles(path) { 51 | const exists = await FS.exists(path); 52 | if (!exists) { 53 | return { files: [], folders: [] }; 54 | } 55 | 56 | const result = await readFolder(path); 57 | const files = []; 58 | const folders = []; 59 | const notes = []; 60 | result.forEach(r => { 61 | if (r.isDirectory() && !r.name.endsWith(largeFileExtension)) { 62 | folders.push(r); 63 | } else if (r.name.endsWith(noteExtension)) { 64 | notes.push(r); 65 | } else { 66 | files.push(r); 67 | } 68 | }); 69 | 70 | return { files, folders, notes }; 71 | } 72 | 73 | export async function viewFile(path) { 74 | try { 75 | await FileViewer.open(path); 76 | } catch (e) { 77 | console.log('open file failed', e); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/lib/openpgp/encryptLargeFile.js: -------------------------------------------------------------------------------- 1 | import { CachesDirectoryPath, mkdir, read } from 'react-native-fs'; 2 | 3 | import { asyncForEach } from '../array'; 4 | import { deleteFile, writeFile } from '../files/actions'; 5 | import { largeFileExtension, precloudExtension } from '../files/constant'; 6 | import { extractFileNameAndExtension, getFolderSize, statFile } from '../files/helpers'; 7 | import { openpgpStatus } from './constant'; 8 | import { encryptFile } from './helpers'; 9 | 10 | const CHUNK_SIZE_IN_BYTES = 20 * 1024 * 1024; 11 | 12 | async function getChunkCount(path) { 13 | const info = await statFile(path); 14 | const reminder = info.size % CHUNK_SIZE_IN_BYTES; 15 | return (info.size - reminder) / CHUNK_SIZE_IN_BYTES + (reminder > 0 ? 1 : 0); 16 | } 17 | 18 | function append0(index) { 19 | const str = index.toString(); 20 | const zeroLength = 10 - str.length; 21 | return `${Array(zeroLength).fill('0').join('')}${str}`; 22 | } 23 | 24 | export async function encryptLargeFile(file, { folder, password }) { 25 | const encryptedName = `${file.name}.${largeFileExtension}`; 26 | const encryptedPath = `${folder.path}/${encryptedName}`; 27 | await mkdir(encryptedPath); 28 | 29 | const { extension } = extractFileNameAndExtension(file.name); 30 | const chunkCount = await getChunkCount(file.path); 31 | 32 | try { 33 | await asyncForEach(Array(chunkCount).fill(0), async (_, index) => { 34 | const chunk = await read( 35 | file.path, 36 | CHUNK_SIZE_IN_BYTES, 37 | index * CHUNK_SIZE_IN_BYTES, 38 | 'base64' 39 | ); 40 | const tmpPath = `${CachesDirectoryPath}/chunk.${precloudExtension}`; 41 | await writeFile(tmpPath, chunk, 'base64'); 42 | const success = await encryptFile( 43 | tmpPath, 44 | `${encryptedPath}/${append0(index)}${extension}.${precloudExtension}`, 45 | password 46 | ); 47 | if (!success) { 48 | throw new Error(openpgpStatus.error); 49 | } 50 | }); 51 | 52 | const size = await getFolderSize(encryptedPath); 53 | return { 54 | name: encryptedName, 55 | path: encryptedPath, 56 | size, 57 | isDirectory: () => true, 58 | isFile: () => false, 59 | }; 60 | } catch (e) { 61 | await deleteFile(encryptedPath); 62 | return null; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /scripts/bump-version.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { writeFileSync, readFileSync } = require('fs'); 3 | const { execSync } = require('child_process'); 4 | 5 | const plist = require('simple-plist'); 6 | const properties = require('properties-parser'); 7 | const semver = require('semver'); 8 | 9 | const pkg = require('../package.json'); 10 | 11 | let version = process.argv[2]; 12 | 13 | if (!semver.valid(version)) { 14 | console.error('Please pass the new version as an argument'); 15 | process.exit(1); 16 | } 17 | 18 | if (semver.lte(version, pkg.version)) { 19 | console.error('Please pass a version that is higher than the old one'); 20 | process.exit(1); 21 | } 22 | 23 | version = semver.clean(version); 24 | 25 | console.log( 26 | `Performing a ${semver.diff(version, pkg.version)} bump from ${pkg.version} to ${version}.` 27 | ); 28 | 29 | const pkgPath = join(__dirname, '../package.json'); 30 | const pkgContent = JSON.stringify({ ...pkg, version }, null, 2); 31 | writeFileSync(pkgPath, `${pkgContent}\n`); 32 | console.log(`Version written to ${pkgPath}`); 33 | 34 | const lock = require('../package-lock.json'); 35 | const lockPath = join(__dirname, '../package-lock.json'); 36 | const lockContent = JSON.stringify({ ...lock, version }, null, 2); 37 | writeFileSync(lockPath, `${lockContent}\n`); 38 | console.log(`Version written to ${lockPath}`); 39 | 40 | const plistPath = join(__dirname, '../ios/PreCloud/Info.plist'); 41 | const infoPlist = plist.parse(String(readFileSync(plistPath))); 42 | const content = plist.stringify({ 43 | ...infoPlist, 44 | CFBundleShortVersionString: version, 45 | }); 46 | const contentTabbed = content.replace(/^\s+/gm, spaces => { 47 | const indent = spaces.length / 2; 48 | return new Array(indent).join('\t'); 49 | }); 50 | writeFileSync(plistPath, `${contentTabbed}\n`); 51 | console.log(`Version written to ${plistPath}`); 52 | 53 | const gradlePath = join(__dirname, '../android/gradle.properties'); 54 | const gradlePropsEditor = properties.createEditor(gradlePath); 55 | gradlePropsEditor.set('releaseVersionName', version); 56 | gradlePropsEditor.save(gradlePath); 57 | console.log(`Version written to ${gradlePath}`); 58 | 59 | // Commit a version change 60 | execSync(`git cc -am v${version} && git tag v${version} && git push && git push --tags`, { 61 | stdio: 'inherit', 62 | }); 63 | -------------------------------------------------------------------------------- /src/components/FolderPicker.js: -------------------------------------------------------------------------------- 1 | import { Button, Modal, Text, VStack } from 'native-base'; 2 | import React, { useState } from 'react'; 3 | 4 | import { routeNames } from '../router/routes'; 5 | import { useStore } from '../store/store'; 6 | import FolderPickerItem from './FolderPickerItem'; 7 | 8 | function FolderPicker({ isOpen, onClose, onSave, navigate, currentFolder }) { 9 | const rootFolders = useStore(state => state.rootFolders); 10 | 11 | const [selectedFolder, setSelectedFolder] = useState(null); 12 | 13 | function handleClose() { 14 | onClose(); 15 | } 16 | 17 | function renderFolders() { 18 | if (!rootFolders?.length) { 19 | return ( 20 | 21 | You don‘t have other folder yet,{' '} 22 | { 25 | handleClose(); 26 | navigate(routeNames.folderForm, { folder: null }); 27 | }} 28 | > 29 | Create one 30 | 31 | 32 | ); 33 | } 34 | 35 | return ( 36 | 37 | {rootFolders.map(f => ( 38 | setSelectedFolder(selected)} 43 | /> 44 | ))} 45 | 46 | ); 47 | } 48 | 49 | return ( 50 | 51 | 52 | Move to ... 53 | {renderFolders()} 54 | 55 | 56 | 59 | 70 | 71 | 72 | 73 | 74 | ); 75 | } 76 | 77 | export default FolderPicker; 78 | -------------------------------------------------------------------------------- /src/views/FolderForm.js: -------------------------------------------------------------------------------- 1 | import { Button, FormControl, Input } from 'native-base'; 2 | import React, { useEffect, useMemo, useState } from 'react'; 3 | 4 | import AppBar from '../components/AppBar'; 5 | import ContentWrapper from '../components/ContentWrapper'; 6 | import ScreenWrapper from '../components/ScreenWrapper'; 7 | import { showToast } from '../lib/toast'; 8 | import { routeNames } from '../router/routes'; 9 | import { useStore } from '../store/store'; 10 | 11 | function FolderForm({ 12 | navigation, 13 | route: { 14 | params: { folder, parentPath }, 15 | }, 16 | }) { 17 | const rootFolders = useStore(state => state.rootFolders); 18 | const folderLabels = useMemo(() => rootFolders.map(n => n.name), [rootFolders]); 19 | const createFolder = useStore(state => state.createFolder); 20 | const renameFolder = useStore(state => state.renameFolder); 21 | 22 | const [label, setLabel] = useState(''); 23 | 24 | useEffect(() => { 25 | setLabel(folder?.name || ''); 26 | }, [folder]); 27 | 28 | async function handleSave() { 29 | const trimed = label.trim(); 30 | if (folderLabels.includes(trimed)) { 31 | showToast('This name is used, please choose another one.', 'error'); 32 | return; 33 | } 34 | 35 | try { 36 | if (folder) { 37 | await renameFolder({ folder, label: trimed }); 38 | navigation.goBack(); 39 | showToast(`Folder "${folder.name}" is renamed to "${trimed}"!`); 40 | } else { 41 | const newFolder = await createFolder(trimed, parentPath); 42 | navigation.replace(routeNames.folder, { path: newFolder.path }); 43 | showToast(`Folder ${trimed} is created!`); 44 | } 45 | } catch (e) { 46 | showToast(`Folder creation failed, please try again`, 'error'); 47 | } 48 | } 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 | Folder name 56 | 57 | 58 | 61 | 62 | 63 | ); 64 | } 65 | 66 | export default FolderForm; 67 | -------------------------------------------------------------------------------- /ios/PreCloud/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | PreCloud 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.15.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 184 25 | LSRequiresIPhoneOS 26 | 27 | NSLocationWhenInUseUsageDescription 28 | 29 | UIAppFonts 30 | 31 | fonts/Ionicons.ttf 32 | 33 | NSAppTransportSecurity 34 | 35 | NSExceptionDomains 36 | 37 | localhost 38 | 39 | NSExceptionAllowsInsecureHTTPLoads 40 | 41 | 42 | 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UIViewControllerBasedStatusBarAppearance 57 | 58 | NSPhotoLibraryUsageDescription 59 | PreCloud needs access to the photo library, so you can encrypt your photos. 60 | NSCameraUsageDescription 61 | PreCloud needs access to take photos, so you can take photo and encrypt it right away. 62 | CFBundleURLTypes 63 | 64 | 65 | CFBundleTypeRole 66 | Editor 67 | CFBundleURLSchemes 68 | 69 | precloud 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/router/BottomTab.js: -------------------------------------------------------------------------------- 1 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 2 | import React from 'react'; 3 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 4 | 5 | import Icon from '../components/Icon'; 6 | import useColors from '../hooks/useColors'; 7 | import useTakePhotoInTabs from '../hooks/useTakePhotoInTabs'; 8 | import { tabbarHeight } from '../lib/constants'; 9 | import Folders from '../views/Folders'; 10 | import Settings from '../views/Settings'; 11 | import TakePhoto from '../views/TakePhoto'; 12 | import { routeNames } from './routes'; 13 | 14 | const Tab = createBottomTabNavigator(); 15 | 16 | function BottomTab() { 17 | const colors = useColors(); 18 | const { bottom } = useSafeAreaInsets(); 19 | const handleTakePhoto = useTakePhotoInTabs(); 20 | 21 | function getIconName(routeName, focused) { 22 | if (routeName === routeNames.folders) { 23 | return focused ? 'lock-closed' : 'lock-closed-outline'; 24 | } else if (routeName === routeNames.takePhoto) { 25 | return 'camera-outline'; 26 | } else if (routeName === routeNames.settings) { 27 | return focused ? 'heart' : 'heart-outline'; 28 | } else { 29 | return null; 30 | } 31 | } 32 | 33 | function getIcon(focused, color, routeName) { 34 | return ( 35 | 41 | ); 42 | } 43 | 44 | return ( 45 | ({ 47 | headerShown: false, 48 | tabBarIcon: ({ focused, color }) => getIcon(focused, color, route.name), 49 | tabBarLabel: () => null, 50 | tabBarActiveTintColor: colors.orange, 51 | tabBarInactiveTintColor: colors.text, 52 | tabBarHideOnKeyboard: true, 53 | tabBarStyle: { 54 | paddingBottom: bottom, 55 | height: tabbarHeight + bottom, 56 | backgroundColor: colors.primary, 57 | }, 58 | })} 59 | > 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | export default BottomTab; 68 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainComponentsRegistry.cpp: -------------------------------------------------------------------------------- 1 | #include "MainComponentsRegistry.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace facebook { 10 | namespace react { 11 | 12 | MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {} 13 | 14 | std::shared_ptr 15 | MainComponentsRegistry::sharedProviderRegistry() { 16 | auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry(); 17 | 18 | // Autolinked providers registered by RN CLI 19 | rncli_registerProviders(providerRegistry); 20 | 21 | // Custom Fabric Components go here. You can register custom 22 | // components coming from your App or from 3rd party libraries here. 23 | // 24 | // providerRegistry->add(concreteComponentDescriptorProvider< 25 | // AocViewerComponentDescriptor>()); 26 | return providerRegistry; 27 | } 28 | 29 | jni::local_ref 30 | MainComponentsRegistry::initHybrid( 31 | jni::alias_ref, 32 | ComponentFactory *delegate) { 33 | auto instance = makeCxxInstance(delegate); 34 | 35 | auto buildRegistryFunction = 36 | [](EventDispatcher::Weak const &eventDispatcher, 37 | ContextContainer::Shared const &contextContainer) 38 | -> ComponentDescriptorRegistry::Shared { 39 | auto registry = MainComponentsRegistry::sharedProviderRegistry() 40 | ->createComponentDescriptorRegistry( 41 | {eventDispatcher, contextContainer}); 42 | 43 | auto mutableRegistry = 44 | std::const_pointer_cast(registry); 45 | 46 | mutableRegistry->setFallbackComponentDescriptor( 47 | std::make_shared( 48 | ComponentDescriptorParameters{ 49 | eventDispatcher, contextContainer, nullptr})); 50 | 51 | return registry; 52 | }; 53 | 54 | delegate->buildRegistryFunction = buildRegistryFunction; 55 | return instance; 56 | } 57 | 58 | void MainComponentsRegistry::registerNatives() { 59 | registerHybrid({ 60 | makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid), 61 | }); 62 | } 63 | 64 | } // namespace react 65 | } // namespace facebook 66 | -------------------------------------------------------------------------------- /src/lib/openpgp/encryptFiles.js: -------------------------------------------------------------------------------- 1 | import { asyncForEach } from '../array'; 2 | import { moveFile } from '../files/actions'; 3 | import { largeFileExtension, precloudExtension } from '../files/constant'; 4 | import { getFolderSize } from '../files/helpers'; 5 | import { showToast } from '../toast'; 6 | import { 7 | ENCRYPTION_LIMIT_IN_BYTES, 8 | ENCRYPTION_LIMIT_IN_GIGABYTES, 9 | LARGE_FILE_SIZE_IN_BYTES, 10 | } from './constant'; 11 | import { encryptLargeFile } from './encryptLargeFile'; 12 | import { encryptSmallFile } from './encryptSmallFile'; 13 | 14 | export async function encryptFiles(files, { folder, onEncrypted, password }) { 15 | if (!files?.length) { 16 | return []; 17 | } 18 | 19 | const encryptedFiles = []; 20 | 21 | await asyncForEach(files, async file => { 22 | let encrypted = null; 23 | 24 | if (file.name.endsWith(precloudExtension)) { 25 | const newPath = `${folder.path}/${file.name}`; 26 | await moveFile(file.path, newPath); 27 | 28 | encrypted = { 29 | name: file.name, 30 | path: newPath, 31 | size: file.size, 32 | isDirectory: () => false, 33 | isFile: () => true, 34 | }; 35 | } else if (file.name.endsWith(largeFileExtension)) { 36 | const newPath = `${folder.path}/${file.name}`; 37 | await moveFile(file.path, newPath); 38 | const size = await getFolderSize(newPath); 39 | 40 | encrypted = { 41 | name: file.name, 42 | path: newPath, 43 | size, 44 | isDirectory: () => true, 45 | isFile: () => false, 46 | }; 47 | } else if (file.size > ENCRYPTION_LIMIT_IN_BYTES) { 48 | showToast( 49 | `${file.name} is bigger than ${ENCRYPTION_LIMIT_IN_GIGABYTES}GB, PreCloud can't encrypt such large files for now.`, 50 | 'info' 51 | ); 52 | } else if (file.size > LARGE_FILE_SIZE_IN_BYTES) { 53 | showToast( 54 | `${file.name} is a large file, encryption may take a while, keep PreCloud active and be patient :)`, 55 | 'info' 56 | ); 57 | encrypted = await encryptLargeFile(file, { folder, password }); 58 | } else { 59 | encrypted = await encryptSmallFile(file, { folder, password }); 60 | } 61 | 62 | if (encrypted) { 63 | encryptedFiles.push(encrypted); 64 | if (onEncrypted) { 65 | onEncrypted(encrypted); 66 | } 67 | } else { 68 | showToast(`Encrypting ${file.name} failed.`, 'error'); 69 | } 70 | }); 71 | 72 | return encryptedFiles; 73 | } 74 | -------------------------------------------------------------------------------- /patches/react-native-pell-rich-editor+1.8.8.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-pell-rich-editor/src/editor.js b/node_modules/react-native-pell-rich-editor/src/editor.js 2 | index 4252bd9..6dab669 100644 3 | --- a/node_modules/react-native-pell-rich-editor/src/editor.js 4 | +++ b/node_modules/react-native-pell-rich-editor/src/editor.js 5 | @@ -316,7 +316,9 @@ function createHTML(options = {}) { 6 | result: function(url, style) { 7 | if (url){ 8 | exec('insertHTML', ""); 9 | - Actions.UPDATE_HEIGHT(); 10 | + setTimeout(() => { 11 | + Actions.UPDATE_HEIGHT(); 12 | + }, 50); 13 | } 14 | } 15 | }, 16 | @@ -324,7 +326,9 @@ function createHTML(options = {}) { 17 | result: function (html){ 18 | if (html){ 19 | exec('insertHTML', html); 20 | - Actions.UPDATE_HEIGHT(); 21 | + setTimeout(() => { 22 | + Actions.UPDATE_HEIGHT(); 23 | + }, 50); 24 | } 25 | } 26 | }, 27 | @@ -335,7 +339,9 @@ function createHTML(options = {}) { 28 | var thumbnail = url.replace(/.(mp4|m3u8)/g, '') + '-thumbnail'; 29 | var html = "


"; 30 | exec('insertHTML', html); 31 | - Actions.UPDATE_HEIGHT(); 32 | + setTimeout(() => { 33 | + Actions.UPDATE_HEIGHT(); 34 | + }, 50); 35 | } 36 | } 37 | }, 38 | @@ -362,7 +368,7 @@ function createHTML(options = {}) { 39 | }, 40 | content: { 41 | setDisable: function(dis){ this.blur(); editor.content.contentEditable = !dis}, 42 | - setHtml: function(html) { editor.content.innerHTML = html; Actions.UPDATE_HEIGHT(); }, 43 | + setHtml: function(html) { editor.content.innerHTML = html; setTimeout(() => Actions.UPDATE_HEIGHT(), 50); }, 44 | getHtml: function() { return editor.content.innerHTML; }, 45 | blur: function() { editor.content.blur(); }, 46 | focus: function() { focusCurrent(); }, 47 | -------------------------------------------------------------------------------- /src/views/PasswordGenerator.js: -------------------------------------------------------------------------------- 1 | import Clipboard from '@react-native-clipboard/clipboard'; 2 | import { Button, Checkbox, Slider, Text } from 'native-base'; 3 | import React, { useState } from 'react'; 4 | 5 | import AppBar from '../components/AppBar'; 6 | import ContentWrapper from '../components/ContentWrapper'; 7 | import ScreenWrapper from '../components/ScreenWrapper'; 8 | import { generatePassword } from '../lib/password'; 9 | import { showToast } from '../lib/toast'; 10 | 11 | const defaultLength = 20; 12 | 13 | function PasswordGenerator() { 14 | const [passwordLength, setPasswordLength] = useState(defaultLength); 15 | const [hasSpecialCharacters, setHasSpecialCharacters] = useState(true); 16 | const [password, setPassword] = useState(generatePassword(passwordLength, hasSpecialCharacters)); 17 | 18 | return ( 19 | 20 | 21 | 22 | Password length: {passwordLength} 23 | { 31 | setPasswordLength(length); 32 | setPassword(generatePassword(length, hasSpecialCharacters)); 33 | }} 34 | my={2} 35 | > 36 | 37 | 38 | 39 | 40 | 41 | { 44 | setHasSpecialCharacters(checked); 45 | setPassword(generatePassword(passwordLength, checked)); 46 | }} 47 | > 48 | Has special characters 49 | 50 | 58 | {!!password && ( 59 | <> 60 | {password} 61 | 71 | 72 | )} 73 | 74 | 75 | ); 76 | } 77 | 78 | export default PasswordGenerator; 79 | -------------------------------------------------------------------------------- /patches/react-native-toast-message+2.1.5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-toast-message/lib/src/components/BaseToast.js b/node_modules/react-native-toast-message/lib/src/components/BaseToast.js 2 | index 21c4786..717b27b 100644 3 | --- a/node_modules/react-native-toast-message/lib/src/components/BaseToast.js 4 | +++ b/node_modules/react-native-toast-message/lib/src/components/BaseToast.js 5 | @@ -3,7 +3,7 @@ import { Text, View } from 'react-native'; 6 | import { getTestId } from '../utils/test-id'; 7 | import { styles } from './BaseToast.styles'; 8 | import { Touchable } from './Touchable'; 9 | -export function BaseToast({ text1, text2, onPress, activeOpacity, style, touchableContainerProps, contentContainerStyle, contentContainerProps, text1Style, text1NumberOfLines = 1, text1Props, text2Style, text2NumberOfLines = 1, text2Props, renderLeadingIcon, renderTrailingIcon }) { 10 | +export function BaseToast({ text1, text2, onPress, activeOpacity, style, touchableContainerProps, contentContainerStyle, contentContainerProps, text1Style, text1NumberOfLines = 0, text1Props, text2Style, text2NumberOfLines = 0, text2Props, renderLeadingIcon, renderTrailingIcon }) { 11 | return ( 12 | {renderLeadingIcon && renderLeadingIcon()} 13 | 14 | diff --git a/node_modules/react-native-toast-message/lib/src/useToast.js b/node_modules/react-native-toast-message/lib/src/useToast.js 15 | index 5794b67..b578b78 100644 16 | --- a/node_modules/react-native-toast-message/lib/src/useToast.js 17 | +++ b/node_modules/react-native-toast-message/lib/src/useToast.js 18 | @@ -1,4 +1,5 @@ 19 | import React from 'react'; 20 | +import { useSafeAreaInsets } from 'react-native-safe-area-context'; 21 | import { useLogger } from './contexts'; 22 | import { useTimeout } from './hooks'; 23 | import { noop } from './utils/func'; 24 | @@ -26,6 +27,7 @@ export function useToast({ defaultOptions }) { 25 | const [data, setData] = React.useState(DEFAULT_DATA); 26 | const initialOptions = mergeIfDefined(DEFAULT_OPTIONS, defaultOptions); 27 | const [options, setOptions] = React.useState(initialOptions); 28 | + const {top} = useSafeAreaInsets() 29 | const onAutoHide = React.useCallback(() => { 30 | log('Auto hiding'); 31 | setIsVisible(false); 32 | @@ -50,7 +52,7 @@ export function useToast({ defaultOptions }) { 33 | position, 34 | autoHide, 35 | visibilityTime, 36 | - topOffset, 37 | + topOffset: topOffset + top, 38 | bottomOffset, 39 | keyboardOffset, 40 | onShow, 41 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | activesupport (6.1.5.1) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 1.6, < 2) 9 | minitest (>= 5.1) 10 | tzinfo (~> 2.0) 11 | zeitwerk (~> 2.3) 12 | addressable (2.8.0) 13 | public_suffix (>= 2.0.2, < 5.0) 14 | algoliasearch (1.27.5) 15 | httpclient (~> 2.8, >= 2.8.3) 16 | json (>= 1.5.1) 17 | atomos (0.1.3) 18 | claide (1.1.0) 19 | cocoapods (1.11.3) 20 | addressable (~> 2.8) 21 | claide (>= 1.0.2, < 2.0) 22 | cocoapods-core (= 1.11.3) 23 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 24 | cocoapods-downloader (>= 1.4.0, < 2.0) 25 | cocoapods-plugins (>= 1.0.0, < 2.0) 26 | cocoapods-search (>= 1.0.0, < 2.0) 27 | cocoapods-trunk (>= 1.4.0, < 2.0) 28 | cocoapods-try (>= 1.1.0, < 2.0) 29 | colored2 (~> 3.1) 30 | escape (~> 0.0.4) 31 | fourflusher (>= 2.3.0, < 3.0) 32 | gh_inspector (~> 1.0) 33 | molinillo (~> 0.8.0) 34 | nap (~> 1.0) 35 | ruby-macho (>= 1.0, < 3.0) 36 | xcodeproj (>= 1.21.0, < 2.0) 37 | cocoapods-core (1.11.3) 38 | activesupport (>= 5.0, < 7) 39 | addressable (~> 2.8) 40 | algoliasearch (~> 1.0) 41 | concurrent-ruby (~> 1.1) 42 | fuzzy_match (~> 2.0.4) 43 | nap (~> 1.0) 44 | netrc (~> 0.11) 45 | public_suffix (~> 4.0) 46 | typhoeus (~> 1.0) 47 | cocoapods-deintegrate (1.0.5) 48 | cocoapods-downloader (1.6.3) 49 | cocoapods-plugins (1.0.0) 50 | nap 51 | cocoapods-search (1.0.1) 52 | cocoapods-trunk (1.6.0) 53 | nap (>= 0.8, < 2.0) 54 | netrc (~> 0.11) 55 | cocoapods-try (1.2.0) 56 | colored2 (3.1.2) 57 | concurrent-ruby (1.1.10) 58 | escape (0.0.4) 59 | ethon (0.15.0) 60 | ffi (>= 1.15.0) 61 | ffi (1.15.5) 62 | fourflusher (2.3.1) 63 | fuzzy_match (2.0.4) 64 | gh_inspector (1.1.3) 65 | httpclient (2.8.3) 66 | i18n (1.10.0) 67 | concurrent-ruby (~> 1.0) 68 | json (2.6.1) 69 | minitest (5.15.0) 70 | molinillo (0.8.0) 71 | nanaimo (0.3.0) 72 | nap (1.1.0) 73 | netrc (0.11.0) 74 | public_suffix (4.0.7) 75 | rexml (3.2.5) 76 | ruby-macho (2.5.1) 77 | typhoeus (1.4.0) 78 | ethon (>= 0.9.0) 79 | tzinfo (2.0.4) 80 | concurrent-ruby (~> 1.0) 81 | xcodeproj (1.21.0) 82 | CFPropertyList (>= 2.3.3, < 4.0) 83 | atomos (~> 0.1.3) 84 | claide (>= 1.0.2, < 2.0) 85 | colored2 (~> 3.1) 86 | nanaimo (~> 0.3.0) 87 | rexml (~> 3.2.4) 88 | zeitwerk (2.5.4) 89 | 90 | PLATFORMS 91 | ruby 92 | 93 | DEPENDENCIES 94 | cocoapods (~> 1.11, >= 1.11.2) 95 | 96 | RUBY VERSION 97 | ruby 2.7.5p203 98 | 99 | BUNDLED WITH 100 | 2.2.27 101 | -------------------------------------------------------------------------------- /src/views/Passwords.js: -------------------------------------------------------------------------------- 1 | import { Alert, Button, Radio, Text, VStack } from 'native-base'; 2 | import React from 'react'; 3 | 4 | import AppBar from '../components/AppBar'; 5 | import ContentWrapper from '../components/ContentWrapper'; 6 | import PasswordItem from '../components/PasswordItem'; 7 | import ScreenWrapper from '../components/ScreenWrapper'; 8 | import { showToast } from '../lib/toast'; 9 | import { routeNames } from '../router/routes'; 10 | import { useStore } from '../store/store'; 11 | 12 | function Passwords({ navigation }) { 13 | const passwords = useStore(state => state.passwords); 14 | const activePasswordId = useStore(state => state.activePasswordId); 15 | const setActivePassword = useStore(state => state.setActivePassword); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Save your passwords in a safe place! 27 | 28 | 29 | Save all your passwords in a password manager. If you lose your passwords, You can 30 | not decrypt your texts or files. 31 | 32 | 33 | 34 | 35 | {!!passwords?.length && ( 36 | { 41 | await setActivePassword(id); 42 | const password = passwords.find(p => p.id === id); 43 | if (password) { 44 | showToast( 45 | `From now on you will use the "${password.label}" password to encrypt and decrypt.`, 46 | 'info' 47 | ); 48 | } 49 | }} 50 | > 51 | {passwords.map((password, index) => ( 52 | 58 | ))} 59 | 60 | )} 61 | 62 | 69 | 70 | 71 | 72 | ); 73 | } 74 | 75 | export default Passwords; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "precloud", 3 | "version": "1.15.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-native-async-storage/async-storage": "1.17.10", 7 | "@react-native-clipboard/clipboard": "1.10.0", 8 | "@react-navigation/bottom-tabs": "6.4.0", 9 | "@react-navigation/native": "6.0.13", 10 | "@react-navigation/native-stack": "6.9.0", 11 | "date-fns": "2.29.3", 12 | "fast-text-encoding": "1.0.6", 13 | "native-base": "3.4.19", 14 | "react": "18.1.0", 15 | "react-native": "0.70.5", 16 | "react-native-compressor": "1.6.1", 17 | "react-native-device-info": "10.0.2", 18 | "react-native-document-picker": "8.1.2", 19 | "react-native-fast-openpgp": "2.5.1", 20 | "react-native-file-viewer": "2.1.5", 21 | "react-native-fs": "2.20.0", 22 | "react-native-iap": "12.4.0", 23 | "react-native-image-picker": "4.10.1", 24 | "react-native-keychain": "8.1.1", 25 | "react-native-pell-rich-editor": "1.8.8", 26 | "react-native-safe-area-context": "4.3.1", 27 | "react-native-screens": "3.17.0", 28 | "react-native-share": "7.6.6", 29 | "react-native-splash-screen": "3.3.0", 30 | "react-native-svg": "12.4.3", 31 | "react-native-toast-message": "2.1.5", 32 | "react-native-url-polyfill": "1.3.0", 33 | "react-native-use-keyboard-height": "0.1.1", 34 | "react-native-vector-icons": "9.2.0", 35 | "react-native-webview": "11.23.1", 36 | "react-native-zip-archive": "6.0.8", 37 | "zustand": "4.0.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "7.18.9", 41 | "@babel/runtime": "7.18.9", 42 | "@react-native-community/eslint-config": "3.0.3", 43 | "babel-jest": "28.1.3", 44 | "eslint": "8.20.0", 45 | "eslint-plugin-import": "2.26.0", 46 | "eslint-plugin-react": "7.30.1", 47 | "eslint-plugin-react-hooks": "4.6.0", 48 | "jest": "28.1.3", 49 | "metro-react-native-babel-preset": "0.72.3", 50 | "patch-package": "6.4.7", 51 | "prettier": "2.7.1", 52 | "properties-parser": "0.3.1", 53 | "react-dom": "18.1.0", 54 | "react-test-renderer": "18.1.0", 55 | "semver": "7.3.7", 56 | "simple-plist": "1.3.1" 57 | }, 58 | "scripts": { 59 | "android": "react-native run-android", 60 | "ios": "react-native run-ios --simulator \"iPhone 14\"", 61 | "postinstall": "patch-package", 62 | "start": "watchman watch-del '/Users/peng/peng/PreCloud' ; watchman watch-project '/Users/peng/peng/PreCloud' && react-native start", 63 | "test": "jest", 64 | "lint": "eslint .", 65 | "clean-android": "cd android && ./gradlew clean && cd ..", 66 | "build-android": "npm run bump-build-numbers && cd android && ./gradlew clean && ./gradlew bundleRelease && cd ..", 67 | "bump-build-numbers": "node ./scripts/bump-build-numbers.js", 68 | "bump-version": "node ./scripts/bump-version.js" 69 | }, 70 | "jest": { 71 | "preset": "react-native", 72 | "roots": [ 73 | "src" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/DownloadRemoteFileButton.js: -------------------------------------------------------------------------------- 1 | import Clipboard from '@react-native-clipboard/clipboard'; 2 | import { Actionsheet, Button, Modal, Text } from 'native-base'; 3 | import React, { useState } from 'react'; 4 | 5 | import useColors from '../hooks/useColors'; 6 | import { downloadRemoteFile } from '../lib/files/actions'; 7 | import { showToast } from '../lib/toast'; 8 | import Icon from './Icon'; 9 | 10 | function DownloadRemoteFileButton({ isDisabled, isLoading, onClose, onStart, onDownloaded }) { 11 | const colors = useColors(); 12 | 13 | const [showModal, setShowModal] = useState(false); 14 | const [url, setUrl] = useState(''); 15 | const [isDownloading, setIsDownloading] = useState(false); 16 | 17 | async function handlePress() { 18 | setIsDownloading(true); 19 | const file = await downloadRemoteFile(url); 20 | 21 | if (file) { 22 | handleClose(); 23 | showToast('Downloaded! Encrypting ...'); 24 | setIsDownloading(false); 25 | onStart(true); 26 | await onDownloaded(file); 27 | onStart(false); 28 | onClose(); 29 | } else { 30 | showToast('Download file failed', 'error'); 31 | setIsDownloading(false); 32 | } 33 | } 34 | 35 | function handleClose() { 36 | setShowModal(false); 37 | setUrl(''); 38 | setIsDownloading(false); 39 | } 40 | 41 | return ( 42 | <> 43 | } 47 | onPress={() => { 48 | setShowModal(true); 49 | }} 50 | > 51 | Download remote files 52 | 53 | 54 | 55 | 56 | Download and encrypt file from web 57 | 58 | This feature is not very stable yet, please don‘t get angry if the download failed :) 59 | 69 | {!!url && {url}} 70 | 71 | 72 | 73 | 76 | 83 | 84 | 85 | 86 | 87 | 88 | ); 89 | } 90 | 91 | export default DownloadRemoteFileButton; 92 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /ios/PreCloud/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/precloud/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.precloud; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.react.config.ReactFeatureFlags; 11 | import com.facebook.soloader.SoLoader; 12 | import com.precloud.newarchitecture.MainApplicationReactNativeHost; 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.util.List; 15 | 16 | public class MainApplication extends Application implements ReactApplication { 17 | 18 | private final ReactNativeHost mReactNativeHost = 19 | new ReactNativeHost(this) { 20 | @Override 21 | public boolean getUseDeveloperSupport() { 22 | return BuildConfig.DEBUG; 23 | } 24 | 25 | @Override 26 | protected List getPackages() { 27 | @SuppressWarnings("UnnecessaryLocalVariable") 28 | List packages = new PackageList(this).getPackages(); 29 | // Packages that cannot be autolinked yet can be added manually here, for example: 30 | // packages.add(new MyReactNativePackage()); 31 | return packages; 32 | } 33 | 34 | @Override 35 | protected String getJSMainModuleName() { 36 | return "index"; 37 | } 38 | }; 39 | 40 | private final ReactNativeHost mNewArchitectureNativeHost = 41 | new MainApplicationReactNativeHost(this); 42 | 43 | @Override 44 | public ReactNativeHost getReactNativeHost() { 45 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 46 | return mNewArchitectureNativeHost; 47 | } else { 48 | return mReactNativeHost; 49 | } 50 | } 51 | 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | // If you opted-in for the New Architecture, we enable the TurboModule system 56 | ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 57 | SoLoader.init(this, /* native exopackage */ false); 58 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 59 | } 60 | 61 | /** 62 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 63 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 64 | * 65 | * @param context 66 | * @param reactInstanceManager 67 | */ 68 | private static void initializeFlipper( 69 | Context context, ReactInstanceManager reactInstanceManager) { 70 | if (BuildConfig.DEBUG) { 71 | try { 72 | /* 73 | We use reflection here to pick up the class that initializes Flipper, 74 | since Flipper library is not available in release mode 75 | */ 76 | Class aClass = Class.forName("com.precloud.ReactNativeFlipper"); 77 | aClass 78 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 79 | .invoke(null, context, reactInstanceManager); 80 | } catch (ClassNotFoundException e) { 81 | e.printStackTrace(); 82 | } catch (NoSuchMethodException e) { 83 | e.printStackTrace(); 84 | } catch (IllegalAccessException e) { 85 | e.printStackTrace(); 86 | } catch (InvocationTargetException e) { 87 | e.printStackTrace(); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/components/DonateBanner.js: -------------------------------------------------------------------------------- 1 | import { addMonths, addSeconds, addWeeks } from 'date-fns'; 2 | import { Box, HStack, Text } from 'native-base'; 3 | import React, { useEffect, useState } from 'react'; 4 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 5 | 6 | import useColors from '../hooks/useColors'; 7 | import { tabbarHeight } from '../lib/constants'; 8 | import { LocalStorage, LocalStorageKeys } from '../lib/localstorage'; 9 | import { navigationRef } from '../router/navigationRef'; 10 | import { routeNames } from '../router/routes'; 11 | import Icon from './Icon'; 12 | 13 | function DonateBanner() { 14 | const { bottom } = useSafeAreaInsets(); 15 | const colors = useColors(); 16 | 17 | const [checkDate, setCheckDate] = useState(); 18 | const [donateDate, setDonateDate] = useState(); 19 | 20 | useEffect(() => { 21 | LocalStorage.get(LocalStorageKeys.donateBannerCheckDate).then(date => { 22 | setCheckDate(date); 23 | if (!date) { 24 | LocalStorage.set(LocalStorageKeys.donateBannerCheckDate, Date.now()); 25 | } 26 | }); 27 | 28 | LocalStorage.get(LocalStorageKeys.donateBannerDonateDate).then(date => { 29 | setDonateDate(date); 30 | }); 31 | }, []); 32 | 33 | function updateCheckDate() { 34 | const date = Date.now(); 35 | LocalStorage.set(LocalStorageKeys.donateBannerCheckDate, date); 36 | setCheckDate(date); 37 | } 38 | 39 | function updateDonateDate() { 40 | const date = Date.now(); 41 | LocalStorage.set(LocalStorageKeys.donateBannerDonateDate, date); 42 | setDonateDate(date); 43 | } 44 | 45 | function canShowBanner() { 46 | if (!checkDate) { 47 | return false; 48 | } 49 | 50 | // eslint-disable-next-line no-undef 51 | if (!__DEV__ && addWeeks(+checkDate, 1).getTime() > Date.now()) { 52 | return false; 53 | } 54 | 55 | // eslint-disable-next-line no-undef 56 | if (__DEV__ && addSeconds(+checkDate, 10).getTime() > Date.now()) { 57 | return false; 58 | } 59 | 60 | // eslint-disable-next-line no-undef 61 | if (!__DEV__ && donateDate && addMonths(+donateDate, 1).getTime() > Date.now()) { 62 | return false; 63 | } 64 | 65 | // eslint-disable-next-line no-undef 66 | if (__DEV__ && donateDate && addSeconds(+checkDate, 20).getTime() > Date.now()) { 67 | return false; 68 | } 69 | 70 | return true; 71 | } 72 | 73 | if (!canShowBanner()) { 74 | return null; 75 | } 76 | 77 | return ( 78 | 88 | 89 | 🫶 Consider donating $1 to this free and open source app:{' '} 90 | { 95 | updateCheckDate(); 96 | updateDonateDate(); 97 | navigationRef.navigate(routeNames.donation); 98 | }} 99 | > 100 | Donate 101 | 102 | 103 | 104 | 105 | 106 | 107 | ); 108 | } 109 | 110 | export default DonateBanner; 111 | -------------------------------------------------------------------------------- /android/app/src/debug/java/com/precloud/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.precloud; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceEventListener; 23 | import com.facebook.react.ReactInstanceManager; 24 | import com.facebook.react.bridge.ReactContext; 25 | import com.facebook.react.modules.network.NetworkingModule; 26 | import okhttp3.OkHttpClient; 27 | 28 | public class ReactNativeFlipper { 29 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 30 | if (FlipperUtils.shouldEnableFlipper(context)) { 31 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 32 | 33 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 34 | client.addPlugin(new ReactFlipperPlugin()); 35 | client.addPlugin(new DatabasesFlipperPlugin(context)); 36 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 37 | client.addPlugin(CrashReporterPlugin.getInstance()); 38 | 39 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 40 | NetworkingModule.setCustomClientBuilder( 41 | new NetworkingModule.CustomClientBuilder() { 42 | @Override 43 | public void apply(OkHttpClient.Builder builder) { 44 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 45 | } 46 | }); 47 | client.addPlugin(networkFlipperPlugin); 48 | client.start(); 49 | 50 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 51 | // Hence we run if after all native modules have been initialized 52 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 53 | if (reactContext == null) { 54 | reactInstanceManager.addReactInstanceEventListener( 55 | new ReactInstanceEventListener() { 56 | @Override 57 | public void onReactContextInitialized(ReactContext reactContext) { 58 | reactInstanceManager.removeReactInstanceEventListener(this); 59 | reactContext.runOnNativeModulesQueueThread( 60 | new Runnable() { 61 | @Override 62 | public void run() { 63 | client.addPlugin(new FrescoFlipperPlugin()); 64 | } 65 | }); 66 | } 67 | }); 68 | } else { 69 | client.addPlugin(new FrescoFlipperPlugin()); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/views/PasswordForm.js: -------------------------------------------------------------------------------- 1 | import { Button, FormControl, HStack, Input, Text } from 'native-base'; 2 | import React, { useEffect, useMemo, useState } from 'react'; 3 | 4 | import AppBar from '../components/AppBar'; 5 | import ContentWrapper from '../components/ContentWrapper'; 6 | import Icon from '../components/Icon'; 7 | import ScreenWrapper from '../components/ScreenWrapper'; 8 | import useColors from '../hooks/useColors'; 9 | import { showToast } from '../lib/toast'; 10 | import { routeNames } from '../router/routes'; 11 | import { useStore } from '../store/store'; 12 | 13 | function PasswordForm({ 14 | navigation, 15 | route: { 16 | params: { selectedPassword }, 17 | }, 18 | }) { 19 | const colors = useColors(); 20 | const passwords = useStore(state => state.passwords); 21 | const savePassword = useStore(state => state.savePassword); 22 | const passwordLabels = useMemo(() => passwords.map(p => p.label), [passwords]); 23 | 24 | const [label, setLabel] = useState(''); 25 | const [password, setPassword] = useState(''); 26 | const [showPassword, setShowPassword] = useState(false); 27 | const [error, setError] = useState(''); 28 | 29 | useEffect(() => { 30 | setLabel(selectedPassword?.label || ''); 31 | setPassword(selectedPassword?.password || ''); 32 | }, [selectedPassword]); 33 | 34 | async function handleSavePassword() { 35 | setError(''); 36 | if (passwordLabels.includes(label)) { 37 | setError('This password name is used, please choose another one.'); 38 | return; 39 | } 40 | 41 | try { 42 | await savePassword({ id: selectedPassword?.id, label: label.trim(), password }); 43 | navigation.goBack(); 44 | showToast('Password is saved in secure storage.') 45 | } catch (e) { 46 | setError('Save password failed. Please choose another one.'); 47 | } 48 | } 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 | Name 56 | 57 | Password 58 | 66 | setShowPassword(!showPassword)} 71 | /> 72 | 73 | } 74 | /> 75 | {!selectedPassword && ( 76 | 77 | Recommend: use a password manager to generate a strong password.{' '} 78 | navigation.navigate(routeNames.passwordGenerator)}> 79 | Generate 80 | 81 | 82 | )} 83 | {!!error && {error}} 84 | 85 | 88 | 89 | 90 | ); 91 | } 92 | 93 | export default PasswordForm; 94 | -------------------------------------------------------------------------------- /ios/PreCloud.xcodeproj/xcshareddata/xcschemes/PreCloud.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/Donation.js: -------------------------------------------------------------------------------- 1 | import { Alert, Divider, Heading, Spinner, Text, VStack } from 'native-base'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { Linking } from 'react-native'; 4 | import { requestPurchase, useIAP } from 'react-native-iap'; 5 | 6 | import AppBar from '../components/AppBar'; 7 | import ContentWrapper from '../components/ContentWrapper'; 8 | import DonationCard from '../components/DonationCard'; 9 | import ScreenWrapper from '../components/ScreenWrapper'; 10 | import useColors from '../hooks/useColors'; 11 | import { isAndroid } from '../lib/device'; 12 | import { showDonate } from '../lib/money'; 13 | 14 | function Donation() { 15 | const colors = useColors(); 16 | const { products, getProducts } = useIAP(); 17 | 18 | const [isLoading, setIsLoading] = useState(false); 19 | const [donations, setDonations] = useState([]); 20 | 21 | async function loadProducts() { 22 | setIsLoading(true); 23 | try { 24 | await getProducts({ skus: ['donation1', 'donation2', 'donation3'] }); 25 | } catch (e) { 26 | console.log('load products failed', e); 27 | } 28 | setIsLoading(false); 29 | } 30 | 31 | useEffect(() => { 32 | loadProducts(); 33 | // eslint-disable-next-line react-hooks/exhaustive-deps 34 | }, []); 35 | 36 | useEffect(() => { 37 | setDonations((products || []).sort((a, b) => a.price - b.price)); 38 | }, [products]); 39 | 40 | async function handleDonate(product) { 41 | try { 42 | const payload = isAndroid() 43 | ? { skus: [product.productId] } 44 | : { sku: product.productId, andDangerouslyFinishTransactionAutomaticallyIOS: false }; 45 | await requestPurchase(payload); 46 | } catch (e) { 47 | console.log('donate failed', e); 48 | } 49 | } 50 | 51 | return ( 52 | 53 | 54 | 55 | 56 | 57 | 58 | PreCloud is free forever, open sourced, has no tracking. Please consider donating to 59 | this project. 🫶 60 | 61 | 62 | 63 | {showDonate() && ( 64 | <> 65 | 66 | Donate with PayPal or Ko-Fi 67 | 68 | Please use PayPal or Ko-Fi to donate, so we don‘t need to pay the 15% fee to{' '} 69 | {isAndroid() ? 'Google' : 'Apple'}. 70 | 71 | { 76 | Linking.openURL(`https://ko-fi.com/penghuili`); 77 | }} 78 | > 79 | Ko-Fi 80 | 81 | { 86 | Linking.openURL(`https://paypal.me/penghuili/`); 87 | }} 88 | > 89 | PayPal 90 | 91 | 92 | 93 | 94 | 95 | )} 96 | 97 | {isLoading && } 98 | {!!donations?.length && ( 99 | 100 | Donate with in-app-purchase 101 | {donations.map(d => ( 102 | 103 | ))} 104 | 105 | )} 106 | 107 | 108 | 109 | ); 110 | } 111 | 112 | export default Donation; 113 | -------------------------------------------------------------------------------- /src/views/NoteDetails.js: -------------------------------------------------------------------------------- 1 | import { Heading, Input, KeyboardAvoidingView, VStack } from 'native-base'; 2 | import React, { useEffect, useRef, useState } from 'react'; 3 | 4 | import AppBar from '../components/AppBar'; 5 | import Editor from '../components/Editor'; 6 | import NoteItemActions from '../components/NoteItemActions'; 7 | import ScreenWrapper from '../components/ScreenWrapper'; 8 | import { deleteFile, writeFile } from '../lib/files/actions'; 9 | import { cachePath } from '../lib/files/cache'; 10 | import { noteExtension } from '../lib/files/constant'; 11 | import { getNoteTitle } from '../lib/files/note'; 12 | import { encryptFile } from '../lib/openpgp/helpers'; 13 | import { showToast } from '../lib/toast'; 14 | import { useStore } from '../store/store'; 15 | 16 | function NoteDetails({ 17 | navigation, 18 | route: { 19 | params: { folder }, 20 | }, 21 | }) { 22 | const editorRef = useRef(); 23 | 24 | const password = useStore(state => state.activePassword); 25 | const activeNote = useStore(state => state.activeNote); 26 | const noteContent = useStore(state => state.noteContent); 27 | 28 | const [isEditing, setIsEditing] = useState(false); 29 | const [title, setTitle] = useState(''); 30 | const [content, setContent] = useState(''); 31 | const [showActions, setShowActions] = useState(false); 32 | 33 | useEffect(() => { 34 | setTitle(getNoteTitle(activeNote)); 35 | }, [activeNote]); 36 | 37 | async function handleSave() { 38 | if (!title.trim()) { 39 | showToast('Please add a title', 'error'); 40 | return; 41 | } 42 | 43 | const trimedTitle = title.trim(); 44 | const inputPath = `${cachePath}/${trimedTitle}.txt`; 45 | await writeFile(inputPath, content || '', 'utf8'); 46 | const outputPath = `${folder.path}/${trimedTitle}.${noteExtension}`; 47 | const success = await encryptFile(inputPath, outputPath, password); 48 | 49 | if (success) { 50 | if (activeNote?.fileName && trimedTitle !== activeNote?.fileName) { 51 | await deleteFile(activeNote.path); 52 | } 53 | 54 | showToast('Your note is encrypted and saved on your phone.'); 55 | if (activeNote) { 56 | setIsEditing(false); 57 | } else { 58 | navigation.goBack(); 59 | } 60 | } else { 61 | showToast('Encryption failed.', 'error'); 62 | } 63 | } 64 | 65 | const editable = !activeNote || isEditing; 66 | 67 | return ( 68 | 69 | { 74 | if (editable) { 75 | handleSave(); 76 | } else { 77 | setShowActions(true); 78 | } 79 | }} 80 | /> 81 | 82 | 83 | {editable ? ( 84 | 85 | ) : ( 86 | {title} 87 | )} 88 | 89 | { 93 | setContent(noteContent); 94 | editorRef.current.setContentHTML(noteContent); 95 | }} 96 | onChange={setContent} 97 | /> 98 | 99 | 100 | 101 | setShowActions(false)} 106 | isNoteDetails 107 | onEdit={() => { 108 | setShowActions(false); 109 | setIsEditing(true); 110 | }} 111 | navigation={navigation} 112 | /> 113 | 114 | ); 115 | } 116 | 117 | export default NoteDetails; 118 | -------------------------------------------------------------------------------- /src/components/NoteItemActions.js: -------------------------------------------------------------------------------- 1 | import { Actionsheet, Text } from 'native-base'; 2 | import React, { useState } from 'react'; 3 | 4 | import useColors from '../hooks/useColors'; 5 | import { deleteFile, downloadFile, moveFile, shareFile } from '../lib/files/actions'; 6 | import { getSizeText } from '../lib/files/helpers'; 7 | import { showToast } from '../lib/toast'; 8 | import FolderPicker from './FolderPicker'; 9 | import Icon from './Icon'; 10 | 11 | function NoteItemActions({ 12 | folder, 13 | note, 14 | isOpen, 15 | onClose, 16 | isNoteDetails, 17 | onView, 18 | onEdit, 19 | onMoved, 20 | navigation, 21 | }) { 22 | const colors = useColors(); 23 | 24 | const [showFolderPicker, setShowFolderPicker] = useState(false); 25 | 26 | async function handleShare() { 27 | onClose(); 28 | const success = await shareFile({ 29 | name: note.name, 30 | path: note.path, 31 | saveToFiles: false, 32 | }); 33 | 34 | if (success) { 35 | showToast('Shared!'); 36 | } 37 | } 38 | 39 | async function handleDownload() { 40 | onClose(); 41 | const message = await downloadFile({ 42 | name: note.name, 43 | path: note.path, 44 | }); 45 | if (message) { 46 | showToast(message); 47 | } 48 | } 49 | 50 | async function handleMove(newFolder) { 51 | await moveFile(note.path, `${newFolder.path}/${note.name}`); 52 | setShowFolderPicker(false); 53 | if (isNoteDetails) { 54 | navigation.goBack(); 55 | } else { 56 | onMoved(note); 57 | } 58 | showToast('Moved!'); 59 | } 60 | 61 | async function handleDelete() { 62 | onClose(); 63 | await deleteFile(note.path); 64 | if (isNoteDetails) { 65 | navigation.goBack(); 66 | } else { 67 | onMoved(note); 68 | } 69 | showToast('Deleted!'); 70 | } 71 | 72 | return ( 73 | <> 74 | setShowFolderPicker(false)} 77 | onSave={handleMove} 78 | navigate={navigation.navigate} 79 | currentFolder={folder} 80 | /> 81 | 82 | 83 | {isNoteDetails ? ( 84 | } 86 | onPress={onEdit} 87 | > 88 | Edit 89 | 90 | ) : ( 91 | } 93 | onPress={onView} 94 | > 95 | Open 96 | 97 | )} 98 | } 100 | onPress={handleShare} 101 | > 102 | Share 103 | 104 | } 106 | onPress={handleDownload} 107 | > 108 | Download 109 | 110 | } 112 | onPress={() => { 113 | onClose(); 114 | setShowFolderPicker(true); 115 | }} 116 | > 117 | Move to ... 118 | 119 | } 121 | onPress={handleDelete} 122 | > 123 | Delete 124 | 125 | 126 | {!!note?.size && ( 127 | 128 | {getSizeText(note.size)} 129 | 130 | )} 131 | 132 | 133 | 134 | ); 135 | } 136 | 137 | export default NoteItemActions; 138 | -------------------------------------------------------------------------------- /src/lib/files/helpers.js: -------------------------------------------------------------------------------- 1 | import FS from 'react-native-fs'; 2 | 3 | import { filesFolder, largeFileExtension, precloudExtension } from './constant'; 4 | 5 | export function extractFileNameAndExtension(name) { 6 | if (!name) { 7 | return { 8 | fileName: '', 9 | extension: '', 10 | extensionWithoutDot: '', 11 | }; 12 | } 13 | 14 | const parts = name.split('.'); 15 | const last = (parts[parts.length - 1] || '').toLowerCase(); 16 | let extension = ''; 17 | let extensionWithoutDot = ''; 18 | if (last === precloudExtension) { 19 | extension = `.${precloudExtension}`; 20 | extensionWithoutDot = precloudExtension; 21 | parts.pop(); 22 | } 23 | 24 | if (last === largeFileExtension) { 25 | extension = `.${largeFileExtension}`; 26 | extensionWithoutDot = largeFileExtension; 27 | parts.pop(); 28 | } 29 | 30 | if (parts.length === 1) { 31 | return name.startsWith('.') 32 | ? { 33 | fileName: '', 34 | extension: `.${parts[0]}${extension}`, 35 | extensionWithoutDot: `${parts[0]}${extension}`, 36 | } 37 | : { fileName: parts[0], extension, extensionWithoutDot }; 38 | } 39 | 40 | return { 41 | fileName: parts.slice(0, parts.length - 1).join('.'), 42 | extension: `.${parts[parts.length - 1]}${extension}`, 43 | extensionWithoutDot: `${parts[parts.length - 1]}${extension}`, 44 | }; 45 | } 46 | 47 | export async function statFile(path) { 48 | try { 49 | const info = await FS.stat(path); 50 | return { ...info, name: info.path.split('/').pop() }; 51 | } catch (e) { 52 | console.log('stat file failed', e); 53 | return null; 54 | } 55 | } 56 | 57 | export async function getFolderSize(folderPath) { 58 | if (!folderPath) { 59 | return 0; 60 | } 61 | 62 | try { 63 | const info = await FS.stat(folderPath); 64 | if (info.isFile()) { 65 | return info.size; 66 | } 67 | 68 | const filesOrFolders = await FS.readDir(folderPath); 69 | if (!filesOrFolders.length) { 70 | return 0; 71 | } 72 | 73 | let size = 0; 74 | for (let i = 0; i < filesOrFolders.length; i += 1) { 75 | const fileOrFolder = filesOrFolders[i]; 76 | if (fileOrFolder.isFile()) { 77 | size += fileOrFolder.size; 78 | } else { 79 | const folderSize = await getFolderSize(fileOrFolder.path); 80 | size += folderSize; 81 | } 82 | } 83 | 84 | return size; 85 | } catch (e) { 86 | return 0; 87 | } 88 | } 89 | 90 | export async function emptyFolder(folderPath) { 91 | try { 92 | const filesOrFolders = await FS.readDir(folderPath); 93 | if (!filesOrFolders.length) { 94 | return; 95 | } 96 | 97 | for (let i = 0; i < filesOrFolders.length; i += 1) { 98 | try { 99 | await FS.unlink(filesOrFolders[i].path); 100 | } catch (e) {} 101 | } 102 | } catch (e) {} 103 | } 104 | 105 | export function extractFilePath(path) { 106 | if (path.startsWith('file:///')) { 107 | return path.slice(7); 108 | } else if (path.startsWith('file://')) { 109 | return path.slice(6); 110 | } else if (path.startsWith('file:/')) { 111 | return path.slice(5); 112 | } 113 | return path; 114 | } 115 | 116 | export function getOriginalFileName(encryptedFileName) { 117 | const parts = encryptedFileName.split('.'); 118 | parts.pop(); 119 | return parts.join('.'); 120 | } 121 | 122 | export function getParentPath(path) { 123 | if (!path) { 124 | return null; 125 | } 126 | 127 | const parts = path.split('/'); 128 | parts.pop(); 129 | const parentPath = parts.join('/'); 130 | 131 | return parentPath; 132 | } 133 | 134 | export function getFileName(path) { 135 | if (!path) { 136 | return null; 137 | } 138 | 139 | const parts = path.split('/'); 140 | return parts.pop(); 141 | } 142 | 143 | function toFixed2(number) { 144 | return +number.toFixed(2); 145 | } 146 | 147 | export function getSizeText(size) { 148 | if (!size) { 149 | return '0KB'; 150 | } 151 | 152 | const kbs = size / 1024; 153 | if (kbs < 1024) { 154 | return `${toFixed2(kbs)}KB`; 155 | } 156 | 157 | return `${toFixed2(kbs / 1024)}MB`; 158 | } 159 | 160 | export function isRootFolder(path) { 161 | if (!path) { 162 | return false; 163 | } 164 | 165 | const parentPath = getParentPath(path); 166 | 167 | return parentPath === filesFolder; 168 | } 169 | 170 | export function isLargeFile(file) { 171 | return !!file?.isDirectory && !!file.isDirectory() && file.name.endsWith(largeFileExtension); 172 | } 173 | -------------------------------------------------------------------------------- /src/components/FolderActions.js: -------------------------------------------------------------------------------- 1 | import { Actionsheet, Divider } from 'native-base'; 2 | import React, { useEffect, useState } from 'react'; 3 | 4 | import { asyncForEach } from '../lib/array'; 5 | import { platforms } from '../lib/constants'; 6 | import { deleteFile } from '../lib/files/actions'; 7 | import { encryptFiles } from '../lib/openpgp/encryptFiles'; 8 | import { showToast } from '../lib/toast'; 9 | import { useStore } from '../store/store'; 10 | import AddNoteButton from './AddNoteButton'; 11 | import DownloadRemoteFileButton from './DownloadRemoteFileButton'; 12 | import PickFilesButton from './PickFilesButton'; 13 | import PickImagesButton from './PickImagesButton'; 14 | import PickNotesButton from './PickNotesButton'; 15 | import PlatformToggle from './PlatformToggle'; 16 | import TakePhotoButton from './TakePhotoButton'; 17 | 18 | async function deleteFiles(pickedFiles) { 19 | await asyncForEach( 20 | pickedFiles.map(f => f.path), 21 | async path => { 22 | await deleteFile(path); 23 | } 24 | ); 25 | } 26 | 27 | function FolderActions({ folder, isOpen, onClose, onAddFile, onPickNotes, selectedFiles, navigate }) { 28 | const password = useStore(state => state.activePassword); 29 | 30 | const [isEncryptingFiles, setIsEncryptingFiles] = useState(false); 31 | const [isEncryptingImages, setIsEncryptingImages] = useState(false); 32 | const [isEncryptingNewImage, setIsEncryptingNewImage] = useState(false); 33 | const [isDownloading, setIsDownloading] = useState(false); 34 | const [isPickingNotes, setIsPickingNotes] = useState(false); 35 | 36 | useEffect(() => { 37 | if (selectedFiles?.length) { 38 | handleEncrypt(selectedFiles, setIsEncryptingNewImage); 39 | } 40 | // eslint-disable-next-line react-hooks/exhaustive-deps 41 | }, [selectedFiles]); 42 | 43 | async function handleEncrypt(pickedFiles, setIsEncrypting) { 44 | setIsEncrypting(true); 45 | 46 | const encryptedFiles = await encryptFiles(pickedFiles, { 47 | folder, 48 | onEncrypted: onAddFile, 49 | password, 50 | }); 51 | if (encryptedFiles.length) { 52 | showToast('Your files are encrypted and saved on your phone!'); 53 | } 54 | 55 | setIsEncrypting(false); 56 | await deleteFiles(pickedFiles); 57 | } 58 | 59 | const isLoading = 60 | isEncryptingFiles || isEncryptingImages || isEncryptingNewImage || isDownloading; 61 | return ( 62 | 63 | 64 | { 69 | await handleEncrypt([photo], setIsEncryptingNewImage); 70 | }} 71 | /> 72 | 73 | { 79 | await handleEncrypt(images, setIsEncryptingImages); 80 | }} 81 | /> 82 | 83 | { 89 | await handleEncrypt(files, setIsEncryptingFiles); 90 | }} 91 | /> 92 | { 98 | await handleEncrypt([file], setIsEncryptingFiles); 99 | }} 100 | /> 101 | 102 | 108 | { 115 | await onPickNotes(notes); 116 | }} 117 | /> 118 | 119 | 120 | ); 121 | } 122 | 123 | export default FolderActions; 124 | --------------------------------------------------------------------------------