Compare commits
174 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fab8b26cb | |||
| deb9d3d8e8 | |||
| fcc54566fc | |||
| 0118fcd6ce | |||
| 298eeafdba | |||
| 77ea075c23 | |||
| 38f91c2e25 | |||
| d9a1cd7895 | |||
| e954ddf991 | |||
| 74c570da38 | |||
| 8c99f2c6a3 | |||
| 34c1eae991 | |||
| e37d44c506 | |||
| 33dba03c47 | |||
| a7bdd4b6a6 | |||
| 1c1d64e719 | |||
| 95ce1b6768 | |||
| f73564a1a4 | |||
| 5c235b5197 | |||
| 31bcece857 | |||
| aa12f8f249 | |||
| 800b831958 | |||
| 3b45a32727 | |||
| c311900e27 | |||
| 71d81904ad | |||
| 6fb98f7193 | |||
| 8721bc484c | |||
| 68d8a55034 | |||
| 36a77111ce | |||
| 1e504a6143 | |||
| 2b116c375d | |||
| c35afa2dbf | |||
| 6b441294d9 | |||
| 99069bac8e | |||
| 3633108006 | |||
| 7a6643cc1a | |||
| f223616ee1 | |||
| ae2a05f9b0 | |||
| 81cc3087fd | |||
| d8c4278fec | |||
| e87d569b8b | |||
| 829b4bf242 | |||
| dcf7c07a85 | |||
| 4f24357420 | |||
| 3b187a5a4e | |||
| 5b9d904a02 | |||
| f018332dcc | |||
| 1e94c6d7c9 | |||
| 40f9ea23f3 | |||
| 33a657cda4 | |||
| 229f864b54 | |||
| 09c808f99b | |||
| 37b607aee2 | |||
| f56b94463f | |||
| c2af69cfec | |||
| 8b3a5359e8 | |||
| 670857f5ff | |||
| 52094573e2 | |||
| 1f3cdb9438 | |||
| 6b5ed65e90 | |||
| 9940c2c46e | |||
| f7fc81d865 | |||
| 10b9ec8818 | |||
| 712f65e001 | |||
| 7d109878fc | |||
| bce21ff9a9 | |||
| 95b0cc5059 | |||
| f0e6cae055 | |||
| dea67c88f5 | |||
| 8a29f08dca | |||
| 655f6b31d8 | |||
| 0cebe910c4 | |||
| 77c7a57463 | |||
| 342f9798b2 | |||
| 9cfc93cd8e | |||
| 977ed07d97 | |||
| 1813bf82f9 | |||
| 9b5a717c7d | |||
| 76f81841f4 | |||
| 585017eadd | |||
| fe9d9b4fdc | |||
| a37607eae6 | |||
| 54bc836811 | |||
| 992161e3ce | |||
| 534f554986 | |||
| 407fa834ab | |||
| 16d2afaf1e | |||
| 765a8ae3b2 | |||
| 0eb3c93a17 | |||
| 96d7deb47c | |||
| 2a49e43065 | |||
| 6aef594b29 | |||
| eb53b8b87e | |||
| f34df0233c | |||
| 37e1d54358 | |||
| 7faa18bd60 | |||
| 3c02dd77bb | |||
| 272b1f387d | |||
| c5aaf1b29b | |||
| 2bbcadcf53 | |||
| f4ef00d4ea | |||
| 953e080032 | |||
| 10fdcecf61 | |||
| cfef925414 | |||
| 7cab47665f | |||
| 9fb08fb66f | |||
| 388985034f | |||
| f842ebe13c | |||
| 070c1487af | |||
| a624b5c015 | |||
| 7f945c9fdd | |||
| d6a18bb44d | |||
| 09de52b5df | |||
| 1584d2d368 | |||
| d1ac12d165 | |||
| 03b359019e | |||
| b93983243e | |||
| 009b6b38e1 | |||
| f462b349fc | |||
| 64124c2fd5 | |||
| cce8e4ef0e | |||
| 7d6a577eeb | |||
| dc8f829d34 | |||
| 1f5ba4de12 | |||
| fbc561622c | |||
| 426d35ab78 | |||
| 1ec6fd9296 | |||
| a53c8ce4f7 | |||
| c6ffdaa46e | |||
| 3625d5a4bb | |||
| 50d01cbaa4 | |||
| 1e734082af | |||
| 449c84e75e | |||
| f282e24308 | |||
| d1efd1d587 | |||
| 4d32c523ff | |||
| d2a452e3c9 | |||
| 4bff5a3d68 | |||
| 233d14968d | |||
| 9596f8e52f | |||
| 4f93d30872 | |||
| f188f018b9 | |||
| 65d0e4f3a1 | |||
| c60347badf | |||
| 6c1fa662f9 | |||
| 0bf7f2525e | |||
| 7c70f7454e | |||
| 4fedb1928c | |||
| 87a68ba9c5 | |||
| 4b06f03aec | |||
| 948c7c0b24 | |||
| 40083d819f | |||
| c5162beb3b | |||
| f65d06edb3 | |||
| e08c6be97c | |||
| ea669c1fac | |||
| 940c7806ee | |||
| 8b8ae0d436 | |||
| 3fd9cc0e02 | |||
| 1514e21726 | |||
| 0b447178c5 | |||
| f3cabe5ca1 | |||
| e0f64173bf | |||
| 36ce29c346 | |||
| 4676c50504 | |||
| 886a952e53 | |||
| 5b1544c8f4 | |||
| f0155b342f | |||
| e532c3d94c | |||
| 2535d056b7 | |||
| 9ff117ce4d | |||
| 78e4d109c7 | |||
| aa2de9e335 | |||
| e78337a8b3 |
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"mocha": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"extends": ["eslint:recommended", "plugin:react/recommended"],
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"ecmaVersion": 2018
|
|
||||||
},
|
|
||||||
"plugins": ["react"],
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": require("./package.json").dependencies.react
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"indent": ["error", 2],
|
|
||||||
"no-console": ["error", { "allow": ["warn", "error"] }],
|
|
||||||
"space-before-function-paren": 0,
|
|
||||||
"semi": ["warn", "never"],
|
|
||||||
"space-infix-ops": ["warn"],
|
|
||||||
"no-var": "error",
|
|
||||||
"prefer-const": "error",
|
|
||||||
"no-trailing-spaces": "error",
|
|
||||||
"react/prop-types": 2,
|
|
||||||
"max-len": [
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
"ignoreStrings": true,
|
|
||||||
"ignoreComments": true,
|
|
||||||
"ignoreTemplateLiterals": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-multi-spaces": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['eslint:recommended', 'plugin:react/recommended'],
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
mocha: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
parser: '@babel/eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
requireConfigFile: false,
|
||||||
|
babelOptions: {
|
||||||
|
presets: ['@babel/preset-react'],
|
||||||
|
},
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
},
|
||||||
|
plugins: ['react'],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: require('./package.json').dependencies.react,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||||
|
'no-var': 'error',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'react/prop-types': 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
+5
-1
@@ -23,7 +23,7 @@ DerivedData
|
|||||||
*.hmap
|
*.hmap
|
||||||
*.ipa
|
*.ipa
|
||||||
*.xcuserstate
|
*.xcuserstate
|
||||||
project.xcworkspace
|
xcshareddata
|
||||||
ios/Index/DataStore
|
ios/Index/DataStore
|
||||||
|
|
||||||
# Android/IntelliJ
|
# Android/IntelliJ
|
||||||
@@ -44,6 +44,7 @@ yarn-error.log
|
|||||||
buck-out/
|
buck-out/
|
||||||
\.buckd/
|
\.buckd/
|
||||||
*.keystore
|
*.keystore
|
||||||
|
!debug.keystore
|
||||||
|
|
||||||
# fastlane
|
# fastlane
|
||||||
#
|
#
|
||||||
@@ -59,6 +60,9 @@ buck-out/
|
|||||||
# Bundle artifact
|
# Bundle artifact
|
||||||
*.jsbundle
|
*.jsbundle
|
||||||
|
|
||||||
|
# CocoaPods
|
||||||
|
/ios/Pods/
|
||||||
|
|
||||||
# RN android release
|
# RN android release
|
||||||
android/app/bin/
|
android/app/bin/
|
||||||
android/app/release/
|
android/app/release/
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
image: node:8
|
image: node:14
|
||||||
|
|
||||||
# This folder is cached between builds
|
# This folder is cached between builds
|
||||||
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
|
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
|
open-pull-requests-limit: 3
|
||||||
|
reviewers:
|
||||||
|
- "bl00dymarie"
|
||||||
|
- "mariyaz"
|
||||||
|
- "sdvig"
|
||||||
|
- "LisaHill"
|
||||||
|
allow:
|
||||||
|
- dependency-type: direct
|
||||||
|
- dependency-type: production
|
||||||
|
rebase-strategy: "auto"
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx pretty-quick --staged
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
semi: false,
|
||||||
|
}
|
||||||
+1
-1
@@ -14,7 +14,7 @@ So good to see you here, hello :wave\_tone1: :wave\_tone2: :wave\_tone3: :wave\_
|
|||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
You just want to say hello? Send us a [nice email](mailto:drip@mailbox.org?Subject=Nice%20incoming%20mail) :postbox: or tweet :bird: at us [@bl00dyhealth](https://twitter.com/bl00dyhealth).
|
You just want to say hello? Send us a [nice email](mailto:drip@mailbox.org?Subject=Nice%20incoming%20mail) :postbox:, ask to [join our Slack](mailto:drip@mailbox.org?Subject=Join%20Slack) or tweet :bird: at us [@dripberlin](https://twitter.com/dripberlin).
|
||||||
|
|
||||||
## What should I know before I get started?
|
## What should I know before I get started?
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# drip, the open-source cycle tracking app
|
# drip, the open-source cycle tracking app
|
||||||
|
|
||||||
A menstrual cycle tracking app that's open-source and leaves your data on your phone. Use it to track your menstrual cycle and/or for fertility awareness!
|
A menstrual cycle tracking app that's open-source and leaves your data on your phone. Use it to track your menstrual cycle and/or for fertility awareness!
|
||||||
Find more information on [our website](https://bloodyhealth.gitlab.io/).
|
Find more information on [our website](https://dripapp.org/).
|
||||||
|
|
||||||
[<img src="https://bloodyhealth.gitlab.io/assets/get.png"
|
[<img src="https://dripapp.org/assets/get.png"
|
||||||
alt="Get it here"
|
alt="Get it here"
|
||||||
height="55">](https://bloodyhealth.gitlab.io/release/5.apk)
|
height="55">](https://dripapp.org/release/8.apk)
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||||
alt="Get it on F-Droid"
|
alt="Get it on F-Droid"
|
||||||
height="80">](https://f-droid.org/packages/com.drip/)
|
height="80">](https://f-droid.org/packages/com.drip/)
|
||||||
@@ -25,14 +25,12 @@ The app is built in React Native and currently developed for Android.
|
|||||||
|
|
||||||
Install [Android Studio](https://developer.android.com/studio/) - you'll need it to install some dependencies.
|
Install [Android Studio](https://developer.android.com/studio/) - you'll need it to install some dependencies.
|
||||||
|
|
||||||
#### 2. Node version
|
#### 2. Node & npm version
|
||||||
|
|
||||||
Make sure you are running Node 10 (newer versions won't work). It's easiest to switch Node versions using `nvm`, here's how to do it:
|
|
||||||
|
|
||||||
|
Make sure you are running Node 14 and npm 6.14.17. It's easiest to switch Node versions using `nvm`, here's how to do it:
|
||||||
|
|
||||||
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
|
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
|
||||||
$ nvm install v10
|
$ nvm install v14.19.3
|
||||||
|
|
||||||
|
|
||||||
#### 3. Get this repository
|
#### 3. Get this repository
|
||||||
|
|
||||||
@@ -53,61 +51,108 @@ and run
|
|||||||
|
|
||||||
Open Android Studio and click on "Open an existing Android Studio project". Navigate to the drip repository you cloned and double click the android folder. It detects, downloads and cofigures requirements that might be missing, like the NDK and CMake to build the native code part of the project. Also see the [nodejs-mobile repository](https://github.com/janeasystems/nodejs-mobile) for the necessary prerequisites for your system.
|
Open Android Studio and click on "Open an existing Android Studio project". Navigate to the drip repository you cloned and double click the android folder. It detects, downloads and cofigures requirements that might be missing, like the NDK and CMake to build the native code part of the project. Also see the [nodejs-mobile repository](https://github.com/janeasystems/nodejs-mobile) for the necessary prerequisites for your system.
|
||||||
|
|
||||||
#### 5. Run the app
|
#### 5. Run the app on Android
|
||||||
|
|
||||||
Either start a [virtual device in Android Studio](https://developer.android.com/studio/run/emulator) or [set your physical device like your Android phone up](https://developer.android.com/training/basics/firstapp/running-app) to run the app.
|
Either start a [virtual device in Android Studio](https://developer.android.com/studio/run/emulator) or [set your physical device like your Android phone up](https://developer.android.com/training/basics/firstapp/running-app) to run the app.
|
||||||
|
|
||||||
1. Open a terminal and run
|
1. Open a terminal and run
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm run android
|
$ npm run android
|
||||||
```
|
```
|
||||||
|
|
||||||
1. To see logging output, run the following command in another tab:
|
2. To see logging output, run the following command in another tab:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm run log
|
$ npm run log
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Run the following command and select enable hot reloading (see https://facebook.github.io/react-native/docs/debugging.html):
|
3. Run the following command and select enable hot reloading (see https://facebook.github.io/react-native/docs/debugging.html):
|
||||||
|
|
||||||
```
|
```
|
||||||
$ adb shell input keyevent 82
|
$ adb shell input keyevent 82
|
||||||
```
|
```
|
||||||
|
|
||||||
1. We recommend installing an [ESLint plugin in your editor](https://eslint.org/docs/user-guide/integrations#editors). There's an `.eslintrc` file in this project which will be used by the plugin to check your code for style errors and potential bugs.
|
4. We recommend installing an [ESLint plugin in your editor](https://eslint.org/docs/user-guide/integrations#editors). There's an `.eslintrc` file in this project which will be used by the plugin to check your code for style errors and potential bugs.
|
||||||
|
|
||||||
|
#### 6. Run app on iOS
|
||||||
|
|
||||||
|
Minimum system requirements to run iOS app are as follows:
|
||||||
|
|
||||||
|
- MacOS 10.15.7 for Mac users
|
||||||
|
- Xcode 12.3 (I assume, that only command line tools might be enough)
|
||||||
|
|
||||||
|
1. Install XCode dependencies by running the following command from the root project directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd ios && pod install && cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
2. To run app either open drip workspace ('drip.xcworkspace' file) with XCode and run "Build" or run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run ios
|
||||||
|
```
|
||||||
|
|
||||||
|
3. If you are building the app with XCode make sure you are running this as well:
|
||||||
|
`$ npm start`
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
#### [MacOS] Java problems
|
#### [MacOS] Java problems
|
||||||
|
|
||||||
Make sure that you have Java 1.8 by running `java -version`.
|
Make sure that you have Java 1.8 by running `java -version`.
|
||||||
|
|
||||||
If you don't have Java installed, or your Java version is different, the app may not work. You can try just using Android Studio's Java by prepending it to your `$PATH` in your shell profile:
|
If you don't have Java installed, or your Java version is different, the app may not work. You can try just using Android Studio's Java by prepending it to your `$PATH` in your shell profile:
|
||||||
|
`$ export PATH="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin:${PATH}"`
|
||||||
```
|
|
||||||
$ export PATH="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin:${PATH}"
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, `which java` should output `/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java`, and the correct Java version should be used.
|
Now, `which java` should output `/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java`, and the correct Java version should be used.
|
||||||
|
|
||||||
#### [MacOS] Ninja
|
#### [MacOS] Ninja
|
||||||
|
|
||||||
If `npm` says `CMake was unable to find a build program corresponding to "Ninja".`:
|
If `npm` says `CMake was unable to find a build program corresponding to "Ninja".`:
|
||||||
```
|
`$ brew install ninja`
|
||||||
$ brew install ninja
|
|
||||||
```
|
|
||||||
|
|
||||||
### [MacOS] adb not on the path
|
### [MacOS] adb not on the path
|
||||||
|
|
||||||
If you get error messages about `adb` not being found on your path:
|
If you get error messages about `adb` not being found on your path:
|
||||||
```
|
`$ ln -s ~/Library/Android/sdk/platform-tools/adb /usr/local/bin/adb`
|
||||||
$ ln -s ~/Library/Android/sdk/platform-tools/adb /usr/local/bin/adb
|
|
||||||
```
|
### [MacOS] and XCode 12.5
|
||||||
|
|
||||||
|
If you run XCode 12.5, more likely you'll have problems building app for iOS. Please use the following fix: https://stackoverflow.com/a/67320887.
|
||||||
|
|
||||||
|
If you experience any further issues, please feel free to check out the following threads:
|
||||||
|
|
||||||
|
- [react-native run-ios build failure on XCode 12.5 beta](https://github.com/react-native-community/cli/issues/1365)
|
||||||
|
- [Xcode 12.5 troubleshooting guide (RN 0.61/0.62/0.63/0.64)](https://github.com/facebook/react-native/issues/31480)
|
||||||
|
|
||||||
|
### Clearing project cache
|
||||||
|
|
||||||
|
If you would like to clear project cache and/or re-install project libraries, you can run clear script as follows:
|
||||||
|
|
||||||
|
$ npm run clear
|
||||||
|
|
||||||
|
Script accepts the following options:
|
||||||
|
"none" - script will delete all caches and re-install project libraries,
|
||||||
|
"ios" - script will delete ios-related cache
|
||||||
|
"android" - script will delete android-related cache
|
||||||
|
"cache" - script will purge Watchman, Metrobundler, Pachager and React caches
|
||||||
|
"npm" - script will reinstall project libraries.
|
||||||
|
|
||||||
|
For example, if you would like to clear android part of the project and re-install project libraries, you can run the following command:
|
||||||
|
|
||||||
|
$ npm run clear android npm
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
### Unit tests
|
### Unit tests
|
||||||
|
|
||||||
You can run the tests with:
|
You can run the tests with:
|
||||||
```
|
`$ npm test`
|
||||||
$ npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
### End to end tests
|
### End to end tests
|
||||||
|
|
||||||
1. Check what testing device is specified in [package.json](https://gitlab.com/bloodyhealth/drip/blob/master/package.json) under:
|
1. Check what testing device is specified in [package.json](https://gitlab.com/bloodyhealth/drip/blob/master/package.json) under:
|
||||||
```
|
```
|
||||||
{"detox":
|
{"detox":
|
||||||
@@ -119,28 +164,26 @@ You can run the tests with:
|
|||||||
2. Check if the current device is already installed on your machine. Go to `cd ~/Android/sdk/emulator/` or wherever you have Android installed on your machine. Here you can run `./emulator -list-avds` and compare the devices with the one you found in step 1.
|
2. Check if the current device is already installed on your machine. Go to `cd ~/Android/sdk/emulator/` or wherever you have Android installed on your machine. Here you can run `./emulator -list-avds` and compare the devices with the one you found in step 1.
|
||||||
3. Open Android Studio and go to -> Tools -> AVD manager -> `+Create virtual device` and select the device checked in the previous step
|
3. Open Android Studio and go to -> Tools -> AVD manager -> `+Create virtual device` and select the device checked in the previous step
|
||||||
4. Use the emulator on your machine to run it without heavy Android Studio, e.g. in `~/Android/Sdk/emulator` OR chose to run the emulator within Android Studio
|
4. Use the emulator on your machine to run it without heavy Android Studio, e.g. in `~/Android/Sdk/emulator` OR chose to run the emulator within Android Studio
|
||||||
4.1 Here run: `$ ./emulator -avd NEXUS_DEVICE_OR_WHATEVER_SPECIFIED_DEVICE`
|
4.1 Here run: `$ ./emulator -avd NEXUS_DEVICE_OR_WHATEVER_SPECIFIED_DEVICE`
|
||||||
4.2 You might need to specify the following environment variables in your zsh or bash file according to where you have it installed. You can find exact path in Android Studio (Android Studio Preferences → Appearance and Behavior → System Settings → Android SDK). After adding environment variables, you might need to restart your terminal or source the modified bash profile (i.e. "source ~/.bash_profile").
|
4.2 You might need to specify the following environment variables in your zsh or bash file according to where you have it installed. You can find exact path in Android Studio (Android Studio Preferences → Appearance and Behavior → System Settings → Android SDK). After adding environment variables, you might need to restart your terminal or source the modified bash profile (i.e. "source ~/.bash_profile").
|
||||||
```
|
`export ANDROID_HOME="/home/myname/Android/Sdk" export ANDROID_SDK_ROOT="/home/myname/Android/Sdk" export ANDROID_AVD_HOME="/home/myname/.android/avd"`
|
||||||
export ANDROID_HOME="/home/myname/Android/Sdk"
|
|
||||||
export ANDROID_SDK_ROOT="/home/myname/Android/Sdk"
|
|
||||||
export ANDROID_AVD_HOME="/home/myname/.android/avd"
|
|
||||||
```
|
|
||||||
5. For the first time you need to get the app on the phone or if you run into this error:
|
5. For the first time you need to get the app on the phone or if you run into this error:
|
||||||
`'app-debug-androidTest.apk' could not be found`
|
`'app-debug-androidTest.apk' could not be found`
|
||||||
--> open a new 2nd tab and run (in your drip folder): `cd android and ./gradlew assembleAndroidTest`
|
--> open a new 2nd tab and run (in your drip folder): `cd android and ./gradlew assembleAndroidTest`
|
||||||
Otherwise just open a new 2nd tab to run (in your drip folder) `npm run android`
|
Otherwise just open a new 2nd tab to run (in your drip folder) `npm run android`
|
||||||
6. Open a new 3rd tab to run `./node_modules/.bin/detox test -c android.emu.debug`
|
6. Open a new 3rd tab to run `./node_modules/.bin/detox test -c android.emu.debug`
|
||||||
|
|
||||||
Hopefully you see the magic happening clicking through the app and happy test results on your console :sun_with_face: !
|
Hopefully you see the magic happening clicking through the app and happy test results on your console :sun_with_face: !
|
||||||
|
|
||||||
## Debugging
|
## Debugging
|
||||||
|
|
||||||
In order to see logging output from the app, run `npm run log` in a separate terminal. You can output specific code you want to see, with:
|
In order to see logging output from the app, run `npm run log` in a separate terminal. You can output specific code you want to see, with:
|
||||||
`console.log(theVariableIWantToSeeHere)`
|
`console.log(theVariableIWantToSeeHere)`
|
||||||
or just a random string to check if this piece of code is actually running:
|
or just a random string to check if this piece of code is actually running:
|
||||||
`console.log("HELLO")`.
|
`console.log("HELLO")`.
|
||||||
|
|
||||||
## NFP rules
|
## NFP rules
|
||||||
|
|
||||||
More information about how the app calculates fertility status and bleeding predictions in the [wiki on Gitlab](https://gitlab.com/bloodyhealth/drip/wikis/home).
|
More information about how the app calculates fertility status and bleeding predictions in the [wiki on Gitlab](https://gitlab.com/bloodyhealth/drip/wikis/home).
|
||||||
|
|
||||||
## Adding a new tracking icon
|
## Adding a new tracking icon
|
||||||
@@ -153,3 +196,7 @@ More information about how the app calculates fertility status and bleeding pred
|
|||||||
$ react-native link
|
$ react-native link
|
||||||
```
|
```
|
||||||
5. You should be able to use the icon now within drip, e.g. in Cycle Day Overview and on the chart.
|
5. You should be able to use the icon now within drip, e.g. in Cycle Day Overview and on the chart.
|
||||||
|
|
||||||
|
## Translation
|
||||||
|
|
||||||
|
We are using [Weblate](https://weblate.org/) as translation software.
|
||||||
|
|||||||
+52
-12
@@ -18,6 +18,9 @@ import com.android.build.OutputFile
|
|||||||
* // the entry file for bundle generation
|
* // the entry file for bundle generation
|
||||||
* entryFile: "index.android.js",
|
* entryFile: "index.android.js",
|
||||||
*
|
*
|
||||||
|
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
|
||||||
|
* bundleCommand: "ram-bundle",
|
||||||
|
*
|
||||||
* // whether to bundle JS and assets in debug mode
|
* // whether to bundle JS and assets in debug mode
|
||||||
* bundleInDebug: false,
|
* bundleInDebug: false,
|
||||||
*
|
*
|
||||||
@@ -73,7 +76,8 @@ import com.android.build.OutputFile
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
project.ext.react = [
|
project.ext.react = [
|
||||||
entryFile: "index.js"
|
entryFile: "index.js",
|
||||||
|
enableHermes: false, // clean and rebuild if changing
|
||||||
]
|
]
|
||||||
|
|
||||||
apply from: "../../node_modules/react-native/react.gradle"
|
apply from: "../../node_modules/react-native/react.gradle"
|
||||||
@@ -94,6 +98,27 @@ def enableSeparateBuildPerCPUArchitecture = false
|
|||||||
*/
|
*/
|
||||||
def enableProguardInReleaseBuilds = false
|
def enableProguardInReleaseBuilds = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The preferred build flavor of JavaScriptCore.
|
||||||
|
*
|
||||||
|
* For example, to use the international variant, you can use:
|
||||||
|
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||||
|
*
|
||||||
|
* The international variant includes ICU i18n library and necessary data
|
||||||
|
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||||
|
* give correct results when using with locales other than en-US. Note that
|
||||||
|
* this variant is about 6MiB larger per architecture than default.
|
||||||
|
*/
|
||||||
|
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||||
|
/**
|
||||||
|
* Whether to enable the Hermes VM.
|
||||||
|
*
|
||||||
|
* This should be set on project.ext.react and mirrored here. If it is not set
|
||||||
|
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
|
||||||
|
* and the benefits of using Hermes will therefore be sharply reduced.
|
||||||
|
*/
|
||||||
|
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
@@ -110,12 +135,18 @@ android {
|
|||||||
versionCode 8
|
versionCode 8
|
||||||
versionName "1.2102.28"
|
versionName "1.2102.28"
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
abiFilters "armeabi-v7a", "x86"
|
||||||
}
|
}
|
||||||
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type
|
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
debug {
|
||||||
|
storeFile file('debug.keystore')
|
||||||
|
storePassword 'android'
|
||||||
|
keyAlias 'androiddebugkey'
|
||||||
|
keyPassword 'android'
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
if (project.hasProperty('DRIP_RELEASE_STORE_FILE')) {
|
if (project.hasProperty('DRIP_RELEASE_STORE_FILE')) {
|
||||||
storeFile file(DRIP_RELEASE_STORE_FILE)
|
storeFile file(DRIP_RELEASE_STORE_FILE)
|
||||||
@@ -134,7 +165,13 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
|
// Caution! In production, you need to generate your own keystore file.
|
||||||
|
// see https://facebook.github.io/react-native/docs/signed-apk-android.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
minifyEnabled enableProguardInReleaseBuilds
|
minifyEnabled enableProguardInReleaseBuilds
|
||||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
@@ -144,8 +181,8 @@ android {
|
|||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
variant.outputs.each { output ->
|
variant.outputs.each { output ->
|
||||||
// For each separate APK per architecture, set a unique version code as described here:
|
// For each separate APK per architecture, set a unique version code as described here:
|
||||||
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
|
// https://developer.android.com/studio/build/configure-apk-splits.html
|
||||||
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a":3, "x86_64":4]
|
def versionCodes = ["armeabi-v7a": 1, "x86": 2]
|
||||||
def abi = output.getFilter(OutputFile.ABI)
|
def abi = output.getFilter(OutputFile.ABI)
|
||||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||||
output.versionCodeOverride =
|
output.versionCodeOverride =
|
||||||
@@ -156,18 +193,19 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':realm')
|
|
||||||
implementation project(':react-native-vector-icons')
|
|
||||||
implementation project(':react-native-share')
|
|
||||||
implementation project(':react-native-restart')
|
|
||||||
implementation project(':react-native-push-notification')
|
|
||||||
implementation project(':react-native-fs')
|
|
||||||
implementation project(':react-native-document-picker')
|
|
||||||
implementation project(':nodejs-mobile-react-native')
|
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||||
implementation 'androidx.annotation:annotation:1.1.0'
|
implementation 'androidx.annotation:annotation:1.1.0'
|
||||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||||
|
|
||||||
|
if (enableHermes) {
|
||||||
|
def hermesPath = "../../node_modules/hermes-engine/android/";
|
||||||
|
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||||
|
releaseImplementation files(hermesPath + "hermes-release.aar")
|
||||||
|
} else {
|
||||||
|
implementation jscFlavor
|
||||||
|
}
|
||||||
|
|
||||||
androidTestImplementation('com.wix:detox:+') { transitive = true }
|
androidTestImplementation('com.wix:detox:+') { transitive = true }
|
||||||
androidTestImplementation 'junit:junit:4.12'
|
androidTestImplementation 'junit:junit:4.12'
|
||||||
}
|
}
|
||||||
@@ -178,3 +216,5 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
|||||||
from configurations.compile
|
from configurations.compile
|
||||||
into 'libs'
|
into 'libs'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||||
|
|||||||
Binary file not shown.
@@ -10,6 +10,8 @@
|
|||||||
<uses-permission tools:node="remove" android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission tools:node="remove" android:name="android.permission.READ_PHONE_STATE" />
|
||||||
<uses-permission tools:node="remove" android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission tools:node="remove" android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission tools:node="remove" android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission tools:node="remove" android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
|
||||||
<permission
|
<permission
|
||||||
android:name="${applicationId}.permission.C2D_MESSAGE"
|
android:name="${applicationId}.permission.C2D_MESSAGE"
|
||||||
@@ -49,20 +51,29 @@
|
|||||||
android:resource="@xml/filepaths" />
|
android:resource="@xml/filepaths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_name"
|
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
|
||||||
android:value="drip-notification"/>
|
android:value="false"/>
|
||||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_description"
|
|
||||||
android:value="notifications from drip"/>
|
|
||||||
<!-- Change the resource name to your App's accent color - or any other color you want -->
|
<!-- Change the resource name to your App's accent color - or any other color you want -->
|
||||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
|
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
|
||||||
android:resource="@android:color/white"/>
|
android:resource="@android:color/white"/> <!-- or @android:color/{name} to use a standard color -->
|
||||||
|
|
||||||
|
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
|
||||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
|
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
|
||||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
|
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
|
||||||
|
android:exported="false" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,8 +5,8 @@ import com.facebook.react.ReactActivity;
|
|||||||
public class MainActivity extends ReactActivity {
|
public class MainActivity extends ReactActivity {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the main component registered from JavaScript.
|
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||||
* This is used to schedule rendering of the component.
|
* rendering of the component.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected String getMainComponentName() {
|
protected String getMainComponentName() {
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
package com.drip;
|
package com.drip;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
import com.facebook.react.PackageList;
|
||||||
import com.facebook.react.ReactApplication;
|
import com.facebook.react.ReactApplication;
|
||||||
import com.janeasystems.rn_nodejs_mobile.RNNodeJsMobilePackage;
|
|
||||||
import com.avishayil.rnrestart.ReactNativeRestartPackage;
|
|
||||||
import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage;
|
|
||||||
import com.oblador.vectoricons.VectorIconsPackage;
|
|
||||||
import com.rnfs.RNFSPackage;
|
|
||||||
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
|
|
||||||
import cl.json.RNSharePackage;
|
|
||||||
import cl.json.ShareApplication;
|
import cl.json.ShareApplication;
|
||||||
import io.realm.react.RealmReactPackage;
|
|
||||||
import com.facebook.react.ReactNativeHost;
|
import com.facebook.react.ReactNativeHost;
|
||||||
import com.facebook.react.ReactPackage;
|
import com.facebook.react.ReactPackage;
|
||||||
import com.facebook.react.shell.MainReactPackage;
|
|
||||||
import com.facebook.soloader.SoLoader;
|
import com.facebook.soloader.SoLoader;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class MainApplication extends Application implements ReactApplication, ShareApplication {
|
public class MainApplication extends Application implements ReactApplication, ShareApplication {
|
||||||
|
|
||||||
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
|
private final ReactNativeHost mReactNativeHost =
|
||||||
|
new ReactNativeHost(this) {
|
||||||
@Override
|
@Override
|
||||||
public boolean getUseDeveloperSupport() {
|
public boolean getUseDeveloperSupport() {
|
||||||
return BuildConfig.DEBUG;
|
return BuildConfig.DEBUG;
|
||||||
@@ -30,17 +22,11 @@ public class MainApplication extends Application implements ReactApplication, Sh
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<ReactPackage> getPackages() {
|
protected List<ReactPackage> getPackages() {
|
||||||
return Arrays.<ReactPackage>asList(
|
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||||
new MainReactPackage(),
|
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||||
new RNNodeJsMobilePackage(),
|
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||||
new ReactNativeRestartPackage(),
|
// packages.add(new MyReactNativePackage());
|
||||||
new ReactNativePushNotificationPackage(),
|
return packages;
|
||||||
new VectorIconsPackage(),
|
|
||||||
new RNFSPackage(),
|
|
||||||
new ReactNativeDocumentPicker(),
|
|
||||||
new RNSharePackage(),
|
|
||||||
new RealmReactPackage()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -58,6 +44,32 @@ public class MainApplication extends Application implements ReactApplication, Sh
|
|||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
SoLoader.init(this, /* native exopackage */ false);
|
SoLoader.init(this, /* native exopackage */ false);
|
||||||
|
initializeFlipper(this); // Remove this line if you don't want Flipper enabled
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Loads Flipper in React Native templates.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
*/
|
||||||
|
private static void initializeFlipper(Context context) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
We use reflection here to pick up the class that initializes Flipper,
|
||||||
|
since Flipper library is not available in release mode
|
||||||
|
*/
|
||||||
|
Class<?> aClass = Class.forName("com.facebook.flipper.ReactNativeFlipper");
|
||||||
|
aClass.getMethod("initializeFlipper", Context.class).invoke(null, context);
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="android:textColor">#000000</item>
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
|||||||
+12
-5
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
ext.kotlinVersion = '1.3.10'
|
ext.kotlinVersion = '1.3.10'
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.4.0'
|
classpath("com.android.tools.build:gradle:3.4.2")
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
@@ -18,16 +18,21 @@ buildscript {
|
|||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
jcenter()
|
|
||||||
maven {
|
maven {
|
||||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||||
url "$rootDir/../node_modules/react-native/android"
|
url("$rootDir/../node_modules/react-native/android")
|
||||||
}
|
}
|
||||||
|
maven {
|
||||||
|
// Android JSC is installed from npm
|
||||||
|
url("$rootDir/../node_modules/jsc-android/dist")
|
||||||
|
}
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
maven {
|
maven {
|
||||||
url 'https://maven.google.com/'
|
url 'https://maven.google.com/'
|
||||||
name 'Google'
|
name 'Google'
|
||||||
}
|
}
|
||||||
google()
|
|
||||||
maven {
|
maven {
|
||||||
// All of Detox' artifacts are provided via the npm module
|
// All of Detox' artifacts are provided via the npm module
|
||||||
url "$rootDir/../node_modules/detox/Detox-android"
|
url "$rootDir/../node_modules/detox/Detox-android"
|
||||||
@@ -36,11 +41,13 @@ allprojects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
googlePlayServicesVersion = "+" // default: "+"
|
||||||
|
firebaseMessagingVersion = "21.1.0" // default: "+"
|
||||||
|
|
||||||
buildToolsVersion = "29.0.3"
|
buildToolsVersion = "29.0.3"
|
||||||
minSdkVersion = 23
|
minSdkVersion = 23
|
||||||
compileSdkVersion = 29
|
compileSdkVersion = 29
|
||||||
targetSdkVersion = 29
|
targetSdkVersion = 29
|
||||||
supportLibVersion = "29.0.0"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
keystore(
|
|
||||||
name = "debug",
|
|
||||||
properties = "debug.keystore.properties",
|
|
||||||
store = "debug.keystore",
|
|
||||||
visibility = [
|
|
||||||
"PUBLIC",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
key.store=debug.keystore
|
|
||||||
key.alias=androiddebugkey
|
|
||||||
key.store.password=android
|
|
||||||
key.alias.password=android
|
|
||||||
+1
-16
@@ -1,19 +1,4 @@
|
|||||||
rootProject.name = 'drip'
|
rootProject.name = 'drip'
|
||||||
include ':nodejs-mobile-react-native'
|
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||||
project(':nodejs-mobile-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/nodejs-mobile-react-native/android')
|
|
||||||
include ':react-native-restart'
|
|
||||||
project(':react-native-restart').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-restart/android')
|
|
||||||
include ':react-native-push-notification'
|
|
||||||
project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android')
|
|
||||||
include ':react-native-vector-icons'
|
|
||||||
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
|
|
||||||
include ':react-native-fs'
|
|
||||||
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
|
|
||||||
include ':react-native-document-picker'
|
|
||||||
project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android')
|
|
||||||
include ':react-native-share'
|
|
||||||
project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android')
|
|
||||||
include ':realm'
|
|
||||||
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
|
|
||||||
|
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "drip",
|
"name": "drip.",
|
||||||
"displayName": "drip"
|
"displayName": "drip."
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { ScrollView, StyleSheet, View } from 'react-native'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { navigate } from '../slices/navigation'
|
||||||
|
import { getDate, setDate } from '../slices/date'
|
||||||
|
|
||||||
|
import AppText from './common/app-text'
|
||||||
|
import Button from './common/button'
|
||||||
|
|
||||||
|
import cycleModule from '../lib/cycle'
|
||||||
|
import { getFertilityStatusForDay } from '../lib/sympto-adapter'
|
||||||
|
import { determinePredictionText, formatWithOrdinalSuffix } from './helpers/home'
|
||||||
|
|
||||||
|
import { Colors, Fonts, Sizes, Spacing } from '../styles'
|
||||||
|
import { LocalDate } from 'js-joda'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const Home = ({ navigate, setDate }) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
function navigateToCycleDayView() {
|
||||||
|
setDate(todayDateString)
|
||||||
|
navigate('CycleDay')
|
||||||
|
}
|
||||||
|
|
||||||
|
const todayDateString = LocalDate.now().toString()
|
||||||
|
const { getCycleDayNumber, getPredictedMenses } = cycleModule()
|
||||||
|
const cycleDayNumber = getCycleDayNumber(todayDateString)
|
||||||
|
const { status, phase, statusText } =
|
||||||
|
getFertilityStatusForDay(todayDateString)
|
||||||
|
const prediction = determinePredictionText(getPredictedMenses(), t)
|
||||||
|
|
||||||
|
const cycleDayText = cycleDayNumber ? formatWithOrdinalSuffix(cycleDayNumber) : ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
style={styles.container}
|
||||||
|
contentContainerStyle={styles.contentContainer}
|
||||||
|
>
|
||||||
|
<AppText style={styles.title}>{moment().format("MMM Do YYYY")}</AppText>
|
||||||
|
|
||||||
|
{cycleDayNumber &&
|
||||||
|
<View style={styles.line}>
|
||||||
|
<AppText style={styles.whiteSubtitle}>{cycleDayText}</AppText>
|
||||||
|
<AppText style={styles.turquoiseText}>{t('labels.home.cycleDay')}</AppText>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
{phase &&
|
||||||
|
<View style={styles.line}>
|
||||||
|
<AppText style={styles.whiteSubtitle}>
|
||||||
|
{formatWithOrdinalSuffix(phase)}
|
||||||
|
</AppText>
|
||||||
|
<AppText style={styles.turquoiseText}>
|
||||||
|
{t('labels.home.cyclePhase')}
|
||||||
|
</AppText>
|
||||||
|
<AppText style={styles.turquoiseText}>{status}</AppText>
|
||||||
|
<Asterisk />
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
<View style={styles.line}>
|
||||||
|
<AppText style={styles.turquoiseText}>{prediction}</AppText>
|
||||||
|
</View>
|
||||||
|
<Button isCTA isSmall={false} onPress={navigateToCycleDayView}>
|
||||||
|
{t('labels.home.addDataForToday')}
|
||||||
|
</Button>
|
||||||
|
{phase && (
|
||||||
|
<View style={styles.asteriskLine}>
|
||||||
|
<Asterisk />
|
||||||
|
<AppText linkStyle={styles.whiteText} style={styles.greyText}>
|
||||||
|
{statusText}
|
||||||
|
</AppText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Asterisk = () => {
|
||||||
|
return <AppText style={styles.asterisk}>*</AppText>
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
asterisk: {
|
||||||
|
color: Colors.orange,
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
backgroundColor: Colors.purple,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
padding: Spacing.base,
|
||||||
|
paddingTop: 0,
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignContent: 'flex-start',
|
||||||
|
marginBottom: Spacing.tiny,
|
||||||
|
marginTop: Spacing.small,
|
||||||
|
},
|
||||||
|
asteriskLine: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignContent: 'flex-start',
|
||||||
|
marginBottom: Spacing.tiny,
|
||||||
|
marginTop: Spacing.small,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: Colors.purpleLight,
|
||||||
|
fontFamily: Fonts.bold,
|
||||||
|
fontSize: Sizes.huge,
|
||||||
|
marginVertical: Spacing.small,
|
||||||
|
},
|
||||||
|
turquoiseText: {
|
||||||
|
color: Colors.turquoise,
|
||||||
|
fontSize: Sizes.subtitle,
|
||||||
|
},
|
||||||
|
whiteSubtitle: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: Sizes.subtitle,
|
||||||
|
},
|
||||||
|
whiteText: {
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
greyText: {
|
||||||
|
color: Colors.greyLight,
|
||||||
|
paddingLeft: Spacing.base,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
return ({
|
||||||
|
date: getDate(state),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
return ({
|
||||||
|
navigate: (page) => dispatch(navigate(page)),
|
||||||
|
setDate: (date) => dispatch(setDate(date)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Home.propTypes = {
|
||||||
|
navigate: PropTypes.func,
|
||||||
|
setDate: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(Home)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { BackHandler, StyleSheet, View } from 'react-native'
|
import { BackHandler, StyleSheet, View } from 'react-native'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import AppPage from './common/app-page'
|
import AppPage from './common/app-page'
|
||||||
import AppText from './common/app-text'
|
import AppText from './common/app-text'
|
||||||
@@ -9,28 +10,27 @@ import Segment from './common/segment'
|
|||||||
|
|
||||||
import { saveLicenseFlag } from '../local-storage'
|
import { saveLicenseFlag } from '../local-storage'
|
||||||
|
|
||||||
import { shared } from '../i18n/en/labels'
|
|
||||||
import settingsLabels from '../i18n/en/settings'
|
|
||||||
import { Containers } from '../styles'
|
import { Containers } from '../styles'
|
||||||
|
|
||||||
const labels = settingsLabels.license
|
|
||||||
|
|
||||||
export default function License({ setLicense }) {
|
export default function License({ setLicense }) {
|
||||||
const onAcceptLicense = async () => {
|
const onAcceptLicense = async () => {
|
||||||
await saveLicenseFlag()
|
await saveLicenseFlag()
|
||||||
setLicense()
|
setLicense()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppPage testID="licensePage">
|
<AppPage testID="licensePage">
|
||||||
<Segment last testID="test" title={labels.title}>
|
<Segment last testID="test" title={t("settings.license.title")}>
|
||||||
<AppText testID="test">{labels.text}</AppText>
|
<AppText testID="test">{t("settings.license.text", { currentYear })}</AppText>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Button onPress={BackHandler.exitApp} testID="licenseCancelButton">
|
<Button onPress={BackHandler.exitApp} testID="licenseCancelButton">
|
||||||
{shared.cancel}
|
{t("labels.shared.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button isCTA onPress={onAcceptLicense} testID="licenseOkButton">
|
<Button isCTA onPress={onAcceptLicense} testID="licenseOkButton">
|
||||||
{shared.ok}
|
{t("labels.shared.ok")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</Segment>
|
</Segment>
|
||||||
+24
-10
@@ -1,16 +1,18 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
|
import { StyleSheet, View } from 'react-native'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
import nodejs from 'nodejs-mobile-react-native'
|
import nodejs from 'nodejs-mobile-react-native'
|
||||||
|
|
||||||
import { getLicenseFlag, saveEncryptionFlag } from '../local-storage'
|
import { getLicenseFlag, saveEncryptionFlag } from '../local-storage'
|
||||||
import { openDb } from '../db'
|
import { openDb } from '../db'
|
||||||
|
|
||||||
import App from './app'
|
import App from './app'
|
||||||
import PasswordPrompt from './password-prompt'
|
|
||||||
import License from './license'
|
|
||||||
import AppLoadingView from './common/app-loading'
|
import AppLoadingView from './common/app-loading'
|
||||||
|
import AppStatusBar from './common/app-status-bar'
|
||||||
|
import License from './License'
|
||||||
|
import PasswordPrompt from './password-prompt'
|
||||||
|
|
||||||
import store from "../store"
|
import store from '../store'
|
||||||
import { Provider } from 'react-redux'
|
|
||||||
|
|
||||||
export default class AppWrapper extends Component {
|
export default class AppWrapper extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -49,7 +51,7 @@ export default class AppWrapper extends Component {
|
|||||||
enableShowLicenseAgreement = () => {
|
enableShowLicenseAgreement = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
shouldShowLicenseAgreement: true,
|
shouldShowLicenseAgreement: true,
|
||||||
isCheckingLicenseAgreement: false
|
isCheckingLicenseAgreement: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +62,7 @@ export default class AppWrapper extends Component {
|
|||||||
enableShowApp = () => {
|
enableShowApp = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
shouldShowApp: true,
|
shouldShowApp: true,
|
||||||
shouldShowPasswordPrompt: false
|
shouldShowPasswordPrompt: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,14 +79,26 @@ export default class AppWrapper extends Component {
|
|||||||
if (isCheckingLicenseAgreement) {
|
if (isCheckingLicenseAgreement) {
|
||||||
initialView = <AppLoadingView />
|
initialView = <AppLoadingView />
|
||||||
} else if (shouldShowLicenseAgreement) {
|
} else if (shouldShowLicenseAgreement) {
|
||||||
initialView = <License setLicense={this.disableShowLicenseAgreement}/>
|
initialView = <License setLicense={this.disableShowLicenseAgreement} />
|
||||||
} else if (shouldShowPasswordPrompt) {
|
} else if (shouldShowPasswordPrompt) {
|
||||||
initialView = <PasswordPrompt enableShowApp={this.enableShowApp} />
|
initialView = <PasswordPrompt enableShowApp={this.enableShowApp} />
|
||||||
} else if (shouldShowApp) {
|
} else if (shouldShowApp) {
|
||||||
initialView = <App />
|
initialView = <App restartApp={() => this.checkDbPasswordSet()} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Provider store={store}>{initialView}</Provider>
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<AppStatusBar />
|
||||||
|
{initialView}
|
||||||
|
</View>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
+12
-15
@@ -17,12 +17,12 @@ import setupNotifications from '../lib/notifications'
|
|||||||
import { getCycleDay, closeDb } from '../db'
|
import { getCycleDay, closeDb } from '../db'
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
date: PropTypes.string,
|
date: PropTypes.string,
|
||||||
navigation: PropTypes.object.isRequired,
|
navigation: PropTypes.object.isRequired,
|
||||||
navigate: PropTypes.func,
|
navigate: PropTypes.func,
|
||||||
goBack: PropTypes.func,
|
goBack: PropTypes.func,
|
||||||
|
restartApp: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -54,7 +54,7 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { date, navigation, goBack } = this.props
|
const { date, navigation, goBack, restartApp } = this.props
|
||||||
const { currentPage } = navigation
|
const { currentPage } = navigation
|
||||||
|
|
||||||
if (!currentPage) {
|
if (!currentPage) {
|
||||||
@@ -80,8 +80,8 @@ class App extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header { ...headerProps } />
|
<Header {...headerProps} />
|
||||||
<Page { ...pageProps } />
|
<Page {...pageProps} restartApp={restartApp} />
|
||||||
<Menu />
|
<Menu />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -90,25 +90,22 @@ class App extends Component {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1
|
flex: 1,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
return({
|
return {
|
||||||
date: getDate(state),
|
date: getDate(state),
|
||||||
navigation: getNavigation(state)
|
navigation: getNavigation(state),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
const mapDispatchToProps = (dispatch) => {
|
||||||
return({
|
return {
|
||||||
navigate: (page) => dispatch(navigate(page)),
|
navigate: (page) => dispatch(navigate(page)),
|
||||||
goBack: () => dispatch(goBack()),
|
goBack: () => dispatch(goBack()),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(App)
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(App)
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { ActivityIndicator, FlatList, Dimensions, StyleSheet, View } from 'react-native'
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Dimensions,
|
||||||
|
FlatList,
|
||||||
|
PixelRatio,
|
||||||
|
StyleSheet,
|
||||||
|
View
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
import AppLoadingView from '../common/app-loading'
|
import AppLoadingView from '../common/app-loading'
|
||||||
import AppPage from '../common/app-page'
|
import AppPage from '../common/app-page'
|
||||||
@@ -22,9 +29,10 @@ import { makeColumnInfo, nfpLines } from '../helpers/chart'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CHART_COLUMN_WIDTH,
|
CHART_COLUMN_WIDTH,
|
||||||
SYMPTOMS,
|
CHART_GRID_LINE_HORIZONTAL_WIDTH,
|
||||||
CHART_SYMPTOM_HEIGHT_RATIO,
|
CHART_SYMPTOM_HEIGHT_RATIO,
|
||||||
CHART_XAXIS_HEIGHT_RATIO
|
CHART_XAXIS_HEIGHT_RATIO,
|
||||||
|
SYMPTOMS
|
||||||
} from '../../config'
|
} from '../../config'
|
||||||
import { shared } from '../../i18n/en/labels'
|
import { shared } from '../../i18n/en/labels'
|
||||||
import { Colors, Spacing } from '../../styles'
|
import { Colors, Spacing } from '../../styles'
|
||||||
@@ -105,9 +113,13 @@ class CycleChart extends Component {
|
|||||||
|
|
||||||
this.xAxisHeight = height * 0.7 * CHART_XAXIS_HEIGHT_RATIO
|
this.xAxisHeight = height * 0.7 * CHART_XAXIS_HEIGHT_RATIO
|
||||||
const remainingHeight = height * 0.7 - this.xAxisHeight
|
const remainingHeight = height * 0.7 - this.xAxisHeight
|
||||||
this.symptomHeight = remainingHeight * CHART_SYMPTOM_HEIGHT_RATIO
|
this.symptomHeight = PixelRatio.roundToNearestPixel(
|
||||||
this.symptomRowHeight = this.symptomRowSymptoms.length *
|
remainingHeight
|
||||||
this.symptomHeight
|
* CHART_SYMPTOM_HEIGHT_RATIO
|
||||||
|
)
|
||||||
|
this.symptomRowHeight = PixelRatio.roundToNearestPixel(
|
||||||
|
this.symptomRowSymptoms.length * this.symptomHeight
|
||||||
|
) + CHART_GRID_LINE_HORIZONTAL_WIDTH
|
||||||
this.columnHeight = remainingHeight - this.symptomRowHeight
|
this.columnHeight = remainingHeight - this.symptomRowHeight
|
||||||
const chartHeight = this.shouldShowTemperatureColumn ?
|
const chartHeight = this.shouldShowTemperatureColumn ?
|
||||||
height * 0.7 : (this.symptomRowHeight + this.xAxisHeight)
|
height * 0.7 : (this.symptomRowHeight + this.xAxisHeight)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import AppText from '../common/app-text'
|
|||||||
|
|
||||||
import cycleModule from '../../lib/cycle'
|
import cycleModule from '../../lib/cycle'
|
||||||
import { getOrdinalSuffix } from '../helpers/home'
|
import { getOrdinalSuffix } from '../helpers/home'
|
||||||
import { Containers, Typography, Sizes } from '../../styles'
|
import { Typography, Sizes } from '../../styles'
|
||||||
|
|
||||||
const CycleDayLabel = ({ height, date }) => {
|
const CycleDayLabel = ({ height, date }) => {
|
||||||
const cycleDayNumber = cycleModule().getCycleDayNumber(date)
|
const cycleDayNumber = cycleModule().getCycleDayNumber(date)
|
||||||
@@ -24,11 +24,11 @@ const CycleDayLabel = ({ height, date }) => {
|
|||||||
<AppText style={styles.text}>
|
<AppText style={styles.text}>
|
||||||
{isFirstDayOfMonth ? momentDate.format('MMM') : dayOfMonth}
|
{isFirstDayOfMonth ? momentDate.format('MMM') : dayOfMonth}
|
||||||
</AppText>
|
</AppText>
|
||||||
{!isFirstDayOfMonth &&
|
{!isFirstDayOfMonth && (
|
||||||
<AppText style={styles.textLight}>
|
<AppText style={styles.textLight}>
|
||||||
{getOrdinalSuffix(dayOfMonth)}
|
{getOrdinalSuffix(dayOfMonth)}
|
||||||
</AppText>
|
</AppText>
|
||||||
}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -45,15 +45,12 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
left: 4,
|
left: 4,
|
||||||
},
|
},
|
||||||
containerRow: {
|
|
||||||
...Containers.rowContainer
|
|
||||||
},
|
|
||||||
text: {
|
text: {
|
||||||
...Typography.label,
|
...Typography.label,
|
||||||
fontSize: Sizes.small,
|
fontSize: Sizes.small,
|
||||||
},
|
},
|
||||||
textBold: {
|
textBold: {
|
||||||
...Typography.labelBold
|
...Typography.labelBold,
|
||||||
},
|
},
|
||||||
textLight: {
|
textLight: {
|
||||||
...Typography.labelLight,
|
...Typography.labelLight,
|
||||||
@@ -62,7 +59,7 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default CycleDayLabel
|
export default CycleDayLabel
|
||||||
|
|||||||
@@ -105,7 +105,8 @@ class DayColumn extends Component {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{ symptomRowSymptoms.map((symptom, i) => {
|
{ symptomRowSymptoms.map((symptom, i) => {
|
||||||
const hasSymptomData = this.data.hasOwnProperty(symptom)
|
const hasSymptomData =
|
||||||
|
Object.prototype.hasOwnProperty.call(this.data, symptom)
|
||||||
return (
|
return (
|
||||||
<SymptomCell
|
<SymptomCell
|
||||||
index={i}
|
index={i}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
|
|||||||
import AppText from '../common/app-text'
|
import AppText from '../common/app-text'
|
||||||
|
|
||||||
import { Sizes } from '../../styles'
|
import { Sizes } from '../../styles'
|
||||||
|
import { CHART_TICK_WIDTH } from '../../config'
|
||||||
|
|
||||||
const Tick = ({ yPosition, height, isBold, shouldShowLabel, label }) => {
|
const Tick = ({ yPosition, height, isBold, shouldShowLabel, label }) => {
|
||||||
const top = yPosition - height / 2
|
const top = yPosition - height / 2
|
||||||
@@ -28,16 +29,14 @@ Tick.propTypes = {
|
|||||||
|
|
||||||
|
|
||||||
const text = {
|
const text = {
|
||||||
lineHeight: Sizes.base,
|
textAlign: 'right',
|
||||||
right: 4,
|
|
||||||
textAlign: 'right'
|
|
||||||
}
|
}
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: 0,
|
right: 0,
|
||||||
width: 40
|
width: CHART_TICK_WIDTH
|
||||||
},
|
},
|
||||||
textBold: {
|
textBold: {
|
||||||
fontSize: Sizes.base,
|
fontSize: Sizes.base,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { SafeAreaView, StatusBar, StyleSheet, View } from 'react-native'
|
||||||
|
|
||||||
|
import { Colors } from '../../styles'
|
||||||
|
import { STATUSBAR_HEIGHT } from '../../config'
|
||||||
|
|
||||||
|
const AppStatusBar = () => (
|
||||||
|
<View style={styles.statusBar}>
|
||||||
|
<SafeAreaView>
|
||||||
|
<StatusBar
|
||||||
|
backgroundColor={Colors.purple}
|
||||||
|
barStyle="light-content"
|
||||||
|
translucent
|
||||||
|
/>
|
||||||
|
</SafeAreaView>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
statusBar: {
|
||||||
|
backgroundColor: Colors.purple,
|
||||||
|
height: STATUSBAR_HEIGHT,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default AppStatusBar
|
||||||
@@ -38,6 +38,7 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: Spacing.base,
|
marginTop: Spacing.base,
|
||||||
minWidth: '80%',
|
minWidth: '80%',
|
||||||
paddingHorizontal: Spacing.base,
|
paddingHorizontal: Spacing.base,
|
||||||
|
paddingVertical: Spacing.tiny,
|
||||||
...Typography.mainText
|
...Typography.mainText
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { StyleSheet, View } from 'react-native'
|
||||||
|
|
||||||
|
const ButtonRow = ({ children }) => {
|
||||||
|
return <View style={styles.container}>{children}</View>
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonRow.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ButtonRow
|
||||||
+15
-13
@@ -5,7 +5,7 @@ import { StyleSheet, TouchableOpacity } from 'react-native'
|
|||||||
import AppIcon from './app-icon'
|
import AppIcon from './app-icon'
|
||||||
import AppText from './app-text'
|
import AppText from './app-text'
|
||||||
|
|
||||||
import { Colors, Fonts, Spacing } from '../../styles'
|
import { Colors, Fonts, Sizes, Spacing } from '../../styles'
|
||||||
|
|
||||||
const Button = ({
|
const Button = ({
|
||||||
children,
|
children,
|
||||||
@@ -39,49 +39,51 @@ Button.propTypes = {
|
|||||||
isCTA: PropTypes.bool,
|
isCTA: PropTypes.bool,
|
||||||
isSmall: PropTypes.bool,
|
isSmall: PropTypes.bool,
|
||||||
onPress: PropTypes.func,
|
onPress: PropTypes.func,
|
||||||
testID: PropTypes.string
|
testID: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
Button.defaultProps = {
|
Button.defaultProps = {
|
||||||
isSmall: true
|
isSmall: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = {
|
const text = {
|
||||||
padding: Spacing.base,
|
padding: Spacing.base,
|
||||||
textTransform: 'uppercase'
|
textTransform: 'uppercase',
|
||||||
}
|
}
|
||||||
|
|
||||||
const textSmall = {
|
const textSmall = {
|
||||||
fontSize: Fonts.small,
|
fontSize: Sizes.small,
|
||||||
padding: Spacing.small,
|
padding: Spacing.small,
|
||||||
textTransform: 'uppercase'
|
textTransform: 'uppercase',
|
||||||
}
|
}
|
||||||
|
|
||||||
const button = {
|
const button = {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
alignSelf: 'center',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
margin: Spacing.base,
|
marginTop: Spacing.base,
|
||||||
minWidth: '15%'
|
paddingHorizontal: Spacing.tiny,
|
||||||
|
minWidth: '15%',
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
regular: {
|
regular: {
|
||||||
...button
|
...button,
|
||||||
},
|
},
|
||||||
cta: {
|
cta: {
|
||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
borderRadius: 25,
|
borderRadius: 25,
|
||||||
...button
|
...button,
|
||||||
},
|
},
|
||||||
buttonTextBold: {
|
buttonTextBold: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontFamily: Fonts.bold
|
fontFamily: Fonts.bold,
|
||||||
},
|
},
|
||||||
buttonTextRegular: {
|
buttonTextRegular: {
|
||||||
color: Colors.greyDark,
|
color: Colors.greyDark,
|
||||||
fontFamily: Fonts.main
|
fontFamily: Fonts.main,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default Button
|
export default Button
|
||||||
|
|||||||
@@ -4,28 +4,31 @@ import { StyleSheet, TouchableOpacity } from 'react-native'
|
|||||||
|
|
||||||
import AppIcon from './app-icon'
|
import AppIcon from './app-icon'
|
||||||
|
|
||||||
|
import { HIT_SLOP} from '../../config'
|
||||||
import { Colors, Sizes } from '../../styles'
|
import { Colors, Sizes } from '../../styles'
|
||||||
|
|
||||||
const CloseIcon = ({ onClose, ...props }) => {
|
const CloseIcon = ({ onClose, color, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
hitSlop={HIT_SLOP}
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<AppIcon name='cross' color={Colors.orange} />
|
<AppIcon name='cross' color={color ? color : Colors.orange} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
CloseIcon.propTypes = {
|
CloseIcon.propTypes = {
|
||||||
onClose: PropTypes.func.isRequired
|
onClose: PropTypes.func.isRequired,
|
||||||
|
color: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
alignSelf: 'flex-start',
|
alignSelf: 'flex-start',
|
||||||
marginBottom: Sizes.base
|
marginBottom: Sizes.base,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -57,10 +57,11 @@ const styles = StyleSheet.create({
|
|||||||
accentOrange: {
|
accentOrange: {
|
||||||
...Typography.accentOrange,
|
...Typography.accentOrange,
|
||||||
fontSize: Sizes.small,
|
fontSize: Sizes.small,
|
||||||
|
margin: Sizes.tiny,
|
||||||
},
|
},
|
||||||
accentPurpleBig: {
|
accentPurpleBig: {
|
||||||
...Typography.accentPurpleBig,
|
...Typography.accentPurpleBig,
|
||||||
marginRight: Spacing.small,
|
marginRight: Spacing.tiny
|
||||||
},
|
},
|
||||||
cellLeft: {
|
cellLeft: {
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
@@ -68,12 +69,13 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
cellRight: {
|
cellRight: {
|
||||||
flex: 6,
|
flex: 5,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
row: {
|
row: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
marginBottom: Spacing.tiny,
|
marginBottom: Spacing.tiny,
|
||||||
|
marginLeft: Spacing.tiny
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { StyleSheet, View, TouchableOpacity } from 'react-native'
|
import { StyleSheet, View, TouchableOpacity } from 'react-native'
|
||||||
|
import { scale } from 'react-native-size-matters'
|
||||||
|
|
||||||
import AppText from '../common/app-text'
|
import AppText from '../common/app-text'
|
||||||
import DripIcon from '../../assets/drip-icons'
|
import DripIcon from '../../assets/drip-icons'
|
||||||
@@ -14,7 +15,6 @@ import { Colors, Sizes, Spacing } from '../../styles'
|
|||||||
import { headerTitles as symptomTitles } from '../../i18n/en/labels'
|
import { headerTitles as symptomTitles } from '../../i18n/en/labels'
|
||||||
|
|
||||||
class SymptomBox extends Component {
|
class SymptomBox extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
date: PropTypes.string.isRequired,
|
date: PropTypes.string.isRequired,
|
||||||
isSymptomEdited: PropTypes.bool,
|
isSymptomEdited: PropTypes.bool,
|
||||||
@@ -32,7 +32,7 @@ class SymptomBox extends Component {
|
|||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isSymptomEdited: props.isSymptomEdited
|
isSymptomEdited: props.isSymptomEdited,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,24 +57,24 @@ class SymptomBox extends Component {
|
|||||||
const iconName = `drip-icon-${symptom}`
|
const iconName = `drip-icon-${symptom}`
|
||||||
const symptomNameStyle = [
|
const symptomNameStyle = [
|
||||||
styles.symptomName,
|
styles.symptomName,
|
||||||
(isSymptomDisabled && styles.symptomNameDisabled),
|
isSymptomDisabled && styles.symptomNameDisabled,
|
||||||
(isExcluded && styles.symptomNameExcluded)
|
isExcluded && styles.symptomNameExcluded,
|
||||||
]
|
]
|
||||||
const textStyle = [
|
const textStyle = [
|
||||||
styles.text,
|
styles.text,
|
||||||
(isSymptomDisabled && styles.textDisabled),
|
isSymptomDisabled && styles.textDisabled,
|
||||||
(isExcluded && styles.textExcluded)
|
isExcluded && styles.textExcluded,
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{isSymptomEdited &&
|
{isSymptomEdited && (
|
||||||
<SymptomEditView
|
<SymptomEditView
|
||||||
symptom={symptom}
|
symptom={symptom}
|
||||||
symptomData={symptomData}
|
symptomData={symptomData}
|
||||||
onClose={this.onFinishEditing}
|
onClose={this.onFinishEditing}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
disabled={isSymptomDisabled}
|
disabled={isSymptomDisabled}
|
||||||
@@ -86,17 +86,17 @@ class SymptomBox extends Component {
|
|||||||
color={iconColor}
|
color={iconColor}
|
||||||
isActive={!isSymptomDisabled}
|
isActive={!isSymptomDisabled}
|
||||||
name={iconName}
|
name={iconName}
|
||||||
size={40}
|
size={Sizes.icon}
|
||||||
/>
|
/>
|
||||||
<View style={styles.textContainer}>
|
<View style={styles.textContainer}>
|
||||||
<AppText style={symptomNameStyle}>
|
<AppText style={symptomNameStyle}>
|
||||||
{symptomTitles[symptom].toLowerCase()}
|
{symptomTitles[symptom].toLowerCase()}
|
||||||
</AppText>
|
</AppText>
|
||||||
{symptomDataToDisplay &&
|
{symptomDataToDisplay && (
|
||||||
<AppText style={textStyle} numberOfLines={4}>
|
<AppText style={textStyle} numberOfLines={4}>
|
||||||
{symptomDataToDisplay}
|
{symptomDataToDisplay}
|
||||||
</AppText>
|
</AppText>
|
||||||
}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@@ -104,65 +104,53 @@ class SymptomBox extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hint = {
|
|
||||||
fontSize: Sizes.small,
|
|
||||||
fontStyle: 'italic'
|
|
||||||
}
|
|
||||||
|
|
||||||
const main = {
|
|
||||||
fontSize: Sizes.base,
|
|
||||||
height: Sizes.base * 2,
|
|
||||||
lineHeight: Sizes.base,
|
|
||||||
marginBottom: (-1) * Sizes.tiny,
|
|
||||||
textAlignVertical: 'center'
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
borderRadius: 10,
|
borderRadius: scale(10),
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
height: 110,
|
height: scale(110),
|
||||||
marginBottom: Spacing.base,
|
marginBottom: Spacing.base,
|
||||||
paddingHorizontal: Spacing.small,
|
paddingHorizontal: Spacing.small,
|
||||||
paddingVertical: Spacing.base,
|
paddingVertical: Spacing.base,
|
||||||
width: Spacing.symptomTileWidth
|
width: Spacing.symptomTileWidth,
|
||||||
},
|
},
|
||||||
symptomName: {
|
symptomName: {
|
||||||
|
paddingTop: Sizes.tiny,
|
||||||
color: Colors.purple,
|
color: Colors.purple,
|
||||||
...main
|
fontSize: Sizes.base,
|
||||||
|
lineHeight: Sizes.base,
|
||||||
},
|
},
|
||||||
symptomNameDisabled: {
|
symptomNameDisabled: {
|
||||||
color: Colors.grey
|
color: Colors.grey,
|
||||||
},
|
},
|
||||||
symptomNameExcluded: {
|
symptomNameExcluded: {
|
||||||
color: Colors.greyDark,
|
color: Colors.greyDark,
|
||||||
},
|
},
|
||||||
textContainer: {
|
textContainer: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
marginLeft: Spacing.small,
|
justifyContent: 'center',
|
||||||
maxWidth: Spacing.textWidth
|
marginLeft: Spacing.tiny,
|
||||||
|
maxWidth: Spacing.textWidth,
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
...hint
|
fontSize: Sizes.small,
|
||||||
|
fontStyle: 'italic',
|
||||||
},
|
},
|
||||||
textDisabled: {
|
textDisabled: {
|
||||||
color: Colors.greyLight
|
color: Colors.greyLight,
|
||||||
},
|
},
|
||||||
textExcluded: {
|
textExcluded: {
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
return({
|
return {
|
||||||
date: getDate(state),
|
date: getDate(state),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, null)(SymptomBox)
|
||||||
mapStateToProps,
|
|
||||||
null,
|
|
||||||
)(SymptomBox)
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Dimensions, ScrollView, StyleSheet, View } from 'react-native'
|
import { Dimensions, ScrollView, StyleSheet, View } from 'react-native'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
|
||||||
import AppModal from '../common/app-modal'
|
import AppModal from '../common/app-modal'
|
||||||
import AppSwitch from '../common/app-switch'
|
import AppSwitch from '../common/app-switch'
|
||||||
@@ -13,21 +14,20 @@ import SelectBoxGroup from './select-box-group'
|
|||||||
import SelectTabGroup from './select-tab-group'
|
import SelectTabGroup from './select-tab-group'
|
||||||
import Temperature from './temperature'
|
import Temperature from './temperature'
|
||||||
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import { getDate } from '../../slices/date'
|
import { getDate } from '../../slices/date'
|
||||||
import { blank, save, shouldShow, symtomPage } from '../helpers/cycle-day'
|
import { blank, save, shouldShow, symtomPage } from '../helpers/cycle-day'
|
||||||
|
import { showToast } from '../helpers/general'
|
||||||
|
|
||||||
import { shared as sharedLabels } from '../../i18n/en/labels'
|
import { shared as sharedLabels } from '../../i18n/en/labels'
|
||||||
import info from '../../i18n/en/symptom-info'
|
import info from '../../i18n/en/symptom-info'
|
||||||
import { Colors, Containers, Sizes, Spacing } from '../../styles'
|
import { Colors, Containers, Sizes, Spacing } from '../../styles'
|
||||||
|
|
||||||
class SymptomEditView extends Component {
|
class SymptomEditView extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
date: PropTypes.string.isRequired,
|
date: PropTypes.string.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
symptom: PropTypes.string.isRequired,
|
symptom: PropTypes.string.isRequired,
|
||||||
symptomData: PropTypes.object
|
symptomData: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -48,7 +48,7 @@ class SymptomEditView extends Component {
|
|||||||
shouldShowInfo: false,
|
shouldShowInfo: false,
|
||||||
shouldShowNote,
|
shouldShowNote,
|
||||||
shouldBoxGroup,
|
shouldBoxGroup,
|
||||||
shouldTabGroup
|
shouldTabGroup,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,18 +84,20 @@ class SymptomEditView extends Component {
|
|||||||
|
|
||||||
onRemove = () => {
|
onRemove = () => {
|
||||||
this.saveData(true)
|
this.saveData(true)
|
||||||
|
showToast(sharedLabels.dataDeleted)
|
||||||
this.props.onClose()
|
this.props.onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
onSave = () => {
|
onSave = () => {
|
||||||
this.saveData()
|
this.saveData()
|
||||||
|
showToast(sharedLabels.dataSaved)
|
||||||
this.props.onClose()
|
this.props.onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
onSaveTemperature = (value, field) => {
|
onSaveTemperature = (value, field) => {
|
||||||
const data = this.getParsedData()
|
const data = this.getParsedData()
|
||||||
const dataToSave = field === 'value'
|
const dataToSave =
|
||||||
? { [field]: Number(value) } : { [field]: value }
|
field === 'value' ? { [field]: Number(value) } : { [field]: value }
|
||||||
Object.assign(data, { ...dataToSave })
|
Object.assign(data, { ...dataToSave })
|
||||||
|
|
||||||
this.setState({ data })
|
this.setState({ data })
|
||||||
@@ -103,10 +105,10 @@ class SymptomEditView extends Component {
|
|||||||
|
|
||||||
onSelectBox = (key) => {
|
onSelectBox = (key) => {
|
||||||
const data = this.getParsedData()
|
const data = this.getParsedData()
|
||||||
if (key === "other") {
|
if (key === 'other') {
|
||||||
Object.assign(data, {
|
Object.assign(data, {
|
||||||
note: null,
|
note: null,
|
||||||
[key]: !this.state.data[key]
|
[key]: !this.state.data[key],
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Object.assign(data, { [key]: !this.state.data[key] })
|
Object.assign(data, { [key]: !this.state.data[key] })
|
||||||
@@ -115,7 +117,7 @@ class SymptomEditView extends Component {
|
|||||||
this.setState({ data })
|
this.setState({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectBoxNote= (value) => {
|
onSelectBoxNote = (value) => {
|
||||||
const data = this.getParsedData()
|
const data = this.getParsedData()
|
||||||
Object.assign(data, { note: value !== '' ? value : null })
|
Object.assign(data, { note: value !== '' ? value : null })
|
||||||
|
|
||||||
@@ -135,94 +137,102 @@ class SymptomEditView extends Component {
|
|||||||
save[symptom](data, date, shouldDeleteData)
|
save[symptom](data, date, shouldDeleteData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeView = () => {
|
||||||
|
const { onClose } = this.props
|
||||||
|
|
||||||
|
showToast(sharedLabels.dataSaved)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onClose, symptom } = this.props
|
const { symptom } = this.props
|
||||||
const { data,
|
const {
|
||||||
|
data,
|
||||||
shouldShowExclude,
|
shouldShowExclude,
|
||||||
shouldShowInfo,
|
shouldShowInfo,
|
||||||
shouldShowNote,
|
shouldShowNote,
|
||||||
shouldBoxGroup,
|
shouldBoxGroup,
|
||||||
shouldTabGroup
|
shouldTabGroup,
|
||||||
} = this.state
|
} = this.state
|
||||||
const iconName = shouldShowInfo ? "chevron-down" : "chevron-up"
|
const iconName = shouldShowInfo ? 'chevron-up' : 'chevron-down'
|
||||||
const noteText = symptom === 'note' ? data.value : data.note
|
const noteText = symptom === 'note' ? data.value : data.note
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppModal onClose={onClose}>
|
<AppModal onClose={this.closeView}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={styles.modalContainer}
|
contentContainerStyle={styles.modalContainer}
|
||||||
style={styles.modalWindow}
|
style={styles.modalWindow}
|
||||||
>
|
>
|
||||||
<View style={styles.headerContainer}>
|
<View style={styles.headerContainer}>
|
||||||
<CloseIcon onClose={onClose} />
|
<CloseIcon onClose={this.closeView} />
|
||||||
</View>
|
</View>
|
||||||
{symptom === 'temperature' &&
|
{symptom === 'temperature' && (
|
||||||
<Temperature
|
<Temperature
|
||||||
data={data}
|
data={data}
|
||||||
save={(value, field) => this.onSaveTemperature(value, field)}
|
save={(value, field) => this.onSaveTemperature(value, field)}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{shouldTabGroup && symtomPage[symptom].selectTabGroups.map(group => {
|
{shouldTabGroup &&
|
||||||
|
symtomPage[symptom].selectTabGroups.map((group) => {
|
||||||
return (
|
return (
|
||||||
<Segment key={group.key} style={styles.segmentBorder}>
|
<Segment key={group.key} style={styles.segmentBorder}>
|
||||||
<AppText style={styles.title}>{group.title}</AppText>
|
<AppText style={styles.title}>{group.title}</AppText>
|
||||||
<SelectTabGroup
|
<SelectTabGroup
|
||||||
activeButton={data[group.key]}
|
activeButton={data[group.key]}
|
||||||
buttons={group.options}
|
buttons={group.options}
|
||||||
onSelect={value => this.onSelectTab(group, value)}
|
onSelect={(value) => this.onSelectTab(group, value)}
|
||||||
/>
|
/>
|
||||||
</Segment>
|
</Segment>
|
||||||
)
|
)
|
||||||
})
|
})}
|
||||||
}
|
{shouldBoxGroup &&
|
||||||
{shouldBoxGroup && symtomPage[symptom].selectBoxGroups.map(group => {
|
symtomPage[symptom].selectBoxGroups.map((group) => {
|
||||||
const isOtherSelected =
|
const isOtherSelected =
|
||||||
data['other'] !== null
|
data['other'] !== null &&
|
||||||
&& data['other'] !== false
|
data['other'] !== false &&
|
||||||
&& Object.keys(group.options).includes('other')
|
Object.keys(group.options).includes('other')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Segment key={group.key} style={styles.segmentBorder} >
|
<Segment key={group.key} style={styles.segmentBorder}>
|
||||||
<AppText style={styles.title}>{group.title}</AppText>
|
<AppText style={styles.title}>{group.title}</AppText>
|
||||||
<SelectBoxGroup
|
<SelectBoxGroup
|
||||||
labels={group.options}
|
labels={group.options}
|
||||||
onSelect={value => this.onSelectBox(value)}
|
onSelect={(value) => this.onSelectBox(value)}
|
||||||
optionsState={data}
|
optionsState={data}
|
||||||
/>
|
/>
|
||||||
{isOtherSelected &&
|
{isOtherSelected && (
|
||||||
<AppTextInput
|
<AppTextInput
|
||||||
multiline={true}
|
multiline={true}
|
||||||
placeholder={sharedLabels.enter}
|
placeholder={sharedLabels.enter}
|
||||||
value={data.note}
|
value={data.note}
|
||||||
onChangeText={value => this.onSelectBoxNote(value)}
|
onChangeText={(value) => this.onSelectBoxNote(value)}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</Segment>
|
</Segment>
|
||||||
)
|
)
|
||||||
})
|
})}
|
||||||
}
|
{shouldShowExclude && (
|
||||||
{shouldShowExclude &&
|
<Segment style={styles.segmentBorder}>
|
||||||
<Segment style={styles.segmentBorder} >
|
|
||||||
<AppSwitch
|
<AppSwitch
|
||||||
onToggle={this.onExcludeToggle}
|
onToggle={this.onExcludeToggle}
|
||||||
text={symtomPage[symptom].excludeText}
|
text={symtomPage[symptom].excludeText}
|
||||||
value={data.exclude}
|
value={data.exclude}
|
||||||
/>
|
/>
|
||||||
</Segment>
|
</Segment>
|
||||||
}
|
)}
|
||||||
{shouldShowNote &&
|
{shouldShowNote && (
|
||||||
<Segment style={styles.segmentBorder} >
|
<Segment style={styles.segmentBorder}>
|
||||||
<AppText>{symtomPage[symptom].note}</AppText>
|
<AppText>{symtomPage[symptom].note}</AppText>
|
||||||
<AppTextInput
|
<AppTextInput
|
||||||
multiline={true}
|
multiline={true}
|
||||||
numberOfLines={3}
|
numberOfLines={3}
|
||||||
onChangeText={this.onEditNote}
|
onChangeText={this.onEditNote}
|
||||||
placeholder={sharedLabels.enter}
|
placeholder={sharedLabels.enter}
|
||||||
testID='noteInput'
|
testID="noteInput"
|
||||||
value={noteText !== null ? noteText : ''}
|
value={noteText !== null ? noteText : ''}
|
||||||
/>
|
/>
|
||||||
</Segment>
|
</Segment>
|
||||||
}
|
)}
|
||||||
<View style={styles.buttonsContainer}>
|
<View style={styles.buttonsContainer}>
|
||||||
<Button iconName={iconName} isSmall onPress={this.onPressLearnMore}>
|
<Button iconName={iconName} isSmall onPress={this.onPressLearnMore}>
|
||||||
{sharedLabels.learnMore}
|
{sharedLabels.learnMore}
|
||||||
@@ -234,11 +244,11 @@ class SymptomEditView extends Component {
|
|||||||
{sharedLabels.save}
|
{sharedLabels.save}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
{shouldShowInfo &&
|
{shouldShowInfo && (
|
||||||
<Segment last style={styles.segmentBorder} >
|
<Segment last style={styles.segmentBorder}>
|
||||||
<AppText>{info[symptom].text}</AppText>
|
<AppText>{info[symptom].text}</AppText>
|
||||||
</Segment>
|
</Segment>
|
||||||
}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</AppModal>
|
</AppModal>
|
||||||
)
|
)
|
||||||
@@ -247,7 +257,7 @@ class SymptomEditView extends Component {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
buttonsContainer: {
|
buttonsContainer: {
|
||||||
...Containers.rowContainer
|
...Containers.rowContainer,
|
||||||
},
|
},
|
||||||
headerContainer: {
|
headerContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -265,23 +275,20 @@ const styles = StyleSheet.create({
|
|||||||
marginVertical: Sizes.huge * 2,
|
marginVertical: Sizes.huge * 2,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
minHeight: '40%',
|
minHeight: '40%',
|
||||||
maxHeight: Dimensions.get('window').height * 0.7
|
maxHeight: Dimensions.get('window').height * 0.7,
|
||||||
},
|
},
|
||||||
segmentBorder: {
|
segmentBorder: {
|
||||||
borderBottomColor: Colors.greyLight
|
borderBottomColor: Colors.greyLight,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: Sizes.subtitle
|
fontSize: Sizes.subtitle,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
return({
|
return {
|
||||||
date: getDate(state),
|
date: getDate(state),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, null)(SymptomEditView)
|
||||||
mapStateToProps,
|
|
||||||
null,
|
|
||||||
)(SymptomEditView)
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const SymptomPageTitle = ({
|
|||||||
reloadSymptomData(nextDay)
|
reloadSymptomData(nextDay)
|
||||||
setDate(nextDay)
|
setDate(nextDay)
|
||||||
}
|
}
|
||||||
|
const formattedTitle = title.length > 21
|
||||||
|
? title.substring(0, 18) + '...'
|
||||||
|
: title
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@@ -31,7 +34,7 @@ const SymptomPageTitle = ({
|
|||||||
<AppIcon name='chevron-left' color={Colors.orange}/>
|
<AppIcon name='chevron-left' color={Colors.orange}/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.textContainer}>
|
<View style={styles.textContainer}>
|
||||||
<AppText style={styles.title}>{title}</AppText>
|
<AppText style={styles.title}>{formattedTitle}</AppText>
|
||||||
{subtitle && <AppText style={styles.subtitle}>{subtitle}</AppText>}
|
{subtitle && <AppText style={styles.subtitle}>{subtitle}</AppText>}
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={() => navigate(true)} hitSlop={HIT_SLOP}>
|
<TouchableOpacity onPress={() => navigate(true)} hitSlop={HIT_SLOP}>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { Platform, StyleSheet, View } from 'react-native'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Keyboard } from 'react-native'
|
import { Keyboard } from 'react-native'
|
||||||
import DateTimePicker from 'react-native-modal-datetime-picker-nevo'
|
import DateTimePicker from 'react-native-modal-datetime-picker'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import AppText from '../common/app-text'
|
import AppText from '../common/app-text'
|
||||||
import AppTextInput from '../common/app-text-input'
|
import AppTextInput from '../common/app-text-input'
|
||||||
@@ -11,86 +12,53 @@ import Segment from '../common/segment'
|
|||||||
|
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { getDate } from '../../slices/date'
|
import { getDate } from '../../slices/date'
|
||||||
import { isTemperatureOutOfRange, isPreviousTemperature } from '../helpers/cycle-day'
|
import {
|
||||||
|
getTemperatureOutOfRangeMessage,
|
||||||
|
getPreviousTemperature,
|
||||||
|
formatTemperature,
|
||||||
|
} from '../helpers/cycle-day'
|
||||||
|
|
||||||
import { temperature as labels } from '../../i18n/en/cycle-day'
|
import { temperature as labels } from '../../i18n/en/cycle-day'
|
||||||
|
|
||||||
import { Colors, Containers, Sizes, Spacing } from '../../styles'
|
import { Colors, Containers, Sizes, Spacing } from '../../styles'
|
||||||
|
|
||||||
const formatTemperature = value => value === null
|
const Temperature = ({ data, date, save }) => {
|
||||||
? value
|
const { t } = useTranslation()
|
||||||
: Number.parseFloat(value).toFixed(2)
|
const [isTimePickerVisible, setIsTimePickerVisible] = useState(false)
|
||||||
|
const [temperature, setTemperature] = useState(
|
||||||
|
formatTemperature(data.value) || getPreviousTemperature(date)
|
||||||
|
)
|
||||||
|
|
||||||
class Temperature extends Component {
|
// update state in parent component once to ensure
|
||||||
|
// that pre-filled values are saved on button click
|
||||||
|
useEffect(() => {
|
||||||
|
if (temperature) {
|
||||||
|
save(temperature, 'value')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
static propTypes = {
|
function onChangeTemperature(value) {
|
||||||
data: PropTypes.object,
|
const formattedValue = value.replace(',', '.').trim()
|
||||||
date: PropTypes.string.isRequired,
|
if (!Number(formattedValue) && value !== '') return false
|
||||||
save: PropTypes.func
|
setTemperature(formattedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
function onShowTimePicker() {
|
||||||
super(props)
|
|
||||||
|
|
||||||
const { data, date } = this.props
|
|
||||||
const { value } = data
|
|
||||||
const { shouldShowSuggestion, suggestedTemperature } =
|
|
||||||
isPreviousTemperature(date)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isTimePickerVisible: false,
|
|
||||||
shouldShowSuggestion,
|
|
||||||
suggestedTemperature: formatTemperature(suggestedTemperature),
|
|
||||||
value: formatTemperature(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCancelTimePicker = () => {
|
|
||||||
this.setState({ isTimePickerVisible: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeTemperature = (value) => {
|
|
||||||
if (!Number(value)) return false
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
value: value.trim(),
|
|
||||||
shouldShowSuggestion: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onShowTimePicker = () => {
|
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
this.setState({ isTimePickerVisible: true })
|
setIsTimePickerVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
setTemperature = () => {
|
function setTime(jsDate) {
|
||||||
const { value } = this.state
|
|
||||||
this.props.save(value, 'value')
|
|
||||||
}
|
|
||||||
|
|
||||||
setTime = (jsDate) => {
|
|
||||||
const time = moment(jsDate).format('HH:mm')
|
const time = moment(jsDate).format('HH:mm')
|
||||||
const isTimePickerVisible = false
|
|
||||||
|
|
||||||
this.props.save(time, 'time')
|
save(time, 'time')
|
||||||
this.setState({ isTimePickerVisible })
|
setIsTimePickerVisible(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const { time } = data
|
||||||
const { shouldShowSuggestion, suggestedTemperature, value } = this.state
|
|
||||||
const { time } = this.props.data
|
|
||||||
|
|
||||||
const inputStyle = (shouldShowSuggestion && value === null)
|
const inputStyle = { color: Colors.greyDark }
|
||||||
? { color: Colors.grey }
|
const outOfRangeWarning = getTemperatureOutOfRangeMessage(temperature)
|
||||||
: {color: Colors.greyDark}
|
|
||||||
const outOfRangeWarning = isTemperatureOutOfRange(value)
|
|
||||||
let temperatureToShow = null
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
temperatureToShow = value
|
|
||||||
} else if (shouldShowSuggestion) {
|
|
||||||
temperatureToShow = suggestedTemperature
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@@ -98,9 +66,9 @@ class Temperature extends Component {
|
|||||||
<AppText style={styles.title}>{labels.temperature.explainer}</AppText>
|
<AppText style={styles.title}>{labels.temperature.explainer}</AppText>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<AppTextInput
|
<AppTextInput
|
||||||
value={temperatureToShow === null ? '' : temperatureToShow}
|
value={temperature}
|
||||||
onChangeText={this.onChangeTemperature}
|
onChangeText={onChangeTemperature}
|
||||||
onEndEditing={this.setTemperature}
|
onEndEditing={() => save(temperature, 'value')}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
maxLength={5}
|
maxLength={5}
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
@@ -109,55 +77,58 @@ class Temperature extends Component {
|
|||||||
/>
|
/>
|
||||||
<AppText>°C</AppText>
|
<AppText>°C</AppText>
|
||||||
</View>
|
</View>
|
||||||
{ outOfRangeWarning !== null &&
|
{!!outOfRangeWarning && (
|
||||||
<View style={styles.hintContainer}>
|
<View style={styles.hintContainer}>
|
||||||
<AppText style={styles.hint}>{outOfRangeWarning}</AppText>
|
<AppText style={styles.hint}>{outOfRangeWarning}</AppText>
|
||||||
</View>
|
</View>
|
||||||
}
|
)}
|
||||||
</Segment>
|
</Segment>
|
||||||
<Segment>
|
<Segment>
|
||||||
<AppText style={styles.title}>{labels.time}</AppText>
|
<AppText style={styles.title}>{labels.time}</AppText>
|
||||||
<AppTextInput
|
<AppTextInput
|
||||||
onFocus={this.onShowTimePicker}
|
onFocus={onShowTimePicker}
|
||||||
testID='timeInput'
|
testID="timeInput"
|
||||||
value={time}
|
value={time}
|
||||||
/>
|
/>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
isVisible={this.state.isTimePickerVisible}
|
isVisible={isTimePickerVisible}
|
||||||
mode="time"
|
mode="time"
|
||||||
onConfirm={this.setTime}
|
onConfirm={setTime}
|
||||||
onCancel={this.onCancelTimePicker}
|
onCancel={() => setIsTimePickerVisible(false)}
|
||||||
|
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||||
|
headerTextIOS={t('labels.shared.dateTimePickerTitle')}
|
||||||
/>
|
/>
|
||||||
</Segment>
|
</Segment>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
...Containers.rowContainer
|
...Containers.rowContainer,
|
||||||
},
|
},
|
||||||
hint: {
|
hint: {
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
fontSize: Sizes.small
|
fontSize: Sizes.small,
|
||||||
},
|
},
|
||||||
hintContainer: {
|
hintContainer: {
|
||||||
marginVertical: Spacing.tiny
|
marginVertical: Spacing.tiny,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: Sizes.subtitle
|
fontSize: Sizes.subtitle,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Temperature.propTypes = {
|
||||||
const mapStateToProps = (state) => {
|
data: PropTypes.object.isRequired,
|
||||||
return({
|
date: PropTypes.string.isRequired,
|
||||||
date: getDate(state),
|
save: PropTypes.func.isRequired,
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
const mapStateToProps = (state) => {
|
||||||
mapStateToProps,
|
return {
|
||||||
null,
|
date: getDate(state),
|
||||||
)(Temperature)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null)(Temperature)
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import { Modal, StyleSheet, TouchableOpacity, View } from 'react-native'
|
import {
|
||||||
|
Modal,
|
||||||
|
Platform,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
import AppIcon from '../common/app-icon'
|
import AppIcon from '../common/app-icon'
|
||||||
|
import CloseIcon from '../common/close-icon'
|
||||||
import MenuItem from './menu-item'
|
import MenuItem from './menu-item'
|
||||||
|
|
||||||
import { Colors, Sizes } from '../../styles'
|
import { Colors, Sizes } from '../../styles'
|
||||||
@@ -14,6 +21,7 @@ const settingsMenuItems = [
|
|||||||
{ name: menuItems.settings, component: 'SettingsMenu' },
|
{ name: menuItems.settings, component: 'SettingsMenu' },
|
||||||
{ name: menuItems.about, component: 'About' },
|
{ name: menuItems.about, component: 'About' },
|
||||||
{ name: menuItems.license, component: 'License' },
|
{ name: menuItems.license, component: 'License' },
|
||||||
|
{ name: menuItems.privacyPolicy, component: 'PrivacyPolicy' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default class HamburgerMenu extends Component {
|
export default class HamburgerMenu extends Component {
|
||||||
@@ -34,12 +42,12 @@ export default class HamburgerMenu extends Component {
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!shouldShowMenu && (
|
{!shouldShowMenu && (
|
||||||
<TouchableOpacity onPress={this.toggleMenu} hitSlop={HIT_SLOP}>
|
<TouchableOpacity onPress={this.toggleMenu} hitSlop={HIT_SLOP}>
|
||||||
<AppIcon name='dots-three-vertical' color={Colors.orange} />
|
<AppIcon name="dots-three-vertical" color={Colors.orange} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{shouldShowMenu && (
|
{shouldShowMenu && (
|
||||||
<Modal
|
<Modal
|
||||||
animationType='fade'
|
animationType="fade"
|
||||||
onRequestClose={this.toggleMenu}
|
onRequestClose={this.toggleMenu}
|
||||||
transparent={true}
|
transparent={true}
|
||||||
visible={shouldShowMenu}
|
visible={shouldShowMenu}
|
||||||
@@ -49,12 +57,9 @@ export default class HamburgerMenu extends Component {
|
|||||||
style={styles.blackBackground}
|
style={styles.blackBackground}
|
||||||
></TouchableOpacity>
|
></TouchableOpacity>
|
||||||
<View style={styles.menu}>
|
<View style={styles.menu}>
|
||||||
<TouchableOpacity
|
<View style={styles.iconContainer}>
|
||||||
onPress={this.toggleMenu}
|
<CloseIcon color={'black'} onClose={() => this.toggleMenu()} />
|
||||||
style={styles.iconContainer}
|
</View>
|
||||||
>
|
|
||||||
<AppIcon name='cross' color='black' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
{settingsMenuItems.map((item) => (
|
{settingsMenuItems.map((item) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
item={item}
|
item={item}
|
||||||
@@ -85,6 +90,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
padding: Sizes.base,
|
padding: Sizes.base,
|
||||||
|
paddingTop: Platform.OS === 'ios' ? Sizes.huge : Sizes.base,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '60%',
|
width: '60%',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LocalDate } from 'js-joda'
|
import { LocalDate } from 'js-joda'
|
||||||
|
import { verticalScale } from 'react-native-size-matters'
|
||||||
|
|
||||||
import { Colors, Fonts, Sizes } from '../../styles'
|
import { Colors, Fonts, Sizes } from '../../styles'
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ export const toCalFormat = (bleedingDaysSortedByDate) => {
|
|||||||
customStyles: {
|
customStyles: {
|
||||||
container: {
|
container: {
|
||||||
backgroundColor: shades[day.bleeding.value],
|
backgroundColor: shades[day.bleeding.value],
|
||||||
paddingTop: 2,
|
paddingTop: verticalScale(2),
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
color: Colors.turquoiseLight,
|
color: Colors.turquoiseLight,
|
||||||
@@ -62,8 +63,9 @@ export const todayToCalFormat = () => {
|
|||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
calendarToday: {
|
calendarToday: {
|
||||||
fontFamily: Fonts.bold,
|
fontFamily: 'Jost-Bold',
|
||||||
color: Colors.purple,
|
fontWeight: 'bold',
|
||||||
|
color: Colors.purple
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +75,7 @@ export const calendarTheme = {
|
|||||||
monthTextColor: Colors.purple,
|
monthTextColor: Colors.purple,
|
||||||
textDayFontFamily: Fonts.main,
|
textDayFontFamily: Fonts.main,
|
||||||
textMonthFontFamily: Fonts.bold,
|
textMonthFontFamily: Fonts.bold,
|
||||||
|
textMonthFontWeight: 'bold',
|
||||||
textDayHeaderFontFamily: Fonts.bold,
|
textDayHeaderFontFamily: Fonts.bold,
|
||||||
textDayFontSize: Sizes.small,
|
textDayFontSize: Sizes.small,
|
||||||
textMonthFontSize: Sizes.subtitle,
|
textMonthFontSize: Sizes.subtitle,
|
||||||
|
|||||||
+137
-104
@@ -1,6 +1,10 @@
|
|||||||
import { ChronoUnit, LocalDate, LocalTime } from 'js-joda'
|
import { ChronoUnit, LocalDate, LocalTime } from 'js-joda'
|
||||||
|
|
||||||
import { getPreviousTemperature, saveSymptom } from '../../db'
|
import {
|
||||||
|
getPreviousTemperatureForDate,
|
||||||
|
saveSymptom,
|
||||||
|
mapRealmObjToJsObj,
|
||||||
|
} from '../../db'
|
||||||
import { scaleObservable } from '../../local-storage'
|
import { scaleObservable } from '../../local-storage'
|
||||||
|
|
||||||
import * as labels from '../../i18n/en/cycle-day'
|
import * as labels from '../../i18n/en/cycle-day'
|
||||||
@@ -23,39 +27,35 @@ const temperatureLabels = labels.temperature
|
|||||||
const minutes = ChronoUnit.MINUTES
|
const minutes = ChronoUnit.MINUTES
|
||||||
|
|
||||||
const isNumber = (value) => typeof value === 'number'
|
const isNumber = (value) => typeof value === 'number'
|
||||||
export const shouldShow = (value) => value !== null ? true : false
|
export const shouldShow = (value) => (value !== null ? true : false)
|
||||||
|
|
||||||
export const isPreviousTemperature = (temperature) => {
|
export const formatTemperature = (temperature) =>
|
||||||
const previousTemperature = getPreviousTemperature(temperature)
|
!temperature
|
||||||
const shouldShowSuggestion = previousTemperature ? true : false
|
? temperature
|
||||||
const suggestedTemperature = previousTemperature ?
|
: Number.parseFloat(temperature.toString()).toFixed(2)
|
||||||
previousTemperature.toString() : null
|
|
||||||
|
|
||||||
return { shouldShowSuggestion, suggestedTemperature }
|
export const getPreviousTemperature = (date) => {
|
||||||
|
const previousTemperature = getPreviousTemperatureForDate(date)
|
||||||
|
return formatTemperature(previousTemperature)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isTemperatureOutOfRange = (temperature) => {
|
export const getTemperatureOutOfRangeMessage = (temperature) => {
|
||||||
if (!temperature) return null
|
if (!temperature) return null
|
||||||
|
|
||||||
const value = Number(temperature)
|
const value = Number(temperature)
|
||||||
const range = { min: TEMP_MIN, max: TEMP_MAX }
|
|
||||||
const scale = scaleObservable.value
|
const scale = scaleObservable.value
|
||||||
|
|
||||||
let warningMsg = null
|
return value < TEMP_MIN || value > TEMP_MAX
|
||||||
|
? labels.temperature.outOfAbsoluteRangeWarning
|
||||||
if (value < range.min || value > range.max) {
|
: value < scale.min || value > scale.max
|
||||||
warningMsg = labels.temperature.outOfAbsoluteRangeWarning
|
? labels.temperature.outOfRangeWarning
|
||||||
} else if (value < scale.min || value > scale.max) {
|
: ''
|
||||||
warningMsg = labels.temperature.outOfRangeWarning
|
|
||||||
}
|
|
||||||
|
|
||||||
return warningMsg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const blank = {
|
export const blank = {
|
||||||
bleeding: {
|
bleeding: {
|
||||||
exclude: false,
|
exclude: false,
|
||||||
value: null
|
value: null,
|
||||||
},
|
},
|
||||||
cervix: {
|
cervix: {
|
||||||
exclude: false,
|
exclude: false,
|
||||||
@@ -64,9 +64,9 @@ export const blank = {
|
|||||||
position: null,
|
position: null,
|
||||||
},
|
},
|
||||||
desire: {
|
desire: {
|
||||||
value: null
|
value: null,
|
||||||
},
|
},
|
||||||
mood:{
|
mood: {
|
||||||
happy: null,
|
happy: null,
|
||||||
sad: null,
|
sad: null,
|
||||||
stressed: null,
|
stressed: null,
|
||||||
@@ -77,16 +77,16 @@ export const blank = {
|
|||||||
fatigue: null,
|
fatigue: null,
|
||||||
angry: null,
|
angry: null,
|
||||||
other: null,
|
other: null,
|
||||||
note: null
|
note: null,
|
||||||
},
|
},
|
||||||
mucus: {
|
mucus: {
|
||||||
exclude: false,
|
exclude: false,
|
||||||
feeling: null,
|
feeling: null,
|
||||||
texture: null,
|
texture: null,
|
||||||
value: null
|
value: null,
|
||||||
},
|
},
|
||||||
note: {
|
note: {
|
||||||
value: null
|
value: null,
|
||||||
},
|
},
|
||||||
pain: {
|
pain: {
|
||||||
cramps: null,
|
cramps: null,
|
||||||
@@ -97,7 +97,7 @@ export const blank = {
|
|||||||
tenderBreasts: null,
|
tenderBreasts: null,
|
||||||
migraine: null,
|
migraine: null,
|
||||||
other: null,
|
other: null,
|
||||||
note: null
|
note: null,
|
||||||
},
|
},
|
||||||
sex: {
|
sex: {
|
||||||
solo: null,
|
solo: null,
|
||||||
@@ -111,14 +111,14 @@ export const blank = {
|
|||||||
diaphragm: null,
|
diaphragm: null,
|
||||||
none: null,
|
none: null,
|
||||||
other: null,
|
other: null,
|
||||||
note: null
|
note: null,
|
||||||
},
|
},
|
||||||
temperature: {
|
temperature: {
|
||||||
exclude: false,
|
exclude: false,
|
||||||
note: null,
|
note: null,
|
||||||
time: LocalTime.now().truncatedTo(minutes).toString(),
|
time: LocalTime.now().truncatedTo(minutes).toString(),
|
||||||
value: null
|
value: null,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const symtomPage = {
|
export const symtomPage = {
|
||||||
@@ -126,11 +126,13 @@ export const symtomPage = {
|
|||||||
excludeText: labels.bleeding.exclude.explainer,
|
excludeText: labels.bleeding.exclude.explainer,
|
||||||
note: null,
|
note: null,
|
||||||
selectBoxGroups: null,
|
selectBoxGroups: null,
|
||||||
selectTabGroups: [{
|
selectTabGroups: [
|
||||||
|
{
|
||||||
key: 'value',
|
key: 'value',
|
||||||
options: getLabelsList(bleedingLabels),
|
options: getLabelsList(bleedingLabels),
|
||||||
title: labels.bleeding.heaviness.explainer,
|
title: labels.bleeding.heaviness.explainer,
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
cervix: {
|
cervix: {
|
||||||
excludeText: cervixLabels.excludeExplainer,
|
excludeText: cervixLabels.excludeExplainer,
|
||||||
@@ -151,18 +153,20 @@ export const symtomPage = {
|
|||||||
key: 'position',
|
key: 'position',
|
||||||
options: getLabelsList(cervixLabels.position.categories),
|
options: getLabelsList(cervixLabels.position.categories),
|
||||||
title: cervixLabels.position.explainer,
|
title: cervixLabels.position.explainer,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
desire: {
|
desire: {
|
||||||
excludeText: null,
|
excludeText: null,
|
||||||
note: null,
|
note: null,
|
||||||
selectBoxGroups: null,
|
selectBoxGroups: null,
|
||||||
selectTabGroups: [{
|
selectTabGroups: [
|
||||||
|
{
|
||||||
key: 'value',
|
key: 'value',
|
||||||
options: getLabelsList(intensityLabels),
|
options: getLabelsList(intensityLabels),
|
||||||
title: labels.desire.explainer
|
title: labels.desire.explainer,
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
mucus: {
|
mucus: {
|
||||||
excludeText: mucusLabels.excludeExplainer,
|
excludeText: mucusLabels.excludeExplainer,
|
||||||
@@ -178,34 +182,38 @@ export const symtomPage = {
|
|||||||
key: 'texture',
|
key: 'texture',
|
||||||
options: getLabelsList(mucusLabels.texture.categories),
|
options: getLabelsList(mucusLabels.texture.categories),
|
||||||
title: mucusLabels.texture.explainer,
|
title: mucusLabels.texture.explainer,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
mood: {
|
mood: {
|
||||||
excludeText: null,
|
excludeText: null,
|
||||||
note: null,
|
note: null,
|
||||||
selectBoxGroups: [{
|
selectBoxGroups: [
|
||||||
|
{
|
||||||
key: 'mood',
|
key: 'mood',
|
||||||
options: moodLabels,
|
options: moodLabels,
|
||||||
title: labels.mood.explainer
|
title: labels.mood.explainer,
|
||||||
}],
|
},
|
||||||
selectTabGroups: null
|
],
|
||||||
|
selectTabGroups: null,
|
||||||
},
|
},
|
||||||
note: {
|
note: {
|
||||||
excludeText: null,
|
excludeText: null,
|
||||||
note: noteDescription,
|
note: noteDescription,
|
||||||
selectBoxGroups: null,
|
selectBoxGroups: null,
|
||||||
selectTabGroups: null
|
selectTabGroups: null,
|
||||||
},
|
},
|
||||||
pain: {
|
pain: {
|
||||||
excludeText: null,
|
excludeText: null,
|
||||||
note: null,
|
note: null,
|
||||||
selectBoxGroups: [{
|
selectBoxGroups: [
|
||||||
|
{
|
||||||
key: 'pain',
|
key: 'pain',
|
||||||
options: painLabels,
|
options: painLabels,
|
||||||
title: labels.pain.explainer
|
title: labels.pain.explainer,
|
||||||
}],
|
},
|
||||||
selectTabGroups: null
|
],
|
||||||
|
selectTabGroups: null,
|
||||||
},
|
},
|
||||||
sex: {
|
sex: {
|
||||||
excludeText: null,
|
excludeText: null,
|
||||||
@@ -220,40 +228,42 @@ export const symtomPage = {
|
|||||||
key: 'contraceptives',
|
key: 'contraceptives',
|
||||||
options: contraceptiveLabels,
|
options: contraceptiveLabels,
|
||||||
title: labels.contraceptives.explainer,
|
title: labels.contraceptives.explainer,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
selectTabGroups: null
|
selectTabGroups: null,
|
||||||
},
|
},
|
||||||
temperature: {
|
temperature: {
|
||||||
excludeText: temperatureLabels.exclude.explainer,
|
excludeText: temperatureLabels.exclude.explainer,
|
||||||
note: temperatureLabels.note.explainer,
|
note: temperatureLabels.note.explainer,
|
||||||
selectBoxGroups: null,
|
selectBoxGroups: null,
|
||||||
selectTabGroups: null
|
selectTabGroups: null,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const save = {
|
export const save = {
|
||||||
bleeding: (data, date, shouldDeleteData) => {
|
bleeding: (data, date, shouldDeleteData) => {
|
||||||
const { exclude, value } = data
|
const { exclude, value } = data
|
||||||
const isDataEntered = isNumber(value)
|
const isDataEntered = isNumber(value)
|
||||||
const valuesToSave = shouldDeleteData || !isDataEntered
|
const valuesToSave =
|
||||||
? null : { value, exclude }
|
shouldDeleteData || !isDataEntered ? null : { value, exclude }
|
||||||
|
|
||||||
saveSymptom('bleeding', date, valuesToSave)
|
saveSymptom('bleeding', date, valuesToSave)
|
||||||
},
|
},
|
||||||
cervix: (data, date, shouldDeleteData) => {
|
cervix: (data, date, shouldDeleteData) => {
|
||||||
const { opening, firmness, position, exclude } = data
|
const { opening, firmness, position, exclude } = data
|
||||||
const isDataEntered = ['opening', 'firmness', 'position'].some(
|
const isDataEntered = ['opening', 'firmness', 'position'].some((value) =>
|
||||||
value => isNumber(data[value]))
|
isNumber(data[value])
|
||||||
const valuesToSave = shouldDeleteData || !isDataEntered
|
)
|
||||||
? null : { opening, firmness, position, exclude }
|
const valuesToSave =
|
||||||
|
shouldDeleteData || !isDataEntered
|
||||||
|
? null
|
||||||
|
: { opening, firmness, position, exclude }
|
||||||
|
|
||||||
saveSymptom('cervix', date, valuesToSave)
|
saveSymptom('cervix', date, valuesToSave)
|
||||||
},
|
},
|
||||||
desire: (data, date, shouldDeleteData) => {
|
desire: (data, date, shouldDeleteData) => {
|
||||||
const { value } = data
|
const { value } = data
|
||||||
const valuesToSave = shouldDeleteData || !isNumber(value)
|
const valuesToSave = shouldDeleteData || !isNumber(value) ? null : { value }
|
||||||
? null : { value }
|
|
||||||
|
|
||||||
saveSymptom('desire', date, valuesToSave)
|
saveSymptom('desire', date, valuesToSave)
|
||||||
},
|
},
|
||||||
@@ -262,10 +272,18 @@ export const save = {
|
|||||||
},
|
},
|
||||||
mucus: (data, date, shouldDeleteData) => {
|
mucus: (data, date, shouldDeleteData) => {
|
||||||
const { feeling, texture, exclude } = data
|
const { feeling, texture, exclude } = data
|
||||||
const isDataEntered = ['feeling', 'texture'].some(
|
const isDataEntered = ['feeling', 'texture'].some((value) =>
|
||||||
value => isNumber(data[value]))
|
isNumber(data[value])
|
||||||
const valuesToSave = shouldDeleteData || !isDataEntered
|
)
|
||||||
? null : { feeling, texture, value: computeNfpValue(feeling, texture), exclude }
|
const valuesToSave =
|
||||||
|
shouldDeleteData || !isDataEntered
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
feeling,
|
||||||
|
texture,
|
||||||
|
value: computeNfpValue(feeling, texture),
|
||||||
|
exclude,
|
||||||
|
}
|
||||||
|
|
||||||
saveSymptom('mucus', date, valuesToSave)
|
saveSymptom('mucus', date, valuesToSave)
|
||||||
},
|
},
|
||||||
@@ -288,21 +306,20 @@ export const save = {
|
|||||||
exclude,
|
exclude,
|
||||||
note,
|
note,
|
||||||
time,
|
time,
|
||||||
value: Number(value)
|
value: Number(value),
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSymptom(
|
saveSymptom(
|
||||||
'temperature',
|
'temperature',
|
||||||
date,
|
date,
|
||||||
(shouldDeleteData || value === null) ? null : valuesToSave
|
shouldDeleteData || value === null ? null : valuesToSave
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveBoxSymptom = (data, date, shouldDeleteData, symptom) => {
|
const saveBoxSymptom = (data, date, shouldDeleteData, symptom) => {
|
||||||
const isDataEntered = Object.keys(data).some(key => data[key] !== null)
|
const isDataEntered = Object.keys(data).some((key) => data[key] !== null)
|
||||||
const valuesToSave = shouldDeleteData || !isDataEntered
|
const valuesToSave = shouldDeleteData || !isDataEntered ? null : data
|
||||||
? null : data
|
|
||||||
|
|
||||||
saveSymptom(symptom, date, valuesToSave)
|
saveSymptom(symptom, date, valuesToSave)
|
||||||
}
|
}
|
||||||
@@ -326,46 +343,60 @@ const label = {
|
|||||||
return temperatureLabel
|
return temperatureLabel
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mucus: mucus => {
|
mucus: (mucus) => {
|
||||||
const filledCategories = ['feeling', 'texture'].filter(c => isNumber(mucus[c]))
|
const filledCategories = ['feeling', 'texture'].filter((c) =>
|
||||||
let label = filledCategories.map(category => {
|
isNumber(mucus[c])
|
||||||
return labels.mucus.subcategories[category] + ': ' + labels.mucus[category].categories[mucus[category]]
|
)
|
||||||
}).join(', ')
|
let label = filledCategories
|
||||||
|
.map((category) => {
|
||||||
|
return (
|
||||||
|
labels.mucus.subcategories[category] +
|
||||||
|
': ' +
|
||||||
|
labels.mucus[category].categories[mucus[category]]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
|
||||||
if (isNumber(mucus.value)) label += `\n => ${labels.mucusNFP[mucus.value]}`
|
if (isNumber(mucus.value)) label += ` => ${labels.mucusNFP[mucus.value]}`
|
||||||
if (mucus.exclude) label = `(${label})`
|
if (mucus.exclude) label = `(${label})`
|
||||||
|
|
||||||
return label
|
return label
|
||||||
},
|
},
|
||||||
cervix: cervix => {
|
cervix: (cervix) => {
|
||||||
const filledCategories = ['opening', 'firmness', 'position'].filter(c => isNumber(cervix[c]))
|
const filledCategories = ['opening', 'firmness', 'position'].filter((c) =>
|
||||||
let label = filledCategories.map(category => {
|
isNumber(cervix[c])
|
||||||
return labels.cervix.subcategories[category] + ': ' + labels.cervix[category].categories[cervix[category]]
|
)
|
||||||
}).join(', ')
|
let label = filledCategories
|
||||||
|
.map((category) => {
|
||||||
|
return (
|
||||||
|
labels.cervix.subcategories[category] +
|
||||||
|
': ' +
|
||||||
|
labels.cervix[category].categories[cervix[category]]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
|
||||||
if (cervix.exclude) label = `(${label})`
|
if (cervix.exclude) label = `(${label})`
|
||||||
|
|
||||||
return label
|
return label
|
||||||
},
|
},
|
||||||
note: note => note.value,
|
note: (note) => note.value,
|
||||||
desire: ({ value }) => {
|
desire: ({ value }) => {
|
||||||
if (isNumber(value)) {
|
if (isNumber(value)) {
|
||||||
return intensityLabels[value]
|
return intensityLabels[value]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sex: sex => {
|
sex: (sex) => {
|
||||||
|
sex = mapRealmObjToJsObj(sex)
|
||||||
const sexLabel = []
|
const sexLabel = []
|
||||||
if (sex && Object.values({...sex}).some(val => val)){
|
if (sex && Object.values({ ...sex }).some((val) => val)) {
|
||||||
Object.keys(sex).forEach(key => {
|
Object.keys(sex).forEach((key) => {
|
||||||
if(sex[key] && key !== 'other' && key !== 'note') {
|
if (sex[key] && key !== 'other' && key !== 'note') {
|
||||||
sexLabel.push(
|
sexLabel.push(sexLabels[key] || contraceptiveLabels[key])
|
||||||
sexLabels[key] ||
|
|
||||||
contraceptiveLabels[key]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if(key === 'other' && sex.other) {
|
if (key === 'other' && sex.other) {
|
||||||
let label = contraceptiveLabels[key]
|
let label = contraceptiveLabels[key]
|
||||||
if(sex.note) {
|
if (sex.note) {
|
||||||
label = `${label} (${sex.note})`
|
label = `${label} (${sex.note})`
|
||||||
}
|
}
|
||||||
sexLabel.push(label)
|
sexLabel.push(label)
|
||||||
@@ -374,16 +405,17 @@ const label = {
|
|||||||
return sexLabel.join(', ')
|
return sexLabel.join(', ')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pain: pain => {
|
pain: (pain) => {
|
||||||
|
pain = mapRealmObjToJsObj(pain)
|
||||||
const painLabel = []
|
const painLabel = []
|
||||||
if (pain && Object.values({...pain}).some(val => val)){
|
if (pain && Object.values({ ...pain }).some((val) => val)) {
|
||||||
Object.keys(pain).forEach(key => {
|
Object.keys(pain).forEach((key) => {
|
||||||
if(pain[key] && key !== 'other' && key !== 'note') {
|
if (pain[key] && key !== 'other' && key !== 'note') {
|
||||||
painLabel.push(painLabels[key])
|
painLabel.push(painLabels[key])
|
||||||
}
|
}
|
||||||
if(key === 'other' && pain.other) {
|
if (key === 'other' && pain.other) {
|
||||||
let label = painLabels[key]
|
let label = painLabels[key]
|
||||||
if(pain.note) {
|
if (pain.note) {
|
||||||
label = `${label} (${pain.note})`
|
label = `${label} (${pain.note})`
|
||||||
}
|
}
|
||||||
painLabel.push(label)
|
painLabel.push(label)
|
||||||
@@ -392,16 +424,17 @@ const label = {
|
|||||||
return painLabel.join(', ')
|
return painLabel.join(', ')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mood: mood => {
|
mood: (mood) => {
|
||||||
|
mood = mapRealmObjToJsObj(mood)
|
||||||
const moodLabel = []
|
const moodLabel = []
|
||||||
if (mood && Object.values({...mood}).some(val => val)){
|
if (mood && Object.values({ ...mood }).some((val) => val)) {
|
||||||
Object.keys(mood).forEach(key => {
|
Object.keys(mood).forEach((key) => {
|
||||||
if(mood[key] && key !== 'other' && key !== 'note') {
|
if (mood[key] && key !== 'other' && key !== 'note') {
|
||||||
moodLabel.push(moodLabels[key])
|
moodLabel.push(moodLabels[key])
|
||||||
}
|
}
|
||||||
if(key === 'other' && mood.other) {
|
if (key === 'other' && mood.other) {
|
||||||
let label = moodLabels[key]
|
let label = moodLabels[key]
|
||||||
if(mood.note) {
|
if (mood.note) {
|
||||||
label = `${label} (${mood.note})`
|
label = `${label} (${mood.note})`
|
||||||
}
|
}
|
||||||
moodLabel.push(label)
|
moodLabel.push(label)
|
||||||
@@ -409,7 +442,7 @@ const label = {
|
|||||||
})
|
})
|
||||||
return moodLabel.join(', ')
|
return moodLabel.join(', ')
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getData = (symptom, symptomData) => {
|
export const getData = (symptom, symptomData) => {
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import { general as labels } from '../../i18n/en/cycle-day'
|
|||||||
export default function (date) {
|
export default function (date) {
|
||||||
const today = LocalDate.now()
|
const today = LocalDate.now()
|
||||||
const dateToDisplay = LocalDate.parse(date)
|
const dateToDisplay = LocalDate.parse(date)
|
||||||
return today.equals(dateToDisplay) ?
|
return today.equals(dateToDisplay)
|
||||||
labels.today :
|
? labels.today
|
||||||
moment(date).format('MMMM Do YYYY')
|
: moment(date).format('MMMM Do YYYY')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDateForShortText (date) {
|
export function formatDateForShortText(date) {
|
||||||
return moment(date.toString()).format('dddd, MMMM Do')
|
return moment(date.toString()).format('dddd, MMMM Do')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dateToTitle(dateString) {
|
export function dateToTitle(dateString) {
|
||||||
const today = LocalDate.now()
|
const today = LocalDate.now()
|
||||||
const dateToDisplay = LocalDate.parse(dateString)
|
const dateToDisplay = LocalDate.parse(dateString)
|
||||||
return today.equals(dateToDisplay) ?
|
return today.equals(dateToDisplay)
|
||||||
labels.today :
|
? labels.today
|
||||||
moment(dateString).format('dddd, Do MMM YYYY')
|
: moment(dateString).format('ddd DD. MMM YY')
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import Toast from 'react-native-simple-toast'
|
||||||
|
|
||||||
|
export const showToast = (text) => Toast.show(
|
||||||
|
text, Toast.SHORT, ['RCTModalHostViewController', 'UIAlertController']
|
||||||
|
)
|
||||||
@@ -17,8 +17,8 @@ function getTimes(prediction) {
|
|||||||
return { todayDate, predictedBleedingStart, predictedBleedingEnd, daysToEnd }
|
return { todayDate, predictedBleedingStart, predictedBleedingEnd, daysToEnd }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function determinePredictionText(bleedingPrediction) {
|
export function determinePredictionText(bleedingPrediction, t) {
|
||||||
if (!bleedingPrediction.length) return predictLabels.noPrediction
|
if (!bleedingPrediction.length) return t('labels.bleedingPrediction.noPrediction')
|
||||||
const {
|
const {
|
||||||
todayDate,
|
todayDate,
|
||||||
predictedBleedingStart,
|
predictedBleedingStart,
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import { ScrollView, StyleSheet, View } from 'react-native'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import { LocalDate } from 'js-joda'
|
|
||||||
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import { navigate } from '../slices/navigation'
|
|
||||||
import { getDate, setDate } from '../slices/date'
|
|
||||||
|
|
||||||
import AppText from './common/app-text'
|
|
||||||
import Button from './common/button'
|
|
||||||
|
|
||||||
import cycleModule from '../lib/cycle'
|
|
||||||
import { getFertilityStatusForDay } from '../lib/sympto-adapter'
|
|
||||||
import { determinePredictionText, formatWithOrdinalSuffix } from './helpers/home'
|
|
||||||
|
|
||||||
import { Colors, Fonts, Sizes, Spacing } from '../styles'
|
|
||||||
import { home as labels } from '../i18n/en/labels'
|
|
||||||
|
|
||||||
class Home extends Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
navigate: PropTypes.func,
|
|
||||||
setDate: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
const today = LocalDate.now()
|
|
||||||
this.todayDateString = today.toString()
|
|
||||||
const { getCycleDayNumber, getPredictedMenses } = cycleModule()
|
|
||||||
this.cycleDayNumber = getCycleDayNumber(this.todayDateString)
|
|
||||||
const { status, phase, statusText } =
|
|
||||||
getFertilityStatusForDay(this.todayDateString)
|
|
||||||
const prediction = getPredictedMenses()
|
|
||||||
this.prediction = determinePredictionText(prediction)
|
|
||||||
this.title = `${today.dayOfMonth()} ${today.month()} ${today.year()}`
|
|
||||||
|
|
||||||
if (this.cycleDayNumber) {
|
|
||||||
this.cycleDayText = formatWithOrdinalSuffix(this.cycleDayNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (phase) {
|
|
||||||
this.phase = phase
|
|
||||||
this.phaseText = formatWithOrdinalSuffix(phase)
|
|
||||||
this.status = status
|
|
||||||
this.statusText = statusText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToCycleDayView = () => {
|
|
||||||
this.props.setDate(this.todayDateString)
|
|
||||||
this.props.navigate('CycleDay')
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
cycleDayNumber,
|
|
||||||
cycleDayText,
|
|
||||||
phase,
|
|
||||||
phaseText,
|
|
||||||
prediction,
|
|
||||||
status,
|
|
||||||
statusText,
|
|
||||||
title
|
|
||||||
} = this
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView
|
|
||||||
style={styles.container}
|
|
||||||
contentContainerStyle={styles.contentContainer}
|
|
||||||
>
|
|
||||||
<AppText style={styles.title}>{title}</AppText>
|
|
||||||
|
|
||||||
{cycleDayNumber &&
|
|
||||||
<View style={styles.line}>
|
|
||||||
<AppText style={styles.whiteSubtitle}>{cycleDayText}</AppText>
|
|
||||||
<AppText style={styles.turquoiseText}>{labels.cycleDay}</AppText>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
{phase &&
|
|
||||||
<View style={styles.line}>
|
|
||||||
<AppText style={styles.whiteSubtitle}>{phaseText}</AppText>
|
|
||||||
<AppText style={styles.turquoiseText}>
|
|
||||||
{labels.cyclePhase}
|
|
||||||
</AppText>
|
|
||||||
<AppText style={styles.turquoiseText}>{status}</AppText>
|
|
||||||
<Asterisk />
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
<View style={styles.line}>
|
|
||||||
<AppText style={styles.turquoiseText}>{prediction}</AppText>
|
|
||||||
</View>
|
|
||||||
<Button isCTA isSmall={false} onPress={this.navigateToCycleDayView}>
|
|
||||||
{labels.addData}
|
|
||||||
</Button>
|
|
||||||
{phase && (
|
|
||||||
<View style={styles.asteriskLine}>
|
|
||||||
<Asterisk />
|
|
||||||
<AppText linkStyle={styles.whiteText} style={styles.greyText}>
|
|
||||||
{statusText}
|
|
||||||
</AppText>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Asterisk = () => {
|
|
||||||
return <AppText style={styles.asterisk}>*</AppText>
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
asterisk: {
|
|
||||||
color: Colors.orange,
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
backgroundColor: Colors.purple,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
contentContainer: {
|
|
||||||
padding: Spacing.base,
|
|
||||||
paddingTop: 0,
|
|
||||||
},
|
|
||||||
line: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignContent: 'flex-start',
|
|
||||||
marginBottom: Spacing.tiny,
|
|
||||||
marginTop: Spacing.small,
|
|
||||||
},
|
|
||||||
asteriskLine: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignContent: 'flex-start',
|
|
||||||
marginBottom: Spacing.tiny,
|
|
||||||
marginTop: Spacing.small,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
color: Colors.purpleLight,
|
|
||||||
fontFamily: Fonts.bold,
|
|
||||||
fontSize: Sizes.huge,
|
|
||||||
marginVertical: Spacing.small,
|
|
||||||
},
|
|
||||||
turquoiseText: {
|
|
||||||
color: Colors.turquoise,
|
|
||||||
fontSize: Sizes.subtitle,
|
|
||||||
},
|
|
||||||
whiteSubtitle: {
|
|
||||||
color: 'white',
|
|
||||||
fontSize: Sizes.subtitle,
|
|
||||||
},
|
|
||||||
whiteText: {
|
|
||||||
color: 'white',
|
|
||||||
},
|
|
||||||
greyText: {
|
|
||||||
color: Colors.greyLight,
|
|
||||||
paddingLeft: Spacing.base,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
|
||||||
return ({
|
|
||||||
date: getDate(state),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
|
||||||
return ({
|
|
||||||
navigate: (page) => dispatch(navigate(page)),
|
|
||||||
setDate: (date) => dispatch(setDate(date)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
)(Home)
|
|
||||||
+8
-3
@@ -3,8 +3,8 @@ import settingsViews from './settings'
|
|||||||
import settingsLabels from '../i18n/en/settings'
|
import settingsLabels from '../i18n/en/settings'
|
||||||
const labels = settingsLabels.menuItems
|
const labels = settingsLabels.menuItems
|
||||||
|
|
||||||
export const isSettingsView =
|
export const isSettingsView = (page) =>
|
||||||
(page) => Object.keys(settingsViews).includes(page)
|
Object.keys(settingsViews).includes(page)
|
||||||
|
|
||||||
export const pages = [
|
export const pages = [
|
||||||
{
|
{
|
||||||
@@ -70,8 +70,13 @@ export const pages = [
|
|||||||
label: 'License',
|
label: 'License',
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: 'PrivacyPolicy',
|
||||||
|
label: 'PrivacyPolicy',
|
||||||
|
parent: 'SettingsMenu',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'CycleDay',
|
component: 'CycleDay',
|
||||||
parent: 'Home',
|
parent: 'Home',
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
@@ -1,17 +1,32 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Platform, Linking } from 'react-native'
|
||||||
|
|
||||||
import AppPage from '../common/app-page'
|
import AppPage from '../common/app-page'
|
||||||
import AppText from '../common/app-text'
|
import AppText from '../common/app-text'
|
||||||
import Segment from '../common/segment'
|
import Segment from '../common/segment'
|
||||||
|
import Button from '../common/button'
|
||||||
|
import ButtonRow from '../common/button-row'
|
||||||
|
|
||||||
import labels from '../../i18n/en/settings'
|
import labels from '../../i18n/en/settings'
|
||||||
import links from '../../i18n/en/links'
|
import links from '../../i18n/en/links'
|
||||||
|
|
||||||
const AboutSection = () => {
|
const AboutSection = () => {
|
||||||
return (
|
return (
|
||||||
<AppPage title={labels.aboutSection.title} >
|
<AppPage title={labels.aboutSection.title}>
|
||||||
<Segment>
|
<Segment>
|
||||||
<AppText>{labels.aboutSection.text}</AppText>
|
<AppText>{labels.aboutSection.text}</AppText>
|
||||||
|
<ButtonRow>
|
||||||
|
{[links.email, links.gitlab, links.website].map((link) => (
|
||||||
|
<Button
|
||||||
|
key={link.url}
|
||||||
|
isCTA
|
||||||
|
isSmall
|
||||||
|
onPress={() => Linking.openURL(link.url)}
|
||||||
|
>
|
||||||
|
{link.text}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</ButtonRow>
|
||||||
</Segment>
|
</Segment>
|
||||||
<Segment title={labels.philosophy.title}>
|
<Segment title={labels.philosophy.title}>
|
||||||
<AppText>{labels.philosophy.text}</AppText>
|
<AppText>{labels.philosophy.text}</AppText>
|
||||||
@@ -21,9 +36,15 @@ const AboutSection = () => {
|
|||||||
</Segment>
|
</Segment>
|
||||||
<Segment title={labels.donate.title}>
|
<Segment title={labels.donate.title}>
|
||||||
<AppText>{labels.donate.note}</AppText>
|
<AppText>{labels.donate.note}</AppText>
|
||||||
</Segment>
|
{Platform.OS !== 'ios' && (
|
||||||
<Segment title={labels.website.title}>
|
<Button
|
||||||
<AppText>{links.website.url}</AppText>
|
isCTA
|
||||||
|
isSmall
|
||||||
|
onPress={() => Linking.openURL(links.donate.url)}
|
||||||
|
>
|
||||||
|
{links.donate.text}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Segment>
|
</Segment>
|
||||||
<Segment title={labels.version.title} last>
|
<Segment title={labels.version.title} last>
|
||||||
<AppText>{require('../../package.json').version}</AppText>
|
<AppText>{require('../../package.json').version}</AppText>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const EXPORT_FILE_NAME = 'data.csv'
|
export const EXPORT_FILE_NAME = 'drip-data.csv'
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import RNFS from 'react-native-fs'
|
import RNFS from 'react-native-fs'
|
||||||
import { Alert, ToastAndroid } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import Button from '../../common/button'
|
import Button from '../../common/button'
|
||||||
@@ -9,6 +9,7 @@ import ConfirmWithPassword from '../common/confirm-with-password'
|
|||||||
import alertError from '../common/alert-error'
|
import alertError from '../common/alert-error'
|
||||||
|
|
||||||
import { clearDb, isDbEmpty } from '../../../db'
|
import { clearDb, isDbEmpty } from '../../../db'
|
||||||
|
import { showToast } from '../../helpers/general'
|
||||||
import { hasEncryptionObservable } from '../../../local-storage'
|
import { hasEncryptionObservable } from '../../../local-storage'
|
||||||
import settings from '../../../i18n/en/settings'
|
import settings from '../../../i18n/en/settings'
|
||||||
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
||||||
@@ -69,7 +70,7 @@ export default class DeleteData extends Component {
|
|||||||
clearDb()
|
clearDb()
|
||||||
}
|
}
|
||||||
await this.deleteExportedFile()
|
await this.deleteExportedFile()
|
||||||
ToastAndroid.show(success.message, ToastAndroid.LONG)
|
showToast(success.message)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alertError(errors.couldNotDeleteFile)
|
alertError(errors.couldNotDeleteFile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Share from 'react-native-share'
|
import Share from 'react-native-share'
|
||||||
|
|
||||||
import { getCycleDaysSortedByDate } from '../../../db'
|
import { getCycleDaysSortedByDate, mapRealmObjToJsObj } from '../../../db'
|
||||||
import getDataAsCsvDataUri from '../../../lib/import-export/export-to-csv'
|
import getDataAsCsvDataUri from '../../../lib/import-export/export-to-csv'
|
||||||
import alertError from '../common/alert-error'
|
import alertError from '../common/alert-error'
|
||||||
import settings from '../../../i18n/en/settings'
|
import settings from '../../../i18n/en/settings'
|
||||||
@@ -10,7 +10,7 @@ import RNFS from 'react-native-fs'
|
|||||||
export default async function exportData() {
|
export default async function exportData() {
|
||||||
let data
|
let data
|
||||||
const labels = settings.export
|
const labels = settings.export
|
||||||
const cycleDaysByDate = getCycleDaysSortedByDate()
|
const cycleDaysByDate = mapRealmObjToJsObj(getCycleDaysSortedByDate())
|
||||||
|
|
||||||
if (!cycleDaysByDate.length) return alertError(labels.errors.noData)
|
if (!cycleDaysByDate.length) return alertError(labels.errors.noData)
|
||||||
|
|
||||||
@@ -33,12 +33,11 @@ export default async function exportData() {
|
|||||||
url: `file://${path}`,
|
url: `file://${path}`,
|
||||||
subject: labels.subject,
|
subject: labels.subject,
|
||||||
type: 'text/csv',
|
type: 'text/csv',
|
||||||
showAppsToView: true
|
showAppsToView: true,
|
||||||
|
failOnCancel: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
return alertError(labels.errors.problemSharing)
|
return alertError(labels.errors.problemSharing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Alert } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker'
|
import DocumentPicker from 'react-native-document-picker'
|
||||||
import rnfs from 'react-native-fs'
|
import rnfs from 'react-native-fs'
|
||||||
import importCsv from '../../../lib/import-export/import-from-csv'
|
import importCsv from '../../../lib/import-export/import-from-csv'
|
||||||
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
||||||
@@ -7,35 +7,36 @@ import labels from '../../../i18n/en/settings'
|
|||||||
import alertError from '../common/alert-error'
|
import alertError from '../common/alert-error'
|
||||||
|
|
||||||
export function openImportDialog(onImportData) {
|
export function openImportDialog(onImportData) {
|
||||||
Alert.alert(
|
Alert.alert(labels.import.title, labels.import.message, [
|
||||||
labels.import.title,
|
{
|
||||||
labels.import.message,
|
text: sharedLabels.cancel,
|
||||||
[{
|
style: 'cancel',
|
||||||
text: sharedLabels.cancel, style: 'cancel', onPress: () => { }
|
onPress: () => {},
|
||||||
}, {
|
},
|
||||||
text: labels.import.deleteOption,
|
{
|
||||||
onPress: () => onImportData(true)
|
|
||||||
}, {
|
|
||||||
text: labels.import.replaceOption,
|
text: labels.import.replaceOption,
|
||||||
onPress: () => onImportData(false)
|
onPress: () => onImportData(false),
|
||||||
}]
|
},
|
||||||
)
|
{
|
||||||
|
text: labels.import.deleteOption,
|
||||||
|
onPress: () => onImportData(true),
|
||||||
|
},
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileContent() {
|
export async function getFileContent() {
|
||||||
let fileInfo
|
let fileInfo
|
||||||
try {
|
try {
|
||||||
fileInfo = await new Promise((resolve, reject) => {
|
fileInfo = await DocumentPicker.pick({
|
||||||
DocumentPicker.show({
|
type: [DocumentPicker.types.csv, 'text/comma-separated-values'],
|
||||||
filetype: [DocumentPickerUtil.allFiles()],
|
|
||||||
}, (err, res) => {
|
|
||||||
if (err) return reject(err)
|
|
||||||
resolve(res)
|
|
||||||
})
|
})
|
||||||
})
|
} catch (error) {
|
||||||
} catch (err) {
|
if (DocumentPicker.isCancel(error)) {
|
||||||
// because cancelling also triggers an error, we do nothing here
|
// User cancelled the picker, exit any dialogs or menus and move on
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
importError(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileContent
|
let fileContent
|
||||||
@@ -49,11 +50,10 @@ export async function getFileContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function importData(shouldDeleteExistingData, fileContent) {
|
export async function importData(shouldDeleteExistingData, fileContent) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await importCsv(fileContent, shouldDeleteExistingData)
|
await importCsv(fileContent, shouldDeleteExistingData)
|
||||||
Alert.alert(sharedLabels.successTitle, labels.import.success.message)
|
Alert.alert(sharedLabels.successTitle, labels.import.success.message)
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
importError(err.message)
|
importError(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,14 @@ import DataManagement from './data-management'
|
|||||||
import Password from './password'
|
import Password from './password'
|
||||||
import About from './about'
|
import About from './about'
|
||||||
import License from './license'
|
import License from './license'
|
||||||
|
import PrivacyPolicy from './privacy-policy'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Reminders, NfpSettings, DataManagement, Password, About, License
|
Reminders,
|
||||||
|
NfpSettings,
|
||||||
|
DataManagement,
|
||||||
|
Password,
|
||||||
|
About,
|
||||||
|
License,
|
||||||
|
PrivacyPolicy,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import AppPage from '../common/app-page'
|
import AppPage from '../common/app-page'
|
||||||
import AppText from '../common/app-text'
|
import AppText from '../common/app-text'
|
||||||
import Segment from '../common/segment'
|
import Segment from '../common/segment'
|
||||||
|
|
||||||
import labels from '../../i18n/en/settings'
|
|
||||||
|
|
||||||
const License = () => {
|
const License = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppPage title={labels.license.title}>
|
<AppPage title={t("settings.license.title")}>
|
||||||
<Segment last>
|
<Segment last>
|
||||||
<AppText>{labels.license.text}</AppText>
|
<AppText>{t("settings.license.text", { currentYear })}</AppText>
|
||||||
</Segment>
|
</Segment>
|
||||||
</AppPage>
|
</AppPage>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import AppIcon from '../../common/app-icon'
|
|||||||
import AppPage from '../../common/app-page'
|
import AppPage from '../../common/app-page'
|
||||||
import AppSwitch from '../../common/app-switch'
|
import AppSwitch from '../../common/app-switch'
|
||||||
import AppText from '../../common/app-text'
|
import AppText from '../../common/app-text'
|
||||||
import TemperatureSlider from './temperature-slider'
|
// import TemperatureSlider from './temperature-slider'
|
||||||
import Segment from '../../common/segment'
|
import Segment from '../../common/segment'
|
||||||
|
|
||||||
import { useCervixObservable, saveUseCervix } from '../../../local-storage'
|
import { useCervixObservable, saveUseCervix } from '../../../local-storage'
|
||||||
@@ -40,10 +40,11 @@ export default class Settings extends Component {
|
|||||||
value={shouldUseCervix}
|
value={shouldUseCervix}
|
||||||
/>
|
/>
|
||||||
</Segment>
|
</Segment>
|
||||||
<Segment title={labels.tempScale.segmentTitle}>
|
{/* disabled temporarily, TODO https://gitlab.com/bloodyhealth/drip/-/issues/545 */}
|
||||||
|
{/* <Segment title={labels.tempScale.segmentTitle}>
|
||||||
<AppText>{labels.tempScale.segmentExplainer}</AppText>
|
<AppText>{labels.tempScale.segmentExplainer}</AppText>
|
||||||
<TemperatureSlider />
|
<TemperatureSlider />
|
||||||
</Segment>
|
</Segment> */}
|
||||||
<Segment last>
|
<Segment last>
|
||||||
<View style={styles.line}>
|
<View style={styles.line}>
|
||||||
<AppIcon
|
<AppIcon
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import Button from '../../common/button'
|
import Button from '../../common/button'
|
||||||
|
|
||||||
@@ -8,6 +9,10 @@ import showBackUpReminder from './show-backup-reminder'
|
|||||||
import settings from '../../../i18n/en/settings'
|
import settings from '../../../i18n/en/settings'
|
||||||
|
|
||||||
export default class CreatePassword extends Component {
|
export default class CreatePassword extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
changeEncryptionAndRestart: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
@@ -23,7 +28,7 @@ export default class CreatePassword extends Component {
|
|||||||
showBackUpReminder(this.toggleSettingPassword, () => {})
|
showBackUpReminder(this.toggleSettingPassword, () => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { isSettingPassword } = this.state
|
const { isSettingPassword } = this.state
|
||||||
const labels = settings.passwordSettings
|
const labels = settings.passwordSettings
|
||||||
|
|
||||||
@@ -34,8 +39,11 @@ export default class CreatePassword extends Component {
|
|||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return <EnterNewPassword />
|
return (
|
||||||
|
<EnterNewPassword
|
||||||
|
changeEncryptionAndRestart={this.props.changeEncryptionAndRestart}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,13 @@ import PropTypes from 'prop-types'
|
|||||||
import Button from '../../common/button'
|
import Button from '../../common/button'
|
||||||
import ConfirmWithPassword from '../common/confirm-with-password'
|
import ConfirmWithPassword from '../common/confirm-with-password'
|
||||||
|
|
||||||
import { changeEncryptionAndRestartApp } from '../../../db'
|
|
||||||
import labels from '../../../i18n/en/settings'
|
import labels from '../../../i18n/en/settings'
|
||||||
|
|
||||||
export default class DeletePassword extends Component {
|
export default class DeletePassword extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onStartDelete: PropTypes.func,
|
onStartDelete: PropTypes.func,
|
||||||
onCancelDelete: PropTypes.func
|
onCancelDelete: PropTypes.func,
|
||||||
|
changeEncryptionAndRestart: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -24,10 +24,6 @@ export default class DeletePassword extends Component {
|
|||||||
this.props.onStartDelete()
|
this.props.onStartDelete()
|
||||||
}
|
}
|
||||||
|
|
||||||
startDeletePassword = async () => {
|
|
||||||
await changeEncryptionAndRestartApp()
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelConfirmationWithPassword = () => {
|
cancelConfirmationWithPassword = () => {
|
||||||
this.setState({ enteringCurrentPassword: false })
|
this.setState({ enteringCurrentPassword: false })
|
||||||
this.props.onCancelDelete()
|
this.props.onCancelDelete()
|
||||||
@@ -39,7 +35,7 @@ export default class DeletePassword extends Component {
|
|||||||
if (enteringCurrentPassword) {
|
if (enteringCurrentPassword) {
|
||||||
return (
|
return (
|
||||||
<ConfirmWithPassword
|
<ConfirmWithPassword
|
||||||
onSuccess={this.startDeletePassword}
|
onSuccess={this.props.changeEncryptionAndRestart}
|
||||||
onCancel={this.cancelConfirmationWithPassword}
|
onCancel={this.cancelConfirmationWithPassword}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
import nodejs from 'nodejs-mobile-react-native'
|
import nodejs from 'nodejs-mobile-react-native'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import AppText from '../../common/app-text'
|
import AppText from '../../common/app-text'
|
||||||
import AppTextInput from '../../common/app-text-input'
|
import AppTextInput from '../../common/app-text-input'
|
||||||
import Button from '../../common/button'
|
import Button from '../../common/button'
|
||||||
|
|
||||||
import { requestHash, changeEncryptionAndRestartApp } from '../../../db'
|
import { requestHash } from '../../../db'
|
||||||
import { Colors, Spacing } from '../../../styles'
|
import { Colors, Spacing } from '../../../styles'
|
||||||
import settings from '../../../i18n/en/settings'
|
import settings from '../../../i18n/en/settings'
|
||||||
|
|
||||||
const LISTENER_TYPE = 'create-or-change-pw'
|
const LISTENER_TYPE = 'create-or-change-pw'
|
||||||
|
|
||||||
export default class EnterNewPassword extends Component {
|
export default class EnterNewPassword extends Component {
|
||||||
|
static propTypes = {
|
||||||
constructor() {
|
changeEncryptionAndRestart: PropTypes.func,
|
||||||
|
}
|
||||||
|
constructor(props) {
|
||||||
super()
|
super()
|
||||||
this.state = {
|
this.state = {
|
||||||
password: '',
|
password: '',
|
||||||
@@ -23,13 +26,16 @@ export default class EnterNewPassword extends Component {
|
|||||||
}
|
}
|
||||||
nodejs.channel.addListener(
|
nodejs.channel.addListener(
|
||||||
LISTENER_TYPE,
|
LISTENER_TYPE,
|
||||||
changeEncryptionAndRestartApp,
|
props.changeEncryptionAndRestart,
|
||||||
this
|
this
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
nodejs.channel.removeListener(LISTENER_TYPE, changeEncryptionAndRestartApp)
|
nodejs.channel.removeListener(
|
||||||
|
LISTENER_TYPE,
|
||||||
|
this.props.changeEncryptionAndRestart
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
savePassword = () => {
|
savePassword = () => {
|
||||||
@@ -52,15 +58,12 @@ export default class EnterNewPassword extends Component {
|
|||||||
this.setState({ passwordConfirmation })
|
this.setState({ passwordConfirmation })
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const {
|
const { password, passwordConfirmation, shouldShowErrorMessage } =
|
||||||
password,
|
this.state
|
||||||
passwordConfirmation,
|
|
||||||
shouldShowErrorMessage
|
|
||||||
} = this.state
|
|
||||||
const labels = settings.passwordSettings
|
const labels = settings.passwordSettings
|
||||||
const isButtonActive =
|
const isButtonActive =
|
||||||
(password.length > 0) && (passwordConfirmation.length > 0)
|
password.length > 0 && passwordConfirmation.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@@ -80,10 +83,14 @@ export default class EnterNewPassword extends Component {
|
|||||||
value={passwordConfirmation}
|
value={passwordConfirmation}
|
||||||
secureTextEntry={true}
|
secureTextEntry={true}
|
||||||
/>
|
/>
|
||||||
{shouldShowErrorMessage &&
|
{shouldShowErrorMessage && (
|
||||||
<AppText style={styles.error}>{labels.passwordsDontMatch}</AppText>
|
<AppText style={styles.error}>{labels.passwordsDontMatch}</AppText>
|
||||||
}
|
)}
|
||||||
<Button isCTA={isButtonActive} onPress={this.savePassword}>
|
<Button
|
||||||
|
isCTA={isButtonActive}
|
||||||
|
disabled={!isButtonActive}
|
||||||
|
onPress={this.savePassword}
|
||||||
|
>
|
||||||
{labels.savePassword}
|
{labels.savePassword}
|
||||||
</Button>
|
</Button>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@@ -94,6 +101,6 @@ export default class EnterNewPassword extends Component {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
error: {
|
error: {
|
||||||
color: Colors.orange,
|
color: Colors.orange,
|
||||||
marginTop: Spacing.base
|
marginTop: Spacing.base,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import { navigate } from '../../../slices/navigation'
|
||||||
|
|
||||||
|
import { changeDbEncryption } from '../../../db'
|
||||||
|
|
||||||
import AppPage from '../../common/app-page'
|
import AppPage from '../../common/app-page'
|
||||||
import AppText from '../../common/app-text'
|
import AppText from '../../common/app-text'
|
||||||
@@ -11,14 +17,18 @@ import DeletePassword from './delete'
|
|||||||
import { hasEncryptionObservable } from '../../../local-storage'
|
import { hasEncryptionObservable } from '../../../local-storage'
|
||||||
import labels from '../../../i18n/en/settings'
|
import labels from '../../../i18n/en/settings'
|
||||||
|
|
||||||
export default class PasswordSetting extends Component {
|
class PasswordSetting extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
navigate: PropTypes.func,
|
||||||
|
restartApp: PropTypes.func,
|
||||||
|
}
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isPasswordSet: hasEncryptionObservable.value,
|
isPasswordSet: hasEncryptionObservable.value,
|
||||||
isChangingPassword: false,
|
isChangingPassword: false,
|
||||||
isDeletingPassword: false
|
isDeletingPassword: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,19 +48,17 @@ export default class PasswordSetting extends Component {
|
|||||||
this.setState({ isDeletingPassword: false })
|
this.setState({ isDeletingPassword: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeEncryptionAndRestart = async (hash) => {
|
||||||
|
await changeDbEncryption(hash)
|
||||||
|
await this.props.restartApp()
|
||||||
|
this.props.navigate('Home')
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { isPasswordSet, isChangingPassword, isDeletingPassword } = this.state
|
||||||
|
|
||||||
const {
|
const { title, explainerEnabled, explainerDisabled } =
|
||||||
isPasswordSet,
|
labels.passwordSettings
|
||||||
isChangingPassword,
|
|
||||||
isDeletingPassword,
|
|
||||||
} = this.state
|
|
||||||
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
explainerEnabled,
|
|
||||||
explainerDisabled
|
|
||||||
} = labels.passwordSettings
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppPage>
|
<AppPage>
|
||||||
@@ -59,19 +67,25 @@ export default class PasswordSetting extends Component {
|
|||||||
{isPasswordSet ? explainerEnabled : explainerDisabled}
|
{isPasswordSet ? explainerEnabled : explainerDisabled}
|
||||||
</AppText>
|
</AppText>
|
||||||
|
|
||||||
{!isPasswordSet && <CreatePassword/>}
|
{!isPasswordSet && (
|
||||||
|
<CreatePassword
|
||||||
{(isPasswordSet && !isDeletingPassword) && (
|
changeEncryptionAndRestart={this.changeEncryptionAndRestart}
|
||||||
<ChangePassword
|
|
||||||
onStartChange = {this.onChangingPassword}
|
|
||||||
onCancelChange = {this.onCancelChangingPassword}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isPasswordSet && !isChangingPassword) && (
|
{isPasswordSet && !isDeletingPassword && (
|
||||||
|
<ChangePassword
|
||||||
|
onStartChange={this.onChangingPassword}
|
||||||
|
onCancelChange={this.onCancelChangingPassword}
|
||||||
|
changeEncryptionAndRestart={this.changeEncryptionAndRestart}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isPasswordSet && !isChangingPassword && (
|
||||||
<DeletePassword
|
<DeletePassword
|
||||||
onStartDelete = {this.onDeletingPassword}
|
onStartDelete={this.onDeletingPassword}
|
||||||
onCancelDelete = {this.onCancelDeletingPassword}
|
onCancelDelete={this.onCancelDeletingPassword}
|
||||||
|
changeEncryptionAndRestart={this.changeEncryptionAndRestart}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Segment>
|
</Segment>
|
||||||
@@ -79,3 +93,11 @@ export default class PasswordSetting extends Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
return {
|
||||||
|
navigate: (page) => dispatch(navigate(page)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(PasswordSetting)
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
import { Alert } from 'react-native'
|
import { Alert, Platform } from 'react-native'
|
||||||
import { shared } from '../../../i18n/en/labels'
|
import { shared } from '../../../i18n/en/labels'
|
||||||
import labels from '../../../i18n/en/settings'
|
import labels from '../../../i18n/en/settings'
|
||||||
|
|
||||||
export default function showBackUpReminder(okHandler, cancelHandler, isDelete) {
|
export default function showBackUpReminder(okHandler, cancelHandler, isDelete) {
|
||||||
let title, message
|
const { title, message } = isDelete
|
||||||
if (isDelete) {
|
? labels.passwordSettings.deleteBackupReminder
|
||||||
title = labels.passwordSettings.deleteBackupReminderTitle
|
: labels.passwordSettings.backupReminder
|
||||||
message = labels.passwordSettings.deleteBackupReminder
|
|
||||||
} else {
|
const { backupReminderAppendix } = labels.passwordSettings
|
||||||
title = labels.passwordSettings.backupReminderTitle
|
const appendix =
|
||||||
message = labels.passwordSettings.backupReminder
|
Platform.OS === 'ios'
|
||||||
}
|
? backupReminderAppendix.ios
|
||||||
|
: backupReminderAppendix.android
|
||||||
|
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
title,
|
title,
|
||||||
message,
|
message + appendix,
|
||||||
[{
|
[
|
||||||
|
{
|
||||||
text: shared.cancel,
|
text: shared.cancel,
|
||||||
onPress: cancelHandler,
|
onPress: cancelHandler,
|
||||||
style: 'cancel'
|
style: 'cancel',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: shared.ok,
|
text: shared.ok,
|
||||||
onPress: okHandler
|
onPress: okHandler,
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
{ onDismiss: cancelHandler }
|
{ onDismiss: cancelHandler }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,8 @@ import settings from '../../../i18n/en/settings'
|
|||||||
export default class ChangePassword extends Component {
|
export default class ChangePassword extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onStartChange: PropTypes.func,
|
onStartChange: PropTypes.func,
|
||||||
onCancelChange: PropTypes.func
|
onCancelChange: PropTypes.func,
|
||||||
|
changeEncryptionAndRestart: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -21,7 +22,7 @@ export default class ChangePassword extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
currentPassword: null,
|
currentPassword: null,
|
||||||
enteringCurrentPassword: false,
|
enteringCurrentPassword: false,
|
||||||
enteringNewPassword: false
|
enteringNewPassword: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ export default class ChangePassword extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
currentPassword: null,
|
currentPassword: null,
|
||||||
enteringNewPassword: true,
|
enteringNewPassword: true,
|
||||||
enteringCurrentPassword: false
|
enteringCurrentPassword: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,17 +50,14 @@ export default class ChangePassword extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
currentPassword: null,
|
currentPassword: null,
|
||||||
enteringNewPassword: false,
|
enteringNewPassword: false,
|
||||||
enteringCurrentPassword: false
|
enteringCurrentPassword: false,
|
||||||
})
|
})
|
||||||
this.props.onCancelChange()
|
this.props.onCancelChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { enteringCurrentPassword, enteringNewPassword, currentPassword } =
|
||||||
enteringCurrentPassword,
|
this.state
|
||||||
enteringNewPassword,
|
|
||||||
currentPassword
|
|
||||||
} = this.state
|
|
||||||
const labels = settings.passwordSettings
|
const labels = settings.passwordSettings
|
||||||
const isPasswordSet = currentPassword !== null
|
const isPasswordSet = currentPassword !== null
|
||||||
|
|
||||||
@@ -73,7 +71,11 @@ export default class ChangePassword extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (enteringNewPassword) {
|
if (enteringNewPassword) {
|
||||||
return <EnterNewPassword />
|
return (
|
||||||
|
<EnterNewPassword
|
||||||
|
changeEncryptionAndRestart={this.props.changeEncryptionAndRestart}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { StyleSheet } from 'react-native'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import AppPage from '../common/app-page'
|
||||||
|
import AppText from '../common/app-text'
|
||||||
|
import Segment from '../common/segment'
|
||||||
|
|
||||||
|
import { Colors, Sizes } from '../../styles'
|
||||||
|
|
||||||
|
const PrivacyPolicy = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const sections = ['intro', 'dataUse', 'permissions', 'transparency']
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppPage title={t('settings.privacyPolicy.title')}>
|
||||||
|
{sections.map((sectionItem) => {
|
||||||
|
return (
|
||||||
|
<Segment last key={sectionItem.id}>
|
||||||
|
<AppText style={styles.title}>
|
||||||
|
{t(`settings.privacyPolicy.${sectionItem}.title`)}
|
||||||
|
</AppText>
|
||||||
|
<AppText>{t(`settings.privacyPolicy.${sectionItem}.text`)}</AppText>
|
||||||
|
</Segment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</AppPage>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
title: {
|
||||||
|
color: Colors.purple,
|
||||||
|
fontSize: Sizes.subtitle,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PrivacyPolicy
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import DateTimePicker from 'react-native-modal-datetime-picker-nevo'
|
import { Platform } from 'react-native'
|
||||||
|
import DateTimePicker from 'react-native-modal-datetime-picker'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import AppSwitch from '../../common/app-switch'
|
import AppSwitch from '../../common/app-switch'
|
||||||
|
|
||||||
import { saveTempReminder, tempReminderObservable } from '../../../local-storage'
|
import {
|
||||||
|
saveTempReminder,
|
||||||
|
tempReminderObservable,
|
||||||
|
} from '../../../local-storage'
|
||||||
import padWithZeros from '../../helpers/pad-time-with-zeros'
|
import padWithZeros from '../../helpers/pad-time-with-zeros'
|
||||||
|
|
||||||
import labels from '../../../i18n/en/settings'
|
import labels from '../../../i18n/en/settings'
|
||||||
|
import { withTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default class TemperatureReminder extends Component {
|
class TemperatureReminder extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
@@ -42,9 +48,12 @@ export default class TemperatureReminder extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isEnabled, isTimePickerVisible, time } = this.state
|
const { isEnabled, isTimePickerVisible, time } = this.state
|
||||||
|
const { t } = this.props
|
||||||
|
|
||||||
const tempReminderText = time && isEnabled ?
|
const tempReminderText =
|
||||||
labels.tempReminder.timeSet(time) : labels.tempReminder.noTimeSet
|
time && isEnabled
|
||||||
|
? labels.tempReminder.timeSet(time)
|
||||||
|
: labels.tempReminder.noTimeSet
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@@ -58,8 +67,15 @@ export default class TemperatureReminder extends Component {
|
|||||||
mode="time"
|
mode="time"
|
||||||
onConfirm={this.onPickDate}
|
onConfirm={this.onPickDate}
|
||||||
onCancel={this.onPickDateCancel}
|
onCancel={this.onPickDateCancel}
|
||||||
|
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||||
|
headerTextIOS={t('labels.shared.dateTimePickerTitle')}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TemperatureReminder.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
export default withTranslation()(TemperatureReminder)
|
||||||
|
|||||||
+10
-13
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Dimensions, ImageBackground, StyleSheet, View } from 'react-native'
|
import { ImageBackground, View } from 'react-native'
|
||||||
|
import { ScaledSheet } from 'react-native-size-matters'
|
||||||
|
|
||||||
import AppPage from './common/app-page'
|
import AppPage from './common/app-page'
|
||||||
import AppText from './common/app-text'
|
import AppText from './common/app-text'
|
||||||
@@ -11,10 +12,8 @@ import {getCycleLengthStats as getCycleInfo} from '../lib/cycle-length'
|
|||||||
import {stats as labels} from '../i18n/en/labels'
|
import {stats as labels} from '../i18n/en/labels'
|
||||||
|
|
||||||
import { Sizes, Spacing, Typography } from '../styles'
|
import { Sizes, Spacing, Typography } from '../styles'
|
||||||
import { fontRatio } from '../config'
|
|
||||||
|
|
||||||
const image = require('../assets/cycle-icon.png')
|
const image = require('../assets/cycle-icon.png')
|
||||||
const screen = Dimensions.get('screen')
|
|
||||||
|
|
||||||
const Stats = () => {
|
const Stats = () => {
|
||||||
const cycleLengths = cycleModule().getAllCycleLengths()
|
const cycleLengths = cycleModule().getAllCycleLengths()
|
||||||
@@ -28,8 +27,6 @@ const Stats = () => {
|
|||||||
[cycleData.stdDeviation ? cycleData.stdDeviation : '—', labels.stdLabel],
|
[cycleData.stdDeviation ? cycleData.stdDeviation : '—', labels.stdLabel],
|
||||||
[numberOfCycles, labels.basisOfStatsEnd]
|
[numberOfCycles, labels.basisOfStatsEnd]
|
||||||
]
|
]
|
||||||
const height = screen.height * 0.2
|
|
||||||
const marginTop = (height / 8 - Sizes.icon / fontRatio) / 4
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppPage contentContainerStyle={styles.pageContainer}>
|
<AppPage contentContainerStyle={styles.pageContainer}>
|
||||||
@@ -42,12 +39,12 @@ const Stats = () => {
|
|||||||
<ImageBackground
|
<ImageBackground
|
||||||
source={image}
|
source={image}
|
||||||
imageStyle={styles.image}
|
imageStyle={styles.image}
|
||||||
style={[styles.imageContainter, { height }]}
|
style={styles.imageContainter}
|
||||||
>
|
>
|
||||||
<AppText
|
<AppText
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
ellipsizeMode="clip"
|
ellipsizeMode="clip"
|
||||||
style={[styles.accentPurpleGiant, { marginTop }]}
|
style={styles.accentPurpleGiant}
|
||||||
>
|
>
|
||||||
{cycleData.mean}
|
{cycleData.mean}
|
||||||
</AppText>
|
</AppText>
|
||||||
@@ -73,13 +70,14 @@ const column = {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = ScaledSheet.create({
|
||||||
accentOrange: {
|
accentOrange: {
|
||||||
...Typography.accentOrange,
|
...Typography.accentOrange,
|
||||||
fontSize: Sizes.small,
|
fontSize: Sizes.small,
|
||||||
},
|
},
|
||||||
accentPurpleGiant: {
|
accentPurpleGiant: {
|
||||||
...Typography.accentPurpleGiant,
|
...Typography.accentPurpleGiant,
|
||||||
|
marginTop: Spacing.base * (-2),
|
||||||
},
|
},
|
||||||
accentPurpleHuge: {
|
accentPurpleHuge: {
|
||||||
...Typography.accentPurpleHuge,
|
...Typography.accentPurpleHuge,
|
||||||
@@ -89,23 +87,22 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingTop: Spacing.base,
|
paddingTop: Spacing.base
|
||||||
},
|
},
|
||||||
columnLeft: {
|
columnLeft: {
|
||||||
...column,
|
...column,
|
||||||
flex: 4,
|
flex: 2,
|
||||||
},
|
},
|
||||||
columnRight: {
|
columnRight: {
|
||||||
...column,
|
...column,
|
||||||
flex: 5,
|
flex: 3,
|
||||||
paddingTop: Spacing.small,
|
paddingTop: Spacing.small,
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
resizeMode: 'contain',
|
resizeMode: 'contain',
|
||||||
|
|
||||||
},
|
},
|
||||||
imageContainter: {
|
imageContainter: {
|
||||||
paddingTop: Spacing.large * 2,
|
paddingTop: Spacing.large * 2.5,
|
||||||
marginBottom: Spacing.large,
|
marginBottom: Spacing.large,
|
||||||
},
|
},
|
||||||
pageContainer: {
|
pageContainer: {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import Home from './home'
|
import Home from './Home'
|
||||||
import Calendar from './calendar'
|
import Calendar from './calendar'
|
||||||
import CycleDay from './cycle-day/cycle-day-overview'
|
import CycleDay from './cycle-day/cycle-day-overview'
|
||||||
import Chart from './chart/chart'
|
import Chart from './chart/chart'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PixelRatio } from 'react-native'
|
import { PixelRatio, StatusBar } from 'react-native'
|
||||||
|
import { scale, verticalScale } from 'react-native-size-matters'
|
||||||
|
|
||||||
export const ACTION_DELETE = 'delete'
|
export const ACTION_DELETE = 'delete'
|
||||||
export const ACTION_EXPORT = 'export'
|
export const ACTION_EXPORT = 'export'
|
||||||
@@ -16,16 +17,17 @@ export const SYMPTOMS = [
|
|||||||
'note',
|
'note',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const fontRatio = PixelRatio.getFontScale()
|
|
||||||
export const CHART_COLUMN_WIDTH = 32
|
export const CHART_COLUMN_WIDTH = 32
|
||||||
export const CHART_COLUMN_MIDDLE = CHART_COLUMN_WIDTH / 2
|
export const CHART_COLUMN_MIDDLE = CHART_COLUMN_WIDTH / 2
|
||||||
export const CHART_DOT_RADIUS = 6
|
export const CHART_DOT_RADIUS = scale(6)
|
||||||
export const CHART_GRID_LINE_HORIZONTAL_WIDTH = 0.3
|
export const CHART_GRID_LINE_HORIZONTAL_WIDTH =
|
||||||
export const CHART_ICON_SIZE = 20
|
PixelRatio.roundToNearestPixel(0.3)
|
||||||
export const CHART_STROKE_WIDTH = 3
|
export const CHART_ICON_SIZE = scale(20)
|
||||||
export const CHART_SYMPTOM_HEIGHT_RATIO = 0.08
|
export const CHART_STROKE_WIDTH = scale(3)
|
||||||
export const CHART_XAXIS_HEIGHT_RATIO = 0.1
|
export const CHART_SYMPTOM_HEIGHT_RATIO = scale(0.08)
|
||||||
export const CHART_YAXIS_WIDTH = 32
|
export const CHART_XAXIS_HEIGHT_RATIO = scale(0.1)
|
||||||
|
export const CHART_YAXIS_WIDTH = scale(32)
|
||||||
|
export const CHART_TICK_WIDTH = scale(44)
|
||||||
|
|
||||||
export const TEMP_SCALE_MAX = 37.5
|
export const TEMP_SCALE_MAX = 37.5
|
||||||
export const TEMP_SCALE_MIN = 35.5
|
export const TEMP_SCALE_MIN = 35.5
|
||||||
@@ -34,4 +36,11 @@ export const TEMP_MAX = 39
|
|||||||
export const TEMP_MIN = 35
|
export const TEMP_MIN = 35
|
||||||
export const TEMP_SLIDER_STEP = 0.5
|
export const TEMP_SLIDER_STEP = 0.5
|
||||||
|
|
||||||
export const HIT_SLOP = { top: 20, bottom: 20, left: 20, right: 20 }
|
export const HIT_SLOP = {
|
||||||
|
top: verticalScale(20),
|
||||||
|
bottom: verticalScale(20),
|
||||||
|
left: scale(20),
|
||||||
|
right: scale(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STATUSBAR_HEIGHT = StatusBar.currentHeight
|
||||||
|
|||||||
+43
-32
@@ -2,7 +2,7 @@ import Realm from 'realm'
|
|||||||
import { LocalDate, ChronoUnit } from 'js-joda'
|
import { LocalDate, ChronoUnit } from 'js-joda'
|
||||||
import nodejs from 'nodejs-mobile-react-native'
|
import nodejs from 'nodejs-mobile-react-native'
|
||||||
import fs from 'react-native-fs'
|
import fs from 'react-native-fs'
|
||||||
import restart from 'react-native-restart'
|
|
||||||
import schemas from './schemas'
|
import schemas from './schemas'
|
||||||
import cycleModule from '../lib/cycle'
|
import cycleModule from '../lib/cycle'
|
||||||
import maybeSetNewCycleStart from '../lib/set-new-cycle-start'
|
import maybeSetNewCycleStart from '../lib/set-new-cycle-start'
|
||||||
@@ -11,7 +11,7 @@ let db
|
|||||||
let checkIsMensesStart
|
let checkIsMensesStart
|
||||||
let getMensesDaysRightAfter
|
let getMensesDaysRightAfter
|
||||||
|
|
||||||
export async function openDb (hash) {
|
export async function openDb(hash) {
|
||||||
const realmConfig = {}
|
const realmConfig = {}
|
||||||
if (hash) {
|
if (hash) {
|
||||||
realmConfig.encryptionKey = hashToInt8Array(hash)
|
realmConfig.encryptionKey = hashToInt8Array(hash)
|
||||||
@@ -22,7 +22,7 @@ export async function openDb (hash) {
|
|||||||
let tempConnection
|
let tempConnection
|
||||||
try {
|
try {
|
||||||
tempConnection = await Realm.open(realmConfig)
|
tempConnection = await Realm.open(realmConfig)
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
const isErrorDecrypting = err.toString().includes('decrypt')
|
const isErrorDecrypting = err.toString().includes('decrypt')
|
||||||
const isErrorMnemonic = err.toString().includes('Invalid mnemonic')
|
const isErrorMnemonic = err.toString().includes('Invalid mnemonic')
|
||||||
// tried to open without password, but is encrypted or incorrect pwd
|
// tried to open without password, but is encrypted or incorrect pwd
|
||||||
@@ -36,20 +36,16 @@ export async function openDb (hash) {
|
|||||||
let nextSchemaIndex = Realm.schemaVersion(Realm.defaultPath)
|
let nextSchemaIndex = Realm.schemaVersion(Realm.defaultPath)
|
||||||
tempConnection.close()
|
tempConnection.close()
|
||||||
while (nextSchemaIndex < schemas.length - 1) {
|
while (nextSchemaIndex < schemas.length - 1) {
|
||||||
const tempConfig = Object.assign(
|
const tempConfig = Object.assign(realmConfig, schemas[nextSchemaIndex++])
|
||||||
realmConfig,
|
|
||||||
schemas[nextSchemaIndex++]
|
|
||||||
)
|
|
||||||
const migratedRealm = new Realm(tempConfig)
|
const migratedRealm = new Realm(tempConfig)
|
||||||
migratedRealm.close()
|
migratedRealm.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// open the Realm with the latest schema
|
// open the Realm with the latest schema
|
||||||
realmConfig.schema = schemas[schemas.length - 1]
|
realmConfig.schema = schemas[schemas.length - 1]
|
||||||
const connection = await Realm.open(Object.assign(
|
const connection = await Realm.open(
|
||||||
realmConfig,
|
Object.assign(realmConfig, schemas[schemas.length - 1])
|
||||||
schemas[schemas.length - 1]
|
)
|
||||||
))
|
|
||||||
|
|
||||||
db = connection
|
db = connection
|
||||||
const cycle = cycleModule()
|
const cycle = cycleModule()
|
||||||
@@ -62,18 +58,33 @@ export function closeDb() {
|
|||||||
db.close()
|
db.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapRealmObjToJsObj(realmObj) {
|
||||||
|
return realmObj ? JSON.parse(JSON.stringify(realmObj)) : realmObj
|
||||||
|
}
|
||||||
|
|
||||||
export function getBleedingDaysSortedByDate() {
|
export function getBleedingDaysSortedByDate() {
|
||||||
return db.objects('CycleDay').filtered('bleeding != null').sorted('date', true)
|
return db
|
||||||
|
.objects('CycleDay')
|
||||||
|
.filtered('bleeding != null')
|
||||||
|
.sorted('date', true)
|
||||||
}
|
}
|
||||||
export function getTemperatureDaysSortedByDate() {
|
export function getTemperatureDaysSortedByDate() {
|
||||||
return db.objects('CycleDay').filtered('temperature != null').sorted('date', true)
|
return db
|
||||||
|
.objects('CycleDay')
|
||||||
|
.filtered('temperature != null')
|
||||||
|
.sorted('date', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCycleDaysSortedByDate() {
|
export function getCycleDaysSortedByDate() {
|
||||||
return db.objects('CycleDay').sorted('date', true)
|
const cycleDays = db.objects('CycleDay').sorted('date', true)
|
||||||
|
return cycleDays
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCycleStartsSortedByDate() {
|
export function getCycleStartsSortedByDate() {
|
||||||
return db.objects('CycleDay').filtered('isCycleStart = true').sorted('date', true)
|
return db
|
||||||
|
.objects('CycleDay')
|
||||||
|
.filtered('isCycleStart = true')
|
||||||
|
.sorted('date', true)
|
||||||
}
|
}
|
||||||
export function saveSymptom(symptom, date, val) {
|
export function saveSymptom(symptom, date, val) {
|
||||||
let cycleDay = getCycleDay(date)
|
let cycleDay = getCycleDay(date)
|
||||||
@@ -83,7 +94,10 @@ export function saveSymptom(symptom, date, val) {
|
|||||||
if (symptom === 'bleeding') {
|
if (symptom === 'bleeding') {
|
||||||
const mensesDaysAfter = getMensesDaysRightAfter(cycleDay)
|
const mensesDaysAfter = getMensesDaysRightAfter(cycleDay)
|
||||||
maybeSetNewCycleStart({
|
maybeSetNewCycleStart({
|
||||||
val, cycleDay, mensesDaysAfter, checkIsMensesStart
|
val,
|
||||||
|
cycleDay,
|
||||||
|
mensesDaysAfter,
|
||||||
|
checkIsMensesStart,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
cycleDay[symptom] = val
|
cycleDay[symptom] = val
|
||||||
@@ -93,7 +107,7 @@ export function saveSymptom(symptom, date, val) {
|
|||||||
|
|
||||||
export function updateCycleStartsForAllCycleDays() {
|
export function updateCycleStartsForAllCycleDays() {
|
||||||
db.write(() => {
|
db.write(() => {
|
||||||
getBleedingDaysSortedByDate().forEach(day => {
|
getBleedingDaysSortedByDate().forEach((day) => {
|
||||||
if (checkIsMensesStart(day)) {
|
if (checkIsMensesStart(day)) {
|
||||||
day.isCycleStart = true
|
day.isCycleStart = true
|
||||||
}
|
}
|
||||||
@@ -106,7 +120,7 @@ export function createCycleDay(dateString) {
|
|||||||
db.write(() => {
|
db.write(() => {
|
||||||
result = db.create('CycleDay', {
|
result = db.create('CycleDay', {
|
||||||
date: dateString,
|
date: dateString,
|
||||||
isCycleStart: false
|
isCycleStart: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
@@ -116,9 +130,9 @@ export function getCycleDay(dateString) {
|
|||||||
return db.objectForPrimaryKey('CycleDay', dateString)
|
return db.objectForPrimaryKey('CycleDay', dateString)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreviousTemperature(date) {
|
export function getPreviousTemperatureForDate(date) {
|
||||||
const targetDate = LocalDate.parse(date)
|
const targetDate = LocalDate.parse(date)
|
||||||
const winner = getTemperatureDaysSortedByDate().find(candidate => {
|
const winner = getTemperatureDaysSortedByDate().find((candidate) => {
|
||||||
return LocalDate.parse(candidate.date).isBefore(targetDate)
|
return LocalDate.parse(candidate.date).isBefore(targetDate)
|
||||||
})
|
})
|
||||||
if (!winner) return null
|
if (!winner) return null
|
||||||
@@ -171,13 +185,16 @@ export function tryToImportWithoutDelete(cycleDays) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function requestHash(type, pw) {
|
export function requestHash(type, pw) {
|
||||||
nodejs.channel.post('request-SHA512', JSON.stringify({
|
nodejs.channel.post(
|
||||||
|
'request-SHA512',
|
||||||
|
JSON.stringify({
|
||||||
type: type,
|
type: type,
|
||||||
message: pw
|
message: pw,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function changeEncryptionAndRestartApp(hash) {
|
export async function changeDbEncryption(hash) {
|
||||||
let key
|
let key
|
||||||
if (hash) key = hashToInt8Array(hash)
|
if (hash) key = hashToInt8Array(hash)
|
||||||
const defaultPath = db.path
|
const defaultPath = db.path
|
||||||
@@ -187,19 +204,13 @@ export async function changeEncryptionAndRestartApp(hash) {
|
|||||||
const copyPath = dir.join('/')
|
const copyPath = dir.join('/')
|
||||||
const exists = await fs.exists(copyPath)
|
const exists = await fs.exists(copyPath)
|
||||||
if (exists) await fs.unlink(copyPath)
|
if (exists) await fs.unlink(copyPath)
|
||||||
// for some reason, realm complains if we give it a key with value undefined
|
db.writeCopyTo({ path: copyPath, encryptionKey: key })
|
||||||
if (key) {
|
|
||||||
db.writeCopyTo(copyPath, key)
|
|
||||||
} else {
|
|
||||||
db.writeCopyTo(copyPath)
|
|
||||||
}
|
|
||||||
db.close()
|
db.close()
|
||||||
await fs.unlink(defaultPath)
|
await fs.unlink(defaultPath)
|
||||||
await fs.moveFile(copyPath, defaultPath)
|
await fs.moveFile(copyPath, defaultPath)
|
||||||
restart.Restart()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDbEmpty () {
|
export function isDbEmpty() {
|
||||||
return db.empty
|
return db.empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"bleedingPrediction": {
|
||||||
|
"noPrediction": "As soon as you have tracked 3 menstrual cycles, drip. will make predictions for the next ones."
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"cycleDay": " day of your cycle",
|
||||||
|
"cyclePhase": " cycle phase - ",
|
||||||
|
"addDataForToday": "add data for today"
|
||||||
|
},
|
||||||
|
"shared": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"dateTimePickerTitle": "Pick a time",
|
||||||
|
"ok": "OK"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"license": {
|
||||||
|
"title": "drip. an open-source cycle tracking app",
|
||||||
|
"text": "Copyright (C) {{currentYear}} Heart of Code e.V.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details: https://www.gnu.org/licenses/gpl-3.0.html."
|
||||||
|
},
|
||||||
|
"privacyPolicy": {
|
||||||
|
"title": "Privacy Policy",
|
||||||
|
"intro": {
|
||||||
|
"title": "Introduction",
|
||||||
|
"text": "This Privacy Policy sets out how the iOS app 'drip.' uses and protects your personal data that you generate and store within the app."
|
||||||
|
},
|
||||||
|
"dataUse": {
|
||||||
|
"title": "Data use",
|
||||||
|
"text": "Drip. respects and celebrates your privacy. There is no collection of usage data or personal information, no ads, no spyware. Drip. can store data related to menstrual health locally on your device.\n\nThis includes:\n• settings\n• menstrual cycle tracking data\n\nThe data is used to display statistics and apply fertility awareness rules. This data cannot be accessed by other apps.\n\nIf you wish to delete all your app data you can do so by navigating to Settings, Data and Delete app data. This can also be done by uninstalling the app."
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"title": "Permissions",
|
||||||
|
"text": "For using reminders in drip. you need to allow push notifications. If you don't want to use this feature you simply don't allow notifications for the app."
|
||||||
|
},
|
||||||
|
"transparency": {
|
||||||
|
"title": "Transparency",
|
||||||
|
"text": "You can read through the source code of drip. to ensure the given information is correct. The source code is like a recipe: It tells you how much and what kind of ingredients you need and how you prepare them to cook a tasty meal or program a funky app.\n\nBuon appetito!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,7 +113,9 @@ export const mood = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const temperature = {
|
export const temperature = {
|
||||||
outOfRangeWarning: 'This temperature value is out of the current range for the temperature chart. You can change the range in the settings.',
|
// disabled temporarily, TODO https://gitlab.com/bloodyhealth/drip/-/issues/545 */}
|
||||||
|
// outOfRangeWarning: 'This temperature value is out of the current range for the temperature chart. You can change the range in the settings.',
|
||||||
|
outOfRangeWarning: 'This temperature value is too high or low to be shown on the temperature chart.',
|
||||||
outOfAbsoluteRangeWarning: 'This temperature value is too high or low to be shown on the temperature chart.',
|
outOfAbsoluteRangeWarning: 'This temperature value is too high or low to be shown on the temperature chart.',
|
||||||
saveAnyway: 'Save anyway',
|
saveAnyway: 'Save anyway',
|
||||||
temperature: {
|
temperature: {
|
||||||
|
|||||||
+36
-26
@@ -3,19 +3,18 @@ const settingsTitles = labels.menuItems
|
|||||||
|
|
||||||
export const home = {
|
export const home = {
|
||||||
unknown: '?',
|
unknown: '?',
|
||||||
phase: n => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`,
|
phase: (n) => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`,
|
||||||
cycleDay: ' day of your cycle',
|
|
||||||
cyclePhase: ' cycle phase - ',
|
|
||||||
addData: 'add data for today'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chart = {
|
export const chart = {
|
||||||
tutorial: 'You can swipe the chart to view more dates.'
|
tutorial: 'You can swipe the chart to view more dates.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shared = {
|
export const shared = {
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
|
dataSaved: 'Symptom data was saved',
|
||||||
|
dataDeleted: 'Symptom data was deleted',
|
||||||
errorTitle: 'Error',
|
errorTitle: 'Error',
|
||||||
successTitle: 'Success',
|
successTitle: 'Success',
|
||||||
warning: 'Warning',
|
warning: 'Warning',
|
||||||
@@ -26,12 +25,12 @@ export const shared = {
|
|||||||
confirmToProceed: 'Confirm to proceed',
|
confirmToProceed: 'Confirm to proceed',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
loading: 'Loading ...',
|
loading: 'Loading ...',
|
||||||
noDataWarning: 'You haven\'t entered any data yet.',
|
noDataWarning: "You haven't entered any data yet.",
|
||||||
noTemperatureWarning: 'You haven\'t entered any temperature data yet.',
|
noTemperatureWarning: "You haven't entered any temperature data yet.",
|
||||||
noDataButtonText: 'Start entering data now',
|
noDataButtonText: 'Start entering data now',
|
||||||
enter: 'Enter',
|
enter: 'Enter',
|
||||||
remove: 'Remove',
|
remove: 'Remove',
|
||||||
learnMore: 'Learn more'
|
learnMore: 'Learn more',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const headerTitles = {
|
export const headerTitles = {
|
||||||
@@ -46,6 +45,7 @@ export const headerTitles = {
|
|||||||
Password: settingsTitles.password.name,
|
Password: settingsTitles.password.name,
|
||||||
About: 'About',
|
About: 'About',
|
||||||
License: 'License',
|
License: 'License',
|
||||||
|
PrivacyPolicy: 'Privacy Policy',
|
||||||
bleeding: 'Bleeding',
|
bleeding: 'Bleeding',
|
||||||
temperature: 'Temperature',
|
temperature: 'Temperature',
|
||||||
mucus: 'Cervical Mucus',
|
mucus: 'Cervical Mucus',
|
||||||
@@ -54,7 +54,7 @@ export const headerTitles = {
|
|||||||
desire: 'Desire',
|
desire: 'Desire',
|
||||||
sex: 'Sex',
|
sex: 'Sex',
|
||||||
pain: 'Pain',
|
pain: 'Pain',
|
||||||
mood: 'Mood'
|
mood: 'Mood',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const stats = {
|
export const stats = {
|
||||||
@@ -65,49 +65,59 @@ export const stats = {
|
|||||||
averageLabel: 'Average cycle',
|
averageLabel: 'Average cycle',
|
||||||
minLabel: `Shortest`,
|
minLabel: `Shortest`,
|
||||||
maxLabel: `Longest`,
|
maxLabel: `Longest`,
|
||||||
stdLabel: `Standard\ndeviation`
|
stdLabel: `Standard\ndeviation`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bleedingPrediction = {
|
export const bleedingPrediction = {
|
||||||
noPrediction: `As soon as you have tracked 3 menstrual cycles, drip will make predictions for the next ones.`,
|
predictionInFuture: (startDays, endDays) =>
|
||||||
predictionInFuture: (startDays, endDays) => `Your next period is likely to start in ${startDays} to ${endDays} days.`,
|
`Your next period is likely to start in ${startDays} to ${endDays} days.`,
|
||||||
predictionStartedXDaysLeft: (numberOfDays) => `Your period is likely to start today or within the next ${numberOfDays} days.`,
|
predictionStartedXDaysLeft: (numberOfDays) =>
|
||||||
predictionStarted1DayLeft: 'Your period is likely to start today or tomorrow.',
|
`Your period is likely to start today or within the next ${numberOfDays} days.`,
|
||||||
|
predictionStarted1DayLeft:
|
||||||
|
'Your period is likely to start today or tomorrow.',
|
||||||
predictionStartedNoDaysLeft: 'Your period is likely to start today.',
|
predictionStartedNoDaysLeft: 'Your period is likely to start today.',
|
||||||
predictionInPast: (startDate, endDate) => `Based on your documented data, your period was likely to start between ${startDate} and ${endDate}.`
|
predictionInPast: (startDate, endDate) =>
|
||||||
|
`Based on your documented data, your period was likely to start between ${startDate} and ${endDate}.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const passwordPrompt = {
|
export const passwordPrompt = {
|
||||||
title: 'Unlock app',
|
title: 'Unlock app',
|
||||||
enterPassword: 'Enter password here',
|
enterPassword: 'Enter password here',
|
||||||
deleteDatabaseExplainer: "If you've forgotten your password, unfortunately, there is nothing we can do to recover your data, because it is encrypted with the password only you know. You can, however, delete all your encrypted data and start fresh. Once all data has been erased, you can set a new password in the settings, if you like.",
|
deleteDatabaseExplainer:
|
||||||
|
"If you've forgotten your password, unfortunately, there is nothing we can do to recover your data, because it is encrypted with the password only you know. You can, however, delete all your encrypted data and start fresh. Once all data has been erased, you can set a new password in the settings, if you like.",
|
||||||
forgotPassword: 'Forgot your password?',
|
forgotPassword: 'Forgot your password?',
|
||||||
deleteDatabaseTitle: 'Forgot your password?',
|
deleteDatabaseTitle: 'Forgot your password?',
|
||||||
deleteData: 'Yes, delete all my data',
|
deleteData: 'Yes, delete all my data',
|
||||||
areYouSureTitle: 'Are you sure?',
|
areYouSureTitle: 'Are you sure?',
|
||||||
areYouSure: 'Are you absolutely sure you want to permanently delete all your data?',
|
areYouSure:
|
||||||
reallyDeleteData: 'Yes, I am sure'
|
'Are you absolutely sure you want to permanently delete all your data?',
|
||||||
|
reallyDeleteData: 'Yes, I am sure',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fertilityStatus = {
|
export const fertilityStatus = {
|
||||||
fertile: 'fertile',
|
fertile: 'fertile',
|
||||||
infertile: 'infertile',
|
infertile: 'infertile',
|
||||||
fertileUntilEvening: 'Fertile phase ends in the evening',
|
fertileUntilEvening: 'Fertile phase ends in the evening',
|
||||||
unknown: "We cannot show any cycle information because no period data has been added.",
|
unknown:
|
||||||
preOvuText: "With NFP rules, you may assume 5 days of infertility at the beginning of your cycle, provided you don't observe any fertile cervical mucus or cervix values.",
|
'We cannot show any cycle information because no period data has been added.',
|
||||||
periOvuText: "We were not able to detect both a temperature shift and cervical mucus or cervix shift.",
|
preOvuText:
|
||||||
periOvuUntilEveningText: tempRule => {
|
"With NFP rules, you may assume 5 days of infertility at the beginning of your cycle, provided you don't observe any fertile cervical mucus or cervix values.",
|
||||||
|
periOvuText:
|
||||||
|
'We were not able to detect both a temperature shift and cervical mucus or cervix shift.',
|
||||||
|
periOvuUntilEveningText: (tempRule) => {
|
||||||
return (
|
return (
|
||||||
'We detected a temperature shift (' + ['regular', '1st exception', '2nd exception'][tempRule] +
|
'We detected a temperature shift (' +
|
||||||
|
['regular', '1st exception', '2nd exception'][tempRule] +
|
||||||
' temperature rule), as well as a cervical mucus/cervix shift according to NFP rules. In the evening today you may assume infertility, but ' +
|
' temperature rule), as well as a cervical mucus/cervix shift according to NFP rules. In the evening today you may assume infertility, but ' +
|
||||||
'always remember to double-check for yourself. Make sure the data makes sense to you.'
|
'always remember to double-check for yourself. Make sure the data makes sense to you.'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
postOvuText: tempRule => {
|
postOvuText: (tempRule) => {
|
||||||
return (
|
return (
|
||||||
'We detected a temperature shift (' + ['regular', '1st exception', '2nd exception'][tempRule] +
|
'We detected a temperature shift (' +
|
||||||
|
['regular', '1st exception', '2nd exception'][tempRule] +
|
||||||
' temperature rule), as well as a cervical mucus/cervix shift according to NFP rules. You may assume infertility, but always remember to ' +
|
' temperature rule), as well as a cervical mucus/cervix shift according to NFP rules. You may assume infertility, but always remember to ' +
|
||||||
'double-check for yourself. Make sure the data makes sense to you.'
|
'double-check for yourself. Make sure the data makes sense to you.'
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
+7
-6
@@ -1,22 +1,23 @@
|
|||||||
export default {
|
export default {
|
||||||
gitlab: {
|
gitlab: {
|
||||||
url: 'https://gitlab.com/bloodyhealth/drip',
|
url: 'https://gitlab.com/bloodyhealth/drip',
|
||||||
text: 'GitLab'
|
text: 'GitLab',
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
url: 'mailto:bloodyhealth@mailbox.org',
|
url: 'mailto:drip@mailbox.org',
|
||||||
text: 'email'
|
text: 'email',
|
||||||
},
|
},
|
||||||
wiki: {
|
wiki: {
|
||||||
url: 'https://gitlab.com/bloodyhealth/drip/wikis/home',
|
url: 'https://gitlab.com/bloodyhealth/drip/wikis/home',
|
||||||
text: 'our wiki'
|
text: 'our wiki',
|
||||||
},
|
},
|
||||||
website: {
|
website: {
|
||||||
url: 'https://bloodyhealth.gitlab.io/'
|
url: 'https://dripapp.org/',
|
||||||
|
text: 'Website',
|
||||||
},
|
},
|
||||||
donate: {
|
donate: {
|
||||||
url: 'https://ko-fi.com/dripapp',
|
url: 'https://ko-fi.com/dripapp',
|
||||||
text: 'here'
|
text: 'Donate here',
|
||||||
},
|
},
|
||||||
smashicons: {
|
smashicons: {
|
||||||
url: 'https://smashicons.com/',
|
url: 'https://smashicons.com/',
|
||||||
|
|||||||
+70
-53
@@ -1,40 +1,40 @@
|
|||||||
import links from './links'
|
import links from './links'
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear()
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
menuItems: {
|
menuItems: {
|
||||||
reminders: {
|
reminders: {
|
||||||
name: 'Reminders',
|
name: 'Reminders',
|
||||||
text: 'turn on/off reminders'
|
text: 'turn on/off reminders',
|
||||||
},
|
},
|
||||||
nfpSettings: {
|
nfpSettings: {
|
||||||
name:'NFP settings',
|
name: 'NFP settings',
|
||||||
text: 'define how you want to use NFP',
|
text: 'define how you want to use NFP',
|
||||||
},
|
},
|
||||||
dataManagement: {
|
dataManagement: {
|
||||||
name: 'Data',
|
name: 'Data',
|
||||||
text: 'import, export or delete your data'
|
text: 'import, export or delete your data',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
name:'Password',
|
name: 'Password',
|
||||||
text: ''
|
text: '',
|
||||||
},
|
},
|
||||||
about: 'About',
|
about: 'About',
|
||||||
license: 'License',
|
license: 'License',
|
||||||
settings: 'Settings'
|
settings: 'Settings',
|
||||||
|
privacyPolicy: 'Privacy Policy',
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
errors: {
|
errors: {
|
||||||
noData: 'There is no data to export',
|
noData: 'There is no data to export',
|
||||||
couldNotConvert: 'Could not convert data to CSV',
|
couldNotConvert: 'Could not convert data to CSV',
|
||||||
problemSharing: 'There was a problem sharing the data export file'
|
problemSharing: 'There was a problem sharing the data export file',
|
||||||
},
|
},
|
||||||
title: 'My drip data export',
|
title: 'My drip. data export',
|
||||||
subject: 'My drip data export',
|
subject: 'My drip. data export',
|
||||||
button: 'Export data',
|
button: 'Export data',
|
||||||
segmentExplainer: 'Export data in CSV format for backup or so you can use it elsewhere'
|
segmentExplainer:
|
||||||
|
'Export data in CSV format for backup or so you can use it elsewhere',
|
||||||
},
|
},
|
||||||
import: {
|
import: {
|
||||||
button: 'Import data',
|
button: 'Import data',
|
||||||
@@ -47,101 +47,118 @@ export default {
|
|||||||
errors: {
|
errors: {
|
||||||
couldNotOpenFile: 'Could not open file',
|
couldNotOpenFile: 'Could not open file',
|
||||||
postFix: 'No data was imported or changed',
|
postFix: 'No data was imported or changed',
|
||||||
futureEdit: 'Future dates may only contain a note, no other symptoms'
|
futureEdit: 'Future dates may only contain a note, no other symptoms',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
message: 'Data successfully imported'
|
message: 'Data successfully imported',
|
||||||
},
|
},
|
||||||
segmentExplainer: 'Import data in CSV format'
|
segmentExplainer: 'Import data in CSV format',
|
||||||
},
|
},
|
||||||
deleteSegment: {
|
deleteSegment: {
|
||||||
title: 'Delete app data',
|
title: 'Delete app data',
|
||||||
explainer: 'Delete app data from this phone',
|
explainer: 'Delete app data from this phone',
|
||||||
question: 'Do you want to delete app data from this phone?',
|
question: 'Do you want to delete app data from this phone?',
|
||||||
message: 'Please note that deletion of the app data is permanent and irreversible. We recommend exporting existing data before deletion.',
|
message:
|
||||||
|
'Please note that deletion of the app data is permanent and irreversible. We recommend exporting existing data before deletion.',
|
||||||
confirmation: 'Delete app data permanently',
|
confirmation: 'Delete app data permanently',
|
||||||
errors: {
|
errors: {
|
||||||
couldNotDeleteFile: 'Could not delete data',
|
couldNotDeleteFile: 'Could not delete data',
|
||||||
postFix: 'No data was deleted or changed',
|
postFix: 'No data was deleted or changed',
|
||||||
noData: 'There is no data to delete'
|
noData: 'There is no data to delete',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
message: 'App data successfully deleted'
|
message: 'App data successfully deleted',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
tempScale: {
|
tempScale: {
|
||||||
segmentTitle: 'Temperature scale',
|
segmentTitle: 'Temperature scale',
|
||||||
segmentExplainer: 'Change the minimum and maximum value for the temperature chart',
|
segmentExplainer:
|
||||||
|
'Change the minimum and maximum value for the temperature chart',
|
||||||
min: 'Min',
|
min: 'Min',
|
||||||
max: 'Max',
|
max: 'Max',
|
||||||
loadError: 'Could not load saved temperature scale settings',
|
loadError: 'Could not load saved temperature scale settings',
|
||||||
saveError: 'Could not save temperature scale settings'
|
saveError: 'Could not save temperature scale settings',
|
||||||
},
|
},
|
||||||
tempReminder: {
|
tempReminder: {
|
||||||
title: 'Temperature reminder',
|
title: 'Temperature reminder',
|
||||||
noTimeSet: 'Set a time for a daily reminder to take your temperature',
|
noTimeSet: 'Set a time for a daily reminder to take your temperature',
|
||||||
timeSet: time => `Daily reminder set for ${time}`,
|
timeSet: (time) => `Daily reminder set for ${time}`,
|
||||||
notification: 'Record your morning temperature'
|
notification: 'Record your morning temperature',
|
||||||
},
|
},
|
||||||
periodReminder: {
|
periodReminder: {
|
||||||
title: 'Next period reminder',
|
title: 'Next period reminder',
|
||||||
reminderText: 'Get a notification 3 days before your next period is likely to start.',
|
reminderText:
|
||||||
notification: daysToEndOfPrediction => `Your next period is likely to start in 3 to ${daysToEndOfPrediction} days.`
|
'Get a notification 3 days before your next period is likely to start.',
|
||||||
|
notification: (daysToEndOfPrediction) =>
|
||||||
|
`Your next period is likely to start in 3 to ${daysToEndOfPrediction} days.`,
|
||||||
},
|
},
|
||||||
useCervix: {
|
useCervix: {
|
||||||
title: 'Secondary symptom',
|
title: 'Secondary symptom',
|
||||||
cervixModeOn: 'Cervix values are being used for symptothermal fertility detection. You can switch here to use cervical mucus values for symptothermal fertility detection',
|
cervixModeOn:
|
||||||
cervixModeOff: 'By default, cervical mucus values are being used for symptothermal fertility detection. You can switch here to use cervix values for symptothermal fertility detection'
|
'Cervix values are being used for symptothermal fertility detection. You can switch here to use cervical mucus values for symptothermal fertility detection',
|
||||||
|
cervixModeOff:
|
||||||
|
'By default, cervical mucus values are being used for symptothermal fertility detection. You can switch here to use cervix values for symptothermal fertility detection',
|
||||||
},
|
},
|
||||||
passwordSettings: {
|
passwordSettings: {
|
||||||
title: 'App password',
|
title: 'App password',
|
||||||
explainerDisabled: "Encrypt the app's database with a password. You need to enter the password every time the app is started.",
|
explainerDisabled:
|
||||||
explainerEnabled: "Password protection and database encryption is currently enabled",
|
"Encrypt the app's database with a password. You need to enter the password every time the app is started.",
|
||||||
|
explainerEnabled:
|
||||||
|
'Password protection and database encryption is currently enabled',
|
||||||
setPassword: 'Set password',
|
setPassword: 'Set password',
|
||||||
savePassword: 'Save password',
|
savePassword: 'Save password',
|
||||||
changePassword: 'Change password',
|
changePassword: 'Change password',
|
||||||
deletePassword: 'Delete password',
|
deletePassword: 'Delete password',
|
||||||
enterCurrent: "Please enter your current password",
|
enterCurrent: 'Please enter your current password',
|
||||||
enterNew: "Please enter a new password",
|
enterNew: 'Please enter a new password',
|
||||||
confirmPassword: "Please confirm your password",
|
confirmPassword: 'Please confirm your password',
|
||||||
passwordsDontMatch: "Password and confirmation don't match",
|
passwordsDontMatch: "Password and confirmation don't match",
|
||||||
backupReminderTitle: 'Read this before making changes to your password',
|
backupReminder: {
|
||||||
backupReminder: 'Just to be safe, please backup your data using the export function before making changes to your password.\n\nLonger passwords are better! Consider using a passphrase.\n\nPlease also make sure you do not lose your password. There is no way to recover your data if you do.\n\nMaking any changes to your password setting will keep your data as it was before and restart the app.',
|
title: 'Read this before making changes to your password',
|
||||||
deleteBackupReminderTitle: 'Read this before deleting your password',
|
message: `
|
||||||
deleteBackupReminder: 'Deleting your password means your data will no longer be encrypted.\n\nJust to be safe, please backup your data using the export function before deleting your password.\n\nMaking any changes to your password setting will keep your data as it was before and restart the app.',
|
Just to be safe, please backup your data using the export function before making any changes to your password.\n
|
||||||
|
Longer passwords are better! Consider using a passphrase.\n
|
||||||
|
Please also make sure you do not lose your password. There is no way to recover your data if you do.\n
|
||||||
|
Making any changes to your password setting will keep your data as it was before.\n`,
|
||||||
|
},
|
||||||
|
deleteBackupReminder: {
|
||||||
|
title: 'Read this before deleting your password',
|
||||||
|
message: `
|
||||||
|
Deleting your password means your data will no longer be encrypted.\n
|
||||||
|
Just to be safe, please backup your data using the export function before deleting your password.\n
|
||||||
|
Making any changes to your password setting will keep your data as it was before and restart the app.\n
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
backupReminderAppendix: {
|
||||||
|
android:
|
||||||
|
'After the password is updated the app will automatically restart.',
|
||||||
|
ios: 'After the password is updated the app will automatically close. Please reopen it manually.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
aboutSection: {
|
aboutSection: {
|
||||||
title: 'About',
|
title: 'About',
|
||||||
text: `Please note that your data is stored locally on your phone and not on a server. This means your data cannot be read by anyone else unless they have access to your phone. We want to ensure that you stay in control of your own data. If you are planning to switch or reset your phone, please remember to export your data before doing so. You can reinstall the app afterwards and import your data.\n\nIf you encounter any technical issues, don't hesitate to contact us via ${links.email.url}. You can also contribute to the code base on ${links.gitlab.url}`,
|
text: `Please note that your data is stored locally on your phone and not on a server. This means your data cannot be read by anyone else unless they have access to your phone. We want to ensure that you stay in control of your own data. If you are planning to switch or reset your phone, please remember to export your data before doing so. You can reinstall the app afterwards and import your data.\n\nIf you encounter any technical issues, don't hesitate to contact us via email. You can also contribute to the code base on Gitlab and visit our website.`,
|
||||||
},
|
},
|
||||||
philosophy: {
|
philosophy: {
|
||||||
title: 'Remember to think for yourself',
|
title: 'Remember to think for yourself',
|
||||||
text: `drip makes period predictions for you and helps you apply NFP fertility awareness rules. But please remember that this app is made by humans, and humans make mistakes. Always think for yourself: "Does this make sense?" Remember, you don't need an app to understand your cycle! However, drip wants to support you and make period tracking easier, more transparent and secure.`,
|
text: `drip. makes period predictions for you and helps you apply NFP fertility awareness rules. But please remember that this app is made by humans, and humans make mistakes. Always think for yourself: "Does this make sense?" Remember, you don't need an app to understand your cycle! However, drip. wants to support you and make period tracking easier, more transparent and secure.`,
|
||||||
},
|
|
||||||
license: {
|
|
||||||
title: 'drip is an open-source cycle tracking app',
|
|
||||||
text: `Copyright (C) ${currentYear} Bloody Health GbR
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details: https://www.gnu.org/licenses/gpl-3.0.html
|
|
||||||
|
|
||||||
You can contact us by bloodyhealth@mailbox.org.`
|
|
||||||
},
|
},
|
||||||
version: {
|
version: {
|
||||||
title: 'Version'
|
title: 'Version',
|
||||||
},
|
},
|
||||||
website: {
|
website: {
|
||||||
title: 'Website'
|
title: 'Website',
|
||||||
},
|
},
|
||||||
preOvu: {
|
preOvu: {
|
||||||
title: 'Infertile days at cycle start',
|
title: 'Infertile days at cycle start',
|
||||||
note: `drip applies NFP's rules for calculating infertile days at the start of the cycle (see ${links.wiki.url} for more info). However, drip does not currently apply the so called 20-day-rule, which determines infertile days at the cycle start from past cycle lengths in case no past symptothermal info is available.`
|
note: `drip. applies NFP's rules for calculating infertile days at the start of the cycle (see ${links.wiki.url} for more info). However, drip. does not currently apply the so called 20-day-rule, which determines infertile days at the cycle start from past cycle lengths in case no past symptothermal info is available.`,
|
||||||
},
|
},
|
||||||
credits: {
|
credits: {
|
||||||
title: 'Credits',
|
title: 'Credits',
|
||||||
note: `We love the drip team. Thanks and lots of <3 to all of our condriputors. Thanks to Paula Härtel for the symptom tracking icons. All the other icons are made by ${links.smashicons.url}, ${links.pause08.url}, ${links.kazachek.url} & ${links.freepik.url} from ${links.flaticon.url}.`
|
note: `We love the drip. team. Thanks and lots of <3 to all of our condriputors. Thanks to Paula Härtel for the symptom tracking icons. All the other icons are made by ${links.smashicons.url}, ${links.pause08.url}, ${links.kazachek.url} & ${links.freepik.url} from ${links.flaticon.url}.`,
|
||||||
},
|
},
|
||||||
donate: {
|
donate: {
|
||||||
title: 'Buy us a coffee!',
|
title: 'Support us',
|
||||||
note: `The Bloody Health team is always grateful for donations, big or small, that help us maintain this app and develop new features. You can donate ${links.donate.url}. Thank you! You're awesome.`
|
note: `The drips are developing this app on a volunteer basis. We are always grateful for support. This could mean condriputing to the code, giving feedback, suggesting improvements or features, testing or donating. It helps and motivates us maintaining this app and developing new features. Thank you for your support!`,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -11,10 +11,10 @@ export const generalInfo = {
|
|||||||
3. and menstrual bleeding
|
3. and menstrual bleeding
|
||||||
the app helps you identify in which phase of the menstrual cycle you are.
|
the app helps you identify in which phase of the menstrual cycle you are.
|
||||||
|
|
||||||
drip makes period predictions for you and helps you apply NFP fertility awareness rules. But please remember that this app is made by humans, and humans make mistakes. Always think for yourself: "Does this make sense?" Remember, you don't need an app to understand your cycle! However, drip wants to support you and make period tracking easier, more transparent and secure.
|
drip. makes period predictions for you and helps you apply NFP fertility awareness rules. But please remember that this app is made by humans, and humans make mistakes. Always think for yourself: "Does this make sense?" Remember, you don't need an app to understand your cycle! However, drip. wants to support you and make period tracking easier, more transparent and secure.
|
||||||
|
|
||||||
Please find more info on the sympto-thermal method in ${links.wiki.url}.`,
|
Please find more info on the sympto-thermal method in ${links.wiki.url}.`,
|
||||||
noNfpSymptom: `The app allows you to track this symptom for your information, it is not taken into account for any calculation. On the chart you can check how often you track this symptom.`
|
noNfpSymptom: `The app allows you to track this symptom for your information, it is not taken into account for any calculation. On the chart you can check how often you track this symptom.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -22,7 +22,7 @@ export default {
|
|||||||
title: `Tracking menstrual bleeding`,
|
title: `Tracking menstrual bleeding`,
|
||||||
text: `Tracking menstrual bleeding allows you to know the beginning and the end of a menstrual cycle.
|
text: `Tracking menstrual bleeding allows you to know the beginning and the end of a menstrual cycle.
|
||||||
|
|
||||||
After tracking at least 3 menstrual cycles, drip will give you an overview of
|
After tracking at least 3 menstrual cycles, drip. will give you an overview of
|
||||||
· how long your cycles last on average (in "stats"),
|
· how long your cycles last on average (in "stats"),
|
||||||
· whether the length of your cycles varied significantly (in "stats" and in bleeding predictions)
|
· whether the length of your cycles varied significantly (in "stats" and in bleeding predictions)
|
||||||
· and predict your next 3 cycles with a range of 3 or 5 days (on home screen and "calendar").
|
· and predict your next 3 cycles with a range of 3 or 5 days (on home screen and "calendar").
|
||||||
@@ -42,13 +42,13 @@ Tracking how open or closed and how firm or soft the cervix feels can help deter
|
|||||||
By default, the secondary symptom the app uses for NFP evaluation is cervical mucus, but you can change it to cervix in "Settings" -> "NFP Settings".
|
By default, the secondary symptom the app uses for NFP evaluation is cervical mucus, but you can change it to cervix in "Settings" -> "NFP Settings".
|
||||||
|
|
||||||
· How to identify a fertile cervix?
|
· How to identify a fertile cervix?
|
||||||
A fertile cervix is open and feels soft like your earlobes. In contrast, an infertile cervix feels closed and hard, like the tip of your nose. If the cervix feels anything other than closed and hard, drip takes it as a sign of fertility. On the chart, a fertile cervix is colored in dark yellow, and infertile cervix is colored in light yellow.
|
A fertile cervix is open and feels soft like your earlobes. In contrast, an infertile cervix feels closed and hard, like the tip of your nose. If the cervix feels anything other than closed and hard, drip. takes it as a sign of fertility. On the chart, a fertile cervix is colored in dark yellow, and infertile cervix is colored in light yellow.
|
||||||
|
|
||||||
${generalInfo.chartNfp}
|
${generalInfo.chartNfp}
|
||||||
|
|
||||||
${generalInfo.excludeExplainer}
|
${generalInfo.excludeExplainer}
|
||||||
|
|
||||||
${generalInfo.nfpTfyReminder}`
|
${generalInfo.nfpTfyReminder}`,
|
||||||
},
|
},
|
||||||
desire: {
|
desire: {
|
||||||
title: 'Tracking sexual desire',
|
title: 'Tracking sexual desire',
|
||||||
@@ -58,7 +58,7 @@ ${generalInfo.cycleRelation}
|
|||||||
|
|
||||||
${generalInfo.noNfpSymptom}
|
${generalInfo.noNfpSymptom}
|
||||||
|
|
||||||
${generalInfo.curiousNfp}`
|
${generalInfo.curiousNfp}`,
|
||||||
},
|
},
|
||||||
mood: {
|
mood: {
|
||||||
title: 'Tracking mood',
|
title: 'Tracking mood',
|
||||||
@@ -68,7 +68,7 @@ ${generalInfo.cycleRelation}
|
|||||||
|
|
||||||
${generalInfo.noNfpSymptom}
|
${generalInfo.noNfpSymptom}
|
||||||
|
|
||||||
${generalInfo.curiousNfp}`
|
${generalInfo.curiousNfp}`,
|
||||||
},
|
},
|
||||||
mucus: {
|
mucus: {
|
||||||
title: 'Tracking cervical mucus',
|
title: 'Tracking cervical mucus',
|
||||||
@@ -77,7 +77,7 @@ ${generalInfo.curiousNfp}`
|
|||||||
By default the secondary symptom the app uses for NFP evaluation is cervical mucus.
|
By default the secondary symptom the app uses for NFP evaluation is cervical mucus.
|
||||||
|
|
||||||
· How to identify fertile cervical mucus?
|
· How to identify fertile cervical mucus?
|
||||||
Tracking the feeling and the texture of your cervical mucus on a daily basis helps you identify changes of the quality of the cervical mucus. The values you enter for both feeling and texture of your cervical mucus are combined by drip into one of five NFP-conforming values.
|
Tracking the feeling and the texture of your cervical mucus on a daily basis helps you identify changes of the quality of the cervical mucus. The values you enter for both feeling and texture of your cervical mucus are combined by drip. into one of five NFP-conforming values.
|
||||||
From lowest to best quality:
|
From lowest to best quality:
|
||||||
· t = (dry feeling + no texture),
|
· t = (dry feeling + no texture),
|
||||||
· ∅ = (no feeling + no texture),
|
· ∅ = (no feeling + no texture),
|
||||||
@@ -87,13 +87,13 @@ From lowest to best quality:
|
|||||||
|
|
||||||
On the chart, cervical mucus is colored in blue: the darker the shade of blue the better the quality of your cervical mucus.
|
On the chart, cervical mucus is colored in blue: the darker the shade of blue the better the quality of your cervical mucus.
|
||||||
|
|
||||||
Please note that drip does not yet support "parenthesis values": According to NFP rules, you can qualify a cervical mucus value by putting parentheses around it, to indicate that it doesn't fully meet the descriptors of one of the five categories, and instead is in between. This functionality will be supported in the future.
|
Please note that drip. does not yet support "parenthesis values": According to NFP rules, you can qualify a cervical mucus value by putting parentheses around it, to indicate that it doesn't fully meet the descriptors of one of the five categories, and instead is in between. This functionality will be supported in the future.
|
||||||
|
|
||||||
${generalInfo.chartNfp}
|
${generalInfo.chartNfp}
|
||||||
|
|
||||||
${generalInfo.excludeExplainer}
|
${generalInfo.excludeExplainer}
|
||||||
|
|
||||||
${generalInfo.nfpTfyReminder}`
|
${generalInfo.nfpTfyReminder}`,
|
||||||
},
|
},
|
||||||
note: {
|
note: {
|
||||||
title: 'Notes',
|
title: 'Notes',
|
||||||
@@ -101,7 +101,7 @@ ${generalInfo.nfpTfyReminder}`
|
|||||||
|
|
||||||
${generalInfo.noNfpSymptom}
|
${generalInfo.noNfpSymptom}
|
||||||
|
|
||||||
${generalInfo.curiousNfp}`
|
${generalInfo.curiousNfp}`,
|
||||||
},
|
},
|
||||||
pain: {
|
pain: {
|
||||||
title: 'Tracking pain',
|
title: 'Tracking pain',
|
||||||
@@ -111,7 +111,7 @@ ${generalInfo.cycleRelation}
|
|||||||
|
|
||||||
${generalInfo.noNfpSymptom}
|
${generalInfo.noNfpSymptom}
|
||||||
|
|
||||||
${generalInfo.curiousNfp}`
|
${generalInfo.curiousNfp}`,
|
||||||
},
|
},
|
||||||
sex: {
|
sex: {
|
||||||
title: 'Tracking sex and contraceptives',
|
title: 'Tracking sex and contraceptives',
|
||||||
@@ -119,7 +119,7 @@ ${generalInfo.curiousNfp}`
|
|||||||
|
|
||||||
${generalInfo.noNfpSymptom}
|
${generalInfo.noNfpSymptom}
|
||||||
|
|
||||||
${generalInfo.curiousNfp}`
|
${generalInfo.curiousNfp}`,
|
||||||
},
|
},
|
||||||
temperature: {
|
temperature: {
|
||||||
title: 'Tracking body basal temperature',
|
title: 'Tracking body basal temperature',
|
||||||
@@ -140,6 +140,6 @@ ${generalInfo.chartNfp}
|
|||||||
|
|
||||||
${generalInfo.excludeExplainer}
|
${generalInfo.excludeExplainer}
|
||||||
|
|
||||||
${generalInfo.nfpTfyReminder}`
|
${generalInfo.nfpTfyReminder}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
// translation files
|
||||||
|
import en from './en.json';
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
en: { translation: en },
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
i18n
|
||||||
|
// pass the i18n instance to react-i18next.
|
||||||
|
.use(initReactI18next)
|
||||||
|
// init i18next
|
||||||
|
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
debug: true,
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // not needed for react as it escapes by default
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AppRegistry } from 'react-native'
|
import { AppRegistry } from 'react-native'
|
||||||
import AppWrapper from './components/app-wrapper'
|
import AppWrapper from './components/app-wrapper'
|
||||||
|
import './i18n/i18n';
|
||||||
|
|
||||||
AppRegistry.registerComponent('drip', () => AppWrapper)
|
AppRegistry.registerComponent('drip', () => AppWrapper)
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
platform :ios, '10.0'
|
||||||
|
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
|
||||||
|
|
||||||
|
target 'drip' do
|
||||||
|
# Pods for drip
|
||||||
|
pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector"
|
||||||
|
pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec"
|
||||||
|
pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired"
|
||||||
|
pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety"
|
||||||
|
pod 'React', :path => '../node_modules/react-native/'
|
||||||
|
pod 'React-Core', :path => '../node_modules/react-native/'
|
||||||
|
pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules'
|
||||||
|
pod 'React-Core/DevSupport', :path => '../node_modules/react-native/'
|
||||||
|
pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS'
|
||||||
|
pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation'
|
||||||
|
pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob'
|
||||||
|
pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image'
|
||||||
|
pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
|
||||||
|
pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network'
|
||||||
|
pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings'
|
||||||
|
pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text'
|
||||||
|
pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration'
|
||||||
|
pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/'
|
||||||
|
pod 'React-ART', :path => '../node_modules/react-native/Libraries/ART'
|
||||||
|
pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact'
|
||||||
|
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi'
|
||||||
|
pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor'
|
||||||
|
pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector'
|
||||||
|
pod 'ReactCommon/jscallinvoker', :path => "../node_modules/react-native/ReactCommon"
|
||||||
|
pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon"
|
||||||
|
pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga'
|
||||||
|
|
||||||
|
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
|
||||||
|
pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
|
||||||
|
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
|
||||||
|
|
||||||
|
target 'dripTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
# Pods for testing
|
||||||
|
end
|
||||||
|
|
||||||
|
use_native_modules!
|
||||||
|
use_frameworks!
|
||||||
|
# This is fix to make ios build see images, should be removed after upgrade to rn 0.63.2
|
||||||
|
# https://stackoverflow.com/questions/63949851/react-native-ios-not-showing-images-pods-issue
|
||||||
|
post_install do |installer|
|
||||||
|
find_and_replace("../node_modules/react-native/Libraries/Image/RCTUIImageViewAnimated.m",
|
||||||
|
"_currentFrame.CGImage;","_currentFrame.CGImage ;} else { [super displayLayer:layer];")
|
||||||
|
find_and_replace("../node_modules/react-native/React/CxxBridge/RCTCxxBridge.mm",
|
||||||
|
"_initializeModules:(NSArray<id<RCTBridgeModule>> *)modules", "_initializeModules:(NSArray<Class> *)modules")
|
||||||
|
find_and_replace("../node_modules/react-native/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm",
|
||||||
|
"RCTBridgeModuleNameForClass(module))", "RCTBridgeModuleNameForClass(Class(module)))")
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_and_replace(dir, findstr, replacestr)
|
||||||
|
Dir[dir].each do |name|
|
||||||
|
text = File.read(name)
|
||||||
|
replace = text.gsub(findstr,replacestr)
|
||||||
|
if text != replace
|
||||||
|
puts "Fix: " + name
|
||||||
|
File.open(name, "w") { |file| file.puts replace }
|
||||||
|
STDOUT.flush
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Dir[dir + '*/'].each(&method(:find_and_replace))
|
||||||
|
end
|
||||||
|
end
|
||||||
+12
-13
@@ -7,7 +7,7 @@
|
|||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
@@ -22,6 +22,17 @@
|
|||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionDomains</key>
|
||||||
|
<dict>
|
||||||
|
<key>localhost</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
@@ -38,17 +49,5 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
<key>NSAppTransportSecurity</key>
|
|
||||||
<!--See http://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/ -->
|
|
||||||
<dict>
|
|
||||||
<key>NSExceptionDomains</key>
|
|
||||||
<dict>
|
|
||||||
<key>localhost</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
+442
-1538
File diff suppressed because it is too large
Load Diff
@@ -1,129 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Scheme
|
|
||||||
LastUpgradeVersion = "0820"
|
|
||||||
version = "1.3">
|
|
||||||
<BuildAction
|
|
||||||
parallelizeBuildables = "NO"
|
|
||||||
buildImplicitDependencies = "YES">
|
|
||||||
<BuildActionEntries>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "2D2A28121D9B038B00D4039D"
|
|
||||||
BuildableName = "libReact.a"
|
|
||||||
BlueprintName = "React-tvOS"
|
|
||||||
ReferencedContainer = "container:../node_modules/react-native/React/React.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
|
|
||||||
BuildableName = "drip-tvOS.app"
|
|
||||||
BlueprintName = "drip-tvOS"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "NO"
|
|
||||||
buildForArchiving = "NO"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "2D02E48F1E0B4A5D006451C7"
|
|
||||||
BuildableName = "drip-tvOSTests.xctest"
|
|
||||||
BlueprintName = "drip-tvOSTests"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
</BuildActionEntries>
|
|
||||||
</BuildAction>
|
|
||||||
<TestAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
|
||||||
<Testables>
|
|
||||||
<TestableReference
|
|
||||||
skipped = "NO">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "2D02E48F1E0B4A5D006451C7"
|
|
||||||
BuildableName = "drip-tvOSTests.xctest"
|
|
||||||
BlueprintName = "drip-tvOSTests"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</TestableReference>
|
|
||||||
</Testables>
|
|
||||||
<MacroExpansion>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
|
|
||||||
BuildableName = "drip-tvOS.app"
|
|
||||||
BlueprintName = "drip-tvOS"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</MacroExpansion>
|
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</TestAction>
|
|
||||||
<LaunchAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
launchStyle = "0"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
debugServiceExtension = "internal"
|
|
||||||
allowLocationSimulation = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
|
|
||||||
BuildableName = "drip-tvOS.app"
|
|
||||||
BlueprintName = "drip-tvOS"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</LaunchAction>
|
|
||||||
<ProfileAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
savedToolIdentifier = ""
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
debugDocumentVersioning = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
|
|
||||||
BuildableName = "drip-tvOS.app"
|
|
||||||
BlueprintName = "drip-tvOS"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
</ProfileAction>
|
|
||||||
<AnalyzeAction
|
|
||||||
buildConfiguration = "Debug">
|
|
||||||
</AnalyzeAction>
|
|
||||||
<ArchiveAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
revealArchiveInOrganizer = "YES">
|
|
||||||
</ArchiveAction>
|
|
||||||
</Scheme>
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Scheme
|
|
||||||
LastUpgradeVersion = "0620"
|
|
||||||
version = "1.3">
|
|
||||||
<BuildAction
|
|
||||||
parallelizeBuildables = "NO"
|
|
||||||
buildImplicitDependencies = "YES">
|
|
||||||
<BuildActionEntries>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "83CBBA2D1A601D0E00E9B192"
|
|
||||||
BuildableName = "libReact.a"
|
|
||||||
BlueprintName = "React"
|
|
||||||
ReferencedContainer = "container:../node_modules/react-native/React/React.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
|
||||||
BuildableName = "drip.app"
|
|
||||||
BlueprintName = "drip"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "NO"
|
|
||||||
buildForArchiving = "NO"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
|
||||||
BuildableName = "dripTests.xctest"
|
|
||||||
BlueprintName = "dripTests"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
</BuildActionEntries>
|
|
||||||
</BuildAction>
|
|
||||||
<TestAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
|
||||||
<Testables>
|
|
||||||
<TestableReference
|
|
||||||
skipped = "NO">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
|
||||||
BuildableName = "dripTests.xctest"
|
|
||||||
BlueprintName = "dripTests"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</TestableReference>
|
|
||||||
</Testables>
|
|
||||||
<MacroExpansion>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
|
||||||
BuildableName = "drip.app"
|
|
||||||
BlueprintName = "drip"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</MacroExpansion>
|
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</TestAction>
|
|
||||||
<LaunchAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
launchStyle = "0"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
debugServiceExtension = "internal"
|
|
||||||
allowLocationSimulation = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
|
||||||
BuildableName = "drip.app"
|
|
||||||
BlueprintName = "drip"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</LaunchAction>
|
|
||||||
<ProfileAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
savedToolIdentifier = ""
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
debugDocumentVersioning = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
|
||||||
BuildableName = "drip.app"
|
|
||||||
BlueprintName = "drip"
|
|
||||||
ReferencedContainer = "container:drip.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
</ProfileAction>
|
|
||||||
<AnalyzeAction
|
|
||||||
buildConfiguration = "Debug">
|
|
||||||
</AnalyzeAction>
|
|
||||||
<ArchiveAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
revealArchiveInOrganizer = "YES">
|
|
||||||
</ArchiveAction>
|
|
||||||
</Scheme>
|
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:drip.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user