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
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
xcshareddata
|
||||
ios/Index/DataStore
|
||||
|
||||
# Android/IntelliJ
|
||||
@@ -44,6 +44,7 @@ yarn-error.log
|
||||
buck-out/
|
||||
\.buckd/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
|
||||
# fastlane
|
||||
#
|
||||
@@ -59,6 +60,9 @@ buck-out/
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
# CocoaPods
|
||||
/ios/Pods/
|
||||
|
||||
# RN android release
|
||||
android/app/bin/
|
||||
android/app/release/
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
image: node:8
|
||||
image: node:14
|
||||
|
||||
# This folder is cached between builds
|
||||
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
- node_modules/
|
||||
|
||||
test_async:
|
||||
script:
|
||||
- npm install
|
||||
- npm test
|
||||
- npm install
|
||||
- npm test
|
||||
|
||||
@@ -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
|
||||
|
||||
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?
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# 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!
|
||||
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"
|
||||
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"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/com.drip/)
|
||||
@@ -15,37 +15,35 @@ Find more information on [our website](https://bloodyhealth.gitlab.io/).
|
||||
|
||||
The app is built in React Native and currently developed for Android.
|
||||
|
||||
▶ [How to contribute to the project](https://gitlab.com/bloodyhealth/drip/blob/master/CONTRIBUTING.md)
|
||||
▶ [How to contribute to the project](https://gitlab.com/bloodyhealth/drip/blob/master/CONTRIBUTING.md)
|
||||
|
||||
▶ [How to release a new version](https://gitlab.com/bloodyhealth/drip/blob/master/RELEASE.md)
|
||||
|
||||
## Development setup
|
||||
|
||||
#### 1. Android Studio
|
||||
|
||||
|
||||
Install [Android Studio](https://developer.android.com/studio/) - you'll need it to install some dependencies.
|
||||
|
||||
#### 2. Node 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:
|
||||
#### 2. Node & npm version
|
||||
|
||||
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
|
||||
$ nvm install v10
|
||||
|
||||
$ nvm install v14.19.3
|
||||
|
||||
#### 3. Get this repository
|
||||
|
||||
Clone it with SSH
|
||||
|
||||
|
||||
$ git clone git@gitlab.com:bloodyhealth/drip.git
|
||||
|
||||
or clone it with HTTPS
|
||||
|
||||
|
||||
$ git clone https://gitlab.com/bloodyhealth/drip.git
|
||||
|
||||
|
||||
and run
|
||||
|
||||
|
||||
$ cd drip
|
||||
$ npm install
|
||||
|
||||
@@ -53,94 +51,139 @@ 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.
|
||||
|
||||
#### 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.
|
||||
|
||||
1. Open a terminal and run
|
||||
```
|
||||
$ npm run android
|
||||
```
|
||||
1. Open a terminal and run
|
||||
|
||||
1. To see logging output, run the following command in another tab:
|
||||
```
|
||||
$ npm run log
|
||||
```
|
||||
```
|
||||
$ npm run android
|
||||
```
|
||||
|
||||
1. Run the following command and select enable hot reloading (see https://facebook.github.io/react-native/docs/debugging.html):
|
||||
```
|
||||
$ adb shell input keyevent 82
|
||||
```
|
||||
2. To see logging output, run the following command in another tab:
|
||||
|
||||
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.
|
||||
```
|
||||
$ npm run log
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
#### [MacOS] Java problems
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
$ 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.
|
||||
|
||||
#### [MacOS] 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
|
||||
|
||||
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
|
||||
|
||||
### Unit tests
|
||||
|
||||
You can run the tests with:
|
||||
```
|
||||
$ npm test
|
||||
```
|
||||
`$ npm test`
|
||||
|
||||
### End to end tests
|
||||
|
||||
1. Check what testing device is specified in [package.json](https://gitlab.com/bloodyhealth/drip/blob/master/package.json) under:
|
||||
```
|
||||
{"detox":
|
||||
{"configurations":
|
||||
{"name": "NEXUS_DEVICE_OR_WHATEVER_SPECIFIED_DEVICE"}
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
{"detox":
|
||||
{"configurations":
|
||||
{"name": "NEXUS_DEVICE_OR_WHATEVER_SPECIFIED_DEVICE"}
|
||||
}
|
||||
}
|
||||
```
|
||||
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
|
||||
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.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"
|
||||
```
|
||||
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").
|
||||
`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:
|
||||
`'app-debug-androidTest.apk' could not be found`
|
||||
--> 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`
|
||||
`'app-debug-androidTest.apk' could not be found`
|
||||
--> 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`
|
||||
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: !
|
||||
|
||||
## 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:
|
||||
`console.log(theVariableIWantToSeeHere)`
|
||||
or just a random string to check if this piece of code is actually running:
|
||||
`console.log("HELLO")`.
|
||||
|
||||
## 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).
|
||||
|
||||
## Adding a new tracking icon
|
||||
@@ -153,3 +196,7 @@ More information about how the app calculates fertility status and bleeding pred
|
||||
$ 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.
|
||||
|
||||
## 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
|
||||
* 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
|
||||
* bundleInDebug: false,
|
||||
*
|
||||
@@ -73,7 +76,8 @@ import com.android.build.OutputFile
|
||||
*/
|
||||
|
||||
project.ext.react = [
|
||||
entryFile: "index.js"
|
||||
entryFile: "index.js",
|
||||
enableHermes: false, // clean and rebuild if changing
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
@@ -94,6 +98,27 @@ def enableSeparateBuildPerCPUArchitecture = 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 {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
@@ -110,12 +135,18 @@ android {
|
||||
versionCode 8
|
||||
versionName "1.2102.28"
|
||||
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
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
release {
|
||||
if (project.hasProperty('DRIP_RELEASE_STORE_FILE')) {
|
||||
storeFile file(DRIP_RELEASE_STORE_FILE)
|
||||
@@ -134,7 +165,13 @@ android {
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
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
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
signingConfig signingConfigs.release
|
||||
@@ -144,8 +181,8 @@ android {
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
// 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
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a":3, "x86_64":4]
|
||||
// https://developer.android.com/studio/build/configure-apk-splits.html
|
||||
def versionCodes = ["armeabi-v7a": 1, "x86": 2]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
@@ -156,18 +193,19 @@ android {
|
||||
}
|
||||
|
||||
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 'androidx.appcompat:appcompat:1.0.0'
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
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 'junit:junit:4.12'
|
||||
}
|
||||
@@ -178,3 +216,5 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.compile
|
||||
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.WRITE_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
|
||||
android:name="${applicationId}.permission.C2D_MESSAGE"
|
||||
@@ -49,20 +51,29 @@
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_name"
|
||||
android:value="drip-notification"/>
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_description"
|
||||
android:value="notifications from drip"/>
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
|
||||
android:value="false"/>
|
||||
<!-- 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"
|
||||
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.RNPushNotificationBootEventReceiver">
|
||||
<intent-filter>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
</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.
@@ -4,12 +4,12 @@ import com.facebook.react.ReactActivity;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
* This is used to schedule rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "drip";
|
||||
}
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "drip";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,39 @@
|
||||
package com.drip;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.react.PackageList;
|
||||
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 io.realm.react.RealmReactPackage;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication, ShareApplication {
|
||||
|
||||
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new ReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
return Arrays.<ReactPackage>asList(
|
||||
new MainReactPackage(),
|
||||
new RNNodeJsMobilePackage(),
|
||||
new ReactNativeRestartPackage(),
|
||||
new ReactNativePushNotificationPackage(),
|
||||
new VectorIconsPackage(),
|
||||
new RNFSPackage(),
|
||||
new ReactNativeDocumentPicker(),
|
||||
new RNSharePackage(),
|
||||
new RealmReactPackage()
|
||||
);
|
||||
}
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
};
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
@@ -58,6 +44,32 @@ public class MainApplication extends Application implements ReactApplication, Sh
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:textColor">#000000</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
|
||||
+12
-5
@@ -7,7 +7,7 @@ buildscript {
|
||||
}
|
||||
ext.kotlinVersion = '1.3.10'
|
||||
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
|
||||
// in the individual module build.gradle files
|
||||
@@ -18,16 +18,21 @@ buildscript {
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
maven {
|
||||
// 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 {
|
||||
url 'https://maven.google.com/'
|
||||
name 'Google'
|
||||
}
|
||||
google()
|
||||
maven {
|
||||
// All of Detox' artifacts are provided via the npm module
|
||||
url "$rootDir/../node_modules/detox/Detox-android"
|
||||
@@ -36,11 +41,13 @@ allprojects {
|
||||
}
|
||||
|
||||
ext {
|
||||
googlePlayServicesVersion = "+" // default: "+"
|
||||
firebaseMessagingVersion = "21.1.0" // default: "+"
|
||||
|
||||
buildToolsVersion = "29.0.3"
|
||||
minSdkVersion = 23
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
supportLibVersion = "29.0.0"
|
||||
}
|
||||
|
||||
subprojects {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
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'
|
||||
include ':nodejs-mobile-react-native'
|
||||
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')
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
|
||||
include ':app'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "drip",
|
||||
"displayName": "drip"
|
||||
}
|
||||
"name": "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 PropTypes from 'prop-types'
|
||||
import { BackHandler, StyleSheet, View } from 'react-native'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AppPage from './common/app-page'
|
||||
import AppText from './common/app-text'
|
||||
@@ -9,28 +10,27 @@ import Segment from './common/segment'
|
||||
|
||||
import { saveLicenseFlag } from '../local-storage'
|
||||
|
||||
import { shared } from '../i18n/en/labels'
|
||||
import settingsLabels from '../i18n/en/settings'
|
||||
import { Containers } from '../styles'
|
||||
|
||||
const labels = settingsLabels.license
|
||||
|
||||
export default function License({ setLicense }) {
|
||||
const onAcceptLicense = async () => {
|
||||
await saveLicenseFlag()
|
||||
setLicense()
|
||||
}
|
||||
|
||||
const { t } = useTranslation()
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
return (
|
||||
<AppPage testID="licensePage">
|
||||
<Segment last testID="test" title={labels.title}>
|
||||
<AppText testID="test">{labels.text}</AppText>
|
||||
<Segment last testID="test" title={t("settings.license.title")}>
|
||||
<AppText testID="test">{t("settings.license.text", { currentYear })}</AppText>
|
||||
<View style={styles.container}>
|
||||
<Button onPress={BackHandler.exitApp} testID="licenseCancelButton">
|
||||
{shared.cancel}
|
||||
{t("labels.shared.cancel")}
|
||||
</Button>
|
||||
<Button isCTA onPress={onAcceptLicense} testID="licenseOkButton">
|
||||
{shared.ok}
|
||||
{t("labels.shared.ok")}
|
||||
</Button>
|
||||
</View>
|
||||
</Segment>
|
||||
+25
-11
@@ -1,16 +1,18 @@
|
||||
import React, { Component } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { Provider } from 'react-redux'
|
||||
import nodejs from 'nodejs-mobile-react-native'
|
||||
|
||||
import { getLicenseFlag, saveEncryptionFlag } from '../local-storage'
|
||||
import { openDb } from '../db'
|
||||
|
||||
import App from './app'
|
||||
import PasswordPrompt from './password-prompt'
|
||||
import License from './license'
|
||||
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 { Provider } from 'react-redux'
|
||||
import store from '../store'
|
||||
|
||||
export default class AppWrapper extends Component {
|
||||
constructor() {
|
||||
@@ -49,7 +51,7 @@ export default class AppWrapper extends Component {
|
||||
enableShowLicenseAgreement = () => {
|
||||
this.setState({
|
||||
shouldShowLicenseAgreement: true,
|
||||
isCheckingLicenseAgreement: false
|
||||
isCheckingLicenseAgreement: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,7 +62,7 @@ export default class AppWrapper extends Component {
|
||||
enableShowApp = () => {
|
||||
this.setState({
|
||||
shouldShowApp: true,
|
||||
shouldShowPasswordPrompt: false
|
||||
shouldShowPasswordPrompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,14 +79,26 @@ export default class AppWrapper extends Component {
|
||||
if (isCheckingLicenseAgreement) {
|
||||
initialView = <AppLoadingView />
|
||||
} else if (shouldShowLicenseAgreement) {
|
||||
initialView = <License setLicense={this.disableShowLicenseAgreement}/>
|
||||
initialView = <License setLicense={this.disableShowLicenseAgreement} />
|
||||
} else if (shouldShowPasswordPrompt) {
|
||||
initialView = <PasswordPrompt enableShowApp={this.enableShowApp} />
|
||||
} 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'
|
||||
|
||||
class App extends Component {
|
||||
|
||||
static propTypes = {
|
||||
date: PropTypes.string,
|
||||
navigation: PropTypes.object.isRequired,
|
||||
navigate: PropTypes.func,
|
||||
goBack: PropTypes.func,
|
||||
restartApp: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -54,7 +54,7 @@ class App extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { date, navigation, goBack } = this.props
|
||||
const { date, navigation, goBack, restartApp } = this.props
|
||||
const { currentPage } = navigation
|
||||
|
||||
if (!currentPage) {
|
||||
@@ -80,8 +80,8 @@ class App extends Component {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Header { ...headerProps } />
|
||||
<Page { ...pageProps } />
|
||||
<Header {...headerProps} />
|
||||
<Page {...pageProps} restartApp={restartApp} />
|
||||
<Menu />
|
||||
</View>
|
||||
)
|
||||
@@ -90,25 +90,22 @@ class App extends Component {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
}
|
||||
flex: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return({
|
||||
return {
|
||||
date: getDate(state),
|
||||
navigation: getNavigation(state)
|
||||
})
|
||||
navigation: getNavigation(state),
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return({
|
||||
return {
|
||||
navigate: (page) => dispatch(navigate(page)),
|
||||
goBack: () => dispatch(goBack()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(App)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App)
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import React, { Component } from 'react'
|
||||
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 AppPage from '../common/app-page'
|
||||
@@ -22,9 +29,10 @@ import { makeColumnInfo, nfpLines } from '../helpers/chart'
|
||||
|
||||
import {
|
||||
CHART_COLUMN_WIDTH,
|
||||
SYMPTOMS,
|
||||
CHART_GRID_LINE_HORIZONTAL_WIDTH,
|
||||
CHART_SYMPTOM_HEIGHT_RATIO,
|
||||
CHART_XAXIS_HEIGHT_RATIO
|
||||
CHART_XAXIS_HEIGHT_RATIO,
|
||||
SYMPTOMS
|
||||
} from '../../config'
|
||||
import { shared } from '../../i18n/en/labels'
|
||||
import { Colors, Spacing } from '../../styles'
|
||||
@@ -105,9 +113,13 @@ class CycleChart extends Component {
|
||||
|
||||
this.xAxisHeight = height * 0.7 * CHART_XAXIS_HEIGHT_RATIO
|
||||
const remainingHeight = height * 0.7 - this.xAxisHeight
|
||||
this.symptomHeight = remainingHeight * CHART_SYMPTOM_HEIGHT_RATIO
|
||||
this.symptomRowHeight = this.symptomRowSymptoms.length *
|
||||
this.symptomHeight
|
||||
this.symptomHeight = PixelRatio.roundToNearestPixel(
|
||||
remainingHeight
|
||||
* CHART_SYMPTOM_HEIGHT_RATIO
|
||||
)
|
||||
this.symptomRowHeight = PixelRatio.roundToNearestPixel(
|
||||
this.symptomRowSymptoms.length * this.symptomHeight
|
||||
) + CHART_GRID_LINE_HORIZONTAL_WIDTH
|
||||
this.columnHeight = remainingHeight - this.symptomRowHeight
|
||||
const chartHeight = this.shouldShowTemperatureColumn ?
|
||||
height * 0.7 : (this.symptomRowHeight + this.xAxisHeight)
|
||||
|
||||
@@ -7,7 +7,7 @@ import AppText from '../common/app-text'
|
||||
|
||||
import cycleModule from '../../lib/cycle'
|
||||
import { getOrdinalSuffix } from '../helpers/home'
|
||||
import { Containers, Typography, Sizes } from '../../styles'
|
||||
import { Typography, Sizes } from '../../styles'
|
||||
|
||||
const CycleDayLabel = ({ height, date }) => {
|
||||
const cycleDayNumber = cycleModule().getCycleDayNumber(date)
|
||||
@@ -24,11 +24,11 @@ const CycleDayLabel = ({ height, date }) => {
|
||||
<AppText style={styles.text}>
|
||||
{isFirstDayOfMonth ? momentDate.format('MMM') : dayOfMonth}
|
||||
</AppText>
|
||||
{!isFirstDayOfMonth &&
|
||||
{!isFirstDayOfMonth && (
|
||||
<AppText style={styles.textLight}>
|
||||
{getOrdinalSuffix(dayOfMonth)}
|
||||
</AppText>
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -45,15 +45,12 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'flex-end',
|
||||
left: 4,
|
||||
},
|
||||
containerRow: {
|
||||
...Containers.rowContainer
|
||||
},
|
||||
text: {
|
||||
...Typography.label,
|
||||
fontSize: Sizes.small,
|
||||
},
|
||||
textBold: {
|
||||
...Typography.labelBold
|
||||
...Typography.labelBold,
|
||||
},
|
||||
textLight: {
|
||||
...Typography.labelLight,
|
||||
@@ -62,7 +59,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default CycleDayLabel
|
||||
|
||||
@@ -105,7 +105,8 @@ class DayColumn extends Component {
|
||||
/>
|
||||
|
||||
{ symptomRowSymptoms.map((symptom, i) => {
|
||||
const hasSymptomData = this.data.hasOwnProperty(symptom)
|
||||
const hasSymptomData =
|
||||
Object.prototype.hasOwnProperty.call(this.data, symptom)
|
||||
return (
|
||||
<SymptomCell
|
||||
index={i}
|
||||
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
|
||||
import AppText from '../common/app-text'
|
||||
|
||||
import { Sizes } from '../../styles'
|
||||
import { CHART_TICK_WIDTH } from '../../config'
|
||||
|
||||
const Tick = ({ yPosition, height, isBold, shouldShowLabel, label }) => {
|
||||
const top = yPosition - height / 2
|
||||
@@ -28,16 +29,14 @@ Tick.propTypes = {
|
||||
|
||||
|
||||
const text = {
|
||||
lineHeight: Sizes.base,
|
||||
right: 4,
|
||||
textAlign: 'right'
|
||||
textAlign: 'right',
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
width: 40
|
||||
width: CHART_TICK_WIDTH
|
||||
},
|
||||
textBold: {
|
||||
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,
|
||||
minWidth: '80%',
|
||||
paddingHorizontal: Spacing.base,
|
||||
paddingVertical: Spacing.tiny,
|
||||
...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 AppText from './app-text'
|
||||
|
||||
import { Colors, Fonts, Spacing } from '../../styles'
|
||||
import { Colors, Fonts, Sizes, Spacing } from '../../styles'
|
||||
|
||||
const Button = ({
|
||||
children,
|
||||
@@ -39,49 +39,51 @@ Button.propTypes = {
|
||||
isCTA: PropTypes.bool,
|
||||
isSmall: PropTypes.bool,
|
||||
onPress: PropTypes.func,
|
||||
testID: PropTypes.string
|
||||
testID: PropTypes.string,
|
||||
}
|
||||
|
||||
Button.defaultProps = {
|
||||
isSmall: true
|
||||
isSmall: true,
|
||||
}
|
||||
|
||||
const text = {
|
||||
padding: Spacing.base,
|
||||
textTransform: 'uppercase'
|
||||
textTransform: 'uppercase',
|
||||
}
|
||||
|
||||
const textSmall = {
|
||||
fontSize: Fonts.small,
|
||||
fontSize: Sizes.small,
|
||||
padding: Spacing.small,
|
||||
textTransform: 'uppercase'
|
||||
textTransform: 'uppercase',
|
||||
}
|
||||
|
||||
const button = {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: Spacing.base,
|
||||
minWidth: '15%'
|
||||
marginTop: Spacing.base,
|
||||
paddingHorizontal: Spacing.tiny,
|
||||
minWidth: '15%',
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
regular: {
|
||||
...button
|
||||
...button,
|
||||
},
|
||||
cta: {
|
||||
backgroundColor: Colors.orange,
|
||||
borderRadius: 25,
|
||||
...button
|
||||
...button,
|
||||
},
|
||||
buttonTextBold: {
|
||||
color: 'white',
|
||||
fontFamily: Fonts.bold
|
||||
fontFamily: Fonts.bold,
|
||||
},
|
||||
buttonTextRegular: {
|
||||
color: Colors.greyDark,
|
||||
fontFamily: Fonts.main
|
||||
}
|
||||
fontFamily: Fonts.main,
|
||||
},
|
||||
})
|
||||
|
||||
export default Button
|
||||
|
||||
@@ -4,29 +4,32 @@ import { StyleSheet, TouchableOpacity } from 'react-native'
|
||||
|
||||
import AppIcon from './app-icon'
|
||||
|
||||
import { HIT_SLOP} from '../../config'
|
||||
import { Colors, Sizes } from '../../styles'
|
||||
|
||||
const CloseIcon = ({ onClose, ...props }) => {
|
||||
const CloseIcon = ({ onClose, color, ...props }) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
hitSlop={HIT_SLOP}
|
||||
onPress={onClose}
|
||||
style={styles.container}
|
||||
{...props}
|
||||
>
|
||||
<AppIcon name='cross' color={Colors.orange} />
|
||||
<AppIcon name='cross' color={color ? color : Colors.orange} />
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
CloseIcon.propTypes = {
|
||||
onClose: PropTypes.func.isRequired
|
||||
onClose: PropTypes.func.isRequired,
|
||||
color: PropTypes.string
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignSelf: 'flex-start',
|
||||
marginBottom: Sizes.base
|
||||
marginBottom: Sizes.base,
|
||||
}
|
||||
})
|
||||
|
||||
export default CloseIcon
|
||||
export default CloseIcon
|
||||
|
||||
@@ -57,10 +57,11 @@ const styles = StyleSheet.create({
|
||||
accentOrange: {
|
||||
...Typography.accentOrange,
|
||||
fontSize: Sizes.small,
|
||||
margin: Sizes.tiny,
|
||||
},
|
||||
accentPurpleBig: {
|
||||
...Typography.accentPurpleBig,
|
||||
marginRight: Spacing.small,
|
||||
marginRight: Spacing.tiny
|
||||
},
|
||||
cellLeft: {
|
||||
alignItems: 'flex-end',
|
||||
@@ -68,12 +69,13 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
cellRight: {
|
||||
flex: 6,
|
||||
flex: 5,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: Spacing.tiny,
|
||||
marginLeft: Spacing.tiny
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { StyleSheet, View, TouchableOpacity } from 'react-native'
|
||||
import { scale } from 'react-native-size-matters'
|
||||
|
||||
import AppText from '../common/app-text'
|
||||
import DripIcon from '../../assets/drip-icons'
|
||||
@@ -14,7 +15,6 @@ import { Colors, Sizes, Spacing } from '../../styles'
|
||||
import { headerTitles as symptomTitles } from '../../i18n/en/labels'
|
||||
|
||||
class SymptomBox extends Component {
|
||||
|
||||
static propTypes = {
|
||||
date: PropTypes.string.isRequired,
|
||||
isSymptomEdited: PropTypes.bool,
|
||||
@@ -32,7 +32,7 @@ class SymptomBox extends Component {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isSymptomEdited: props.isSymptomEdited
|
||||
isSymptomEdited: props.isSymptomEdited,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,24 +57,24 @@ class SymptomBox extends Component {
|
||||
const iconName = `drip-icon-${symptom}`
|
||||
const symptomNameStyle = [
|
||||
styles.symptomName,
|
||||
(isSymptomDisabled && styles.symptomNameDisabled),
|
||||
(isExcluded && styles.symptomNameExcluded)
|
||||
isSymptomDisabled && styles.symptomNameDisabled,
|
||||
isExcluded && styles.symptomNameExcluded,
|
||||
]
|
||||
const textStyle = [
|
||||
styles.text,
|
||||
(isSymptomDisabled && styles.textDisabled),
|
||||
(isExcluded && styles.textExcluded)
|
||||
isSymptomDisabled && styles.textDisabled,
|
||||
isExcluded && styles.textExcluded,
|
||||
]
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{isSymptomEdited &&
|
||||
{isSymptomEdited && (
|
||||
<SymptomEditView
|
||||
symptom={symptom}
|
||||
symptomData={symptomData}
|
||||
onClose={this.onFinishEditing}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
disabled={isSymptomDisabled}
|
||||
@@ -86,17 +86,17 @@ class SymptomBox extends Component {
|
||||
color={iconColor}
|
||||
isActive={!isSymptomDisabled}
|
||||
name={iconName}
|
||||
size={40}
|
||||
size={Sizes.icon}
|
||||
/>
|
||||
<View style={styles.textContainer}>
|
||||
<AppText style={symptomNameStyle}>
|
||||
{symptomTitles[symptom].toLowerCase()}
|
||||
</AppText>
|
||||
{symptomDataToDisplay &&
|
||||
{symptomDataToDisplay && (
|
||||
<AppText style={textStyle} numberOfLines={4}>
|
||||
{symptomDataToDisplay}
|
||||
</AppText>
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</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({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 10,
|
||||
borderRadius: scale(10),
|
||||
elevation: 4,
|
||||
flexDirection: 'row',
|
||||
height: 110,
|
||||
height: scale(110),
|
||||
marginBottom: Spacing.base,
|
||||
paddingHorizontal: Spacing.small,
|
||||
paddingVertical: Spacing.base,
|
||||
width: Spacing.symptomTileWidth
|
||||
width: Spacing.symptomTileWidth,
|
||||
},
|
||||
symptomName: {
|
||||
paddingTop: Sizes.tiny,
|
||||
color: Colors.purple,
|
||||
...main
|
||||
fontSize: Sizes.base,
|
||||
lineHeight: Sizes.base,
|
||||
},
|
||||
symptomNameDisabled: {
|
||||
color: Colors.grey
|
||||
color: Colors.grey,
|
||||
},
|
||||
symptomNameExcluded: {
|
||||
color: Colors.greyDark,
|
||||
},
|
||||
textContainer: {
|
||||
flexDirection: 'column',
|
||||
marginLeft: Spacing.small,
|
||||
maxWidth: Spacing.textWidth
|
||||
justifyContent: 'center',
|
||||
marginLeft: Spacing.tiny,
|
||||
maxWidth: Spacing.textWidth,
|
||||
},
|
||||
text: {
|
||||
...hint
|
||||
fontSize: Sizes.small,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
textDisabled: {
|
||||
color: Colors.greyLight
|
||||
color: Colors.greyLight,
|
||||
},
|
||||
textExcluded: {
|
||||
color: Colors.grey,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return({
|
||||
return {
|
||||
date: getDate(state),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
)(SymptomBox)
|
||||
export default connect(mapStateToProps, null)(SymptomBox)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Dimensions, ScrollView, StyleSheet, View } from 'react-native'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import AppModal from '../common/app-modal'
|
||||
import AppSwitch from '../common/app-switch'
|
||||
@@ -13,21 +14,20 @@ import SelectBoxGroup from './select-box-group'
|
||||
import SelectTabGroup from './select-tab-group'
|
||||
import Temperature from './temperature'
|
||||
|
||||
import { connect } from 'react-redux'
|
||||
import { getDate } from '../../slices/date'
|
||||
import { blank, save, shouldShow, symtomPage } from '../helpers/cycle-day'
|
||||
import { showToast } from '../helpers/general'
|
||||
|
||||
import { shared as sharedLabels } from '../../i18n/en/labels'
|
||||
import info from '../../i18n/en/symptom-info'
|
||||
import { Colors, Containers, Sizes, Spacing } from '../../styles'
|
||||
|
||||
class SymptomEditView extends Component {
|
||||
|
||||
static propTypes = {
|
||||
date: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
symptom: PropTypes.string.isRequired,
|
||||
symptomData: PropTypes.object
|
||||
symptomData: PropTypes.object,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -48,7 +48,7 @@ class SymptomEditView extends Component {
|
||||
shouldShowInfo: false,
|
||||
shouldShowNote,
|
||||
shouldBoxGroup,
|
||||
shouldTabGroup
|
||||
shouldTabGroup,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,18 +84,20 @@ class SymptomEditView extends Component {
|
||||
|
||||
onRemove = () => {
|
||||
this.saveData(true)
|
||||
showToast(sharedLabels.dataDeleted)
|
||||
this.props.onClose()
|
||||
}
|
||||
|
||||
onSave = () => {
|
||||
this.saveData()
|
||||
showToast(sharedLabels.dataSaved)
|
||||
this.props.onClose()
|
||||
}
|
||||
|
||||
onSaveTemperature = (value, field) => {
|
||||
const data = this.getParsedData()
|
||||
const dataToSave = field === 'value'
|
||||
? { [field]: Number(value) } : { [field]: value }
|
||||
const dataToSave =
|
||||
field === 'value' ? { [field]: Number(value) } : { [field]: value }
|
||||
Object.assign(data, { ...dataToSave })
|
||||
|
||||
this.setState({ data })
|
||||
@@ -103,10 +105,10 @@ class SymptomEditView extends Component {
|
||||
|
||||
onSelectBox = (key) => {
|
||||
const data = this.getParsedData()
|
||||
if (key === "other") {
|
||||
if (key === 'other') {
|
||||
Object.assign(data, {
|
||||
note: null,
|
||||
[key]: !this.state.data[key]
|
||||
[key]: !this.state.data[key],
|
||||
})
|
||||
} else {
|
||||
Object.assign(data, { [key]: !this.state.data[key] })
|
||||
@@ -115,7 +117,7 @@ class SymptomEditView extends Component {
|
||||
this.setState({ data })
|
||||
}
|
||||
|
||||
onSelectBoxNote= (value) => {
|
||||
onSelectBoxNote = (value) => {
|
||||
const data = this.getParsedData()
|
||||
Object.assign(data, { note: value !== '' ? value : null })
|
||||
|
||||
@@ -135,94 +137,102 @@ class SymptomEditView extends Component {
|
||||
save[symptom](data, date, shouldDeleteData)
|
||||
}
|
||||
|
||||
closeView = () => {
|
||||
const { onClose } = this.props
|
||||
|
||||
showToast(sharedLabels.dataSaved)
|
||||
onClose()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onClose, symptom } = this.props
|
||||
const { data,
|
||||
const { symptom } = this.props
|
||||
const {
|
||||
data,
|
||||
shouldShowExclude,
|
||||
shouldShowInfo,
|
||||
shouldShowNote,
|
||||
shouldBoxGroup,
|
||||
shouldTabGroup
|
||||
shouldTabGroup,
|
||||
} = this.state
|
||||
const iconName = shouldShowInfo ? "chevron-down" : "chevron-up"
|
||||
const iconName = shouldShowInfo ? 'chevron-up' : 'chevron-down'
|
||||
const noteText = symptom === 'note' ? data.value : data.note
|
||||
|
||||
return (
|
||||
<AppModal onClose={onClose}>
|
||||
<AppModal onClose={this.closeView}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.modalContainer}
|
||||
style={styles.modalWindow}
|
||||
>
|
||||
<View style={styles.headerContainer}>
|
||||
<CloseIcon onClose={onClose} />
|
||||
<CloseIcon onClose={this.closeView} />
|
||||
</View>
|
||||
{symptom === 'temperature' &&
|
||||
{symptom === 'temperature' && (
|
||||
<Temperature
|
||||
data={data}
|
||||
save={(value, field) => this.onSaveTemperature(value, field)}
|
||||
/>
|
||||
}
|
||||
{shouldTabGroup && symtomPage[symptom].selectTabGroups.map(group => {
|
||||
return (
|
||||
<Segment key={group.key} style={styles.segmentBorder}>
|
||||
<AppText style={styles.title}>{group.title}</AppText>
|
||||
<SelectTabGroup
|
||||
activeButton={data[group.key]}
|
||||
buttons={group.options}
|
||||
onSelect={value => this.onSelectTab(group, value)}
|
||||
/>
|
||||
</Segment>
|
||||
)
|
||||
})
|
||||
}
|
||||
{shouldBoxGroup && symtomPage[symptom].selectBoxGroups.map(group => {
|
||||
const isOtherSelected =
|
||||
data['other'] !== null
|
||||
&& data['other'] !== false
|
||||
&& Object.keys(group.options).includes('other')
|
||||
|
||||
return (
|
||||
<Segment key={group.key} style={styles.segmentBorder} >
|
||||
<AppText style={styles.title}>{group.title}</AppText>
|
||||
<SelectBoxGroup
|
||||
labels={group.options}
|
||||
onSelect={value => this.onSelectBox(value)}
|
||||
optionsState={data}
|
||||
/>
|
||||
{isOtherSelected &&
|
||||
<AppTextInput
|
||||
multiline={true}
|
||||
placeholder={sharedLabels.enter}
|
||||
value={data.note}
|
||||
onChangeText={value => this.onSelectBoxNote(value)}
|
||||
)}
|
||||
{shouldTabGroup &&
|
||||
symtomPage[symptom].selectTabGroups.map((group) => {
|
||||
return (
|
||||
<Segment key={group.key} style={styles.segmentBorder}>
|
||||
<AppText style={styles.title}>{group.title}</AppText>
|
||||
<SelectTabGroup
|
||||
activeButton={data[group.key]}
|
||||
buttons={group.options}
|
||||
onSelect={(value) => this.onSelectTab(group, value)}
|
||||
/>
|
||||
}
|
||||
</Segment>
|
||||
)
|
||||
})
|
||||
}
|
||||
{shouldShowExclude &&
|
||||
<Segment style={styles.segmentBorder} >
|
||||
</Segment>
|
||||
)
|
||||
})}
|
||||
{shouldBoxGroup &&
|
||||
symtomPage[symptom].selectBoxGroups.map((group) => {
|
||||
const isOtherSelected =
|
||||
data['other'] !== null &&
|
||||
data['other'] !== false &&
|
||||
Object.keys(group.options).includes('other')
|
||||
|
||||
return (
|
||||
<Segment key={group.key} style={styles.segmentBorder}>
|
||||
<AppText style={styles.title}>{group.title}</AppText>
|
||||
<SelectBoxGroup
|
||||
labels={group.options}
|
||||
onSelect={(value) => this.onSelectBox(value)}
|
||||
optionsState={data}
|
||||
/>
|
||||
{isOtherSelected && (
|
||||
<AppTextInput
|
||||
multiline={true}
|
||||
placeholder={sharedLabels.enter}
|
||||
value={data.note}
|
||||
onChangeText={(value) => this.onSelectBoxNote(value)}
|
||||
/>
|
||||
)}
|
||||
</Segment>
|
||||
)
|
||||
})}
|
||||
{shouldShowExclude && (
|
||||
<Segment style={styles.segmentBorder}>
|
||||
<AppSwitch
|
||||
onToggle={this.onExcludeToggle}
|
||||
text={symtomPage[symptom].excludeText}
|
||||
value={data.exclude}
|
||||
/>
|
||||
</Segment>
|
||||
}
|
||||
{shouldShowNote &&
|
||||
<Segment style={styles.segmentBorder} >
|
||||
)}
|
||||
{shouldShowNote && (
|
||||
<Segment style={styles.segmentBorder}>
|
||||
<AppText>{symtomPage[symptom].note}</AppText>
|
||||
<AppTextInput
|
||||
multiline={true}
|
||||
numberOfLines={3}
|
||||
onChangeText={this.onEditNote}
|
||||
placeholder={sharedLabels.enter}
|
||||
testID='noteInput'
|
||||
testID="noteInput"
|
||||
value={noteText !== null ? noteText : ''}
|
||||
/>
|
||||
</Segment>
|
||||
}
|
||||
)}
|
||||
<View style={styles.buttonsContainer}>
|
||||
<Button iconName={iconName} isSmall onPress={this.onPressLearnMore}>
|
||||
{sharedLabels.learnMore}
|
||||
@@ -234,11 +244,11 @@ class SymptomEditView extends Component {
|
||||
{sharedLabels.save}
|
||||
</Button>
|
||||
</View>
|
||||
{shouldShowInfo &&
|
||||
<Segment last style={styles.segmentBorder} >
|
||||
{shouldShowInfo && (
|
||||
<Segment last style={styles.segmentBorder}>
|
||||
<AppText>{info[symptom].text}</AppText>
|
||||
</Segment>
|
||||
}
|
||||
)}
|
||||
</ScrollView>
|
||||
</AppModal>
|
||||
)
|
||||
@@ -247,7 +257,7 @@ class SymptomEditView extends Component {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttonsContainer: {
|
||||
...Containers.rowContainer
|
||||
...Containers.rowContainer,
|
||||
},
|
||||
headerContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -265,23 +275,20 @@ const styles = StyleSheet.create({
|
||||
marginVertical: Sizes.huge * 2,
|
||||
position: 'absolute',
|
||||
minHeight: '40%',
|
||||
maxHeight: Dimensions.get('window').height * 0.7
|
||||
maxHeight: Dimensions.get('window').height * 0.7,
|
||||
},
|
||||
segmentBorder: {
|
||||
borderBottomColor: Colors.greyLight
|
||||
borderBottomColor: Colors.greyLight,
|
||||
},
|
||||
title: {
|
||||
fontSize: Sizes.subtitle
|
||||
}
|
||||
fontSize: Sizes.subtitle,
|
||||
},
|
||||
})
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return({
|
||||
return {
|
||||
date: getDate(state),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
)(SymptomEditView)
|
||||
export default connect(mapStateToProps, null)(SymptomEditView)
|
||||
|
||||
@@ -24,6 +24,9 @@ const SymptomPageTitle = ({
|
||||
reloadSymptomData(nextDay)
|
||||
setDate(nextDay)
|
||||
}
|
||||
const formattedTitle = title.length > 21
|
||||
? title.substring(0, 18) + '...'
|
||||
: title
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -31,7 +34,7 @@ const SymptomPageTitle = ({
|
||||
<AppIcon name='chevron-left' color={Colors.orange}/>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.textContainer}>
|
||||
<AppText style={styles.title}>{title}</AppText>
|
||||
<AppText style={styles.title}>{formattedTitle}</AppText>
|
||||
{subtitle && <AppText style={styles.subtitle}>{subtitle}</AppText>}
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => navigate(true)} hitSlop={HIT_SLOP}>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { Component } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Platform, StyleSheet, View } from 'react-native'
|
||||
import PropTypes from 'prop-types'
|
||||
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 { useTranslation } from 'react-i18next'
|
||||
|
||||
import AppText from '../common/app-text'
|
||||
import AppTextInput from '../common/app-text-input'
|
||||
@@ -11,153 +12,123 @@ import Segment from '../common/segment'
|
||||
|
||||
import { connect } from 'react-redux'
|
||||
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 { Colors, Containers, Sizes, Spacing } from '../../styles'
|
||||
|
||||
const formatTemperature = value => value === null
|
||||
? value
|
||||
: Number.parseFloat(value).toFixed(2)
|
||||
const Temperature = ({ data, date, save }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isTimePickerVisible, setIsTimePickerVisible] = useState(false)
|
||||
const [temperature, setTemperature] = useState(
|
||||
formatTemperature(data.value) || getPreviousTemperature(date)
|
||||
)
|
||||
|
||||
class Temperature extends Component {
|
||||
|
||||
static propTypes = {
|
||||
data: PropTypes.object,
|
||||
date: PropTypes.string.isRequired,
|
||||
save: PropTypes.func
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
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)
|
||||
// update state in parent component once to ensure
|
||||
// that pre-filled values are saved on button click
|
||||
useEffect(() => {
|
||||
if (temperature) {
|
||||
save(temperature, 'value')
|
||||
}
|
||||
}, [])
|
||||
|
||||
function onChangeTemperature(value) {
|
||||
const formattedValue = value.replace(',', '.').trim()
|
||||
if (!Number(formattedValue) && value !== '') return false
|
||||
setTemperature(formattedValue)
|
||||
}
|
||||
|
||||
onCancelTimePicker = () => {
|
||||
this.setState({ isTimePickerVisible: false })
|
||||
}
|
||||
|
||||
onChangeTemperature = (value) => {
|
||||
if (!Number(value)) return false
|
||||
|
||||
this.setState({
|
||||
value: value.trim(),
|
||||
shouldShowSuggestion: false
|
||||
})
|
||||
}
|
||||
|
||||
onShowTimePicker = () => {
|
||||
function onShowTimePicker() {
|
||||
Keyboard.dismiss()
|
||||
this.setState({ isTimePickerVisible: true })
|
||||
setIsTimePickerVisible(true)
|
||||
}
|
||||
|
||||
setTemperature = () => {
|
||||
const { value } = this.state
|
||||
this.props.save(value, 'value')
|
||||
}
|
||||
|
||||
setTime = (jsDate) => {
|
||||
function setTime(jsDate) {
|
||||
const time = moment(jsDate).format('HH:mm')
|
||||
const isTimePickerVisible = false
|
||||
|
||||
this.props.save(time, 'time')
|
||||
this.setState({ isTimePickerVisible })
|
||||
save(time, 'time')
|
||||
setIsTimePickerVisible(false)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { shouldShowSuggestion, suggestedTemperature, value } = this.state
|
||||
const { time } = this.props.data
|
||||
const { time } = data
|
||||
|
||||
const inputStyle = (shouldShowSuggestion && value === null)
|
||||
? { color: Colors.grey }
|
||||
: {color: Colors.greyDark}
|
||||
const outOfRangeWarning = isTemperatureOutOfRange(value)
|
||||
let temperatureToShow = null
|
||||
const inputStyle = { color: Colors.greyDark }
|
||||
const outOfRangeWarning = getTemperatureOutOfRangeMessage(temperature)
|
||||
|
||||
if (value) {
|
||||
temperatureToShow = value
|
||||
} else if (shouldShowSuggestion) {
|
||||
temperatureToShow = suggestedTemperature
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Segment>
|
||||
<AppText style={styles.title}>{labels.temperature.explainer}</AppText>
|
||||
<View style={styles.container}>
|
||||
<AppTextInput
|
||||
value={temperatureToShow === null ? '' : temperatureToShow}
|
||||
onChangeText={this.onChangeTemperature}
|
||||
onEndEditing={this.setTemperature}
|
||||
keyboardType="numeric"
|
||||
maxLength={5}
|
||||
style={inputStyle}
|
||||
testID="temperatureInput"
|
||||
underlineColorAndroid="transparent"
|
||||
/>
|
||||
<AppText>°C</AppText>
|
||||
</View>
|
||||
{ outOfRangeWarning !== null &&
|
||||
<View style={styles.hintContainer}>
|
||||
<AppText style={styles.hint}>{outOfRangeWarning}</AppText>
|
||||
</View>
|
||||
}
|
||||
</Segment>
|
||||
<Segment>
|
||||
<AppText style={styles.title}>{labels.time}</AppText>
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Segment>
|
||||
<AppText style={styles.title}>{labels.temperature.explainer}</AppText>
|
||||
<View style={styles.container}>
|
||||
<AppTextInput
|
||||
onFocus={this.onShowTimePicker}
|
||||
testID='timeInput'
|
||||
value={time}
|
||||
value={temperature}
|
||||
onChangeText={onChangeTemperature}
|
||||
onEndEditing={() => save(temperature, 'value')}
|
||||
keyboardType="numeric"
|
||||
maxLength={5}
|
||||
style={inputStyle}
|
||||
testID="temperatureInput"
|
||||
underlineColorAndroid="transparent"
|
||||
/>
|
||||
<DateTimePicker
|
||||
isVisible={this.state.isTimePickerVisible}
|
||||
mode="time"
|
||||
onConfirm={this.setTime}
|
||||
onCancel={this.onCancelTimePicker}
|
||||
/>
|
||||
</Segment>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
<AppText>°C</AppText>
|
||||
</View>
|
||||
{!!outOfRangeWarning && (
|
||||
<View style={styles.hintContainer}>
|
||||
<AppText style={styles.hint}>{outOfRangeWarning}</AppText>
|
||||
</View>
|
||||
)}
|
||||
</Segment>
|
||||
<Segment>
|
||||
<AppText style={styles.title}>{labels.time}</AppText>
|
||||
<AppTextInput
|
||||
onFocus={onShowTimePicker}
|
||||
testID="timeInput"
|
||||
value={time}
|
||||
/>
|
||||
<DateTimePicker
|
||||
isVisible={isTimePickerVisible}
|
||||
mode="time"
|
||||
onConfirm={setTime}
|
||||
onCancel={() => setIsTimePickerVisible(false)}
|
||||
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||
headerTextIOS={t('labels.shared.dateTimePickerTitle')}
|
||||
/>
|
||||
</Segment>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
...Containers.rowContainer
|
||||
...Containers.rowContainer,
|
||||
},
|
||||
hint: {
|
||||
fontStyle: 'italic',
|
||||
fontSize: Sizes.small
|
||||
fontSize: Sizes.small,
|
||||
},
|
||||
hintContainer: {
|
||||
marginVertical: Spacing.tiny
|
||||
marginVertical: Spacing.tiny,
|
||||
},
|
||||
title: {
|
||||
fontSize: Sizes.subtitle
|
||||
}
|
||||
fontSize: Sizes.subtitle,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return({
|
||||
date: getDate(state),
|
||||
})
|
||||
Temperature.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
save: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
)(Temperature)
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
date: getDate(state),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(Temperature)
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
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 CloseIcon from '../common/close-icon'
|
||||
import MenuItem from './menu-item'
|
||||
|
||||
import { Colors, Sizes } from '../../styles'
|
||||
@@ -14,6 +21,7 @@ const settingsMenuItems = [
|
||||
{ name: menuItems.settings, component: 'SettingsMenu' },
|
||||
{ name: menuItems.about, component: 'About' },
|
||||
{ name: menuItems.license, component: 'License' },
|
||||
{ name: menuItems.privacyPolicy, component: 'PrivacyPolicy' },
|
||||
]
|
||||
|
||||
export default class HamburgerMenu extends Component {
|
||||
@@ -34,12 +42,12 @@ export default class HamburgerMenu extends Component {
|
||||
<React.Fragment>
|
||||
{!shouldShowMenu && (
|
||||
<TouchableOpacity onPress={this.toggleMenu} hitSlop={HIT_SLOP}>
|
||||
<AppIcon name='dots-three-vertical' color={Colors.orange} />
|
||||
<AppIcon name="dots-three-vertical" color={Colors.orange} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{shouldShowMenu && (
|
||||
<Modal
|
||||
animationType='fade'
|
||||
animationType="fade"
|
||||
onRequestClose={this.toggleMenu}
|
||||
transparent={true}
|
||||
visible={shouldShowMenu}
|
||||
@@ -49,12 +57,9 @@ export default class HamburgerMenu extends Component {
|
||||
style={styles.blackBackground}
|
||||
></TouchableOpacity>
|
||||
<View style={styles.menu}>
|
||||
<TouchableOpacity
|
||||
onPress={this.toggleMenu}
|
||||
style={styles.iconContainer}
|
||||
>
|
||||
<AppIcon name='cross' color='black' />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.iconContainer}>
|
||||
<CloseIcon color={'black'} onClose={() => this.toggleMenu()} />
|
||||
</View>
|
||||
{settingsMenuItems.map((item) => (
|
||||
<MenuItem
|
||||
item={item}
|
||||
@@ -85,6 +90,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: 'white',
|
||||
height: '100%',
|
||||
padding: Sizes.base,
|
||||
paddingTop: Platform.OS === 'ios' ? Sizes.huge : Sizes.base,
|
||||
position: 'absolute',
|
||||
width: '60%',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LocalDate } from 'js-joda'
|
||||
import { verticalScale } from 'react-native-size-matters'
|
||||
|
||||
import { Colors, Fonts, Sizes } from '../../styles'
|
||||
|
||||
@@ -12,7 +13,7 @@ export const toCalFormat = (bleedingDaysSortedByDate) => {
|
||||
customStyles: {
|
||||
container: {
|
||||
backgroundColor: shades[day.bleeding.value],
|
||||
paddingTop: 2,
|
||||
paddingTop: verticalScale(2),
|
||||
},
|
||||
text: {
|
||||
color: Colors.turquoiseLight,
|
||||
@@ -62,8 +63,9 @@ export const todayToCalFormat = () => {
|
||||
|
||||
const styles = {
|
||||
calendarToday: {
|
||||
fontFamily: Fonts.bold,
|
||||
color: Colors.purple,
|
||||
fontFamily: 'Jost-Bold',
|
||||
fontWeight: 'bold',
|
||||
color: Colors.purple
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,6 +75,7 @@ export const calendarTheme = {
|
||||
monthTextColor: Colors.purple,
|
||||
textDayFontFamily: Fonts.main,
|
||||
textMonthFontFamily: Fonts.bold,
|
||||
textMonthFontWeight: 'bold',
|
||||
textDayHeaderFontFamily: Fonts.bold,
|
||||
textDayFontSize: Sizes.small,
|
||||
textMonthFontSize: Sizes.subtitle,
|
||||
|
||||
+146
-113
@@ -1,6 +1,10 @@
|
||||
import { ChronoUnit, LocalDate, LocalTime } from 'js-joda'
|
||||
|
||||
import { getPreviousTemperature, saveSymptom } from '../../db'
|
||||
import {
|
||||
getPreviousTemperatureForDate,
|
||||
saveSymptom,
|
||||
mapRealmObjToJsObj,
|
||||
} from '../../db'
|
||||
import { scaleObservable } from '../../local-storage'
|
||||
|
||||
import * as labels from '../../i18n/en/cycle-day'
|
||||
@@ -23,39 +27,35 @@ const temperatureLabels = labels.temperature
|
||||
const minutes = ChronoUnit.MINUTES
|
||||
|
||||
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) => {
|
||||
const previousTemperature = getPreviousTemperature(temperature)
|
||||
const shouldShowSuggestion = previousTemperature ? true : false
|
||||
const suggestedTemperature = previousTemperature ?
|
||||
previousTemperature.toString() : null
|
||||
export const formatTemperature = (temperature) =>
|
||||
!temperature
|
||||
? temperature
|
||||
: Number.parseFloat(temperature.toString()).toFixed(2)
|
||||
|
||||
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
|
||||
|
||||
const value = Number(temperature)
|
||||
const range = { min: TEMP_MIN, max: TEMP_MAX }
|
||||
const scale = scaleObservable.value
|
||||
|
||||
let warningMsg = null
|
||||
|
||||
if (value < range.min || value > range.max) {
|
||||
warningMsg = labels.temperature.outOfAbsoluteRangeWarning
|
||||
} else if (value < scale.min || value > scale.max) {
|
||||
warningMsg = labels.temperature.outOfRangeWarning
|
||||
}
|
||||
|
||||
return warningMsg
|
||||
return value < TEMP_MIN || value > TEMP_MAX
|
||||
? labels.temperature.outOfAbsoluteRangeWarning
|
||||
: value < scale.min || value > scale.max
|
||||
? labels.temperature.outOfRangeWarning
|
||||
: ''
|
||||
}
|
||||
|
||||
export const blank = {
|
||||
bleeding: {
|
||||
exclude: false,
|
||||
value: null
|
||||
value: null,
|
||||
},
|
||||
cervix: {
|
||||
exclude: false,
|
||||
@@ -64,9 +64,9 @@ export const blank = {
|
||||
position: null,
|
||||
},
|
||||
desire: {
|
||||
value: null
|
||||
value: null,
|
||||
},
|
||||
mood:{
|
||||
mood: {
|
||||
happy: null,
|
||||
sad: null,
|
||||
stressed: null,
|
||||
@@ -77,16 +77,16 @@ export const blank = {
|
||||
fatigue: null,
|
||||
angry: null,
|
||||
other: null,
|
||||
note: null
|
||||
note: null,
|
||||
},
|
||||
mucus: {
|
||||
exclude: false,
|
||||
feeling: null,
|
||||
texture: null,
|
||||
value: null
|
||||
value: null,
|
||||
},
|
||||
note: {
|
||||
value: null
|
||||
value: null,
|
||||
},
|
||||
pain: {
|
||||
cramps: null,
|
||||
@@ -97,7 +97,7 @@ export const blank = {
|
||||
tenderBreasts: null,
|
||||
migraine: null,
|
||||
other: null,
|
||||
note: null
|
||||
note: null,
|
||||
},
|
||||
sex: {
|
||||
solo: null,
|
||||
@@ -111,14 +111,14 @@ export const blank = {
|
||||
diaphragm: null,
|
||||
none: null,
|
||||
other: null,
|
||||
note: null
|
||||
note: null,
|
||||
},
|
||||
temperature: {
|
||||
exclude: false,
|
||||
note: null,
|
||||
time: LocalTime.now().truncatedTo(minutes).toString(),
|
||||
value: null
|
||||
}
|
||||
value: null,
|
||||
},
|
||||
}
|
||||
|
||||
export const symtomPage = {
|
||||
@@ -126,11 +126,13 @@ export const symtomPage = {
|
||||
excludeText: labels.bleeding.exclude.explainer,
|
||||
note: null,
|
||||
selectBoxGroups: null,
|
||||
selectTabGroups: [{
|
||||
key: 'value',
|
||||
options: getLabelsList(bleedingLabels),
|
||||
title: labels.bleeding.heaviness.explainer,
|
||||
}]
|
||||
selectTabGroups: [
|
||||
{
|
||||
key: 'value',
|
||||
options: getLabelsList(bleedingLabels),
|
||||
title: labels.bleeding.heaviness.explainer,
|
||||
},
|
||||
],
|
||||
},
|
||||
cervix: {
|
||||
excludeText: cervixLabels.excludeExplainer,
|
||||
@@ -151,18 +153,20 @@ export const symtomPage = {
|
||||
key: 'position',
|
||||
options: getLabelsList(cervixLabels.position.categories),
|
||||
title: cervixLabels.position.explainer,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
desire: {
|
||||
excludeText: null,
|
||||
note: null,
|
||||
selectBoxGroups: null,
|
||||
selectTabGroups: [{
|
||||
key: 'value',
|
||||
options: getLabelsList(intensityLabels),
|
||||
title: labels.desire.explainer
|
||||
}]
|
||||
selectTabGroups: [
|
||||
{
|
||||
key: 'value',
|
||||
options: getLabelsList(intensityLabels),
|
||||
title: labels.desire.explainer,
|
||||
},
|
||||
],
|
||||
},
|
||||
mucus: {
|
||||
excludeText: mucusLabels.excludeExplainer,
|
||||
@@ -178,34 +182,38 @@ export const symtomPage = {
|
||||
key: 'texture',
|
||||
options: getLabelsList(mucusLabels.texture.categories),
|
||||
title: mucusLabels.texture.explainer,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
mood: {
|
||||
excludeText: null,
|
||||
note: null,
|
||||
selectBoxGroups: [{
|
||||
key: 'mood',
|
||||
options: moodLabels,
|
||||
title: labels.mood.explainer
|
||||
}],
|
||||
selectTabGroups: null
|
||||
selectBoxGroups: [
|
||||
{
|
||||
key: 'mood',
|
||||
options: moodLabels,
|
||||
title: labels.mood.explainer,
|
||||
},
|
||||
],
|
||||
selectTabGroups: null,
|
||||
},
|
||||
note: {
|
||||
excludeText: null,
|
||||
note: noteDescription,
|
||||
selectBoxGroups: null,
|
||||
selectTabGroups: null
|
||||
selectTabGroups: null,
|
||||
},
|
||||
pain: {
|
||||
excludeText: null,
|
||||
note: null,
|
||||
selectBoxGroups: [{
|
||||
key: 'pain',
|
||||
options: painLabels,
|
||||
title: labels.pain.explainer
|
||||
}],
|
||||
selectTabGroups: null
|
||||
selectBoxGroups: [
|
||||
{
|
||||
key: 'pain',
|
||||
options: painLabels,
|
||||
title: labels.pain.explainer,
|
||||
},
|
||||
],
|
||||
selectTabGroups: null,
|
||||
},
|
||||
sex: {
|
||||
excludeText: null,
|
||||
@@ -220,40 +228,42 @@ export const symtomPage = {
|
||||
key: 'contraceptives',
|
||||
options: contraceptiveLabels,
|
||||
title: labels.contraceptives.explainer,
|
||||
}
|
||||
},
|
||||
],
|
||||
selectTabGroups: null
|
||||
selectTabGroups: null,
|
||||
},
|
||||
temperature: {
|
||||
excludeText: temperatureLabels.exclude.explainer,
|
||||
note: temperatureLabels.note.explainer,
|
||||
selectBoxGroups: null,
|
||||
selectTabGroups: null
|
||||
}
|
||||
selectTabGroups: null,
|
||||
},
|
||||
}
|
||||
|
||||
export const save = {
|
||||
bleeding: (data, date, shouldDeleteData) => {
|
||||
const { exclude, value } = data
|
||||
const isDataEntered = isNumber(value)
|
||||
const valuesToSave = shouldDeleteData || !isDataEntered
|
||||
? null : { value, exclude }
|
||||
const valuesToSave =
|
||||
shouldDeleteData || !isDataEntered ? null : { value, exclude }
|
||||
|
||||
saveSymptom('bleeding', date, valuesToSave)
|
||||
},
|
||||
cervix: (data, date, shouldDeleteData) => {
|
||||
const { opening, firmness, position, exclude } = data
|
||||
const isDataEntered = ['opening', 'firmness', 'position'].some(
|
||||
value => isNumber(data[value]))
|
||||
const valuesToSave = shouldDeleteData || !isDataEntered
|
||||
? null : { opening, firmness, position, exclude }
|
||||
const isDataEntered = ['opening', 'firmness', 'position'].some((value) =>
|
||||
isNumber(data[value])
|
||||
)
|
||||
const valuesToSave =
|
||||
shouldDeleteData || !isDataEntered
|
||||
? null
|
||||
: { opening, firmness, position, exclude }
|
||||
|
||||
saveSymptom('cervix', date, valuesToSave)
|
||||
},
|
||||
desire: (data, date, shouldDeleteData) => {
|
||||
const { value } = data
|
||||
const valuesToSave = shouldDeleteData || !isNumber(value)
|
||||
? null : { value }
|
||||
const valuesToSave = shouldDeleteData || !isNumber(value) ? null : { value }
|
||||
|
||||
saveSymptom('desire', date, valuesToSave)
|
||||
},
|
||||
@@ -262,10 +272,18 @@ export const save = {
|
||||
},
|
||||
mucus: (data, date, shouldDeleteData) => {
|
||||
const { feeling, texture, exclude } = data
|
||||
const isDataEntered = ['feeling', 'texture'].some(
|
||||
value => isNumber(data[value]))
|
||||
const valuesToSave = shouldDeleteData || !isDataEntered
|
||||
? null : { feeling, texture, value: computeNfpValue(feeling, texture), exclude }
|
||||
const isDataEntered = ['feeling', 'texture'].some((value) =>
|
||||
isNumber(data[value])
|
||||
)
|
||||
const valuesToSave =
|
||||
shouldDeleteData || !isDataEntered
|
||||
? null
|
||||
: {
|
||||
feeling,
|
||||
texture,
|
||||
value: computeNfpValue(feeling, texture),
|
||||
exclude,
|
||||
}
|
||||
|
||||
saveSymptom('mucus', date, valuesToSave)
|
||||
},
|
||||
@@ -288,21 +306,20 @@ export const save = {
|
||||
exclude,
|
||||
note,
|
||||
time,
|
||||
value: Number(value)
|
||||
value: Number(value),
|
||||
}
|
||||
|
||||
saveSymptom(
|
||||
'temperature',
|
||||
date,
|
||||
(shouldDeleteData || value === null) ? null : valuesToSave
|
||||
shouldDeleteData || value === null ? null : valuesToSave
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const saveBoxSymptom = (data, date, shouldDeleteData, symptom) => {
|
||||
const isDataEntered = Object.keys(data).some(key => data[key] !== null)
|
||||
const valuesToSave = shouldDeleteData || !isDataEntered
|
||||
? null : data
|
||||
const isDataEntered = Object.keys(data).some((key) => data[key] !== null)
|
||||
const valuesToSave = shouldDeleteData || !isDataEntered ? null : data
|
||||
|
||||
saveSymptom(symptom, date, valuesToSave)
|
||||
}
|
||||
@@ -326,46 +343,60 @@ const label = {
|
||||
return temperatureLabel
|
||||
}
|
||||
},
|
||||
mucus: mucus => {
|
||||
const filledCategories = ['feeling', 'texture'].filter(c => isNumber(mucus[c]))
|
||||
let label = filledCategories.map(category => {
|
||||
return labels.mucus.subcategories[category] + ': ' + labels.mucus[category].categories[mucus[category]]
|
||||
}).join(', ')
|
||||
mucus: (mucus) => {
|
||||
const filledCategories = ['feeling', 'texture'].filter((c) =>
|
||||
isNumber(mucus[c])
|
||||
)
|
||||
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})`
|
||||
|
||||
return label
|
||||
},
|
||||
cervix: cervix => {
|
||||
const filledCategories = ['opening', 'firmness', 'position'].filter(c => isNumber(cervix[c]))
|
||||
let label = filledCategories.map(category => {
|
||||
return labels.cervix.subcategories[category] + ': ' + labels.cervix[category].categories[cervix[category]]
|
||||
}).join(', ')
|
||||
cervix: (cervix) => {
|
||||
const filledCategories = ['opening', 'firmness', 'position'].filter((c) =>
|
||||
isNumber(cervix[c])
|
||||
)
|
||||
let label = filledCategories
|
||||
.map((category) => {
|
||||
return (
|
||||
labels.cervix.subcategories[category] +
|
||||
': ' +
|
||||
labels.cervix[category].categories[cervix[category]]
|
||||
)
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
if (cervix.exclude) label = `(${label})`
|
||||
|
||||
return label
|
||||
},
|
||||
note: note => note.value,
|
||||
note: (note) => note.value,
|
||||
desire: ({ value }) => {
|
||||
if (isNumber(value)) {
|
||||
return intensityLabels[value]
|
||||
}
|
||||
},
|
||||
sex: sex => {
|
||||
sex: (sex) => {
|
||||
sex = mapRealmObjToJsObj(sex)
|
||||
const sexLabel = []
|
||||
if (sex && Object.values({...sex}).some(val => val)){
|
||||
Object.keys(sex).forEach(key => {
|
||||
if(sex[key] && key !== 'other' && key !== 'note') {
|
||||
sexLabel.push(
|
||||
sexLabels[key] ||
|
||||
contraceptiveLabels[key]
|
||||
)
|
||||
if (sex && Object.values({ ...sex }).some((val) => val)) {
|
||||
Object.keys(sex).forEach((key) => {
|
||||
if (sex[key] && key !== 'other' && key !== 'note') {
|
||||
sexLabel.push(sexLabels[key] || contraceptiveLabels[key])
|
||||
}
|
||||
if(key === 'other' && sex.other) {
|
||||
if (key === 'other' && sex.other) {
|
||||
let label = contraceptiveLabels[key]
|
||||
if(sex.note) {
|
||||
if (sex.note) {
|
||||
label = `${label} (${sex.note})`
|
||||
}
|
||||
sexLabel.push(label)
|
||||
@@ -374,16 +405,17 @@ const label = {
|
||||
return sexLabel.join(', ')
|
||||
}
|
||||
},
|
||||
pain: pain => {
|
||||
pain: (pain) => {
|
||||
pain = mapRealmObjToJsObj(pain)
|
||||
const painLabel = []
|
||||
if (pain && Object.values({...pain}).some(val => val)){
|
||||
Object.keys(pain).forEach(key => {
|
||||
if(pain[key] && key !== 'other' && key !== 'note') {
|
||||
if (pain && Object.values({ ...pain }).some((val) => val)) {
|
||||
Object.keys(pain).forEach((key) => {
|
||||
if (pain[key] && key !== 'other' && key !== 'note') {
|
||||
painLabel.push(painLabels[key])
|
||||
}
|
||||
if(key === 'other' && pain.other) {
|
||||
if (key === 'other' && pain.other) {
|
||||
let label = painLabels[key]
|
||||
if(pain.note) {
|
||||
if (pain.note) {
|
||||
label = `${label} (${pain.note})`
|
||||
}
|
||||
painLabel.push(label)
|
||||
@@ -392,16 +424,17 @@ const label = {
|
||||
return painLabel.join(', ')
|
||||
}
|
||||
},
|
||||
mood: mood => {
|
||||
mood: (mood) => {
|
||||
mood = mapRealmObjToJsObj(mood)
|
||||
const moodLabel = []
|
||||
if (mood && Object.values({...mood}).some(val => val)){
|
||||
Object.keys(mood).forEach(key => {
|
||||
if(mood[key] && key !== 'other' && key !== 'note') {
|
||||
if (mood && Object.values({ ...mood }).some((val) => val)) {
|
||||
Object.keys(mood).forEach((key) => {
|
||||
if (mood[key] && key !== 'other' && key !== 'note') {
|
||||
moodLabel.push(moodLabels[key])
|
||||
}
|
||||
if(key === 'other' && mood.other) {
|
||||
if (key === 'other' && mood.other) {
|
||||
let label = moodLabels[key]
|
||||
if(mood.note) {
|
||||
if (mood.note) {
|
||||
label = `${label} (${mood.note})`
|
||||
}
|
||||
moodLabel.push(label)
|
||||
@@ -409,7 +442,7 @@ const label = {
|
||||
})
|
||||
return moodLabel.join(', ')
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const getData = (symptom, symptomData) => {
|
||||
|
||||
@@ -6,19 +6,19 @@ import { general as labels } from '../../i18n/en/cycle-day'
|
||||
export default function (date) {
|
||||
const today = LocalDate.now()
|
||||
const dateToDisplay = LocalDate.parse(date)
|
||||
return today.equals(dateToDisplay) ?
|
||||
labels.today :
|
||||
moment(date).format('MMMM Do YYYY')
|
||||
return today.equals(dateToDisplay)
|
||||
? labels.today
|
||||
: moment(date).format('MMMM Do YYYY')
|
||||
}
|
||||
|
||||
export function formatDateForShortText (date) {
|
||||
export function formatDateForShortText(date) {
|
||||
return moment(date.toString()).format('dddd, MMMM Do')
|
||||
}
|
||||
|
||||
export function dateToTitle(dateString) {
|
||||
const today = LocalDate.now()
|
||||
const dateToDisplay = LocalDate.parse(dateString)
|
||||
return today.equals(dateToDisplay) ?
|
||||
labels.today :
|
||||
moment(dateString).format('dddd, Do MMM YYYY')
|
||||
}
|
||||
return today.equals(dateToDisplay)
|
||||
? labels.today
|
||||
: 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 }
|
||||
}
|
||||
|
||||
export function determinePredictionText(bleedingPrediction) {
|
||||
if (!bleedingPrediction.length) return predictLabels.noPrediction
|
||||
export function determinePredictionText(bleedingPrediction, t) {
|
||||
if (!bleedingPrediction.length) return t('labels.bleedingPrediction.noPrediction')
|
||||
const {
|
||||
todayDate,
|
||||
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)
|
||||
+9
-4
@@ -3,8 +3,8 @@ import settingsViews from './settings'
|
||||
import settingsLabels from '../i18n/en/settings'
|
||||
const labels = settingsLabels.menuItems
|
||||
|
||||
export const isSettingsView =
|
||||
(page) => Object.keys(settingsViews).includes(page)
|
||||
export const isSettingsView = (page) =>
|
||||
Object.keys(settingsViews).includes(page)
|
||||
|
||||
export const pages = [
|
||||
{
|
||||
@@ -70,8 +70,13 @@ export const pages = [
|
||||
label: 'License',
|
||||
parent: 'SettingsMenu',
|
||||
},
|
||||
{
|
||||
component: 'PrivacyPolicy',
|
||||
label: 'PrivacyPolicy',
|
||||
parent: 'SettingsMenu',
|
||||
},
|
||||
{
|
||||
component: 'CycleDay',
|
||||
parent: 'Home',
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import React from 'react'
|
||||
import { Platform, Linking } from 'react-native'
|
||||
|
||||
import AppPage from '../common/app-page'
|
||||
import AppText from '../common/app-text'
|
||||
import Segment from '../common/segment'
|
||||
import Button from '../common/button'
|
||||
import ButtonRow from '../common/button-row'
|
||||
|
||||
import labels from '../../i18n/en/settings'
|
||||
import links from '../../i18n/en/links'
|
||||
|
||||
const AboutSection = () => {
|
||||
return (
|
||||
<AppPage title={labels.aboutSection.title} >
|
||||
<AppPage title={labels.aboutSection.title}>
|
||||
<Segment>
|
||||
<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 title={labels.philosophy.title}>
|
||||
<AppText>{labels.philosophy.text}</AppText>
|
||||
@@ -21,9 +36,15 @@ const AboutSection = () => {
|
||||
</Segment>
|
||||
<Segment title={labels.donate.title}>
|
||||
<AppText>{labels.donate.note}</AppText>
|
||||
</Segment>
|
||||
<Segment title={labels.website.title}>
|
||||
<AppText>{links.website.url}</AppText>
|
||||
{Platform.OS !== 'ios' && (
|
||||
<Button
|
||||
isCTA
|
||||
isSmall
|
||||
onPress={() => Linking.openURL(links.donate.url)}
|
||||
>
|
||||
{links.donate.text}
|
||||
</Button>
|
||||
)}
|
||||
</Segment>
|
||||
<Segment title={labels.version.title} last>
|
||||
<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 RNFS from 'react-native-fs'
|
||||
import { Alert, ToastAndroid } from 'react-native'
|
||||
import { Alert } from 'react-native'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../../common/button'
|
||||
@@ -9,6 +9,7 @@ import ConfirmWithPassword from '../common/confirm-with-password'
|
||||
import alertError from '../common/alert-error'
|
||||
|
||||
import { clearDb, isDbEmpty } from '../../../db'
|
||||
import { showToast } from '../../helpers/general'
|
||||
import { hasEncryptionObservable } from '../../../local-storage'
|
||||
import settings from '../../../i18n/en/settings'
|
||||
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
||||
@@ -69,7 +70,7 @@ export default class DeleteData extends Component {
|
||||
clearDb()
|
||||
}
|
||||
await this.deleteExportedFile()
|
||||
ToastAndroid.show(success.message, ToastAndroid.LONG)
|
||||
showToast(success.message)
|
||||
} catch (err) {
|
||||
alertError(errors.couldNotDeleteFile)
|
||||
}
|
||||
@@ -104,4 +105,4 @@ export default class DeleteData extends Component {
|
||||
DeleteData.propTypes = {
|
||||
isDeletingData: PropTypes.bool,
|
||||
onStartDeletion: PropTypes.func.isRequired
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 alertError from '../common/alert-error'
|
||||
import settings from '../../../i18n/en/settings'
|
||||
@@ -10,7 +10,7 @@ import RNFS from 'react-native-fs'
|
||||
export default async function exportData() {
|
||||
let data
|
||||
const labels = settings.export
|
||||
const cycleDaysByDate = getCycleDaysSortedByDate()
|
||||
const cycleDaysByDate = mapRealmObjToJsObj(getCycleDaysSortedByDate())
|
||||
|
||||
if (!cycleDaysByDate.length) return alertError(labels.errors.noData)
|
||||
|
||||
@@ -33,12 +33,11 @@ export default async function exportData() {
|
||||
url: `file://${path}`,
|
||||
subject: labels.subject,
|
||||
type: 'text/csv',
|
||||
showAppsToView: true
|
||||
showAppsToView: true,
|
||||
failOnCancel: false,
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return alertError(labels.errors.problemSharing)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 importCsv from '../../../lib/import-export/import-from-csv'
|
||||
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
||||
@@ -7,35 +7,36 @@ import labels from '../../../i18n/en/settings'
|
||||
import alertError from '../common/alert-error'
|
||||
|
||||
export function openImportDialog(onImportData) {
|
||||
Alert.alert(
|
||||
labels.import.title,
|
||||
labels.import.message,
|
||||
[{
|
||||
text: sharedLabels.cancel, style: 'cancel', onPress: () => { }
|
||||
}, {
|
||||
text: labels.import.deleteOption,
|
||||
onPress: () => onImportData(true)
|
||||
}, {
|
||||
Alert.alert(labels.import.title, labels.import.message, [
|
||||
{
|
||||
text: sharedLabels.cancel,
|
||||
style: 'cancel',
|
||||
onPress: () => {},
|
||||
},
|
||||
{
|
||||
text: labels.import.replaceOption,
|
||||
onPress: () => onImportData(false)
|
||||
}]
|
||||
)
|
||||
onPress: () => onImportData(false),
|
||||
},
|
||||
{
|
||||
text: labels.import.deleteOption,
|
||||
onPress: () => onImportData(true),
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
export async function getFileContent() {
|
||||
let fileInfo
|
||||
try {
|
||||
fileInfo = await new Promise((resolve, reject) => {
|
||||
DocumentPicker.show({
|
||||
filetype: [DocumentPickerUtil.allFiles()],
|
||||
}, (err, res) => {
|
||||
if (err) return reject(err)
|
||||
resolve(res)
|
||||
})
|
||||
fileInfo = await DocumentPicker.pick({
|
||||
type: [DocumentPicker.types.csv, 'text/comma-separated-values'],
|
||||
})
|
||||
} catch (err) {
|
||||
// because cancelling also triggers an error, we do nothing here
|
||||
return
|
||||
} catch (error) {
|
||||
if (DocumentPicker.isCancel(error)) {
|
||||
// User cancelled the picker, exit any dialogs or menus and move on
|
||||
return
|
||||
} else {
|
||||
importError(error)
|
||||
}
|
||||
}
|
||||
|
||||
let fileContent
|
||||
@@ -49,11 +50,10 @@ export async function getFileContent() {
|
||||
}
|
||||
|
||||
export async function importData(shouldDeleteExistingData, fileContent) {
|
||||
|
||||
try {
|
||||
await importCsv(fileContent, shouldDeleteExistingData)
|
||||
Alert.alert(sharedLabels.successTitle, labels.import.success.message)
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
importError(err.message)
|
||||
}
|
||||
}
|
||||
@@ -61,4 +61,4 @@ export async function importData(shouldDeleteExistingData, fileContent) {
|
||||
function importError(msg) {
|
||||
const postFixed = `${msg}\n\n${labels.import.errors.postFix}`
|
||||
alertError(postFixed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ import DataManagement from './data-management'
|
||||
import Password from './password'
|
||||
import About from './about'
|
||||
import License from './license'
|
||||
import PrivacyPolicy from './privacy-policy'
|
||||
|
||||
export default {
|
||||
Reminders, NfpSettings, DataManagement, Password, About, License
|
||||
Reminders,
|
||||
NfpSettings,
|
||||
DataManagement,
|
||||
Password,
|
||||
About,
|
||||
License,
|
||||
PrivacyPolicy,
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AppPage from '../common/app-page'
|
||||
import AppText from '../common/app-text'
|
||||
import Segment from '../common/segment'
|
||||
|
||||
import labels from '../../i18n/en/settings'
|
||||
|
||||
const License = () => {
|
||||
const { t } = useTranslation()
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
return (
|
||||
<AppPage title={labels.license.title}>
|
||||
<AppPage title={t("settings.license.title")}>
|
||||
<Segment last>
|
||||
<AppText>{labels.license.text}</AppText>
|
||||
<AppText>{t("settings.license.text", { currentYear })}</AppText>
|
||||
</Segment>
|
||||
</AppPage>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import AppIcon from '../../common/app-icon'
|
||||
import AppPage from '../../common/app-page'
|
||||
import AppSwitch from '../../common/app-switch'
|
||||
import AppText from '../../common/app-text'
|
||||
import TemperatureSlider from './temperature-slider'
|
||||
// import TemperatureSlider from './temperature-slider'
|
||||
import Segment from '../../common/segment'
|
||||
|
||||
import { useCervixObservable, saveUseCervix } from '../../../local-storage'
|
||||
@@ -40,10 +40,11 @@ export default class Settings extends Component {
|
||||
value={shouldUseCervix}
|
||||
/>
|
||||
</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>
|
||||
<TemperatureSlider />
|
||||
</Segment>
|
||||
</Segment> */}
|
||||
<Segment last>
|
||||
<View style={styles.line}>
|
||||
<AppIcon
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../../common/button'
|
||||
|
||||
@@ -8,6 +9,10 @@ import showBackUpReminder from './show-backup-reminder'
|
||||
import settings from '../../../i18n/en/settings'
|
||||
|
||||
export default class CreatePassword extends Component {
|
||||
static propTypes = {
|
||||
changeEncryptionAndRestart: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
@@ -23,7 +28,7 @@ export default class CreatePassword extends Component {
|
||||
showBackUpReminder(this.toggleSettingPassword, () => {})
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { isSettingPassword } = this.state
|
||||
const labels = settings.passwordSettings
|
||||
|
||||
@@ -34,8 +39,11 @@ export default class CreatePassword extends Component {
|
||||
</Button>
|
||||
)
|
||||
} else {
|
||||
return <EnterNewPassword />
|
||||
return (
|
||||
<EnterNewPassword
|
||||
changeEncryptionAndRestart={this.props.changeEncryptionAndRestart}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import PropTypes from 'prop-types'
|
||||
import Button from '../../common/button'
|
||||
import ConfirmWithPassword from '../common/confirm-with-password'
|
||||
|
||||
import { changeEncryptionAndRestartApp } from '../../../db'
|
||||
import labels from '../../../i18n/en/settings'
|
||||
|
||||
export default class DeletePassword extends Component {
|
||||
static propTypes = {
|
||||
onStartDelete: PropTypes.func,
|
||||
onCancelDelete: PropTypes.func
|
||||
onCancelDelete: PropTypes.func,
|
||||
changeEncryptionAndRestart: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@@ -24,10 +24,6 @@ export default class DeletePassword extends Component {
|
||||
this.props.onStartDelete()
|
||||
}
|
||||
|
||||
startDeletePassword = async () => {
|
||||
await changeEncryptionAndRestartApp()
|
||||
}
|
||||
|
||||
cancelConfirmationWithPassword = () => {
|
||||
this.setState({ enteringCurrentPassword: false })
|
||||
this.props.onCancelDelete()
|
||||
@@ -39,7 +35,7 @@ export default class DeletePassword extends Component {
|
||||
if (enteringCurrentPassword) {
|
||||
return (
|
||||
<ConfirmWithPassword
|
||||
onSuccess={this.startDeletePassword}
|
||||
onSuccess={this.props.changeEncryptionAndRestart}
|
||||
onCancel={this.cancelConfirmationWithPassword}
|
||||
/>
|
||||
)
|
||||
@@ -51,4 +47,4 @@ export default class DeletePassword extends Component {
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React, { Component } from 'react'
|
||||
import { StyleSheet } from 'react-native'
|
||||
import nodejs from 'nodejs-mobile-react-native'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import AppText from '../../common/app-text'
|
||||
import AppTextInput from '../../common/app-text-input'
|
||||
import Button from '../../common/button'
|
||||
|
||||
import { requestHash, changeEncryptionAndRestartApp } from '../../../db'
|
||||
import { requestHash } from '../../../db'
|
||||
import { Colors, Spacing } from '../../../styles'
|
||||
import settings from '../../../i18n/en/settings'
|
||||
|
||||
const LISTENER_TYPE = 'create-or-change-pw'
|
||||
|
||||
export default class EnterNewPassword extends Component {
|
||||
|
||||
constructor() {
|
||||
static propTypes = {
|
||||
changeEncryptionAndRestart: PropTypes.func,
|
||||
}
|
||||
constructor(props) {
|
||||
super()
|
||||
this.state = {
|
||||
password: '',
|
||||
@@ -23,13 +26,16 @@ export default class EnterNewPassword extends Component {
|
||||
}
|
||||
nodejs.channel.addListener(
|
||||
LISTENER_TYPE,
|
||||
changeEncryptionAndRestartApp,
|
||||
props.changeEncryptionAndRestart,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
nodejs.channel.removeListener(LISTENER_TYPE, changeEncryptionAndRestartApp)
|
||||
nodejs.channel.removeListener(
|
||||
LISTENER_TYPE,
|
||||
this.props.changeEncryptionAndRestart
|
||||
)
|
||||
}
|
||||
|
||||
savePassword = () => {
|
||||
@@ -52,15 +58,12 @@ export default class EnterNewPassword extends Component {
|
||||
this.setState({ passwordConfirmation })
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
password,
|
||||
passwordConfirmation,
|
||||
shouldShowErrorMessage
|
||||
} = this.state
|
||||
render() {
|
||||
const { password, passwordConfirmation, shouldShowErrorMessage } =
|
||||
this.state
|
||||
const labels = settings.passwordSettings
|
||||
const isButtonActive =
|
||||
(password.length > 0) && (passwordConfirmation.length > 0)
|
||||
password.length > 0 && passwordConfirmation.length > 0
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -80,10 +83,14 @@ export default class EnterNewPassword extends Component {
|
||||
value={passwordConfirmation}
|
||||
secureTextEntry={true}
|
||||
/>
|
||||
{shouldShowErrorMessage &&
|
||||
{shouldShowErrorMessage && (
|
||||
<AppText style={styles.error}>{labels.passwordsDontMatch}</AppText>
|
||||
}
|
||||
<Button isCTA={isButtonActive} onPress={this.savePassword}>
|
||||
)}
|
||||
<Button
|
||||
isCTA={isButtonActive}
|
||||
disabled={!isButtonActive}
|
||||
onPress={this.savePassword}
|
||||
>
|
||||
{labels.savePassword}
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
@@ -94,6 +101,6 @@ export default class EnterNewPassword extends Component {
|
||||
const styles = StyleSheet.create({
|
||||
error: {
|
||||
color: Colors.orange,
|
||||
marginTop: Spacing.base
|
||||
}
|
||||
})
|
||||
marginTop: Spacing.base,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
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 AppText from '../../common/app-text'
|
||||
@@ -11,14 +17,18 @@ import DeletePassword from './delete'
|
||||
import { hasEncryptionObservable } from '../../../local-storage'
|
||||
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) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isPasswordSet: hasEncryptionObservable.value,
|
||||
isChangingPassword: false,
|
||||
isDeletingPassword: false
|
||||
isDeletingPassword: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,19 +48,17 @@ export default class PasswordSetting extends Component {
|
||||
this.setState({ isDeletingPassword: false })
|
||||
}
|
||||
|
||||
changeEncryptionAndRestart = async (hash) => {
|
||||
await changeDbEncryption(hash)
|
||||
await this.props.restartApp()
|
||||
this.props.navigate('Home')
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isPasswordSet, isChangingPassword, isDeletingPassword } = this.state
|
||||
|
||||
const {
|
||||
isPasswordSet,
|
||||
isChangingPassword,
|
||||
isDeletingPassword,
|
||||
} = this.state
|
||||
|
||||
const {
|
||||
title,
|
||||
explainerEnabled,
|
||||
explainerDisabled
|
||||
} = labels.passwordSettings
|
||||
const { title, explainerEnabled, explainerDisabled } =
|
||||
labels.passwordSettings
|
||||
|
||||
return (
|
||||
<AppPage>
|
||||
@@ -59,19 +67,25 @@ export default class PasswordSetting extends Component {
|
||||
{isPasswordSet ? explainerEnabled : explainerDisabled}
|
||||
</AppText>
|
||||
|
||||
{!isPasswordSet && <CreatePassword/>}
|
||||
|
||||
{(isPasswordSet && !isDeletingPassword) && (
|
||||
<ChangePassword
|
||||
onStartChange = {this.onChangingPassword}
|
||||
onCancelChange = {this.onCancelChangingPassword}
|
||||
{!isPasswordSet && (
|
||||
<CreatePassword
|
||||
changeEncryptionAndRestart={this.changeEncryptionAndRestart}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(isPasswordSet && !isChangingPassword) && (
|
||||
{isPasswordSet && !isDeletingPassword && (
|
||||
<ChangePassword
|
||||
onStartChange={this.onChangingPassword}
|
||||
onCancelChange={this.onCancelChangingPassword}
|
||||
changeEncryptionAndRestart={this.changeEncryptionAndRestart}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPasswordSet && !isChangingPassword && (
|
||||
<DeletePassword
|
||||
onStartDelete = {this.onDeletingPassword}
|
||||
onCancelDelete = {this.onCancelDeletingPassword}
|
||||
onStartDelete={this.onDeletingPassword}
|
||||
onCancelDelete={this.onCancelDeletingPassword}
|
||||
changeEncryptionAndRestart={this.changeEncryptionAndRestart}
|
||||
/>
|
||||
)}
|
||||
</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 labels from '../../../i18n/en/settings'
|
||||
|
||||
export default function showBackUpReminder(okHandler, cancelHandler, isDelete) {
|
||||
let title, message
|
||||
if (isDelete) {
|
||||
title = labels.passwordSettings.deleteBackupReminderTitle
|
||||
message = labels.passwordSettings.deleteBackupReminder
|
||||
} else {
|
||||
title = labels.passwordSettings.backupReminderTitle
|
||||
message = labels.passwordSettings.backupReminder
|
||||
}
|
||||
const { title, message } = isDelete
|
||||
? labels.passwordSettings.deleteBackupReminder
|
||||
: labels.passwordSettings.backupReminder
|
||||
|
||||
const { backupReminderAppendix } = labels.passwordSettings
|
||||
const appendix =
|
||||
Platform.OS === 'ios'
|
||||
? backupReminderAppendix.ios
|
||||
: backupReminderAppendix.android
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
message,
|
||||
[{
|
||||
text: shared.cancel,
|
||||
onPress: cancelHandler,
|
||||
style: 'cancel'
|
||||
}, {
|
||||
text: shared.ok,
|
||||
onPress: okHandler
|
||||
}],
|
||||
message + appendix,
|
||||
[
|
||||
{
|
||||
text: shared.cancel,
|
||||
onPress: cancelHandler,
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: shared.ok,
|
||||
onPress: okHandler,
|
||||
},
|
||||
],
|
||||
{ onDismiss: cancelHandler }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import settings from '../../../i18n/en/settings'
|
||||
export default class ChangePassword extends Component {
|
||||
static propTypes = {
|
||||
onStartChange: PropTypes.func,
|
||||
onCancelChange: PropTypes.func
|
||||
onCancelChange: PropTypes.func,
|
||||
changeEncryptionAndRestart: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@@ -21,7 +22,7 @@ export default class ChangePassword extends Component {
|
||||
this.state = {
|
||||
currentPassword: null,
|
||||
enteringCurrentPassword: false,
|
||||
enteringNewPassword: false
|
||||
enteringNewPassword: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +42,7 @@ export default class ChangePassword extends Component {
|
||||
this.setState({
|
||||
currentPassword: null,
|
||||
enteringNewPassword: true,
|
||||
enteringCurrentPassword: false
|
||||
enteringCurrentPassword: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,17 +50,14 @@ export default class ChangePassword extends Component {
|
||||
this.setState({
|
||||
currentPassword: null,
|
||||
enteringNewPassword: false,
|
||||
enteringCurrentPassword: false
|
||||
enteringCurrentPassword: false,
|
||||
})
|
||||
this.props.onCancelChange()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
enteringCurrentPassword,
|
||||
enteringNewPassword,
|
||||
currentPassword
|
||||
} = this.state
|
||||
const { enteringCurrentPassword, enteringNewPassword, currentPassword } =
|
||||
this.state
|
||||
const labels = settings.passwordSettings
|
||||
const isPasswordSet = currentPassword !== null
|
||||
|
||||
@@ -73,7 +71,11 @@ export default class ChangePassword extends Component {
|
||||
}
|
||||
|
||||
if (enteringNewPassword) {
|
||||
return <EnterNewPassword />
|
||||
return (
|
||||
<EnterNewPassword
|
||||
changeEncryptionAndRestart={this.props.changeEncryptionAndRestart}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -86,4 +88,4 @@ export default class ChangePassword extends Component {
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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 { saveTempReminder, tempReminderObservable } from '../../../local-storage'
|
||||
import {
|
||||
saveTempReminder,
|
||||
tempReminderObservable,
|
||||
} from '../../../local-storage'
|
||||
import padWithZeros from '../../helpers/pad-time-with-zeros'
|
||||
|
||||
import labels from '../../../i18n/en/settings'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
|
||||
export default class TemperatureReminder extends Component {
|
||||
class TemperatureReminder extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
@@ -42,9 +48,12 @@ export default class TemperatureReminder extends Component {
|
||||
|
||||
render() {
|
||||
const { isEnabled, isTimePickerVisible, time } = this.state
|
||||
const { t } = this.props
|
||||
|
||||
const tempReminderText = time && isEnabled ?
|
||||
labels.tempReminder.timeSet(time) : labels.tempReminder.noTimeSet
|
||||
const tempReminderText =
|
||||
time && isEnabled
|
||||
? labels.tempReminder.timeSet(time)
|
||||
: labels.tempReminder.noTimeSet
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -58,8 +67,15 @@ export default class TemperatureReminder extends Component {
|
||||
mode="time"
|
||||
onConfirm={this.onPickDate}
|
||||
onCancel={this.onPickDateCancel}
|
||||
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||
headerTextIOS={t('labels.shared.dateTimePickerTitle')}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TemperatureReminder.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
}
|
||||
export default withTranslation()(TemperatureReminder)
|
||||
|
||||
+10
-13
@@ -1,5 +1,6 @@
|
||||
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 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 { Sizes, Spacing, Typography } from '../styles'
|
||||
import { fontRatio } from '../config'
|
||||
|
||||
const image = require('../assets/cycle-icon.png')
|
||||
const screen = Dimensions.get('screen')
|
||||
|
||||
const Stats = () => {
|
||||
const cycleLengths = cycleModule().getAllCycleLengths()
|
||||
@@ -28,8 +27,6 @@ const Stats = () => {
|
||||
[cycleData.stdDeviation ? cycleData.stdDeviation : '—', labels.stdLabel],
|
||||
[numberOfCycles, labels.basisOfStatsEnd]
|
||||
]
|
||||
const height = screen.height * 0.2
|
||||
const marginTop = (height / 8 - Sizes.icon / fontRatio) / 4
|
||||
|
||||
return (
|
||||
<AppPage contentContainerStyle={styles.pageContainer}>
|
||||
@@ -42,12 +39,12 @@ const Stats = () => {
|
||||
<ImageBackground
|
||||
source={image}
|
||||
imageStyle={styles.image}
|
||||
style={[styles.imageContainter, { height }]}
|
||||
style={styles.imageContainter}
|
||||
>
|
||||
<AppText
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="clip"
|
||||
style={[styles.accentPurpleGiant, { marginTop }]}
|
||||
style={styles.accentPurpleGiant}
|
||||
>
|
||||
{cycleData.mean}
|
||||
</AppText>
|
||||
@@ -73,13 +70,14 @@ const column = {
|
||||
flexDirection: 'column',
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const styles = ScaledSheet.create({
|
||||
accentOrange: {
|
||||
...Typography.accentOrange,
|
||||
fontSize: Sizes.small,
|
||||
},
|
||||
accentPurpleGiant: {
|
||||
...Typography.accentPurpleGiant,
|
||||
marginTop: Spacing.base * (-2),
|
||||
},
|
||||
accentPurpleHuge: {
|
||||
...Typography.accentPurpleHuge,
|
||||
@@ -89,23 +87,22 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingTop: Spacing.base,
|
||||
paddingTop: Spacing.base
|
||||
},
|
||||
columnLeft: {
|
||||
...column,
|
||||
flex: 4,
|
||||
flex: 2,
|
||||
},
|
||||
columnRight: {
|
||||
...column,
|
||||
flex: 5,
|
||||
flex: 3,
|
||||
paddingTop: Spacing.small,
|
||||
},
|
||||
image: {
|
||||
resizeMode: 'contain',
|
||||
|
||||
},
|
||||
imageContainter: {
|
||||
paddingTop: Spacing.large * 2,
|
||||
paddingTop: Spacing.large * 2.5,
|
||||
marginBottom: Spacing.large,
|
||||
},
|
||||
pageContainer: {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import Home from './home'
|
||||
import Home from './Home'
|
||||
import Calendar from './calendar'
|
||||
import CycleDay from './cycle-day/cycle-day-overview'
|
||||
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_EXPORT = 'export'
|
||||
@@ -16,16 +17,17 @@ export const SYMPTOMS = [
|
||||
'note',
|
||||
]
|
||||
|
||||
export const fontRatio = PixelRatio.getFontScale()
|
||||
export const CHART_COLUMN_WIDTH = 32
|
||||
export const CHART_COLUMN_MIDDLE = CHART_COLUMN_WIDTH / 2
|
||||
export const CHART_DOT_RADIUS = 6
|
||||
export const CHART_GRID_LINE_HORIZONTAL_WIDTH = 0.3
|
||||
export const CHART_ICON_SIZE = 20
|
||||
export const CHART_STROKE_WIDTH = 3
|
||||
export const CHART_SYMPTOM_HEIGHT_RATIO = 0.08
|
||||
export const CHART_XAXIS_HEIGHT_RATIO = 0.1
|
||||
export const CHART_YAXIS_WIDTH = 32
|
||||
export const CHART_DOT_RADIUS = scale(6)
|
||||
export const CHART_GRID_LINE_HORIZONTAL_WIDTH =
|
||||
PixelRatio.roundToNearestPixel(0.3)
|
||||
export const CHART_ICON_SIZE = scale(20)
|
||||
export const CHART_STROKE_WIDTH = scale(3)
|
||||
export const CHART_SYMPTOM_HEIGHT_RATIO = scale(0.08)
|
||||
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_MIN = 35.5
|
||||
@@ -34,4 +36,11 @@ export const TEMP_MAX = 39
|
||||
export const TEMP_MIN = 35
|
||||
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
|
||||
|
||||
+44
-33
@@ -2,7 +2,7 @@ import Realm from 'realm'
|
||||
import { LocalDate, ChronoUnit } from 'js-joda'
|
||||
import nodejs from 'nodejs-mobile-react-native'
|
||||
import fs from 'react-native-fs'
|
||||
import restart from 'react-native-restart'
|
||||
|
||||
import schemas from './schemas'
|
||||
import cycleModule from '../lib/cycle'
|
||||
import maybeSetNewCycleStart from '../lib/set-new-cycle-start'
|
||||
@@ -11,7 +11,7 @@ let db
|
||||
let checkIsMensesStart
|
||||
let getMensesDaysRightAfter
|
||||
|
||||
export async function openDb (hash) {
|
||||
export async function openDb(hash) {
|
||||
const realmConfig = {}
|
||||
if (hash) {
|
||||
realmConfig.encryptionKey = hashToInt8Array(hash)
|
||||
@@ -22,7 +22,7 @@ export async function openDb (hash) {
|
||||
let tempConnection
|
||||
try {
|
||||
tempConnection = await Realm.open(realmConfig)
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
const isErrorDecrypting = err.toString().includes('decrypt')
|
||||
const isErrorMnemonic = err.toString().includes('Invalid mnemonic')
|
||||
// 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)
|
||||
tempConnection.close()
|
||||
while (nextSchemaIndex < schemas.length - 1) {
|
||||
const tempConfig = Object.assign(
|
||||
realmConfig,
|
||||
schemas[nextSchemaIndex++]
|
||||
)
|
||||
const tempConfig = Object.assign(realmConfig, schemas[nextSchemaIndex++])
|
||||
const migratedRealm = new Realm(tempConfig)
|
||||
migratedRealm.close()
|
||||
}
|
||||
|
||||
// open the Realm with the latest schema
|
||||
realmConfig.schema = schemas[schemas.length - 1]
|
||||
const connection = await Realm.open(Object.assign(
|
||||
realmConfig,
|
||||
schemas[schemas.length - 1]
|
||||
))
|
||||
const connection = await Realm.open(
|
||||
Object.assign(realmConfig, schemas[schemas.length - 1])
|
||||
)
|
||||
|
||||
db = connection
|
||||
const cycle = cycleModule()
|
||||
@@ -62,18 +58,33 @@ export function closeDb() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
export function mapRealmObjToJsObj(realmObj) {
|
||||
return realmObj ? JSON.parse(JSON.stringify(realmObj)) : realmObj
|
||||
}
|
||||
|
||||
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() {
|
||||
return db.objects('CycleDay').filtered('temperature != null').sorted('date', true)
|
||||
return db
|
||||
.objects('CycleDay')
|
||||
.filtered('temperature != null')
|
||||
.sorted('date', true)
|
||||
}
|
||||
|
||||
export function getCycleDaysSortedByDate() {
|
||||
return db.objects('CycleDay').sorted('date', true)
|
||||
const cycleDays = db.objects('CycleDay').sorted('date', true)
|
||||
return cycleDays
|
||||
}
|
||||
|
||||
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) {
|
||||
let cycleDay = getCycleDay(date)
|
||||
@@ -83,7 +94,10 @@ export function saveSymptom(symptom, date, val) {
|
||||
if (symptom === 'bleeding') {
|
||||
const mensesDaysAfter = getMensesDaysRightAfter(cycleDay)
|
||||
maybeSetNewCycleStart({
|
||||
val, cycleDay, mensesDaysAfter, checkIsMensesStart
|
||||
val,
|
||||
cycleDay,
|
||||
mensesDaysAfter,
|
||||
checkIsMensesStart,
|
||||
})
|
||||
} else {
|
||||
cycleDay[symptom] = val
|
||||
@@ -93,7 +107,7 @@ export function saveSymptom(symptom, date, val) {
|
||||
|
||||
export function updateCycleStartsForAllCycleDays() {
|
||||
db.write(() => {
|
||||
getBleedingDaysSortedByDate().forEach(day => {
|
||||
getBleedingDaysSortedByDate().forEach((day) => {
|
||||
if (checkIsMensesStart(day)) {
|
||||
day.isCycleStart = true
|
||||
}
|
||||
@@ -106,7 +120,7 @@ export function createCycleDay(dateString) {
|
||||
db.write(() => {
|
||||
result = db.create('CycleDay', {
|
||||
date: dateString,
|
||||
isCycleStart: false
|
||||
isCycleStart: false,
|
||||
})
|
||||
})
|
||||
return result
|
||||
@@ -116,9 +130,9 @@ export function getCycleDay(dateString) {
|
||||
return db.objectForPrimaryKey('CycleDay', dateString)
|
||||
}
|
||||
|
||||
export function getPreviousTemperature(date) {
|
||||
export function getPreviousTemperatureForDate(date) {
|
||||
const targetDate = LocalDate.parse(date)
|
||||
const winner = getTemperatureDaysSortedByDate().find(candidate => {
|
||||
const winner = getTemperatureDaysSortedByDate().find((candidate) => {
|
||||
return LocalDate.parse(candidate.date).isBefore(targetDate)
|
||||
})
|
||||
if (!winner) return null
|
||||
@@ -171,13 +185,16 @@ export function tryToImportWithoutDelete(cycleDays) {
|
||||
}
|
||||
|
||||
export function requestHash(type, pw) {
|
||||
nodejs.channel.post('request-SHA512', JSON.stringify({
|
||||
type: type,
|
||||
message: pw
|
||||
}))
|
||||
nodejs.channel.post(
|
||||
'request-SHA512',
|
||||
JSON.stringify({
|
||||
type: type,
|
||||
message: pw,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export async function changeEncryptionAndRestartApp(hash) {
|
||||
export async function changeDbEncryption(hash) {
|
||||
let key
|
||||
if (hash) key = hashToInt8Array(hash)
|
||||
const defaultPath = db.path
|
||||
@@ -187,19 +204,13 @@ export async function changeEncryptionAndRestartApp(hash) {
|
||||
const copyPath = dir.join('/')
|
||||
const exists = await fs.exists(copyPath)
|
||||
if (exists) await fs.unlink(copyPath)
|
||||
// for some reason, realm complains if we give it a key with value undefined
|
||||
if (key) {
|
||||
db.writeCopyTo(copyPath, key)
|
||||
} else {
|
||||
db.writeCopyTo(copyPath)
|
||||
}
|
||||
db.writeCopyTo({ path: copyPath, encryptionKey: key })
|
||||
db.close()
|
||||
await fs.unlink(defaultPath)
|
||||
await fs.moveFile(copyPath, defaultPath)
|
||||
restart.Restart()
|
||||
}
|
||||
|
||||
export function isDbEmpty () {
|
||||
export function isDbEmpty() {
|
||||
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 = {
|
||||
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.',
|
||||
saveAnyway: 'Save anyway',
|
||||
temperature: {
|
||||
|
||||
+37
-27
@@ -3,19 +3,18 @@ const settingsTitles = labels.menuItems
|
||||
|
||||
export const home = {
|
||||
unknown: '?',
|
||||
phase: n => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`,
|
||||
cycleDay: ' day of your cycle',
|
||||
cyclePhase: ' cycle phase - ',
|
||||
addData: 'add data for today'
|
||||
phase: (n) => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`,
|
||||
}
|
||||
|
||||
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 = {
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
dataSaved: 'Symptom data was saved',
|
||||
dataDeleted: 'Symptom data was deleted',
|
||||
errorTitle: 'Error',
|
||||
successTitle: 'Success',
|
||||
warning: 'Warning',
|
||||
@@ -26,12 +25,12 @@ export const shared = {
|
||||
confirmToProceed: 'Confirm to proceed',
|
||||
date: 'Date',
|
||||
loading: 'Loading ...',
|
||||
noDataWarning: 'You haven\'t entered any data yet.',
|
||||
noTemperatureWarning: 'You haven\'t entered any temperature data yet.',
|
||||
noDataWarning: "You haven't entered any data yet.",
|
||||
noTemperatureWarning: "You haven't entered any temperature data yet.",
|
||||
noDataButtonText: 'Start entering data now',
|
||||
enter: 'Enter',
|
||||
remove: 'Remove',
|
||||
learnMore: 'Learn more'
|
||||
learnMore: 'Learn more',
|
||||
}
|
||||
|
||||
export const headerTitles = {
|
||||
@@ -46,6 +45,7 @@ export const headerTitles = {
|
||||
Password: settingsTitles.password.name,
|
||||
About: 'About',
|
||||
License: 'License',
|
||||
PrivacyPolicy: 'Privacy Policy',
|
||||
bleeding: 'Bleeding',
|
||||
temperature: 'Temperature',
|
||||
mucus: 'Cervical Mucus',
|
||||
@@ -54,7 +54,7 @@ export const headerTitles = {
|
||||
desire: 'Desire',
|
||||
sex: 'Sex',
|
||||
pain: 'Pain',
|
||||
mood: 'Mood'
|
||||
mood: 'Mood',
|
||||
}
|
||||
|
||||
export const stats = {
|
||||
@@ -65,49 +65,59 @@ export const stats = {
|
||||
averageLabel: 'Average cycle',
|
||||
minLabel: `Shortest`,
|
||||
maxLabel: `Longest`,
|
||||
stdLabel: `Standard\ndeviation`
|
||||
stdLabel: `Standard\ndeviation`,
|
||||
}
|
||||
|
||||
export const bleedingPrediction = {
|
||||
noPrediction: `As soon as you have tracked 3 menstrual cycles, drip will make predictions for the next ones.`,
|
||||
predictionInFuture: (startDays, endDays) => `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.`,
|
||||
predictionStarted1DayLeft: 'Your period is likely to start today or tomorrow.',
|
||||
predictionInFuture: (startDays, endDays) =>
|
||||
`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.`,
|
||||
predictionStarted1DayLeft:
|
||||
'Your period is likely to start today or tomorrow.',
|
||||
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 = {
|
||||
title: 'Unlock app',
|
||||
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?',
|
||||
deleteDatabaseTitle: 'Forgot your password?',
|
||||
deleteData: 'Yes, delete all my data',
|
||||
areYouSureTitle: 'Are you sure?',
|
||||
areYouSure: 'Are you absolutely sure you want to permanently delete all your data?',
|
||||
reallyDeleteData: 'Yes, I am sure'
|
||||
areYouSure:
|
||||
'Are you absolutely sure you want to permanently delete all your data?',
|
||||
reallyDeleteData: 'Yes, I am sure',
|
||||
}
|
||||
|
||||
export const fertilityStatus = {
|
||||
fertile: 'fertile',
|
||||
infertile: 'infertile',
|
||||
fertileUntilEvening: 'Fertile phase ends in the evening',
|
||||
unknown: "We cannot show any cycle information because no period data has been added.",
|
||||
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.",
|
||||
periOvuText: "We were not able to detect both a temperature shift and cervical mucus or cervix shift.",
|
||||
periOvuUntilEveningText: tempRule => {
|
||||
unknown:
|
||||
'We cannot show any cycle information because no period data has been added.',
|
||||
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.",
|
||||
periOvuText:
|
||||
'We were not able to detect both a temperature shift and cervical mucus or cervix shift.',
|
||||
periOvuUntilEveningText: (tempRule) => {
|
||||
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 ' +
|
||||
'always remember to double-check for yourself. Make sure the data makes sense to you.'
|
||||
)
|
||||
},
|
||||
postOvuText: tempRule => {
|
||||
postOvuText: (tempRule) => {
|
||||
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 ' +
|
||||
'double-check for yourself. Make sure the data makes sense to you.'
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
+7
-6
@@ -1,22 +1,23 @@
|
||||
export default {
|
||||
gitlab: {
|
||||
url: 'https://gitlab.com/bloodyhealth/drip',
|
||||
text: 'GitLab'
|
||||
text: 'GitLab',
|
||||
},
|
||||
email: {
|
||||
url: 'mailto:bloodyhealth@mailbox.org',
|
||||
text: 'email'
|
||||
url: 'mailto:drip@mailbox.org',
|
||||
text: 'email',
|
||||
},
|
||||
wiki: {
|
||||
url: 'https://gitlab.com/bloodyhealth/drip/wikis/home',
|
||||
text: 'our wiki'
|
||||
text: 'our wiki',
|
||||
},
|
||||
website: {
|
||||
url: 'https://bloodyhealth.gitlab.io/'
|
||||
url: 'https://dripapp.org/',
|
||||
text: 'Website',
|
||||
},
|
||||
donate: {
|
||||
url: 'https://ko-fi.com/dripapp',
|
||||
text: 'here'
|
||||
text: 'Donate here',
|
||||
},
|
||||
smashicons: {
|
||||
url: 'https://smashicons.com/',
|
||||
|
||||
+70
-53
@@ -1,40 +1,40 @@
|
||||
import links from './links'
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
export default {
|
||||
title: 'Settings',
|
||||
menuItems: {
|
||||
reminders: {
|
||||
name: 'Reminders',
|
||||
text: 'turn on/off reminders'
|
||||
text: 'turn on/off reminders',
|
||||
},
|
||||
nfpSettings: {
|
||||
name:'NFP settings',
|
||||
name: 'NFP settings',
|
||||
text: 'define how you want to use NFP',
|
||||
},
|
||||
dataManagement: {
|
||||
name: 'Data',
|
||||
text: 'import, export or delete your data'
|
||||
text: 'import, export or delete your data',
|
||||
},
|
||||
password: {
|
||||
name:'Password',
|
||||
text: ''
|
||||
name: 'Password',
|
||||
text: '',
|
||||
},
|
||||
about: 'About',
|
||||
license: 'License',
|
||||
settings: 'Settings'
|
||||
settings: 'Settings',
|
||||
privacyPolicy: 'Privacy Policy',
|
||||
},
|
||||
export: {
|
||||
errors: {
|
||||
noData: 'There is no data to export',
|
||||
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',
|
||||
subject: 'My drip data export',
|
||||
title: 'My drip. data export',
|
||||
subject: 'My drip. data export',
|
||||
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: {
|
||||
button: 'Import data',
|
||||
@@ -47,101 +47,118 @@ export default {
|
||||
errors: {
|
||||
couldNotOpenFile: 'Could not open file',
|
||||
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: {
|
||||
message: 'Data successfully imported'
|
||||
message: 'Data successfully imported',
|
||||
},
|
||||
segmentExplainer: 'Import data in CSV format'
|
||||
segmentExplainer: 'Import data in CSV format',
|
||||
},
|
||||
deleteSegment: {
|
||||
title: 'Delete app data',
|
||||
explainer: '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',
|
||||
errors: {
|
||||
couldNotDeleteFile: 'Could not delete data',
|
||||
postFix: 'No data was deleted or changed',
|
||||
noData: 'There is no data to delete'
|
||||
noData: 'There is no data to delete',
|
||||
},
|
||||
success: {
|
||||
message: 'App data successfully deleted'
|
||||
}
|
||||
message: 'App data successfully deleted',
|
||||
},
|
||||
},
|
||||
tempScale: {
|
||||
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',
|
||||
max: 'Max',
|
||||
loadError: 'Could not load saved temperature scale settings',
|
||||
saveError: 'Could not save temperature scale settings'
|
||||
saveError: 'Could not save temperature scale settings',
|
||||
},
|
||||
tempReminder: {
|
||||
title: 'Temperature reminder',
|
||||
noTimeSet: 'Set a time for a daily reminder to take your temperature',
|
||||
timeSet: time => `Daily reminder set for ${time}`,
|
||||
notification: 'Record your morning temperature'
|
||||
timeSet: (time) => `Daily reminder set for ${time}`,
|
||||
notification: 'Record your morning temperature',
|
||||
},
|
||||
periodReminder: {
|
||||
title: 'Next period reminder',
|
||||
reminderText: '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.`
|
||||
reminderText:
|
||||
'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: {
|
||||
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',
|
||||
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'
|
||||
cervixModeOn:
|
||||
'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: {
|
||||
title: 'App password',
|
||||
explainerDisabled: "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",
|
||||
explainerDisabled:
|
||||
"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',
|
||||
savePassword: 'Save password',
|
||||
changePassword: 'Change password',
|
||||
deletePassword: 'Delete password',
|
||||
enterCurrent: "Please enter your current password",
|
||||
enterNew: "Please enter a new password",
|
||||
confirmPassword: "Please confirm your password",
|
||||
enterCurrent: 'Please enter your current password',
|
||||
enterNew: 'Please enter a new password',
|
||||
confirmPassword: 'Please confirm your password',
|
||||
passwordsDontMatch: "Password and confirmation don't match",
|
||||
backupReminderTitle: 'Read this before making changes to your password',
|
||||
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.',
|
||||
deleteBackupReminderTitle: 'Read this before deleting your password',
|
||||
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.',
|
||||
backupReminder: {
|
||||
title: 'Read this before making changes to your password',
|
||||
message: `
|
||||
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: {
|
||||
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: {
|
||||
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.`,
|
||||
},
|
||||
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.`
|
||||
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.`,
|
||||
},
|
||||
version: {
|
||||
title: 'Version'
|
||||
title: 'Version',
|
||||
},
|
||||
website: {
|
||||
title: 'Website'
|
||||
title: 'Website',
|
||||
},
|
||||
preOvu: {
|
||||
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: {
|
||||
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: {
|
||||
title: 'Buy us a coffee!',
|
||||
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.`
|
||||
}
|
||||
title: 'Support us',
|
||||
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!`,
|
||||
},
|
||||
}
|
||||
|
||||
+22
-22
@@ -11,10 +11,10 @@ export const generalInfo = {
|
||||
3. and menstrual bleeding
|
||||
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}.`,
|
||||
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 {
|
||||
@@ -22,7 +22,7 @@ export default {
|
||||
title: `Tracking menstrual bleeding`,
|
||||
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"),
|
||||
· 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").
|
||||
@@ -33,7 +33,7 @@ Excluding bleeding values is for tracking bleeding when it's not marking the sta
|
||||
|
||||
${generalInfo.nfpTfyReminder}`,
|
||||
},
|
||||
cervix: {
|
||||
cervix: {
|
||||
title: `Tracking your cervix`,
|
||||
text: `The cervix is located inside of the body at the end of the vaginal canal, between the vagina and the uterus.
|
||||
|
||||
@@ -42,15 +42,15 @@ 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".
|
||||
|
||||
· 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.excludeExplainer}
|
||||
|
||||
${generalInfo.nfpTfyReminder}`
|
||||
${generalInfo.nfpTfyReminder}`,
|
||||
},
|
||||
desire: {
|
||||
desire: {
|
||||
title: 'Tracking sexual desire',
|
||||
text: `The app allows you to track sexual desire independently from sexual activity.
|
||||
|
||||
@@ -58,9 +58,9 @@ ${generalInfo.cycleRelation}
|
||||
|
||||
${generalInfo.noNfpSymptom}
|
||||
|
||||
${generalInfo.curiousNfp}`
|
||||
${generalInfo.curiousNfp}`,
|
||||
},
|
||||
mood: {
|
||||
mood: {
|
||||
title: 'Tracking mood',
|
||||
text: `The app allows you to track your mood.
|
||||
|
||||
@@ -68,16 +68,16 @@ ${generalInfo.cycleRelation}
|
||||
|
||||
${generalInfo.noNfpSymptom}
|
||||
|
||||
${generalInfo.curiousNfp}`
|
||||
${generalInfo.curiousNfp}`,
|
||||
},
|
||||
mucus: {
|
||||
mucus: {
|
||||
title: 'Tracking cervical mucus',
|
||||
text: `Cervical mucus can help determine in which phase of the menstrual cycle you are.
|
||||
|
||||
By default the secondary symptom the app uses for NFP evaluation is 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:
|
||||
· t = (dry feeling + no texture),
|
||||
· ∅ = (no feeling + no texture),
|
||||
@@ -87,23 +87,23 @@ 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.
|
||||
|
||||
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.excludeExplainer}
|
||||
|
||||
${generalInfo.nfpTfyReminder}`
|
||||
${generalInfo.nfpTfyReminder}`,
|
||||
},
|
||||
note: {
|
||||
note: {
|
||||
title: 'Notes',
|
||||
text: `Note allows you to track any extra information you want to save. It is the only category that can store information for a date in the future. This can be helpful for reminding you of an appointment.
|
||||
|
||||
${generalInfo.noNfpSymptom}
|
||||
|
||||
${generalInfo.curiousNfp}`
|
||||
${generalInfo.curiousNfp}`,
|
||||
},
|
||||
pain: {
|
||||
pain: {
|
||||
title: 'Tracking pain',
|
||||
text: `The app allows you to keep track of different kinds of pain you experience.
|
||||
|
||||
@@ -111,17 +111,17 @@ ${generalInfo.cycleRelation}
|
||||
|
||||
${generalInfo.noNfpSymptom}
|
||||
|
||||
${generalInfo.curiousNfp}`
|
||||
${generalInfo.curiousNfp}`,
|
||||
},
|
||||
sex: {
|
||||
sex: {
|
||||
title: 'Tracking sex and contraceptives',
|
||||
text: `The app allows you to track sex independently from sexual desire. You can differentiate between masturbation and sex with a partner/partners. Here you can also track your contraceptive method(s). Only sexual activity will be shown in the "chart" section, lighter purple indicating solo sex and darker purple partner sex. Did you know that having an orgasm can help release cramps?
|
||||
|
||||
${generalInfo.noNfpSymptom}
|
||||
|
||||
${generalInfo.curiousNfp}`
|
||||
${generalInfo.curiousNfp}`,
|
||||
},
|
||||
temperature: {
|
||||
temperature: {
|
||||
title: 'Tracking body basal temperature',
|
||||
text: `One of the body signs you need to track for knowing your fertility status is your body basal temperature. The body temperature changes over the course of a menstrual cycle, it rises after ovulation.
|
||||
|
||||
@@ -140,6 +140,6 @@ ${generalInfo.chartNfp}
|
||||
|
||||
${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 AppWrapper from './components/app-wrapper'
|
||||
import './i18n/i18n';
|
||||
|
||||
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>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
@@ -22,6 +22,17 @@
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
@@ -38,17 +49,5 @@
|
||||
<false/>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<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>
|
||||
</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