Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8b0d60fdd | |||
| 5e32f4d7dc | |||
| 8008c8e2cc | |||
| 2cc5ffb50f | |||
| e7949377a2 | |||
| 592f2b3e16 | |||
| 7506e911f7 | |||
| 09bbfe8a7d | |||
| bb1bd949c3 | |||
| 406b71250e | |||
| 6228cd8953 | |||
| 2623fedec9 | |||
| bfdad895ce | |||
| dce8138e12 | |||
| 7864ebfc41 | |||
| 442bcc5cee | |||
| e8aac6d41e | |||
| 2bbe737101 | |||
| 8856b3f1bc | |||
| aadc36f45c | |||
| 6c56b6f83c | |||
| 018ec3bcda | |||
| bc358bd9ed | |||
| f8eef66810 | |||
| ac2bad1de0 | |||
| 6cbfcb9d64 | |||
| 39df9c2ec0 | |||
| 138f1d28b3 | |||
| e2abc4dbeb | |||
| 0cfc85933c | |||
| c6fd5e6db2 | |||
| 8e57febf9e | |||
| ed38a5b450 | |||
| 16b3199892 | |||
| d13a776970 | |||
| 418b9c0f5d | |||
| c35f333daa | |||
| 668acb4afe | |||
| ac0690ec9f | |||
| cef2e850d7 | |||
| 4b469f2f49 | |||
| b0b2e8ceb5 | |||
| 82a9bf0a0f | |||
| 417083b0c1 | |||
| 25ed0d168e | |||
| d7b599d03b | |||
| 3f52cae04b | |||
| 59c3636139 | |||
| 472d793627 | |||
| 43b98ed9a5 | |||
| 69d2517dd2 | |||
| 0f6567e66b | |||
| c81f7d6291 | |||
| e9c18add5e | |||
| 4071c30b8b | |||
| ff1afcb8dc | |||
| d2c0891a68 | |||
| a557733d30 | |||
| 0f7ab6c803 | |||
| b7088c44f2 | |||
| 00294ff6f6 | |||
| 1d61675ef7 | |||
| 6a8d22f9f0 | |||
| 6811b5a692 | |||
| 9ff37e2874 | |||
| f022fb6b8a | |||
| 37564621e0 | |||
| 5057e1a38e | |||
| e781919434 | |||
| 3715e0c4d2 | |||
| 3ff996e946 | |||
| 6011bd9208 | |||
| d1e16abe34 | |||
| bc13f5c1e6 | |||
| ecf3ebf16d | |||
| 08fd3afc34 | |||
| 2528c03315 | |||
| d322e557a3 | |||
| 9fd17d769e | |||
| ca68186351 | |||
| 8d6f5d637b | |||
| c6790fe271 | |||
| 6402370eaf | |||
| f1ca709f25 | |||
| 51160b033b | |||
| c4884f8f8f | |||
| 647567abd2 | |||
| f123dbf730 | |||
| bc0d36ed54 | |||
| 745f874ccf | |||
| ea36d4ec7a | |||
| bb5623a621 | |||
| cf6c601c47 | |||
| 8efe906e87 | |||
| 3924c04e56 | |||
| a9401d4a0f | |||
| 53ec882e53 | |||
| 7387db4a69 | |||
| 9ef84f9b31 | |||
| f9c928d45c | |||
| 70860860d5 | |||
| 6a98b28427 | |||
| 9ee7819462 | |||
| 5a320e148c | |||
| 767aa23841 | |||
| 227aa69677 | |||
| 722c372f2d | |||
| 92e02c48dc | |||
| 3449746ead | |||
| ee52f75927 | |||
| 9c8cc8de2e | |||
| ea279338e0 | |||
| f15dc75908 | |||
| c00684ebbb | |||
| d0f4ae5109 | |||
| 187fbef207 | |||
| 4c655b13c3 | |||
| 2276b37741 | |||
| 7c5d0b9ae2 | |||
| b8b2d3d2bf | |||
| 3ec4d2218e | |||
| dc5b34546a | |||
| 6577038f22 | |||
| 93abaf99be | |||
| dd24e6058f | |||
| ded3a79bbc | |||
| ab0970e33f | |||
| 49d5d77a82 | |||
| 0cf8820e94 | |||
| 1ef3585cdc | |||
| 25ff6ca01f | |||
| dd648da56f | |||
| d1e87835d1 | |||
| c715b16e44 | |||
| 31b428fa53 | |||
| a291c78379 | |||
| c22bff5948 | |||
| 772a277315 | |||
| a4545fedcf | |||
| 9a9ca10283 | |||
| e7b5987837 | |||
| fa59f35719 | |||
| 3d245127c7 | |||
| 7e3eae1758 | |||
| 60d1b3d157 | |||
| 3940e3b4dd | |||
| 75a3837417 | |||
| 95afd305a9 | |||
| 3621bfb351 | |||
| f567510b7b | |||
| feaa7dab56 | |||
| 3f723aa823 | |||
| 512250eff4 | |||
| 70d28f7a4b | |||
| d431c2b947 | |||
| c140cdd602 | |||
| ccce046a55 | |||
| 112d52fa90 | |||
| 0a8dcf87a4 | |||
| 6592d742af | |||
| 3c8f6888fc | |||
| 14001664b2 | |||
| c44faaba0f | |||
| 246a6b3bb7 |
@@ -0,0 +1,62 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## v0.1905.29-beta
|
||||
|
||||
- Auto save functionality for all symptoms
|
||||
- Add donation section to about
|
||||
- Styling fixes
|
||||
- Clearer labels on cycle day overview
|
||||
- Rename mucus to cervical mucus
|
||||
- Set show more on homescreen to default and get rid of more/less switch
|
||||
- Add loading screen to data import
|
||||
- Fix line width in chart
|
||||
- Removes logo and adds header on the main login screen
|
||||
- Nicer formatting for past bleeding prediction
|
||||
- Fixes prediction range in drop on homescreen
|
||||
- Removes permissions not required for debug or production
|
||||
- Temperature screen styling update
|
||||
|
||||
## v0.0.3 - 2019-04-17
|
||||
|
||||
### Changes
|
||||
|
||||
- Removes Google services from notification library and use fork of react-native-push-notification: <https://github.com/github:jfr3000/react-native-push-notification>
|
||||
|
||||
### Fixed
|
||||
|
||||
- Button functionality in settings for password
|
||||
|
||||
## v0.0.2 - 2019-04-09
|
||||
|
||||
## Second updated beta release version
|
||||
|
||||
### Changes
|
||||
|
||||
- First day of the week in calendar is now Monday instead of Sunday
|
||||
- Minor styling consistency
|
||||
|
||||
### Fixed
|
||||
|
||||
- Typos
|
||||
- Bleeding value is visible in shortcut from Homescreen
|
||||
- Delete button for sex, pain and mood
|
||||
- Dates on chart
|
||||
|
||||
## v0.0.1 - 2019-02-15
|
||||
|
||||
## First beta release version
|
||||
|
||||
### Added (list of core functionality)
|
||||
|
||||
- you can track your menstrual bleeding
|
||||
- you can track symptoms related to natural family planning (nfp), i.e. basal temperature and mucus or cervix
|
||||
- you can use nfp symptoms for fertility awareness (drip implements the sympto-thermal method)
|
||||
- you can track other symptoms like desire, sex, pain, mood, or save a note
|
||||
- you can see bleeding days and predicted bleeding days in a calendar
|
||||
- drip gives you an overview of tracked symptoms on a beautiful chart
|
||||
- you can see basic stats about your cycle lengths
|
||||
- you can encrypt your data and protect it with a password
|
||||
- you can import and export your data in a nice format (csv)
|
||||
- you can set reminders (daily reminder for taking your temperature or once per cycle for your next period
|
||||
@@ -3,7 +3,7 @@
|
||||
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/).
|
||||
|
||||
The app is build in React Native and currently developed for Android.
|
||||
The app is built in React Native and currently developed for Android.
|
||||
|
||||
Here --> you will find our [contributing guide](https://gitlab.com/bloodyhealth/drip/blob/master/CONTRIBUTING.md).
|
||||
|
||||
@@ -11,11 +11,11 @@ Here --> you will find our [contributing guide](https://gitlab.com/bloodyhealth/
|
||||
|
||||
1. Install [Android Studio](https://developer.android.com/studio/) - you'll need it to install some dependencies.
|
||||
|
||||
1. Make sure you are running Node 8 (newer versions won’t work). It's easiest to switch Node versions using `nvm`, here’s how to do it:
|
||||
1. 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:
|
||||
|
||||
```
|
||||
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
|
||||
$ nvm install v8
|
||||
$ nvm install v10
|
||||
```
|
||||
|
||||
1. Clone this repository:
|
||||
@@ -75,10 +75,6 @@ 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
|
||||
```
|
||||
|
||||
### [Windows 10] react native problems
|
||||
|
||||
Unfortunately, the react native version we use doesn't work on Windows 10 it seems, find [more info here](https://github.com/facebook/react-native/issues/20015).
|
||||
|
||||
## Tests
|
||||
You can run the tests with:
|
||||
```
|
||||
@@ -96,7 +92,7 @@ More information about how the app calculates fertility status and bleeding pred
|
||||
|
||||
## Adding a new tracking icon
|
||||
|
||||
1. We use [fontello](http://fontello.com/) to create icon fonts for us. You need to upload the complete set of tracking icons (bleeding, mucus, ...) including the new icon you wish to add, all in SVG.
|
||||
1. We use [fontello](http://fontello.com/) to create icon fonts for us. You need to upload the complete set of tracking icons (bleeding, cervical mucus, ...) including the new icon you wish to add, all in SVG.
|
||||
2. Download webfont from fontello
|
||||
3. Copy both the content of `config.json` and `font.tff` into `assets/fonts`, replacing it with the current content of `config-drip-icon-font.json` and `drip-icon-font.tff`.
|
||||
4. Now run the following command in your console:
|
||||
|
||||
+13
-13
@@ -102,10 +102,10 @@ android {
|
||||
applicationId "com.drip"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 3
|
||||
versionName "0.1905.29-beta"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
abiFilters "armeabi-v7a"
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
@@ -123,7 +123,7 @@ android {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk false // If true, also generate a universal APK
|
||||
include "armeabi-v7a", "x86"
|
||||
include "armeabi-v7a", "arm64-v8a"
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
@@ -138,7 +138,7 @@ android {
|
||||
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]
|
||||
def versionCodes = ["armeabi-v7a":1, "arm64-v8a":2]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
@@ -149,14 +149,14 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':nodejs-mobile-react-native')
|
||||
compile project(':react-native-restart')
|
||||
compile project(':react-native-push-notification')
|
||||
compile project(':react-native-vector-icons')
|
||||
compile project(':react-native-fs')
|
||||
compile project(':react-native-document-picker')
|
||||
compile project(':react-native-share')
|
||||
compile project(':realm')
|
||||
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')
|
||||
compile fileTree(dir: "libs", include: ["*.jar"])
|
||||
compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
|
||||
compile "com.facebook.react:react-native:+" // From node_modules
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<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" />
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.C2D_MESSAGE"
|
||||
@@ -52,39 +55,12 @@
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
|
||||
android:resource="@android:color/white"/>
|
||||
|
||||
<!-- < Only if you're using GCM or localNotificationSchedule() > -->
|
||||
<receiver
|
||||
android:name="com.google.android.gms.gcm.GcmReceiver"
|
||||
android:exported="true"
|
||||
android:permission="com.google.android.c2dm.permission.SEND" >
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
|
||||
<category android:name="${applicationId}" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- < Only if you're using GCM or localNotificationSchedule() > -->
|
||||
|
||||
<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" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationRegistrationService"/>
|
||||
<service
|
||||
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
|
||||
android:exported="false" >
|
||||
<intent-filter>
|
||||
<!-- < Only if you're using GCM or localNotificationSchedule() > -->
|
||||
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
|
||||
<!-- < Only if you're using GCM or localNotificationSchedule() > -->
|
||||
|
||||
<!-- <Else> -->
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
<!-- </Else> -->
|
||||
</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.
@@ -0,0 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.drip"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission tools:node="remove" android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission tools:node="remove" android:name="android.permission.INTERNET" />
|
||||
|
||||
</manifest>
|
||||
@@ -29,19 +29,14 @@ allprojects {
|
||||
}
|
||||
|
||||
ext {
|
||||
buildToolsVersion = "26.0.3"
|
||||
buildToolsVersion = "27.0.3"
|
||||
minSdkVersion = 16
|
||||
compileSdkVersion = 26
|
||||
targetSdkVersion = 26
|
||||
supportLibVersion = "26.1.0"
|
||||
compileSdkVersion = 27
|
||||
targetSdkVersion = 27
|
||||
supportLibVersion = "27.1.1"
|
||||
}
|
||||
|
||||
subprojects {project ->
|
||||
buildscript {
|
||||
repositories {
|
||||
maven { url = 'https://dl.bintray.com/android/android-tools/' }
|
||||
}
|
||||
}
|
||||
// https://stackoverflow.com/questions/52613089/getting-verifyreleaseresources-error-after-upgrading-react-native
|
||||
afterEvaluate {
|
||||
if (project.hasProperty("android")) {
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
import { View } from 'react-native'
|
||||
|
||||
import AppText from './app-text'
|
||||
import { shared } from '../i18n/en/labels'
|
||||
|
||||
const AppLoadingView = () => {
|
||||
return (
|
||||
<View flex={1}>
|
||||
<View style={{flex:1, justifyContent: 'center'}}>
|
||||
<AppText style={{alignSelf: 'center'}}>{shared.loading}</AppText>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppLoadingView
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { TextInput } from 'react-native'
|
||||
import styles from '../styles'
|
||||
|
||||
export default function AppTextInput({ style, ...props }) {
|
||||
if (!Array.isArray(style)) style = [style]
|
||||
return (
|
||||
<TextInput
|
||||
style={[styles.textInputField, ...style]}
|
||||
autoFocus={props.autoFocus}
|
||||
onChangeText={props.onChangeText}
|
||||
value={props.value}
|
||||
placeholder={props.placeholder}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
AppTextInput.propTypes = {
|
||||
secureTextEntry: PropTypes.bool
|
||||
}
|
||||
|
||||
AppTextInput.defaultProps = {
|
||||
style: []
|
||||
}
|
||||
@@ -18,19 +18,6 @@ export default function AppText(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export function ActionHint(props) {
|
||||
if(props.isVisible) {
|
||||
return (
|
||||
<AppText
|
||||
style={[styles.actionHint, props.style]}>
|
||||
{props.children}
|
||||
</AppText>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function SymptomSectionHeader(props) {
|
||||
return (
|
||||
<AppText style={styles.symptomViewHeading}>
|
||||
|
||||
+9
-29
@@ -11,7 +11,6 @@ import SettingsMenu from './settings/settings-menu'
|
||||
import settingsViews from './settings'
|
||||
import Stats from './stats'
|
||||
import {headerTitles, menuTitles} from '../i18n/en/labels'
|
||||
import InfoSymptom from './cycle-day/symptoms/info-symptom'
|
||||
import setupNotifications from '../lib/notifications'
|
||||
|
||||
// design wants everyhting lowercased, but we don't
|
||||
@@ -22,7 +21,6 @@ const headerTitlesLowerCase = Object.keys(headerTitles).reduce((acc, curr) => {
|
||||
}, {})
|
||||
|
||||
const HOME_PAGE = 'Home'
|
||||
const INFO_SYMPTOM_PAGE = 'InfoSymptom'
|
||||
const CYCLE_DAY_PAGE = 'CycleDay'
|
||||
const SETTINGS_MENU_PAGE = 'SettingsMenu'
|
||||
|
||||
@@ -49,7 +47,7 @@ export default class App extends Component {
|
||||
if (this.isMenuItem()) {
|
||||
this.menuOrigin = currentPage
|
||||
}
|
||||
if (!this.isSymptomView() && !this.isInfoSymptomView()) {
|
||||
if (!this.isSymptomView()) {
|
||||
this.originForSymptomView = currentPage
|
||||
}
|
||||
this.setState({ currentPage: pageName, currentProps: props })
|
||||
@@ -66,10 +64,6 @@ export default class App extends Component {
|
||||
this.navigate(SETTINGS_MENU_PAGE)
|
||||
} else if (currentPage === CYCLE_DAY_PAGE) {
|
||||
this.navigate(this.menuOrigin)
|
||||
} else if (this.isInfoSymptomView()) {
|
||||
const { date, cycleDay, symptomView } = currentProps
|
||||
this.navigate(
|
||||
symptomView, { date, cycleDay })
|
||||
} else {
|
||||
this.navigate(HOME_PAGE)
|
||||
}
|
||||
@@ -84,10 +78,6 @@ export default class App extends Component {
|
||||
return Object.keys(symptomViews).includes(this.state.currentPage)
|
||||
}
|
||||
|
||||
isInfoSymptomView() {
|
||||
return this.state.currentPage === INFO_SYMPTOM_PAGE
|
||||
}
|
||||
|
||||
isSettingsView() {
|
||||
return Object.keys(settingsViews).includes(this.state.currentPage)
|
||||
}
|
||||
@@ -104,42 +94,32 @@ export default class App extends Component {
|
||||
Calendar,
|
||||
CycleDay,
|
||||
Chart,
|
||||
InfoSymptom,
|
||||
SettingsMenu,
|
||||
...settingsViews,
|
||||
Stats,
|
||||
...symptomViews
|
||||
}
|
||||
const page = pages[currentPage]
|
||||
const Page = pages[currentPage]
|
||||
const title = headerTitlesLowerCase[currentPage]
|
||||
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
{this.isDefaultView() &&
|
||||
<Header title={title} />
|
||||
}
|
||||
{(this.isInfoSymptomView() || this.isSettingsView()) &&
|
||||
{(this.isSettingsView()) &&
|
||||
<Header
|
||||
title={title}
|
||||
showBackButton={true}
|
||||
goBack={this.handleBackButtonPress}
|
||||
/>
|
||||
}
|
||||
{this.isSymptomView() &&
|
||||
<Header
|
||||
title={title}
|
||||
isSymptomView={true}
|
||||
goBack={this.handleBackButtonPress}
|
||||
date={currentProps.date}
|
||||
goToSymptomInfo={() => this.navigate(INFO_SYMPTOM_PAGE, {
|
||||
symptomView: currentPage,
|
||||
...currentProps
|
||||
})}
|
||||
/>}
|
||||
|
||||
{React.createElement(page, {
|
||||
navigate: this.navigate,
|
||||
...currentProps
|
||||
})}
|
||||
<Page
|
||||
navigate={this.navigate}
|
||||
{...currentProps}
|
||||
handleBackButtonPress={this.handleBackButtonPress}
|
||||
/>
|
||||
|
||||
{!this.isSymptomView() &&
|
||||
<Menu navigate={this.navigate} currentPage={currentPage} />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { Component } from 'react'
|
||||
import { CalendarList } from 'react-native-calendars'
|
||||
import {LocalDate} from 'js-joda'
|
||||
import { LocalDate } from 'js-joda'
|
||||
import { getBleedingDaysSortedByDate } from '../db'
|
||||
import cycleModule from '../lib/cycle'
|
||||
import {shadesOfRed} from '../styles/index'
|
||||
import { shadesOfRed, calendarTheme } from '../styles/index'
|
||||
import styles from '../styles/index'
|
||||
import nothingChanged from '../db/db-unchanged'
|
||||
|
||||
@@ -52,6 +52,9 @@ export default class CalendarView extends Component {
|
||||
)
|
||||
}
|
||||
markingType={'custom'}
|
||||
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
||||
firstDay={1}
|
||||
theme={calendarTheme}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+18
-25
@@ -1,6 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import { View, FlatList, ActivityIndicator } from 'react-native'
|
||||
import range from 'date-range'
|
||||
import { LocalDate } from 'js-joda'
|
||||
import { makeYAxisLabels, makeHorizontalGrid } from './y-axis'
|
||||
import nfpLines from './nfp-lines'
|
||||
@@ -11,6 +10,7 @@ import { cycleDayColor } from '../../styles'
|
||||
import { scaleObservable } from '../../local-storage'
|
||||
import config from '../../config'
|
||||
import AppText from '../app-text'
|
||||
import AppLoadingView from '../app-loading'
|
||||
import { shared as labels } from '../../i18n/en/labels'
|
||||
import DripIcon from '../../assets/drip-icons'
|
||||
import DripHomeIcon from '../../assets/drip-home-icons'
|
||||
@@ -124,14 +124,8 @@ export default class CycleChart extends Component {
|
||||
// we don't want the chart to end abruptly before the first data day
|
||||
amountOfCycleDays += 5
|
||||
}
|
||||
const jsDates = getTodayAndPreviousDays(amountOfCycleDays)
|
||||
return jsDates.map(jsDate => {
|
||||
return LocalDate.of(
|
||||
jsDate.getFullYear(),
|
||||
jsDate.getMonth() + 1,
|
||||
jsDate.getDate()
|
||||
).toString()
|
||||
})
|
||||
const localDates = getTodayAndPreviousDays(amountOfCycleDays)
|
||||
return localDates.map(localDate => localDate.toString())
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -140,11 +134,7 @@ export default class CycleChart extends Component {
|
||||
onLayout={this.onLayout}
|
||||
style={{ flexDirection: 'row', flex: 1 }}
|
||||
>
|
||||
{!this.state.chartLoaded &&
|
||||
<View style={{width: '100%', justifyContent: 'center', alignItems: 'center'}}>
|
||||
<AppText>{labels.loading}</AppText>
|
||||
</View>
|
||||
}
|
||||
{!this.state.chartLoaded && <AppLoadingView />}
|
||||
|
||||
{this.state.chartHeight && this.state.chartLoaded &&
|
||||
<View>
|
||||
@@ -170,10 +160,7 @@ export default class CycleChart extends Component {
|
||||
size={styles.yAxis.width - 7}
|
||||
color={cycleDayColor}
|
||||
/>
|
||||
<AppText style={[
|
||||
styles.column.label.date,
|
||||
styles.yAxisLabels.dateLabel
|
||||
]}>
|
||||
<AppText style={[styles.yAxisLabels.dateLabel]}>
|
||||
{labels.date.toLowerCase()}
|
||||
</AppText>
|
||||
</View>
|
||||
@@ -216,12 +203,18 @@ function LoadingMoreView(props) {
|
||||
}
|
||||
|
||||
function getTodayAndPreviousDays(n) {
|
||||
const today = new Date()
|
||||
today.setHours(0)
|
||||
today.setMinutes(0)
|
||||
today.setSeconds(0)
|
||||
today.setMilliseconds(0)
|
||||
const earlierDate = new Date(today - (range.DAY * n))
|
||||
const today = LocalDate.now()
|
||||
const targetDate = today.minusDays(n)
|
||||
|
||||
return range(earlierDate, today).reverse()
|
||||
function getDaysInRange(currDate, range) {
|
||||
if (currDate.isBefore(targetDate)) {
|
||||
return range
|
||||
} else {
|
||||
range.push(currDate)
|
||||
const next = currDate.minusDays(1)
|
||||
return getDaysInRange(next, range)
|
||||
}
|
||||
}
|
||||
|
||||
return getDaysInRange(today, [])
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ export default class DayColumn extends Component {
|
||||
} else if (symptom === 'pain') {
|
||||
// is any pain documented?
|
||||
acc.pain = cycleDay.pain &&
|
||||
Object.values(cycleDay.pain).some(x => x === true)
|
||||
Object.values({...cycleDay.pain}).some(x => x === true)
|
||||
} else if (symptom === 'mood') {
|
||||
// is mood documented?
|
||||
acc.mood = cycleDay.mood &&
|
||||
Object.values(cycleDay.mood).some(x => x === true)
|
||||
Object.values({...cycleDay.mood}).some(x => x === true)
|
||||
}
|
||||
acc[`${symptom}Exclude`] = cycleDay[symptom] && cycleDay[symptom].exclude
|
||||
return acc
|
||||
|
||||
@@ -7,7 +7,8 @@ export const dotRadius = 5
|
||||
const lineWidth = 1.5
|
||||
const colorLtl = '#feb47b'
|
||||
const gridColor = '#d3d3d3'
|
||||
const gridLineWidth = 0.5
|
||||
const gridLineWidthVertical = 0.6
|
||||
const gridLineWidthHorizontal = 0.3
|
||||
const numberLabelFontSize = 13
|
||||
|
||||
const styles = {
|
||||
@@ -34,6 +35,7 @@ const styles = {
|
||||
fontSize: 9,
|
||||
fontWeight: '100',
|
||||
textAlign: 'center',
|
||||
paddingTop: 2.5
|
||||
},
|
||||
number: {
|
||||
color: cycleDayColor,
|
||||
@@ -43,7 +45,7 @@ const styles = {
|
||||
},
|
||||
stroke: {
|
||||
color: gridColor,
|
||||
width: gridLineWidth,
|
||||
width: gridLineWidthVertical,
|
||||
}
|
||||
},
|
||||
symptomIcon: {
|
||||
@@ -101,15 +103,18 @@ const styles = {
|
||||
},
|
||||
dateLabel: {
|
||||
textAlign: 'center',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
color: 'grey',
|
||||
fontSize: 9,
|
||||
fontWeight: '100',
|
||||
}
|
||||
},
|
||||
horizontalGrid: {
|
||||
position:'absolute',
|
||||
borderColor: gridColor,
|
||||
borderWidth: gridLineWidth,
|
||||
width: '100%',
|
||||
borderStyle: 'solid',
|
||||
borderBottomColor: gridColor,
|
||||
borderBottomWidth: gridLineWidthHorizontal,
|
||||
width: '100%',
|
||||
left: config.columnWidth
|
||||
},
|
||||
nfpLine: {
|
||||
|
||||
@@ -11,15 +11,11 @@ import { getCycleDay } from '../../db'
|
||||
import cycleModule from '../../lib/cycle'
|
||||
import styles from '../../styles'
|
||||
import * as labels from '../../i18n/en/cycle-day'
|
||||
import { headerTitles as symptomTitles } from '../../i18n/en/labels'
|
||||
import AppText from '../app-text'
|
||||
import DripIcon from '../../assets/drip-icons'
|
||||
|
||||
const bleedingLabels = labels.bleeding.labels
|
||||
const feelingLabels = labels.mucus.feeling.categories
|
||||
const textureLabels = labels.mucus.texture.categories
|
||||
const openingLabels = labels.cervix.opening.categories
|
||||
const firmnessLabels = labels.cervix.firmness.categories
|
||||
const positionLabels = labels.cervix.position.categories
|
||||
const intensityLabels = labels.intensity
|
||||
const sexLabels = labels.sex.categories
|
||||
const contraceptiveLabels = labels.contraceptives.categories
|
||||
@@ -72,28 +68,25 @@ export default class CycleDayOverView extends Component {
|
||||
}
|
||||
},
|
||||
mucus: mucus => {
|
||||
const categories = ['feeling', 'texture', 'value']
|
||||
if (categories.every(c => isNumber(mucus[c]))) {
|
||||
let mucusLabel = [feelingLabels[mucus.feeling], textureLabels[mucus.texture]].join(', ')
|
||||
mucusLabel += `\n${labels.mucusNFP[mucus.value]}`
|
||||
if (mucus.exclude) mucusLabel = `(${mucusLabel})`
|
||||
return mucusLabel
|
||||
}
|
||||
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 (mucus.exclude) label = `(${label})`
|
||||
|
||||
return label
|
||||
},
|
||||
cervix: cervix => {
|
||||
let cervixLabel = []
|
||||
if (isNumber(cervix.opening) && isNumber(cervix.firmness)) {
|
||||
cervixLabel.push(
|
||||
openingLabels[cervix.opening],
|
||||
firmnessLabels[cervix.firmness]
|
||||
)
|
||||
if (isNumber(cervix.position)) {
|
||||
cervixLabel.push(positionLabels[cervix.position])
|
||||
}
|
||||
cervixLabel = cervixLabel.join(', ')
|
||||
if (cervix.exclude) cervixLabel = `(${cervixLabel})`
|
||||
return cervixLabel
|
||||
}
|
||||
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 => {
|
||||
return note.value
|
||||
@@ -106,7 +99,7 @@ export default class CycleDayOverView extends Component {
|
||||
},
|
||||
sex: sex => {
|
||||
let sexLabel = []
|
||||
if (sex && Object.values(sex).some(val => val)){
|
||||
if (sex && Object.values({...sex}).some(val => val)){
|
||||
Object.keys(sex).forEach(key => {
|
||||
if(sex[key] && key !== 'other' && key !== 'note') {
|
||||
sexLabel.push(
|
||||
@@ -128,7 +121,7 @@ export default class CycleDayOverView extends Component {
|
||||
},
|
||||
pain: pain => {
|
||||
let painLabel = []
|
||||
if (pain && Object.values(pain).some(val => val)){
|
||||
if (pain && Object.values({...pain}).some(val => val)){
|
||||
Object.keys(pain).forEach(key => {
|
||||
if(pain[key] && key !== 'other' && key !== 'note') {
|
||||
painLabel.push(painLabels[key])
|
||||
@@ -147,7 +140,7 @@ export default class CycleDayOverView extends Component {
|
||||
},
|
||||
mood: mood => {
|
||||
let moodLabel = []
|
||||
if (mood && Object.values(mood).some(val => val)){
|
||||
if (mood && Object.values({...mood}).some(val => val)){
|
||||
Object.keys(mood).forEach(key => {
|
||||
if(mood[key] && key !== 'other' && key !== 'note') {
|
||||
moodLabel.push(moodLabels[key])
|
||||
@@ -188,7 +181,7 @@ export default class CycleDayOverView extends Component {
|
||||
<ScrollView>
|
||||
<View style={styles.symptomBoxesView}>
|
||||
<SymptomBox
|
||||
title='Bleeding'
|
||||
title={symptomTitles.bleeding}
|
||||
onPress={() => this.navigate('BleedingEditView')}
|
||||
data={this.getLabel('bleeding')}
|
||||
disabled={dateInFuture}
|
||||
@@ -196,7 +189,7 @@ export default class CycleDayOverView extends Component {
|
||||
>
|
||||
</SymptomBox>
|
||||
<SymptomBox
|
||||
title='Temperature'
|
||||
title={symptomTitles.temperature}
|
||||
onPress={() => this.navigate('TemperatureEditView')}
|
||||
data={this.getLabel('temperature')}
|
||||
disabled={dateInFuture}
|
||||
@@ -204,7 +197,7 @@ export default class CycleDayOverView extends Component {
|
||||
>
|
||||
</SymptomBox>
|
||||
<SymptomBox
|
||||
title='Mucus'
|
||||
title={symptomTitles.mucus}
|
||||
onPress={() => this.navigate('MucusEditView')}
|
||||
data={this.getLabel('mucus')}
|
||||
disabled={dateInFuture}
|
||||
@@ -212,7 +205,7 @@ export default class CycleDayOverView extends Component {
|
||||
>
|
||||
</SymptomBox>
|
||||
<SymptomBox
|
||||
title='Cervix'
|
||||
title={symptomTitles.cervix}
|
||||
onPress={() => this.navigate('CervixEditView')}
|
||||
data={this.getLabel('cervix')}
|
||||
disabled={dateInFuture}
|
||||
@@ -220,7 +213,7 @@ export default class CycleDayOverView extends Component {
|
||||
>
|
||||
</SymptomBox>
|
||||
<SymptomBox
|
||||
title='Desire'
|
||||
title={symptomTitles.desire}
|
||||
onPress={() => this.navigate('DesireEditView')}
|
||||
data={this.getLabel('desire')}
|
||||
disabled={dateInFuture}
|
||||
@@ -228,7 +221,7 @@ export default class CycleDayOverView extends Component {
|
||||
>
|
||||
</SymptomBox>
|
||||
<SymptomBox
|
||||
title='Sex'
|
||||
title={symptomTitles.sex}
|
||||
onPress={() => this.navigate('SexEditView')}
|
||||
data={this.getLabel('sex')}
|
||||
disabled={dateInFuture}
|
||||
@@ -236,7 +229,7 @@ export default class CycleDayOverView extends Component {
|
||||
>
|
||||
</SymptomBox>
|
||||
<SymptomBox
|
||||
title='Pain'
|
||||
title={symptomTitles.pain}
|
||||
onPress={() => this.navigate('PainEditView')}
|
||||
data={this.getLabel('pain')}
|
||||
disabled={dateInFuture}
|
||||
@@ -244,7 +237,7 @@ export default class CycleDayOverView extends Component {
|
||||
>
|
||||
</SymptomBox>
|
||||
<SymptomBox
|
||||
title='Mood'
|
||||
title={symptomTitles.mood}
|
||||
onPress={() => this.navigate('MoodEditView')}
|
||||
data={this.getLabel('mood')}
|
||||
disabled={dateInFuture}
|
||||
@@ -252,7 +245,7 @@ export default class CycleDayOverView extends Component {
|
||||
>
|
||||
</SymptomBox>
|
||||
<SymptomBox
|
||||
title='Note'
|
||||
title={symptomTitles.note}
|
||||
onPress={() => this.navigate('NoteEditView')}
|
||||
data={this.getLabel('note')}
|
||||
iconName='drip-icon-note'
|
||||
@@ -285,7 +278,10 @@ class SymptomBox extends Component {
|
||||
>
|
||||
<View style={[styles.symptomBox, boxActive, disabledStyle]}>
|
||||
<DripIcon name={this.props.iconName} size={50} color={hasData ? 'white' : 'black'}/>
|
||||
<AppText style={[textActive, disabledStyle]}>
|
||||
<AppText
|
||||
style={[textActive, disabledStyle, {fontSize: 15}]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{this.props.title.toLowerCase()}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class SelectTabGroup extends Component {
|
||||
key={i}
|
||||
activeOpacity={1}
|
||||
>
|
||||
<View style={styles.radioButtonTextGroup}>
|
||||
<View>
|
||||
<View style={[
|
||||
styles.selectTab,
|
||||
firstOrLastStyle,
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import {
|
||||
View, TouchableOpacity, Text, Alert, ToastAndroid
|
||||
} from 'react-native'
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import { saveSymptom } from '../../../db'
|
||||
import styles, {iconStyles} from '../../../styles'
|
||||
import {sharedDialogs as labels} from '../../../i18n/en/cycle-day'
|
||||
|
||||
|
||||
export default class ActionButtonFooter extends Component {
|
||||
render() {
|
||||
const {
|
||||
symptom,
|
||||
currentSymptomValue,
|
||||
date,
|
||||
saveAction,
|
||||
saveDisabled,
|
||||
navigate,
|
||||
autoShowDayView = true}
|
||||
= this.props
|
||||
const navigateToOverView = () => navigate('CycleDay', {date})
|
||||
const buttons = [
|
||||
{
|
||||
title: labels.delete,
|
||||
action: () => {
|
||||
Alert.alert(
|
||||
labels.areYouSureTitle,
|
||||
labels.areYouSureToDelete,
|
||||
[{
|
||||
text: labels.cancel,
|
||||
style: 'cancel'
|
||||
}, {
|
||||
text: labels.reallyDeleteData,
|
||||
onPress: () => {
|
||||
saveSymptom(symptom, date)
|
||||
navigateToOverView()
|
||||
}
|
||||
}]
|
||||
)
|
||||
},
|
||||
disabledCondition: !currentSymptomValue,
|
||||
icon: 'delete-outline'
|
||||
}, {
|
||||
title: labels.save,
|
||||
action: () => {
|
||||
if(saveDisabled) {
|
||||
ToastAndroid.show(labels.disabledInfo, ToastAndroid.LONG)
|
||||
} else {
|
||||
saveAction()
|
||||
if (autoShowDayView) navigateToOverView()
|
||||
}
|
||||
|
||||
},
|
||||
disabledCondition: saveDisabled,
|
||||
icon: 'content-save-outline'
|
||||
}
|
||||
]
|
||||
return (
|
||||
<View style={styles.menu}>
|
||||
{buttons.map(({ title, action, disabledCondition, icon }, i) => {
|
||||
const textStyle = [styles.menuText]
|
||||
if (disabledCondition) {
|
||||
textStyle.push(styles.menuTextInActive)
|
||||
}
|
||||
const iconStyle = disabledCondition ?
|
||||
Object.assign(
|
||||
{},
|
||||
iconStyles.menuIcon,
|
||||
iconStyles.menuIconInactive
|
||||
)
|
||||
:
|
||||
iconStyles.menuIcon
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={action}
|
||||
style={styles.menuItem}
|
||||
key={i.toString()}
|
||||
>
|
||||
<Icon name={icon} {...iconStyle} />
|
||||
<Text style={textStyle}>
|
||||
{title.toLowerCase()}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,39 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
View,
|
||||
Switch,
|
||||
ScrollView
|
||||
} from 'react-native'
|
||||
import styles from '../../../styles'
|
||||
import { saveSymptom } from '../../../db'
|
||||
import { bleeding } from '../../../i18n/en/cycle-day'
|
||||
import ActionButtonFooter from './action-button-footer'
|
||||
import SelectTabGroup from '../select-tab-group'
|
||||
import SymptomSection from './symptom-section'
|
||||
import SymptomView from './symptom-view'
|
||||
|
||||
export default class Bleeding extends Component {
|
||||
export default class Bleeding extends SymptomView {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const cycleDay = props.cycleDay
|
||||
this.bleeding = cycleDay && cycleDay.bleeding
|
||||
this.makeActionButtons = props.makeActionButtons
|
||||
this.state = {
|
||||
currentValue: this.bleeding && this.bleeding.value,
|
||||
exclude: this.bleeding ? this.bleeding.exclude : false
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
symptomName = 'bleeding'
|
||||
|
||||
autoSave = () => {
|
||||
if (typeof this.state.currentValue != 'number') {
|
||||
this.deleteSymptomEntry()
|
||||
return
|
||||
}
|
||||
this.saveSymptomEntry({
|
||||
value: this.state.currentValue,
|
||||
exclude: this.state.exclude
|
||||
})
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const bleedingRadioProps = [
|
||||
{ label: bleeding.labels[0], value: 0 },
|
||||
{ label: bleeding.labels[1], value: 1 },
|
||||
@@ -31,7 +41,6 @@ export default class Bleeding extends Component {
|
||||
{ label: bleeding.labels[3], value: 3 },
|
||||
]
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header={bleeding.heaviness.header}
|
||||
@@ -56,20 +65,6 @@ export default class Bleeding extends Component {
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
<ActionButtonFooter
|
||||
symptom='bleeding'
|
||||
date={this.props.date}
|
||||
currentSymptomValue={this.bleeding}
|
||||
saveAction={() => {
|
||||
saveSymptom('bleeding', this.props.date, {
|
||||
value: this.state.currentValue,
|
||||
exclude: this.state.exclude
|
||||
})
|
||||
}}
|
||||
saveDisabled={typeof this.state.currentValue != 'number'}
|
||||
navigate={this.props.navigate}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,40 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
View,
|
||||
Switch,
|
||||
ScrollView
|
||||
} from 'react-native'
|
||||
import styles from '../../../styles'
|
||||
import { saveSymptom } from '../../../db'
|
||||
import { cervix as labels } from '../../../i18n/en/cycle-day'
|
||||
import ActionButtonFooter from './action-button-footer'
|
||||
import SelectTabGroup from '../select-tab-group'
|
||||
import SymptomSection from './symptom-section'
|
||||
import { ActionHint } from '../../app-text'
|
||||
import SymptomView from './symptom-view'
|
||||
|
||||
export default class Cervix extends Component {
|
||||
export default class Cervix extends SymptomView {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const cycleDay = props.cycleDay
|
||||
this.cervix = cycleDay && cycleDay.cervix
|
||||
this.makeActionButtons = props.makeActionButtons
|
||||
this.state = this.cervix ? this.cervix : {}
|
||||
}
|
||||
|
||||
render() {
|
||||
symptomName = 'cervix'
|
||||
|
||||
autoSave = () => {
|
||||
const nothingEntered = ['opening', 'firmness', 'position'].every(val => typeof this.state[val] != 'number')
|
||||
if (nothingEntered) {
|
||||
this.deleteSymptomEntry()
|
||||
return
|
||||
}
|
||||
|
||||
this.saveSymptomEntry({
|
||||
opening: this.state.opening,
|
||||
firmness: this.state.firmness,
|
||||
position: this.state.position,
|
||||
exclude: Boolean(this.state.exclude)
|
||||
})
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const cervixOpeningRadioProps = [
|
||||
{ label: labels.opening.categories[0], value: 0 },
|
||||
{ label: labels.opening.categories[1], value: 1 },
|
||||
@@ -36,9 +49,9 @@ export default class Cervix extends Component {
|
||||
{ label: labels.position.categories[1], value: 1 },
|
||||
{ label: labels.position.categories[2], value: 2 }
|
||||
]
|
||||
const mandatoryNotCompletedYet = typeof this.state.opening != 'number' || typeof this.state.firmness != 'number'
|
||||
// TODO saving this info for notice when leaving incomplete data
|
||||
// const mandatoryNotCompleted = typeof this.state.opening != 'number' || typeof this.state.firmness != 'number'
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header="Opening"
|
||||
@@ -83,23 +96,6 @@ export default class Cervix extends Component {
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
<ActionHint isVisible={mandatoryNotCompletedYet}>{labels.actionHint}</ActionHint>
|
||||
<ActionButtonFooter
|
||||
symptom='cervix'
|
||||
date={this.props.date}
|
||||
currentSymptomValue={this.cervix}
|
||||
saveAction={() => {
|
||||
saveSymptom('cervix', this.props.date, {
|
||||
opening: this.state.opening,
|
||||
firmness: this.state.firmness,
|
||||
position: this.state.position,
|
||||
exclude: Boolean(this.state.exclude)
|
||||
})
|
||||
}}
|
||||
saveDisabled={mandatoryNotCompletedYet}
|
||||
navigate={this.props.navigate}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
View,
|
||||
ScrollView
|
||||
} from 'react-native'
|
||||
import styles from '../../../styles'
|
||||
import { saveSymptom } from '../../../db'
|
||||
import { intensity, desire } from '../../../i18n/en/cycle-day'
|
||||
import ActionButtonFooter from './action-button-footer'
|
||||
import SelectTabGroup from '../select-tab-group'
|
||||
import SymptomSection from './symptom-section'
|
||||
import SymptomView from './symptom-view'
|
||||
|
||||
export default class Desire extends Component {
|
||||
export default class Desire extends SymptomView {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const cycleDay = props.cycleDay
|
||||
this.desire = cycleDay && cycleDay.desire
|
||||
this.makeActionButtons = props.makeActionButtons
|
||||
const desireValue = this.desire && this.desire.value
|
||||
this.state = { currentValue: desireValue }
|
||||
}
|
||||
|
||||
render() {
|
||||
symptomName = 'desire'
|
||||
|
||||
autoSave = () => {
|
||||
if (typeof this.state.currentValue != 'number') {
|
||||
this.deleteSymptomEntry()
|
||||
return
|
||||
}
|
||||
this.saveSymptomEntry({ value: this.state.currentValue })
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const desireRadioProps = [
|
||||
{ label: intensity[0], value: 0 },
|
||||
{ label: intensity[1], value: 1 },
|
||||
{ label: intensity[2], value: 2 }
|
||||
]
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header={desire.header}
|
||||
@@ -40,17 +46,6 @@ export default class Desire extends Component {
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
<ActionButtonFooter
|
||||
symptom='desire'
|
||||
date={this.props.date}
|
||||
currentSymptomValue={this.desire}
|
||||
saveAction={() => {
|
||||
saveSymptom('desire', this.props.date, { value: this.state.currentValue })
|
||||
}}
|
||||
saveDisabled={typeof this.state.currentValue != 'number'}
|
||||
navigate={this.props.navigate}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,22 @@
|
||||
import React, { Component } from 'react'
|
||||
import { ScrollView } from 'react-native'
|
||||
import React from 'react'
|
||||
import { ScrollView, View, TouchableOpacity } from 'react-native'
|
||||
import Icon from 'react-native-vector-icons/SimpleLineIcons'
|
||||
import AppText from '../../app-text'
|
||||
import labels from '../../../i18n/en/symptom-info.js'
|
||||
import FramedSegment from '../../framed-segment'
|
||||
import styles from '../../../styles/index'
|
||||
|
||||
export default class InfoSymptom extends Component {
|
||||
render() {
|
||||
const symptomView = this.props.symptomView
|
||||
const symptomMapping = {
|
||||
BleedingEditView: 'bleeding',
|
||||
CervixEditView: 'cervix',
|
||||
DesireEditView: 'desire',
|
||||
MoodEditView: 'mood',
|
||||
MucusEditView: 'mucus',
|
||||
NoteEditView: 'note',
|
||||
PainEditView: 'pain',
|
||||
SexEditView: 'sex',
|
||||
TemperatureEditView: 'temperature'
|
||||
}
|
||||
const currentSymptom = symptomMapping[symptomView]
|
||||
import styles, {iconStyles} from '../../../styles/index'
|
||||
|
||||
export default function InfoSymptom(props) {
|
||||
return (
|
||||
<ScrollView>
|
||||
<FramedSegment
|
||||
style={styles.framedSegmentLast}
|
||||
title={labels[currentSymptom].title}
|
||||
>
|
||||
<AppText>{labels[currentSymptom].text}</AppText>
|
||||
</FramedSegment>
|
||||
<View style={styles.infoPopUpWrapper}>
|
||||
<View style={styles.dimmed}></View>
|
||||
<View style={styles.infoPopUp}>
|
||||
<TouchableOpacity onPress={props.close} style={styles.infoSymptomClose}>
|
||||
<Icon name='close' {...iconStyles.infoPopUpClose}/>
|
||||
</TouchableOpacity>
|
||||
<ScrollView style={styles.infoSymptomText}>
|
||||
<AppText>{labels[props.symptom].text}</AppText>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
ScrollView,
|
||||
TextInput,
|
||||
View
|
||||
} from 'react-native'
|
||||
import { saveSymptom } from '../../../db'
|
||||
TextInput} from 'react-native'
|
||||
import { mood as labels } from '../../../i18n/en/cycle-day'
|
||||
import ActionButtonFooter from './action-button-footer'
|
||||
import SelectBoxGroup from '../select-box-group'
|
||||
import SymptomSection from './symptom-section'
|
||||
import styles from '../../../styles'
|
||||
import SymptomView from './symptom-view'
|
||||
|
||||
export default class Mood extends Component {
|
||||
export default class Mood extends SymptomView {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const cycleDay = props.cycleDay
|
||||
@@ -25,6 +22,21 @@ export default class Mood extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
symptomName = "mood"
|
||||
|
||||
autoSave = () => {
|
||||
const nothingEntered = Object.values(this.state).every(val => !val)
|
||||
if (nothingEntered) {
|
||||
this.deleteSymptomEntry()
|
||||
return
|
||||
}
|
||||
const copyOfState = Object.assign({}, this.state)
|
||||
if (!copyOfState.other) {
|
||||
copyOfState.note = null
|
||||
}
|
||||
this.saveSymptomEntry(copyOfState)
|
||||
}
|
||||
|
||||
toggleState = (key) => {
|
||||
const curr = this.state[key]
|
||||
this.setState({[key]: !curr})
|
||||
@@ -33,9 +45,8 @@ export default class Mood extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
renderContent() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
explainer={labels.explainer}
|
||||
@@ -58,21 +69,6 @@ export default class Mood extends Component {
|
||||
}
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
<ActionButtonFooter
|
||||
symptom='mood'
|
||||
date={this.props.date}
|
||||
currentSymptomValue={this.state}
|
||||
saveAction={() => {
|
||||
const copyOfState = Object.assign({}, this.state)
|
||||
if (!copyOfState.other) {
|
||||
copyOfState.note = null
|
||||
}
|
||||
saveSymptom('mood', this.props.date, copyOfState)
|
||||
}}
|
||||
saveDisabled={Object.values(this.state).every(value => !value)}
|
||||
navigate={this.props.navigate}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
View,
|
||||
Switch,
|
||||
ScrollView
|
||||
} from 'react-native'
|
||||
import styles from '../../../styles'
|
||||
import { saveSymptom } from '../../../db'
|
||||
import { mucus as labels } from '../../../i18n/en/cycle-day'
|
||||
import computeNfpValue from '../../../lib/nfp-mucus'
|
||||
import ActionButtonFooter from './action-button-footer'
|
||||
import SelectTabGroup from '../select-tab-group'
|
||||
import SymptomSection from './symptom-section'
|
||||
import { ActionHint } from '../../app-text'
|
||||
import SymptomView from './symptom-view'
|
||||
|
||||
export default class Mucus extends Component {
|
||||
export default class Mucus extends SymptomView {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const cycleDay = props.cycleDay
|
||||
this.mucus = cycleDay && cycleDay.mucus
|
||||
this.makeActionButtons = props.makeActionButtons
|
||||
this.state = this.mucus ? this.mucus : {}
|
||||
}
|
||||
|
||||
render() {
|
||||
symptomName = 'mucus'
|
||||
|
||||
autoSave = () => {
|
||||
const nothingEntered = ['feeling', 'texture'].every(val => typeof this.state[val] != 'number')
|
||||
if (nothingEntered) {
|
||||
this.deleteSymptomEntry()
|
||||
return
|
||||
}
|
||||
|
||||
const feeling = this.state.feeling
|
||||
const texture = this.state.texture
|
||||
this.saveSymptomEntry({
|
||||
feeling,
|
||||
texture,
|
||||
value: computeNfpValue(feeling, texture),
|
||||
exclude: Boolean(this.state.exclude)
|
||||
})
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const mucusFeeling = [
|
||||
{ label: labels.feeling.categories[0], value: 0 },
|
||||
{ label: labels.feeling.categories[1], value: 1 },
|
||||
@@ -34,9 +49,9 @@ export default class Mucus extends Component {
|
||||
{ label: labels.texture.categories[1], value: 1 },
|
||||
{ label: labels.texture.categories[2], value: 2 }
|
||||
]
|
||||
const mandatoryNotCompletedYet = typeof this.state.feeling != 'number' || typeof this.state.texture != 'number'
|
||||
// TODO leaving this info for notice when leaving incomplete data
|
||||
// const mandatoryNotCompletedYet = typeof this.state.feeling != 'number' || typeof this.state.texture != 'number'
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header='Feeling'
|
||||
@@ -71,25 +86,6 @@ export default class Mucus extends Component {
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
<ActionHint isVisible={mandatoryNotCompletedYet}>{labels.actionHint}</ActionHint>
|
||||
<ActionButtonFooter
|
||||
symptom='mucus'
|
||||
date={this.props.date}
|
||||
currentSymptomValue={this.mucus}
|
||||
saveAction={() => {
|
||||
const feeling = this.state.feeling
|
||||
const texture = this.state.texture
|
||||
saveSymptom('mucus', this.props.date, {
|
||||
feeling,
|
||||
texture,
|
||||
value: computeNfpValue(feeling, texture),
|
||||
exclude: Boolean(this.state.exclude)
|
||||
})
|
||||
}}
|
||||
saveDisabled={mandatoryNotCompletedYet}
|
||||
navigate={this.props.navigate}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
View,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
} from 'react-native'
|
||||
|
||||
import styles from '../../../styles'
|
||||
import { saveSymptom } from '../../../db'
|
||||
import ActionButtonFooter from './action-button-footer'
|
||||
import SymptomSection from './symptom-section'
|
||||
import { noteExplainer } from '../../../i18n/en/cycle-day'
|
||||
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
||||
import SymptomView from './symptom-view'
|
||||
|
||||
export default class Note extends Component {
|
||||
export default class Note extends SymptomView {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const cycleDay = props.cycleDay
|
||||
this.note = cycleDay && cycleDay.note
|
||||
this.makeActionButtons = props.makeActionButtons
|
||||
|
||||
this.state = {
|
||||
currentValue: this.note && this.note.value || ''
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
symptomName = 'note'
|
||||
|
||||
autoSave = () => {
|
||||
if (!this.state.currentValue) {
|
||||
this.deleteSymptomEntry()
|
||||
return
|
||||
}
|
||||
this.saveSymptomEntry({
|
||||
value: this.state.currentValue
|
||||
})
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
explainer={noteExplainer}
|
||||
@@ -42,19 +50,6 @@ export default class Note extends Component {
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
<ActionButtonFooter
|
||||
symptom='note'
|
||||
date={this.props.date}
|
||||
currentSymptomValue={this.note}
|
||||
saveAction={() => {
|
||||
saveSymptom('note', this.props.date, {
|
||||
value: this.state.currentValue
|
||||
})
|
||||
}}
|
||||
saveDisabled={!this.state.currentValue}
|
||||
navigate={this.props.navigate}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
ScrollView,
|
||||
TextInput,
|
||||
View
|
||||
} from 'react-native'
|
||||
import { saveSymptom } from '../../../db'
|
||||
import { pain as labels } from '../../../i18n/en/cycle-day'
|
||||
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
||||
import ActionButtonFooter from './action-button-footer'
|
||||
import SelectBoxGroup from '../select-box-group'
|
||||
import SymptomSection from './symptom-section'
|
||||
import styles from '../../../styles'
|
||||
import SymptomView from './symptom-view'
|
||||
|
||||
export default class Pain extends Component {
|
||||
export default class Pain extends SymptomView {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const cycleDay = props.cycleDay
|
||||
@@ -26,6 +24,22 @@ export default class Pain extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
symptomName = 'pain'
|
||||
|
||||
autoSave = () => {
|
||||
const nothingEntered = Object.values(this.state).every(val => !val)
|
||||
if (nothingEntered) {
|
||||
this.deleteSymptomEntry()
|
||||
return
|
||||
}
|
||||
|
||||
const copyOfState = Object.assign({}, this.state)
|
||||
if (!copyOfState.other) {
|
||||
copyOfState.note = null
|
||||
}
|
||||
this.saveSymptomEntry(copyOfState)
|
||||
}
|
||||
|
||||
toggleState = (key) => {
|
||||
const curr = this.state[key]
|
||||
this.setState({[key]: !curr})
|
||||
@@ -34,9 +48,8 @@ export default class Pain extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
renderContent() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
explainer={labels.explainer}
|
||||
@@ -58,22 +71,6 @@ export default class Pain extends Component {
|
||||
/>
|
||||
}
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
<ActionButtonFooter
|
||||
symptom='pain'
|
||||
date={this.props.date}
|
||||
currentSymptomValue={this.state}
|
||||
saveAction={() => {
|
||||
const copyOfState = Object.assign({}, this.state)
|
||||
if (!copyOfState.other) {
|
||||
copyOfState.note = null
|
||||
}
|
||||
saveSymptom('pain', this.props.date, copyOfState)
|
||||
}}
|
||||
saveDisabled={Object.values(this.state).every(value => !value)}
|
||||
navigate={this.props.navigate}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
</ScrollView>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
TextInput,
|
||||
View,
|
||||
ScrollView
|
||||
} from 'react-native'
|
||||
import styles from '../../../styles'
|
||||
import { saveSymptom } from '../../../db'
|
||||
import { sex as sexLabels, contraceptives as contraceptivesLabels } from '../../../i18n/en/cycle-day'
|
||||
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
||||
import ActionButtonFooter from './action-button-footer'
|
||||
import SelectBoxGroup from '../select-box-group'
|
||||
import SymptomSection from './symptom-section'
|
||||
import SymptomView from './symptom-view'
|
||||
|
||||
export default class Sex extends Component {
|
||||
export default class Sex extends SymptomView {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const cycleDay = props.cycleDay
|
||||
@@ -26,6 +24,22 @@ export default class Sex extends Component {
|
||||
if (this.state.note) this.state.other = true
|
||||
}
|
||||
|
||||
symptomName = "sex"
|
||||
|
||||
autoSave = () => {
|
||||
const nothingEntered = Object.values(this.state).every(val => !val)
|
||||
if (nothingEntered) {
|
||||
this.deleteSymptomEntry()
|
||||
return
|
||||
}
|
||||
|
||||
const copyOfState = Object.assign({}, this.state)
|
||||
if (!copyOfState.other) {
|
||||
copyOfState.note = null
|
||||
}
|
||||
this.saveSymptomEntry(copyOfState)
|
||||
}
|
||||
|
||||
toggleState = (key) => {
|
||||
const curr = this.state[key]
|
||||
this.setState({[key]: !curr})
|
||||
@@ -34,9 +48,8 @@ export default class Sex extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
renderContent() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header={sexLabels.header}
|
||||
@@ -71,21 +84,6 @@ export default class Sex extends Component {
|
||||
/>
|
||||
}
|
||||
</ScrollView>
|
||||
<ActionButtonFooter
|
||||
symptom='sex'
|
||||
date={this.props.date}
|
||||
currentSymptomValue={this.state}
|
||||
saveAction={() => {
|
||||
const copyOfState = Object.assign({}, this.state)
|
||||
if (!copyOfState.other) {
|
||||
copyOfState.note = null
|
||||
}
|
||||
saveSymptom('sex', this.props.date, copyOfState)
|
||||
}}
|
||||
saveDisabled={Object.values(this.state).every(value => !value)}
|
||||
navigate={this.props.navigate}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import AppText, { SymptomSectionHeader } from '../../app-text'
|
||||
import styles from '../../../styles'
|
||||
|
||||
export default class SymptomSection extends Component {
|
||||
render() {
|
||||
@@ -13,16 +14,20 @@ export default class SymptomSection extends Component {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View style={placeHeadingInline}>
|
||||
<View style={[placeHeadingInline, styles.symptomSection]}>
|
||||
{ p.header &&
|
||||
<SymptomSectionHeader flex={1}>{p.header}</SymptomSectionHeader>
|
||||
}
|
||||
<View
|
||||
flexDirection={p.inline ? 'row' : null}
|
||||
flex={1}
|
||||
alignItems={p.inline ? 'center' : null}
|
||||
>
|
||||
{ p.explainer && (
|
||||
<View flex={1}>
|
||||
<AppText>{p.explainer}</AppText>
|
||||
</View>
|
||||
)}
|
||||
{p.children}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { Component } from 'react'
|
||||
import {
|
||||
View, Alert, TouchableOpacity
|
||||
} from 'react-native'
|
||||
import { saveSymptom } from '../../../db'
|
||||
import InfoPopUp from './info-symptom'
|
||||
import Header from '../../header/symptom-view'
|
||||
import { headerTitles } from '../../../i18n/en/labels'
|
||||
import { sharedDialogs } from '../../../i18n/en/cycle-day'
|
||||
import Icon from 'react-native-vector-icons/Entypo'
|
||||
import styles, { iconStyles } from '../../../styles'
|
||||
|
||||
export default class SymptomView extends Component {
|
||||
constructor(props) {
|
||||
super()
|
||||
this.date = props.date
|
||||
this.navigate = props.navigate
|
||||
this.state = {
|
||||
showInfo: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.autoSave()
|
||||
}
|
||||
|
||||
saveSymptomEntry(entry) {
|
||||
saveSymptom(this.symptomName, this.date, entry)
|
||||
}
|
||||
|
||||
deleteSymptomEntry() {
|
||||
saveSymptom(this.symptomName, this.date)
|
||||
}
|
||||
|
||||
isDeleteIconActive() {
|
||||
const symptomValueHasBeenFilledOut = key => {
|
||||
// the state tracks whether the symptom info should be shown,
|
||||
// we ignore that property
|
||||
if (key === 'showInfo') return
|
||||
// is there any meaningful value in the current state?
|
||||
return this.state[key] || this.state[key] === 0
|
||||
}
|
||||
|
||||
const symptomValues = Object.keys(this.state)
|
||||
|
||||
return symptomValues.some(symptomValueHasBeenFilledOut)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<Header
|
||||
title={headerTitles[this.symptomName].toLowerCase()}
|
||||
date={this.date}
|
||||
goBack={this.props.handleBackButtonPress}
|
||||
deleteIconActive={this.isDeleteIconActive()}
|
||||
deleteEntry={() => {
|
||||
Alert.alert(
|
||||
sharedDialogs.areYouSureTitle,
|
||||
sharedDialogs.areYouSureToDelete,
|
||||
[{
|
||||
text: sharedDialogs.cancel,
|
||||
style: 'cancel'
|
||||
}, {
|
||||
text: sharedDialogs.reallyDeleteData,
|
||||
onPress: () => {
|
||||
this.deleteSymptomEntry()
|
||||
this.props.handleBackButtonPress()
|
||||
}
|
||||
}]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<View flex={1}>
|
||||
{ this.renderContent() }
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
this.setState({showInfo: true})
|
||||
}}
|
||||
style={styles.infoButtonSymptomView}
|
||||
>
|
||||
<Icon
|
||||
name="info-with-circle"
|
||||
style={iconStyles.info}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{ this.state.showInfo &&
|
||||
<InfoPopUp
|
||||
symptom={this.symptomName}
|
||||
close={() => this.setState({showInfo: false})}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,32 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
View,
|
||||
TextInput,
|
||||
Switch,
|
||||
Keyboard,
|
||||
Alert,
|
||||
ScrollView
|
||||
} from 'react-native'
|
||||
import DateTimePicker from 'react-native-modal-datetime-picker-nevo'
|
||||
import padWithZeros from '../../helpers/pad-time-with-zeros'
|
||||
|
||||
import { getPreviousTemperature, saveSymptom } from '../../../db'
|
||||
import { getPreviousTemperature } from '../../../db'
|
||||
import styles from '../../../styles'
|
||||
import { LocalTime, ChronoUnit } from 'js-joda'
|
||||
import { temperature as labels } from '../../../i18n/en/cycle-day'
|
||||
import { scaleObservable } from '../../../local-storage'
|
||||
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
||||
import ActionButtonFooter from './action-button-footer'
|
||||
import config from '../../../config'
|
||||
import AppTextInput from '../../app-text-input'
|
||||
import AppText from '../../app-text'
|
||||
import SymptomSection from './symptom-section'
|
||||
import SymptomView from './symptom-view'
|
||||
|
||||
const minutes = ChronoUnit.MINUTES
|
||||
|
||||
export default class Temp extends Component {
|
||||
export default class Temp extends SymptomView {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const cycleDay = props.cycleDay
|
||||
this.temperature = cycleDay && cycleDay.temperature
|
||||
this.makeActionButtons = props.makeActionButtons
|
||||
|
||||
const temp = this.temperature
|
||||
|
||||
@@ -35,7 +34,6 @@ export default class Temp extends Component {
|
||||
exclude: temp ? temp.exclude : false,
|
||||
time: temp ? temp.time : LocalTime.now().truncatedTo(minutes).toString(),
|
||||
isTimePickerVisible: false,
|
||||
outOfRange: null,
|
||||
note: temp ? temp.note : null
|
||||
}
|
||||
|
||||
@@ -44,83 +42,95 @@ export default class Temp extends Component {
|
||||
if (temp.value === Math.floor(temp.value)) {
|
||||
this.state.temperature = `${this.state.temperature}.0`
|
||||
}
|
||||
this.state.outOfRangeWarning = makeOutOfRangeWarningMessage(this.state.temperature)
|
||||
} else {
|
||||
const prevTemp = getPreviousTemperature(this.props.date)
|
||||
const prevTemp = getPreviousTemperature(props.date)
|
||||
if (prevTemp) {
|
||||
this.state.temperature = prevTemp.toString()
|
||||
this.state.suggestedTemperature = prevTemp.toString()
|
||||
this.state.isSuggestion = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveTemperature = () => {
|
||||
symptomName = 'temperature'
|
||||
|
||||
isDeleteIconActive() {
|
||||
return ['temperature', 'note', 'exclude'].some(key => {
|
||||
// the time is always and the suggested temp sometimes prefilled, so they're not relevant for setting
|
||||
// the delete button active.
|
||||
return this.state[key] || this.state[key] === 0
|
||||
})
|
||||
}
|
||||
|
||||
autoSave = () => {
|
||||
if (typeof this.state.temperature != 'string' || this.state.temperature === '') {
|
||||
this.deleteSymptomEntry()
|
||||
return
|
||||
}
|
||||
|
||||
const dataToSave = {
|
||||
value: Number(this.state.temperature),
|
||||
exclude: this.state.exclude,
|
||||
time: this.state.time,
|
||||
note: this.state.note
|
||||
}
|
||||
saveSymptom('temperature', this.props.date, dataToSave)
|
||||
this.props.navigate('CycleDay', {date: this.props.date})
|
||||
}
|
||||
|
||||
checkRangeAndSave = () => {
|
||||
const value = Number(this.state.temperature)
|
||||
|
||||
const absolute = {
|
||||
min: config.temperatureScale.min,
|
||||
max: config.temperatureScale.max
|
||||
}
|
||||
const scale = scaleObservable.value
|
||||
let warningMsg
|
||||
if (value < absolute.min || value > absolute.max) {
|
||||
warningMsg = labels.outOfAbsoluteRangeWarning
|
||||
} else if (value < scale.min || value > scale.max) {
|
||||
warningMsg = labels.outOfRangeWarning
|
||||
}
|
||||
|
||||
if (warningMsg) {
|
||||
Alert.alert(
|
||||
sharedLabels.warning,
|
||||
warningMsg,
|
||||
[
|
||||
{ text: sharedLabels.cancel },
|
||||
{ text: sharedLabels.save, onPress: this.saveTemperature}
|
||||
]
|
||||
)
|
||||
} else {
|
||||
this.saveTemperature()
|
||||
}
|
||||
|
||||
this.saveSymptomEntry(dataToSave)
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
setTemperature = (temperature) => {
|
||||
if (isNaN(Number(temperature))) return
|
||||
this.setState({
|
||||
temperature, isSuggestion: false,
|
||||
outOfRangeWarning: makeOutOfRangeWarningMessage(temperature)
|
||||
})
|
||||
}
|
||||
|
||||
setNote = (note) => {
|
||||
this.setState({ note })
|
||||
}
|
||||
|
||||
showTimePicker = () => {
|
||||
Keyboard.dismiss()
|
||||
this.setState({ isTimePickerVisible: true })
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const inputStyle = [styles.temperatureTextInput]
|
||||
if (this.state.isSuggestion) {
|
||||
inputStyle.push(styles.temperatureTextInputSuggestion)
|
||||
}
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<View>
|
||||
<SymptomSection
|
||||
header={labels.temperature.header}
|
||||
explainer={labels.temperature.explainer}
|
||||
inline={true}
|
||||
>
|
||||
<TempInput
|
||||
value={this.state.temperature}
|
||||
setState={(val) => this.setState(val)}
|
||||
isSuggestion={this.state.isSuggestion}
|
||||
<View style={styles.framedSegmentInlineChildren}>
|
||||
<AppTextInput
|
||||
style={[inputStyle]}
|
||||
autoFocus={true}
|
||||
value={this.state.temperature || this.state.suggestedTemperature}
|
||||
onChangeText={this.setTemperature}
|
||||
keyboardType='numeric'
|
||||
maxLength={5}
|
||||
/>
|
||||
<AppText style={{ marginLeft: 5 }}>°C</AppText>
|
||||
</View>
|
||||
{this.state.outOfRangeWarning &&
|
||||
<AppText style={styles.hint}>
|
||||
{this.state.outOfRangeWarning}
|
||||
</AppText>
|
||||
}
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={labels.time}
|
||||
inline={true}
|
||||
>
|
||||
<TextInput
|
||||
style={styles.temperatureTextInput}
|
||||
onFocus={() => {
|
||||
Keyboard.dismiss()
|
||||
this.setState({ isTimePickerVisible: true })
|
||||
}}
|
||||
<View style={styles.framedSegmentInlineChildren}>
|
||||
<AppTextInput
|
||||
style={[styles.temperatureTextInput]}
|
||||
onFocus={this.showTimePicker}
|
||||
value={this.state.time}
|
||||
/>
|
||||
<DateTimePicker
|
||||
@@ -134,19 +144,18 @@ export default class Temp extends Component {
|
||||
}}
|
||||
onCancel={() => this.setState({ isTimePickerVisible: false })}
|
||||
/>
|
||||
</View>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={labels.note.header}
|
||||
explainer={labels.note.explainer}
|
||||
>
|
||||
<TextInput
|
||||
<AppTextInput
|
||||
multiline={true}
|
||||
autoFocus={this.state.focusTextArea}
|
||||
placeholder={sharedLabels.enter}
|
||||
value={this.state.note}
|
||||
onChangeText={(val) => {
|
||||
this.setState({ note: val })
|
||||
}}
|
||||
onChangeText={this.setNote}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
@@ -161,53 +170,26 @@ export default class Temp extends Component {
|
||||
value={this.state.exclude}
|
||||
/>
|
||||
</SymptomSection>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<ActionButtonFooter
|
||||
symptom='temperature'
|
||||
date={this.props.date}
|
||||
currentSymptomValue={this.temperature}
|
||||
saveAction={() => this.checkRangeAndSave()}
|
||||
saveDisabled={
|
||||
this.state.temperature === '' ||
|
||||
isNaN(Number(this.state.temperature)) ||
|
||||
isInvalidTime(this.state.time)
|
||||
}
|
||||
navigate={this.props.navigate}
|
||||
autoShowDayView={false}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class TempInput extends Component {
|
||||
render() {
|
||||
const style = [styles.temperatureTextInput]
|
||||
if (this.props.isSuggestion) {
|
||||
style.push(styles.temperatureTextInputSuggestion)
|
||||
}
|
||||
return (
|
||||
<TextInput
|
||||
style={style}
|
||||
onChangeText={(val) => {
|
||||
if (isNaN(Number(val))) return
|
||||
this.props.setState({ temperature: val, isSuggestion: false })
|
||||
}}
|
||||
keyboardType='numeric'
|
||||
value={this.props.value}
|
||||
onBlur={this.checkRange}
|
||||
autoFocus={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
function makeOutOfRangeWarningMessage(temperature) {
|
||||
if (temperature === '') return
|
||||
const value = Number(temperature)
|
||||
const { min, max } = config.temperatureScale
|
||||
const range = { min, max }
|
||||
const scale = scaleObservable.value
|
||||
let warningMsg
|
||||
|
||||
function isInvalidTime(timeString) {
|
||||
try {
|
||||
LocalTime.parse(timeString)
|
||||
} catch (err) {
|
||||
return true
|
||||
if (value < range.min || value > range.max) {
|
||||
warningMsg = labels.outOfAbsoluteRangeWarning
|
||||
} else if (value < scale.min || value > scale.max) {
|
||||
warningMsg = labels.outOfRangeWarning
|
||||
} else {
|
||||
warningMsg = null
|
||||
}
|
||||
return false
|
||||
|
||||
return warningMsg
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { View } from 'react-native'
|
||||
import AppText from './app-text'
|
||||
import styles from '../styles'
|
||||
|
||||
const FramedSegment = ({ children, ...props }) => {
|
||||
const FramedSegment = ({children, ...props}) => {
|
||||
const style = [styles.framedSegment, props.style]
|
||||
if (props.last) style.push(styles.framedSegmentLast)
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native'
|
||||
import styles, { iconStyles } from '../../styles'
|
||||
import styles from '../../styles'
|
||||
import NavigationArrow from './navigation-arrow'
|
||||
import Icon from 'react-native-vector-icons/Entypo'
|
||||
|
||||
export default function BackButtonHeader(props) {
|
||||
return (
|
||||
@@ -21,12 +19,6 @@ export default function BackButtonHeader(props) {
|
||||
{props.title}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.hiddenIcon}>
|
||||
<Icon
|
||||
name={'chevron-thin-right'}
|
||||
{...iconStyles.hiddenIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Dimensions } from 'react-native'
|
||||
import CycleDayHeader from './cycle-day'
|
||||
import DefaultHeader from './default'
|
||||
import BackButtonHeader from './back-button'
|
||||
import SymptomViewHeader from './symptom-view'
|
||||
|
||||
export default function Header(p) {
|
||||
const middle = Dimensions.get('window').width / 2
|
||||
@@ -11,11 +10,7 @@ export default function Header(p) {
|
||||
|
||||
if (props.isCycleDayOverView) {
|
||||
return (<CycleDayHeader {...props} />)
|
||||
}
|
||||
else if (props.isSymptomView) {
|
||||
return (<SymptomViewHeader {...props} />)
|
||||
}
|
||||
else if (props.showBackButton) {
|
||||
} else if (props.showBackButton) {
|
||||
return (<BackButtonHeader {...props} />)
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -8,6 +8,10 @@ export default function NavigationArrow(props) {
|
||||
left: 'chevron-thin-left',
|
||||
right: 'chevron-thin-right'
|
||||
}[props.direction]
|
||||
const iconPosition = {
|
||||
left: 'navigationArrowLeft',
|
||||
right: 'navigationArrowRight'
|
||||
}[props.direction]
|
||||
let pressHandler
|
||||
if (props.goBack) {
|
||||
pressHandler = () => props.goBack()
|
||||
@@ -19,7 +23,7 @@ export default function NavigationArrow(props) {
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.navigationArrow}
|
||||
style={[styles.navigationArrow, styles[iconPosition]]}
|
||||
onPress={pressHandler}
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -2,19 +2,21 @@ import React from 'react'
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity
|
||||
TouchableOpacity,
|
||||
Dimensions
|
||||
} from 'react-native'
|
||||
import styles, { iconStyles } from '../../styles'
|
||||
import FeatherIcon from 'react-native-vector-icons/Feather'
|
||||
import Icon from 'react-native-vector-icons/AntDesign'
|
||||
import NavigationArrow from './navigation-arrow'
|
||||
import formatDate from '../helpers/format-date'
|
||||
|
||||
export default function SymptomViewHeader(props) {
|
||||
const middle = Dimensions.get('window').width / 2
|
||||
return (
|
||||
<View style={[styles.header, styles.headerCycleDay, styles.headerSymptom]}>
|
||||
<View
|
||||
style={styles.accentCircle}
|
||||
left={props.middle - styles.accentCircle.width / 2}
|
||||
left={middle - styles.accentCircle.width / 2}
|
||||
/>
|
||||
<NavigationArrow
|
||||
direction='left'
|
||||
@@ -28,16 +30,19 @@ export default function SymptomViewHeader(props) {
|
||||
{formatDate(props.date)}
|
||||
</Text>
|
||||
</View >
|
||||
{ props.deleteIconActive &&
|
||||
<TouchableOpacity
|
||||
onPress={() => props.goToSymptomInfo()}
|
||||
style={styles.infoButton}
|
||||
onPress={props.deleteEntry}
|
||||
style={[
|
||||
styles.headerDeleteButton,
|
||||
]}
|
||||
>
|
||||
<FeatherIcon
|
||||
name="info"
|
||||
style={styles.symptomInfoIcon}
|
||||
<Icon
|
||||
name="delete"
|
||||
{...iconStyles.symptomHeaderIcons}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -7,3 +7,7 @@ export default function (date) {
|
||||
const formattedDate = today.equals(dateToDisplay) ? 'today' : moment(date).format('MMMM Do YYYY')
|
||||
return formattedDate.toLowerCase()
|
||||
}
|
||||
|
||||
export function formatDateForShortText (date) {
|
||||
return moment(date.toString()).format('dddd, MMMM Do')
|
||||
}
|
||||
+54
-77
@@ -1,39 +1,20 @@
|
||||
import { ChronoUnit, LocalDate } from 'js-joda'
|
||||
import React, { Component } from 'react'
|
||||
import { ScrollView, View, TouchableHighlight, Dimensions } from 'react-native'
|
||||
import { LocalDate, ChronoUnit } from 'js-joda'
|
||||
import Icon from 'react-native-vector-icons/Entypo'
|
||||
import { secondaryColor, cycleDayColor, periodColor } from '../styles'
|
||||
import { ScrollView, View } from 'react-native'
|
||||
|
||||
import DripHomeIcon from '../assets/drip-home-icons'
|
||||
import { getCycleDay } from '../db'
|
||||
import {
|
||||
home as labels,
|
||||
bleedingPrediction as predictLabels,
|
||||
shared,
|
||||
home as labels
|
||||
} from '../i18n/en/labels'
|
||||
import links from '../i18n/en/links'
|
||||
import cycleModule from '../lib/cycle'
|
||||
import { getFertilityStatusForDay } from '../lib/sympto-adapter'
|
||||
import styles from '../styles'
|
||||
import styles, { cycleDayColor, periodColor, secondaryColor } from '../styles'
|
||||
import AppText from './app-text'
|
||||
import DripHomeIcon from '../assets/drip-home-icons'
|
||||
import Button from './button'
|
||||
|
||||
const ShowMoreToggler = ({ isShowingMore, onToggle }) => {
|
||||
const {height, width} = Dimensions.get('window')
|
||||
const leftPosition = isShowingMore ? 10 : width - 40
|
||||
const style = isShowingMore ? styles.showLess : styles.showMore
|
||||
const topPosition = height / 2 - styles.header.height - 30
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={onToggle}
|
||||
style={[style, { top: topPosition, left: leftPosition}]}
|
||||
>
|
||||
<View style={{alignItems: 'center'}}>
|
||||
<AppText>{isShowingMore ? shared.less : shared.more}</AppText>
|
||||
<Icon name='chevron-thin-down' />
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
)
|
||||
}
|
||||
import { formatDateForShortText } from './helpers/format-date'
|
||||
|
||||
const IconText = ({ children, wrapperStyles }) => {
|
||||
return (
|
||||
@@ -49,9 +30,15 @@ const HomeElement = ({ children, onPress, buttonColor, buttonLabel }) => {
|
||||
return (
|
||||
<View
|
||||
onPress={ onPress }
|
||||
style={ styles.homeIconElement }
|
||||
style={ styles.homeElement }
|
||||
>
|
||||
{ children }
|
||||
<View style={styles.homeIconAndText}>
|
||||
{children[0]}
|
||||
{children[1]}
|
||||
</View>
|
||||
|
||||
<View style={{paddingLeft: 15}}>
|
||||
{children.slice(2)}
|
||||
<Button
|
||||
style={styles.homeButton}
|
||||
onPress={ onPress }
|
||||
@@ -59,6 +46,7 @@ const HomeElement = ({ children, onPress, buttonColor, buttonLabel }) => {
|
||||
{ buttonLabel }
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -73,7 +61,6 @@ export default class Home extends Component {
|
||||
const fertilityStatus = getFertilityStatusForDay(this.todayDateString)
|
||||
|
||||
this.state = {
|
||||
isShowingMore: false,
|
||||
cycleDayNumber: this.getCycleDayNumber(this.todayDateString),
|
||||
predictionText: determinePredictionText(prediction),
|
||||
bleedingPredictionRange: getBleedingPredictionRange(prediction),
|
||||
@@ -83,15 +70,14 @@ export default class Home extends Component {
|
||||
|
||||
passTodayTo(componentName) {
|
||||
const { navigate } = this.props
|
||||
navigate(componentName, { date: LocalDate.now().toString() })
|
||||
}
|
||||
|
||||
toggleShowingMore = () => {
|
||||
this.setState({ isShowingMore: !this.state.isShowingMore })
|
||||
navigate(componentName, {
|
||||
date: this.todayDateString,
|
||||
cycleDay: getCycleDay(this.todayDateString)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isShowingMore, cycleDayNumber, phase, status } = this.state
|
||||
const { cycleDayNumber, phase, status } = this.state
|
||||
const { navigate } = this.props
|
||||
const cycleDayMoreText = cycleDayNumber ?
|
||||
labels.cycleDayKnown(cycleDayNumber) :
|
||||
@@ -112,13 +98,11 @@ export default class Home extends Component {
|
||||
<View>
|
||||
<DripHomeIcon name="circle" size={80} color={cycleDayColor}/>
|
||||
</View>
|
||||
<IconText wrapperStyles={styles.wrapperCycle}>
|
||||
<IconText wrapperStyles={styles.wrapperIcon}>
|
||||
{cycleDayNumber || labels.unknown}
|
||||
</IconText>
|
||||
|
||||
{ isShowingMore &&
|
||||
<AppText style={styles.paragraph}>{cycleDayMoreText}</AppText>
|
||||
}
|
||||
<AppText style={styles.homeDescriptionText}>{cycleDayMoreText}</AppText>
|
||||
</HomeElement>
|
||||
|
||||
<HomeElement
|
||||
@@ -126,19 +110,15 @@ export default class Home extends Component {
|
||||
buttonColor={ periodColor }
|
||||
buttonLabel={ labels.trackPeriod }
|
||||
>
|
||||
<View>
|
||||
<DripHomeIcon name="drop" size={105} color={periodColor} />
|
||||
</View>
|
||||
<DripHomeIcon name="drop" size={100} color={periodColor} />
|
||||
|
||||
<IconText wrapperStyles={styles.wrapperDrop}>
|
||||
<IconText wrapperStyles={{top: '45%', ...styles.wrapperIcon}}>
|
||||
{this.state.bleedingPredictionRange}
|
||||
</IconText>
|
||||
|
||||
{ isShowingMore &&
|
||||
<AppText style={styles.paragraph}>
|
||||
<AppText style={styles.homeDescriptionText}>
|
||||
{this.state.predictionText}
|
||||
</AppText>
|
||||
}
|
||||
</HomeElement>
|
||||
|
||||
<HomeElement
|
||||
@@ -148,51 +128,50 @@ export default class Home extends Component {
|
||||
>
|
||||
<View style={styles.homeCircle}/>
|
||||
|
||||
<IconText wrapperStyles={styles.wrapperCircle}>
|
||||
<IconText wrapperStyles={styles.wrapperIcon}>
|
||||
{ phase ? phase.toString() : labels.unknown }
|
||||
</IconText>
|
||||
|
||||
{ phase &&
|
||||
<AppText>{`${labels.phase(phase)} (${status})`}</AppText>
|
||||
}
|
||||
{ isShowingMore &&
|
||||
<View>
|
||||
<AppText styles={styles.paragraph}>
|
||||
{ `${statusText} ${links.moreAboutNfp.url}` }
|
||||
<AppText style={styles.homeDescriptionText}>
|
||||
{`${labels.phase(phase)} (${status})`}
|
||||
</AppText>
|
||||
</View>
|
||||
}
|
||||
<AppText style={styles.homeDescriptionText}>
|
||||
{ `${statusText} Visit ${links.wiki.url}.` }
|
||||
</AppText>
|
||||
</HomeElement>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<ShowMoreToggler
|
||||
isShowingMore={isShowingMore}
|
||||
onToggle={this.toggleShowingMore}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getTimes(prediction) {
|
||||
const todayDate = LocalDate.now()
|
||||
const predictedBleedingStart = LocalDate.parse(prediction[0][0])
|
||||
/* the range of predicted bleeding days can be either 3 or 5 */
|
||||
const predictedBleedingEnd = LocalDate.parse(prediction[0][ prediction[0].length - 1 ])
|
||||
const daysToEnd = todayDate.until(predictedBleedingEnd, ChronoUnit.DAYS)
|
||||
return { todayDate, predictedBleedingStart, predictedBleedingEnd, daysToEnd }
|
||||
}
|
||||
|
||||
function determinePredictionText(bleedingPrediction) {
|
||||
if (!bleedingPrediction.length) return predictLabels.noPrediction
|
||||
const todayDate = LocalDate.now()
|
||||
const bleedingStart = LocalDate.parse(bleedingPrediction[0][0])
|
||||
const bleedingEnd = LocalDate.parse(
|
||||
bleedingPrediction[0][ bleedingPrediction[0].length - 1 ]
|
||||
)
|
||||
if (todayDate.isBefore(bleedingStart)) {
|
||||
const { todayDate, predictedBleedingStart, predictedBleedingEnd, daysToEnd } = getTimes(bleedingPrediction)
|
||||
if (todayDate.isBefore(predictedBleedingStart)) {
|
||||
return predictLabels.predictionInFuture(
|
||||
todayDate.until(bleedingStart, ChronoUnit.DAYS),
|
||||
todayDate.until(bleedingEnd, ChronoUnit.DAYS)
|
||||
todayDate.until(predictedBleedingStart, ChronoUnit.DAYS),
|
||||
todayDate.until(predictedBleedingEnd, ChronoUnit.DAYS)
|
||||
)
|
||||
}
|
||||
if (todayDate.isAfter(bleedingEnd)) {
|
||||
if (todayDate.isAfter(predictedBleedingEnd)) {
|
||||
return predictLabels.predictionInPast(
|
||||
bleedingStart.toString(), bleedingEnd.toString()
|
||||
formatDateForShortText(predictedBleedingStart),
|
||||
formatDateForShortText(predictedBleedingEnd)
|
||||
)
|
||||
}
|
||||
const daysToEnd = todayDate.until(bleedingEnd, ChronoUnit.DAYS)
|
||||
if (daysToEnd === 0) {
|
||||
return predictLabels.predictionStartedNoDaysLeft
|
||||
} else if (daysToEnd === 1) {
|
||||
@@ -204,14 +183,12 @@ function determinePredictionText(bleedingPrediction) {
|
||||
|
||||
function getBleedingPredictionRange(prediction) {
|
||||
if (!prediction.length) return labels.unknown
|
||||
const todayDate = LocalDate.now()
|
||||
const bleedingStart = LocalDate.parse(prediction[0][0])
|
||||
const bleedingEnd = LocalDate.parse(prediction[0][ prediction[0].length - 1 ])
|
||||
if (todayDate.isBefore(bleedingStart)) {
|
||||
return `${todayDate.until(bleedingStart, ChronoUnit.DAYS)}-${todayDate.until(bleedingEnd, ChronoUnit.DAYS)}`
|
||||
const { todayDate, predictedBleedingStart, predictedBleedingEnd, daysToEnd } = getTimes(prediction)
|
||||
if (todayDate.isBefore(predictedBleedingStart)) {
|
||||
return `${todayDate.until(predictedBleedingStart, ChronoUnit.DAYS)}-${todayDate.until(predictedBleedingEnd, ChronoUnit.DAYS)}`
|
||||
}
|
||||
if (todayDate.isAfter(bleedingEnd)) {
|
||||
if (todayDate.isAfter(predictedBleedingEnd)) {
|
||||
return labels.unknown
|
||||
}
|
||||
return '0'
|
||||
return (daysToEnd === 0 ? '0' : `0 - ${daysToEnd}`)
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { Component } from 'react'
|
||||
import { View, TextInput, TouchableOpacity, Alert, Image } from 'react-native'
|
||||
import { View, TextInput, TouchableOpacity, Alert } from 'react-native'
|
||||
import nodejs from 'nodejs-mobile-react-native'
|
||||
import { saveEncryptionFlag } from '../local-storage'
|
||||
import AppText from './app-text'
|
||||
import Header from './header'
|
||||
import styles from '../styles'
|
||||
import { passwordPrompt as labels, shared } from '../i18n/en/labels'
|
||||
import { passwordPrompt as labels, shared, menuTitles } from '../i18n/en/labels'
|
||||
import { requestHash, deleteDbAndOpenNew, openDb } from '../db'
|
||||
|
||||
export default class PasswordPrompt extends Component {
|
||||
@@ -87,12 +88,10 @@ export default class PasswordPrompt extends Component {
|
||||
render() {
|
||||
return (
|
||||
<View flex={1}>
|
||||
<Header title={menuTitles.PasswordPrompt.toLowerCase()} />
|
||||
{this.state.showPasswordPrompt &&
|
||||
<View style={styles.passwordPromptPage}>
|
||||
<Image
|
||||
source={require('../assets/drip_small.png')}
|
||||
style={styles.passwordPromptImage}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
onChangeText={val => this.setState({ password: val })}
|
||||
style={styles.passwordPromptField}
|
||||
|
||||
@@ -18,6 +18,9 @@ export default class AboutSection extends Component {
|
||||
<FramedSegment title={labels.credits.title}>
|
||||
<AppText>{labels.credits.note}</AppText>
|
||||
</FramedSegment>
|
||||
<FramedSegment title={labels.donate.title}>
|
||||
<AppText>{labels.donate.note}</AppText>
|
||||
</FramedSegment>
|
||||
<FramedSegment title={labels.website.title}>
|
||||
<AppText>{links.website.url}</AppText>
|
||||
</FramedSegment>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react'
|
||||
import RNFS from 'react-native-fs'
|
||||
import { Alert, ToastAndroid } from 'react-native'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { clearDb, isDbEmpty } from '../../../db'
|
||||
import { hasEncryptionObservable } from '../../../local-storage'
|
||||
@@ -24,6 +25,7 @@ export default class DeleteData extends Component {
|
||||
}
|
||||
|
||||
onAlertConfirmation = () => {
|
||||
this.props.onStartDeletion()
|
||||
if (this.state.isPasswordSet) {
|
||||
this.setState({ isConfirmingWithPassword: true })
|
||||
} else {
|
||||
@@ -78,8 +80,9 @@ export default class DeleteData extends Component {
|
||||
|
||||
render() {
|
||||
const { isConfirmingWithPassword } = this.state
|
||||
const { isDeletingData } = this.props
|
||||
|
||||
if (isConfirmingWithPassword) {
|
||||
if (isConfirmingWithPassword && isDeletingData) {
|
||||
return (
|
||||
<ConfirmWithPassword
|
||||
onSuccess={this.deleteAppData}
|
||||
@@ -95,3 +98,8 @@ export default class DeleteData extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DeleteData.propTypes = {
|
||||
isDeletingData: PropTypes.bool,
|
||||
onStartDeletion: PropTypes.func.isRequired
|
||||
}
|
||||
@@ -6,23 +6,23 @@ import { shared as sharedLabels } from '../../../i18n/en/labels'
|
||||
import labels from '../../../i18n/en/settings'
|
||||
import alertError from '../shared/alert-error'
|
||||
|
||||
export default function openImportDialogAndImport() {
|
||||
export function openImportDialog(onImportData) {
|
||||
Alert.alert(
|
||||
labels.import.title,
|
||||
labels.import.message,
|
||||
[{
|
||||
text: labels.import.replaceOption,
|
||||
onPress: () => getFileContentAndImport({ deleteExisting: false })
|
||||
text: sharedLabels.cancel, style: 'cancel', onPress: () => { }
|
||||
}, {
|
||||
text: labels.import.deleteOption,
|
||||
onPress: () => getFileContentAndImport({ deleteExisting: true })
|
||||
onPress: () => onImportData(true)
|
||||
}, {
|
||||
text: sharedLabels.cancel, style: 'cancel', onPress: () => { }
|
||||
text: labels.import.replaceOption,
|
||||
onPress: () => onImportData(false)
|
||||
}]
|
||||
)
|
||||
}
|
||||
|
||||
async function getFileContentAndImport({ deleteExisting }) {
|
||||
export async function getFileContent() {
|
||||
let fileInfo
|
||||
try {
|
||||
fileInfo = await new Promise((resolve, reject) => {
|
||||
@@ -45,8 +45,13 @@ async function getFileContentAndImport({ deleteExisting }) {
|
||||
return importError(labels.import.errors.couldNotOpenFile)
|
||||
}
|
||||
|
||||
return fileContent
|
||||
}
|
||||
|
||||
export async function importData(shouldDeleteExistingData, fileContent) {
|
||||
|
||||
try {
|
||||
await importCsv(fileContent, deleteExisting)
|
||||
await importCsv(fileContent, shouldDeleteExistingData)
|
||||
Alert.alert(sharedLabels.successTitle, labels.import.success.message)
|
||||
} catch(err) {
|
||||
importError(err.message)
|
||||
|
||||
@@ -1,25 +1,74 @@
|
||||
import React from 'react'
|
||||
import { ScrollView } from 'react-native'
|
||||
import React, { Component } from 'react'
|
||||
import { ScrollView, View } from 'react-native'
|
||||
import AppText from '../../app-text'
|
||||
import FramedSegment from '../../framed-segment'
|
||||
import AppLoadingView from '../../app-loading'
|
||||
import SettingsButton from '../shared/settings-button'
|
||||
import openImportDialogAndImport from './import-dialog'
|
||||
import { openImportDialog, getFileContent, importData } from './import-dialog'
|
||||
import openShareDialogAndExport from './export-dialog'
|
||||
import DeleteData from './delete-data'
|
||||
import labels from '../../../i18n/en/settings'
|
||||
|
||||
const DataManagement = () => {
|
||||
export default class DataManagement extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
currentAction: null
|
||||
}
|
||||
}
|
||||
|
||||
startLoading = () => {
|
||||
this.setState({ isLoading: true })
|
||||
}
|
||||
|
||||
endLoading = () => {
|
||||
this.setState({ isLoading: false })
|
||||
}
|
||||
|
||||
startImportFlow = async (shouldDeleteExistingData) => {
|
||||
this.startLoading()
|
||||
const fileContent = await getFileContent()
|
||||
if (fileContent) {
|
||||
await importData(shouldDeleteExistingData, fileContent)
|
||||
}
|
||||
this.endLoading()
|
||||
}
|
||||
|
||||
startExport = () => {
|
||||
this.setCurrentAction('export')
|
||||
openShareDialogAndExport()
|
||||
}
|
||||
|
||||
startImport = () => {
|
||||
this.setCurrentAction('import')
|
||||
openImportDialog(this.startImportFlow)
|
||||
}
|
||||
|
||||
setCurrentAction = (action) => {
|
||||
this.setState({ currentAction: action })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentAction } = this.state
|
||||
return (
|
||||
<View flex={1}>
|
||||
{this.state.isLoading && <AppLoadingView />}
|
||||
{!this.state.isLoading &&
|
||||
<ScrollView>
|
||||
<View>
|
||||
<FramedSegment title={labels.export.button}>
|
||||
<AppText>{labels.export.segmentExplainer}</AppText>
|
||||
<SettingsButton onPress={openShareDialogAndExport}>
|
||||
<SettingsButton onPress={this.startExport}>
|
||||
{labels.export.button}
|
||||
</SettingsButton>
|
||||
</FramedSegment>
|
||||
<FramedSegment title={labels.import.button}>
|
||||
<AppText>{labels.import.segmentExplainer}</AppText>
|
||||
<SettingsButton onPress={openImportDialogAndImport}>
|
||||
<SettingsButton
|
||||
onPress= {this.startImport}
|
||||
>
|
||||
{labels.import.button}
|
||||
</SettingsButton>
|
||||
</FramedSegment>
|
||||
@@ -28,10 +77,15 @@ const DataManagement = () => {
|
||||
last
|
||||
>
|
||||
<AppText>{labels.deleteSegment.explainer}</AppText>
|
||||
<DeleteData />
|
||||
<DeleteData
|
||||
isDeletingData = { currentAction === 'delete' }
|
||||
onStartDeletion = {() => this.setCurrentAction('delete')}
|
||||
/>
|
||||
</FramedSegment>
|
||||
</View>
|
||||
</ScrollView>
|
||||
}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DataManagement
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { Component } from 'react'
|
||||
import {
|
||||
ScrollView, View
|
||||
} from 'react-native'
|
||||
import styles, { iconStyles } from '../../../styles'
|
||||
import styles from '../../../styles'
|
||||
import labels from '../../../i18n/en/settings'
|
||||
import AppText from '../../app-text'
|
||||
import FramedSegment from '../../framed-segment'
|
||||
@@ -28,11 +28,10 @@ export default class Settings extends Component {
|
||||
</FramedSegment>
|
||||
<FramedSegment style={styles.framedSegmentLast} >
|
||||
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
||||
<Icon name="info-with-circle" style={iconStyles.infoInHeading}/>
|
||||
<AppText style={styles.framedSegmentTitle}>{`${labels.preOvu.title} `}</AppText>
|
||||
<Icon name="info-with-circle"/>
|
||||
<AppText style={styles.framedSegmentTitle}>{` ${labels.preOvu.title} `}</AppText>
|
||||
</View>
|
||||
<AppText>{labels.preOvu.note}</AppText>
|
||||
<AppText>{labels.preOvu.note}</AppText>
|
||||
</FramedSegment>
|
||||
</ScrollView>
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class CreatePassword extends Component {
|
||||
}
|
||||
|
||||
startSettingPassword = () => {
|
||||
showBackUpReminder(this.toggleSettingPassword)
|
||||
showBackUpReminder(this.toggleSettingPassword, () => {})
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import labels from '../../../i18n/en/settings'
|
||||
import { changeEncryptionAndRestartApp } from '../../../db'
|
||||
import ConfirmWithPassword from '../shared/confirm-with-password'
|
||||
@@ -14,7 +16,7 @@ export default class DeletePassword extends Component {
|
||||
|
||||
startConfirmWithPassword = () => {
|
||||
this.setState({ enteringCurrentPassword: true })
|
||||
this.props.onStartDeletingPassword()
|
||||
this.props.onStartDelete()
|
||||
}
|
||||
|
||||
startDeletePassword = async () => {
|
||||
@@ -23,6 +25,7 @@ export default class DeletePassword extends Component {
|
||||
|
||||
cancelConfirmationWithPassword = () => {
|
||||
this.setState({ enteringCurrentPassword: false })
|
||||
this.props.onCancelDelete()
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -45,3 +48,8 @@ export default class DeletePassword extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DeletePassword.propTypes = {
|
||||
onStartDelete: PropTypes.func,
|
||||
onCancelDelete: PropTypes.func
|
||||
}
|
||||
@@ -24,10 +24,18 @@ export default class PasswordSetting extends Component {
|
||||
this.setState({ isChangingPassword: true })
|
||||
}
|
||||
|
||||
onCancelChangingPassword = () => {
|
||||
this.setState({ isChangingPassword: false })
|
||||
}
|
||||
|
||||
onDeletingPassword = () => {
|
||||
this.setState({ isDeletingPassword: true })
|
||||
}
|
||||
|
||||
onCancelDeletingPassword = () => {
|
||||
this.setState({ isDeletingPassword: false })
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
@@ -53,13 +61,15 @@ export default class PasswordSetting extends Component {
|
||||
|
||||
{ (isPasswordSet && !isDeletingPassword) && (
|
||||
<ChangePassword
|
||||
onStartChangingPassword = {this.onChangingPassword}
|
||||
onStartChange = {this.onChangingPassword}
|
||||
onCancelChange = {this.onCancelChangingPassword}
|
||||
/>
|
||||
)}
|
||||
|
||||
{ (isPasswordSet && !isChangingPassword) && (
|
||||
<DeletePassword
|
||||
onStartDeletingPassword = {this.onDeletingPassword}
|
||||
onStartDelete = {this.onDeletingPassword}
|
||||
onCancelDelete = {this.onCancelDeletingPassword}
|
||||
/>
|
||||
)}
|
||||
</FramedSegment>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Alert } from 'react-native'
|
||||
import { shared } from '../../../i18n/en/labels'
|
||||
import labels from '../../../i18n/en/settings'
|
||||
|
||||
export default function showBackUpReminder(okHandler, isDelete) {
|
||||
export default function showBackUpReminder(okHandler, cancelHandler, isDelete) {
|
||||
let title, message
|
||||
if (isDelete) {
|
||||
title = labels.passwordSettings.deleteBackupReminderTitle
|
||||
@@ -17,10 +17,12 @@ export default function showBackUpReminder(okHandler, isDelete) {
|
||||
message,
|
||||
[{
|
||||
text: shared.cancel,
|
||||
onPress: cancelHandler,
|
||||
style: 'cancel'
|
||||
}, {
|
||||
text: shared.ok,
|
||||
onPress: okHandler
|
||||
}]
|
||||
}],
|
||||
{ onDismiss: cancelHandler }
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import settings from '../../../i18n/en/settings'
|
||||
import EnterNewPassword from './enter-new-password'
|
||||
import SettingsButton from '../shared/settings-button'
|
||||
@@ -17,10 +19,15 @@ export default class ChangePassword extends Component {
|
||||
}
|
||||
|
||||
startChangingPassword = () => {
|
||||
showBackUpReminder(() => {
|
||||
showBackUpReminder(
|
||||
this.startEnteringCurrentPassword,
|
||||
this.cancelConfirmationWithPassword
|
||||
)
|
||||
}
|
||||
|
||||
startEnteringCurrentPassword = () => {
|
||||
this.setState({ enteringCurrentPassword: true })
|
||||
})
|
||||
this.props.onStartChangingPassword()
|
||||
this.props.onStartChange()
|
||||
}
|
||||
|
||||
startEnteringNewPassword = () => {
|
||||
@@ -37,6 +44,7 @@ export default class ChangePassword extends Component {
|
||||
enteringNewPassword: false,
|
||||
enteringCurrentPassword: false
|
||||
})
|
||||
this.props.onCancelChange()
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -72,3 +80,8 @@ export default class ChangePassword extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ChangePassword.propTypes = {
|
||||
onStartChange: PropTypes.func,
|
||||
onCancelChange: PropTypes.func
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { View, Alert } from 'react-native'
|
||||
|
||||
import nodejs from 'nodejs-mobile-react-native'
|
||||
@@ -100,3 +101,8 @@ export default class ConfirmWithPassword extends Component {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmWithPassword.propTypes = {
|
||||
onSuccess: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
import React from 'react'
|
||||
import { TextInput } from 'react-native'
|
||||
import styles, {secondaryColor} from '../../../styles'
|
||||
import PropTypes from 'prop-types'
|
||||
import AppTextInput from '../../app-text-input'
|
||||
|
||||
import styles from '../../../styles'
|
||||
|
||||
export default function PasswordField(props) {
|
||||
return (
|
||||
<TextInput
|
||||
style={styles.passwordField}
|
||||
autoFocus={props.autoFocus === false ? false : true}
|
||||
secureTextEntry={true}
|
||||
onChangeText={props.onChangeText}
|
||||
value={props.value}
|
||||
placeholder={props.placeholder}
|
||||
borderWidth={1}
|
||||
borderColor={secondaryColor}
|
||||
borderStyle={'solid'}
|
||||
<AppTextInput
|
||||
style={ styles.passwordField }
|
||||
secureTextEntry
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
PasswordField.propTypes = {
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChangeText: PropTypes.func,
|
||||
autoFocus: PropTypes.bool
|
||||
}
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
const TemperatureSchema = {
|
||||
name: 'Temperature',
|
||||
properties: {
|
||||
value: 'double',
|
||||
exclude: 'bool',
|
||||
time: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BleedingSchema = {
|
||||
name: 'Bleeding',
|
||||
properties: {
|
||||
value: 'int',
|
||||
exclude: 'bool'
|
||||
}
|
||||
}
|
||||
|
||||
const MucusSchema = {
|
||||
name: 'Mucus',
|
||||
properties: {
|
||||
feeling: { type: 'int', optional: true },
|
||||
texture: { type: 'int', optional: true },
|
||||
value: { type: 'int', optional: true },
|
||||
exclude: 'bool'
|
||||
}
|
||||
}
|
||||
|
||||
const CervixSchema = {
|
||||
name: 'Cervix',
|
||||
properties: {
|
||||
opening: { type: 'int', optional: true },
|
||||
firmness: { type: 'int', optional: true },
|
||||
position: {type: 'int', optional: true },
|
||||
exclude: 'bool'
|
||||
}
|
||||
}
|
||||
|
||||
const NoteSchema = {
|
||||
name: 'Note',
|
||||
properties: {
|
||||
value: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
const DesireSchema = {
|
||||
name: 'Desire',
|
||||
properties: {
|
||||
value: 'int'
|
||||
}
|
||||
}
|
||||
|
||||
const SexSchema = {
|
||||
name: 'Sex',
|
||||
properties: {
|
||||
solo: { type: 'bool', optional: true },
|
||||
partner: { type: 'bool', optional: true },
|
||||
condom: { type: 'bool', optional: true },
|
||||
pill: { type: 'bool', optional: true },
|
||||
iud: { type: 'bool', optional: true },
|
||||
patch: { type: 'bool', optional: true },
|
||||
ring: { type: 'bool', optional: true },
|
||||
implant: { type: 'bool', optional: true },
|
||||
diaphragm: { type: 'bool', optional: true },
|
||||
none: { type: 'bool', optional: true },
|
||||
other: { type: 'bool', optional: true },
|
||||
note: { type: 'string', optional: true }
|
||||
}
|
||||
}
|
||||
|
||||
const PainSchema = {
|
||||
name: 'Pain',
|
||||
properties: {
|
||||
cramps: { type: 'bool', optional: true },
|
||||
ovulationPain: { type: 'bool', optional: true },
|
||||
headache: { type: 'bool', optional: true },
|
||||
backache: { type: 'bool', optional: true },
|
||||
nausea: { type: 'bool', optional: true },
|
||||
tenderBreasts: { type: 'bool', optional: true },
|
||||
migraine: { type: 'bool', optional: true },
|
||||
other: { type: 'bool', optional: true },
|
||||
note: { type: 'string', optional: true }
|
||||
}
|
||||
}
|
||||
|
||||
const MoodSchema = {
|
||||
name: 'Mood',
|
||||
properties: {
|
||||
happy: { type: 'bool', optional: true },
|
||||
sad: { type: 'bool', optional: true },
|
||||
stressed: { type: 'bool', optional: true },
|
||||
balanced: { type: 'bool', optional: true },
|
||||
fine: { type: 'bool', optional: true },
|
||||
anxious: { type: 'bool', optional: true },
|
||||
energetic: { type: 'bool', optional: true },
|
||||
fatigue: { type: 'bool', optional: true },
|
||||
angry: { type: 'bool', optional: true },
|
||||
other: { type: 'bool', optional: true },
|
||||
note: { type: 'string', optional: true }
|
||||
}
|
||||
}
|
||||
|
||||
const CycleDaySchema = {
|
||||
name: 'CycleDay',
|
||||
primaryKey: 'date',
|
||||
properties: {
|
||||
date: 'string',
|
||||
temperature: {
|
||||
type: 'Temperature',
|
||||
optional: true
|
||||
},
|
||||
isCycleStart: 'bool',
|
||||
bleeding: {
|
||||
type: 'Bleeding',
|
||||
optional: true
|
||||
},
|
||||
mucus: {
|
||||
type: 'Mucus',
|
||||
optional: true
|
||||
},
|
||||
cervix: {
|
||||
type: 'Cervix',
|
||||
optional: true
|
||||
},
|
||||
note: {
|
||||
type: 'Note',
|
||||
optional: true
|
||||
},
|
||||
desire: {
|
||||
type: 'Desire',
|
||||
optional: true
|
||||
},
|
||||
sex: {
|
||||
type: 'Sex',
|
||||
optional: true
|
||||
},
|
||||
pain: {
|
||||
type: 'Pain',
|
||||
optional: true
|
||||
},
|
||||
mood: {
|
||||
type: 'Mood',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
schema: [
|
||||
CycleDaySchema,
|
||||
TemperatureSchema,
|
||||
BleedingSchema,
|
||||
MucusSchema,
|
||||
CervixSchema,
|
||||
NoteSchema,
|
||||
DesireSchema,
|
||||
SexSchema,
|
||||
PainSchema,
|
||||
MoodSchema
|
||||
],
|
||||
schemaVersion: 4,
|
||||
migration: (oldRealm) => {
|
||||
if (oldRealm.schemaVersion >= 4) return
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -2,5 +2,6 @@ import schema0 from './0.js'
|
||||
import schema1 from './1.js'
|
||||
import schema2 from './2.js'
|
||||
import schema3 from './3.js'
|
||||
import schema4 from './4.js'
|
||||
|
||||
export default [schema0, schema1, schema2, schema3]
|
||||
export default [schema0, schema1, schema2, schema3, schema4]
|
||||
+39
-30
@@ -14,6 +14,11 @@ export const bleeding = {
|
||||
}
|
||||
|
||||
export const cervix = {
|
||||
subcategories: {
|
||||
opening: 'opening',
|
||||
firmness: 'firmness',
|
||||
position: 'position'
|
||||
},
|
||||
opening: {
|
||||
categories: ['closed', 'medium', 'open'],
|
||||
explainer: 'Is your cervix open or closed?'
|
||||
@@ -30,6 +35,10 @@ export const cervix = {
|
||||
}
|
||||
|
||||
export const mucus = {
|
||||
subcategories: {
|
||||
feeling: 'feeling',
|
||||
texture: 'texture'
|
||||
},
|
||||
feeling: {
|
||||
categories: ['dry', 'nothing', 'wet', 'slippery'],
|
||||
explainer: 'What does your vaginal entrance feel like?'
|
||||
@@ -49,8 +58,8 @@ export const desire = {
|
||||
|
||||
export const sex = {
|
||||
categories:{
|
||||
solo: 'Solo',
|
||||
partner: 'Partner',
|
||||
solo: 'solo',
|
||||
partner: 'partner',
|
||||
},
|
||||
header: "Activity",
|
||||
explainer: 'Were you sexually active today?',
|
||||
@@ -58,15 +67,15 @@ export const sex = {
|
||||
|
||||
export const contraceptives = {
|
||||
categories:{
|
||||
condom: 'Condom',
|
||||
pill: 'Pill',
|
||||
iud: 'IUD',
|
||||
patch: 'Patch',
|
||||
ring: 'Ring',
|
||||
implant: 'Implant',
|
||||
diaphragm: 'Diaphragm',
|
||||
none: 'None',
|
||||
other: 'Other',
|
||||
condom: 'condom',
|
||||
pill: 'pill',
|
||||
iud: 'iud',
|
||||
patch: 'patch',
|
||||
ring: 'ring',
|
||||
implant: 'implant',
|
||||
diaphragm: 'diaphragm',
|
||||
none: 'none',
|
||||
other: 'other',
|
||||
},
|
||||
header: "Contraceptives",
|
||||
explainer: 'Did you use contraceptives?'
|
||||
@@ -74,30 +83,30 @@ export const contraceptives = {
|
||||
|
||||
export const pain = {
|
||||
categories: {
|
||||
cramps: 'Cramps',
|
||||
ovulationPain: 'Ovulation pain',
|
||||
headache: 'Headache',
|
||||
backache: 'Backache',
|
||||
nausea: 'Nausea',
|
||||
tenderBreasts: 'Tender breasts',
|
||||
migraine: 'Migraine',
|
||||
other: 'Other'
|
||||
cramps: 'cramps',
|
||||
ovulationPain: 'ovulation pain',
|
||||
headache: 'headache',
|
||||
backache: 'backache',
|
||||
nausea: 'nausea',
|
||||
tenderBreasts: 'tender breasts',
|
||||
migraine: 'migraine',
|
||||
other: 'other'
|
||||
},
|
||||
explainer: 'How did your body feel today?'
|
||||
}
|
||||
|
||||
export const mood = {
|
||||
categories: {
|
||||
happy: 'Happy',
|
||||
sad: 'Sad',
|
||||
stressed: 'Stressed',
|
||||
balanced: 'Balanced',
|
||||
fine: 'Fine',
|
||||
anxious: 'Anxious',
|
||||
energetic: 'Energetic',
|
||||
fatigue: 'Fatigue',
|
||||
angry: 'Angry',
|
||||
other: 'Other'
|
||||
happy: 'happy',
|
||||
sad: 'sad',
|
||||
stressed: 'stressed',
|
||||
balanced: 'balanced',
|
||||
fine: 'fine',
|
||||
anxious: 'anxious',
|
||||
energetic: 'energetic',
|
||||
fatigue: 'fatigue',
|
||||
angry: 'angry',
|
||||
other: 'other'
|
||||
},
|
||||
explainer: 'How did you feel today?'
|
||||
}
|
||||
@@ -107,7 +116,7 @@ export const temperature = {
|
||||
outOfAbsoluteRangeWarning: 'This temperature value is too high or low to be shown on the temperature chart.',
|
||||
saveAnyway: 'Save anyway',
|
||||
temperature: {
|
||||
header: "Temperature (°C)",
|
||||
header: "Temperature",
|
||||
explainer: 'Take your temperature right after waking up, before getting out of bed'
|
||||
},
|
||||
time: "Time",
|
||||
|
||||
+13
-14
@@ -15,8 +15,6 @@ export const shared = {
|
||||
date: 'Date',
|
||||
cycleDayWithLinebreak: 'Cycle\nday',
|
||||
loading: 'Loading ...',
|
||||
more: 'more',
|
||||
less: 'less',
|
||||
enter: 'Enter'
|
||||
}
|
||||
|
||||
@@ -32,15 +30,15 @@ export const headerTitles = {
|
||||
Password: settingsTitles.password,
|
||||
About: settingsTitles.about,
|
||||
License: settingsTitles.license,
|
||||
BleedingEditView: 'Bleeding',
|
||||
TemperatureEditView: 'Temperature',
|
||||
MucusEditView: 'Mucus',
|
||||
CervixEditView: 'Cervix',
|
||||
NoteEditView: 'Note',
|
||||
DesireEditView: 'Desire',
|
||||
SexEditView: 'Sex',
|
||||
PainEditView: 'Pain',
|
||||
MoodEditView: 'Mood',
|
||||
bleeding: 'Bleeding',
|
||||
temperature: 'Temperature',
|
||||
mucus: 'Cervical Mucus',
|
||||
cervix: 'Cervix',
|
||||
note: 'Note',
|
||||
desire: 'Desire',
|
||||
sex: 'Sex',
|
||||
pain: 'Pain',
|
||||
mood: 'Mood',
|
||||
InfoSymptom: 'Info'
|
||||
}
|
||||
|
||||
@@ -50,6 +48,7 @@ export const menuTitles = {
|
||||
Chart: 'Chart',
|
||||
Stats: 'Stats',
|
||||
Settings: 'Settings',
|
||||
PasswordPrompt: 'Drip'
|
||||
}
|
||||
|
||||
export const stats = {
|
||||
@@ -110,12 +109,12 @@ export const fertilityStatus = {
|
||||
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 mucus or cervix values.",
|
||||
periOvuText: "We have not been able to detect both a temperature shift and mucus or cervix shift.",
|
||||
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 have not been able to detect both a temperature shift and cervical mucus or cervix shift. Please find more information on NFP rules here:",
|
||||
postOvuText: tempRule => {
|
||||
return (
|
||||
'We have detected a temperature shift (' + ['regular', '1st exception', '2nd exception'][tempRule] +
|
||||
' temperature rule), as well as a mucus shift according to NFP rules. You may assume infertility, but always remember to ' +
|
||||
' temperature rule), as well as a cervical mucus 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.'
|
||||
)
|
||||
}
|
||||
|
||||
+5
-5
@@ -9,13 +9,13 @@ export default {
|
||||
},
|
||||
wiki: {
|
||||
url: 'https://gitlab.com/bloodyhealth/drip/wikis/home',
|
||||
text: 'wiki'
|
||||
text: 'our wiki'
|
||||
},
|
||||
website: {
|
||||
url: 'https://bloodyhealth.gitlab.io/'
|
||||
},
|
||||
moreAboutNfp: {
|
||||
url: 'https://gitlab.com/bloodyhealth/drip/wikis/nfp/intro',
|
||||
text: 'More'
|
||||
},
|
||||
donate: {
|
||||
url: 'https://ko-fi.com/dripapp',
|
||||
text: 'here'
|
||||
}
|
||||
}
|
||||
|
||||
+11
-7
@@ -15,8 +15,8 @@ export default {
|
||||
couldNotConvert: 'Could not convert data to CSV',
|
||||
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'
|
||||
},
|
||||
@@ -74,8 +74,8 @@ export default {
|
||||
},
|
||||
useCervix: {
|
||||
title: 'Secondary symptom',
|
||||
cervixModeOn: 'Cervix values are being used for symptothermal fertility detection. You can switch here to use mucus values for symptothermal fertility detection',
|
||||
cervixModeOff: 'By default, 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',
|
||||
@@ -96,11 +96,11 @@ export default {
|
||||
},
|
||||
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 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 ${links.email.url}. You can also contribute to the code base on ${links.gitlab.url}`,
|
||||
},
|
||||
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.`,
|
||||
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',
|
||||
@@ -118,10 +118,14 @@ You can contact us by bloodyhealth@mailbox.org.`
|
||||
},
|
||||
preOvu: {
|
||||
title: 'Infertile days at cycle start',
|
||||
note: `drip applies NFP's rules for calculating infertile days at the start of the cycle (see the ${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: 'Thanks and lots of <3 to all of our contributors as well as Susanne Umscheid for the wonderful design, and Paula Härtel for the symptom icons.'
|
||||
},
|
||||
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.`
|
||||
}
|
||||
}
|
||||
|
||||
+15
-27
@@ -1,13 +1,19 @@
|
||||
import links from './links'
|
||||
|
||||
export const generalInfo = {
|
||||
chartNfp: `On the chart, you can track fertility signs. When both a valid temperature shift and a mucus or cervix shift have been detected, an orange line will be displayed on the chart. This indicates the end of the peri-ovulatory and the beginning of the post-ovulatory phase.`,
|
||||
curiousNfp: `If you are curious to learn more about the sympto-thermal method that is used for fertility tracking within the app, you can visit our ${links.wiki.url}.`,
|
||||
chartNfp: `On the chart, you can track fertility signs. When both a valid temperature shift and a cervical mucus or cervix shift have been detected, an orange line will be displayed on the chart. This indicates the end of the peri-ovulatory and the beginning of the post-ovulatory phase.`,
|
||||
curiousNfp: `If you are curious to learn more about the sympto-thermal method that is used for fertility tracking within the app, you can visit ${links.wiki.url}.`,
|
||||
cycleRelation: `It may be influenced by or have an impact on your menstrual cycles and its hormonal changes.`,
|
||||
excludeExplainer: `You can exclude these values, so they won't be taken into account for any fertility calculation.`,
|
||||
nfpTfyReminder: `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.
|
||||
nfpTfyReminder: `When - on a daily/regular basis - you track:
|
||||
1. your basal body temperature,
|
||||
2. your cervical mucus OR your cervix,
|
||||
3. and menstrual bleeding
|
||||
the app helps you identify in which phase of the menstrual cycle you are.
|
||||
|
||||
Please find more info on the sympto-thermal method in our ${links.wiki.url}.`,
|
||||
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.`
|
||||
}
|
||||
|
||||
@@ -25,12 +31,6 @@ The app allows you to track different intensities of bleeding. On the chart and
|
||||
|
||||
Excluding bleeding values is for tracking bleeding when it's not marking the start of a new cycle or the continuation of menstrual bleeding the day(s) before, e.g. bleeding caused by ovulation or a miscarriage.
|
||||
|
||||
When - on a daily/regular basis - you track:
|
||||
1. your basal body temperature,
|
||||
2. your cervical mucus OR your cervix,
|
||||
3. and menstrual bleeding
|
||||
the app helps you identify in which phase of your cycle you are.
|
||||
|
||||
${generalInfo.nfpTfyReminder}`,
|
||||
},
|
||||
cervix: {
|
||||
@@ -39,11 +39,7 @@ ${generalInfo.nfpTfyReminder}`,
|
||||
|
||||
Tracking how open or closed and how firm or soft the cervix feels 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, but you can change it to cervix in "Settings" -> "NFP Settings". When - on a daily/regular basis - you track:
|
||||
1. your basal body temperature,
|
||||
2. your cervical mucus OR your cervix,
|
||||
3. and menstrual bleeding
|
||||
the app helps you identify in which phase of your cycle you are.
|
||||
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.
|
||||
@@ -78,11 +74,7 @@ ${generalInfo.curiousNfp}`
|
||||
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. When - on a daily/regular basis - you track:
|
||||
1. your basal body temperature,
|
||||
2. your cervical mucus OR your cervix,
|
||||
3. and menstrual bleeding
|
||||
the app helps you identify in which phase of your 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.
|
||||
@@ -93,9 +85,9 @@ From lowest to best quality:
|
||||
· S = (no OR wet feeling + creamy texture),
|
||||
· S+ = (any feeling + egg white texture) OR (slippery feeling + any texture).
|
||||
|
||||
On the chart, mucus is colored in blue: the darker the shade of blue the better the quality of your mucus.
|
||||
On the chart, cervical mucus is colored in blue: the darker the shade of blue the better the quality of your cervical mucus.
|
||||
|
||||
Please note that drip does not yet support "parenthesis values": According to NFP rules, you can qualify a 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}
|
||||
|
||||
@@ -133,11 +125,7 @@ ${generalInfo.curiousNfp}`
|
||||
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.
|
||||
|
||||
By default the secondary symptom is cervical mucus, but you can change it to cervix in "Settings" -> "NFP Settings". When - on a daily/regular basis - you track:
|
||||
1. your basal body temperature,
|
||||
2. your cervical mucus OR your cervix,
|
||||
3. and menstrual bleeding
|
||||
the app helps you identify in which phase of your cycle you are.
|
||||
By default the secondary symptom is cervical mucus, but you can change it to cervix in "Settings" -> "NFP Settings".
|
||||
|
||||
· What is body basal temperature?
|
||||
It's your temperature after lying still for at least 6 hours. For many, this is when they are waking up in the morning after sleeping at least 6 hours and before getting up.
|
||||
|
||||
+542
-303
File diff suppressed because it is too large
Load Diff
+29
-1
@@ -9,6 +9,7 @@
|
||||
|
||||
#import <React/RCTBundleURLProvider.h>
|
||||
#import <React/RCTRootView.h>
|
||||
#import <React/RCTPushNotificationManager.h>
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
@@ -19,7 +20,7 @@
|
||||
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
|
||||
|
||||
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
|
||||
moduleName:@"drip"
|
||||
moduleName:@"home"
|
||||
initialProperties:nil
|
||||
launchOptions:launchOptions];
|
||||
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
|
||||
@@ -32,4 +33,31 @@
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Required to register for notifications
|
||||
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
|
||||
{
|
||||
[RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings];
|
||||
}
|
||||
// Required for the register event.
|
||||
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
|
||||
{
|
||||
[RCTPushNotificationManager didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
|
||||
}
|
||||
// Required for the notification event. You must call the completion handler after handling the remote notification.
|
||||
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
|
||||
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
|
||||
{
|
||||
[RCTPushNotificationManager didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
|
||||
}
|
||||
// Required for the registrationError event.
|
||||
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
|
||||
{
|
||||
[RCTPushNotificationManager didFailToRegisterForRemoteNotificationsWithError:error];
|
||||
}
|
||||
// Required for the localNotification event.
|
||||
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
|
||||
{
|
||||
[RCTPushNotificationManager didReceiveLocalNotification:notification];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -86,6 +86,8 @@
|
||||
<string>OpenSans-Light.ttf</string>
|
||||
<string>OpenSans-Regular.ttf</string>
|
||||
<string>OpenSans-SemiBold.ttf</string>
|
||||
<string>AntDesign.ttf</string>
|
||||
<string>OpenSans-LightItalic.ttf</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export default function (feeling, texture) {
|
||||
|
||||
if (typeof feeling != 'number' || typeof texture != 'number') return null
|
||||
|
||||
const feelingMapping = {
|
||||
0: 0,
|
||||
1: 1,
|
||||
|
||||
@@ -58,9 +58,9 @@ export default function setupNotifications(navigate) {
|
||||
function setupPeriodReminder() {
|
||||
const bleedingPrediction = cycleModule().getPredictedMenses()
|
||||
if (bleedingPrediction.length > 0) {
|
||||
const bleedingStart = Moment(bleedingPrediction[0][0], "YYYY-MM-DD")
|
||||
const predictedBleedingStart = Moment(bleedingPrediction[0][0], "YYYY-MM-DD")
|
||||
// 3 days before and at 6 am
|
||||
const reminderDate = bleedingStart
|
||||
const reminderDate = predictedBleedingStart
|
||||
.subtract(3, 'days')
|
||||
.hours(6)
|
||||
.minutes(0)
|
||||
|
||||
+13
-1
@@ -108,7 +108,15 @@ function formatCycleForSympto(cycle) {
|
||||
if (day[symptomName] && day[symptomName].exclude) {
|
||||
delete day[symptomName]
|
||||
}
|
||||
});
|
||||
})
|
||||
// remove days with incomplete cervix values
|
||||
if (hasIncompleteCervixValue(day)) {
|
||||
delete day.cervix
|
||||
}
|
||||
// remove days with incomplete mucus value (because nfp-mucus returns null when that's the case)
|
||||
if (day.mucus && day.mucus.value === null) {
|
||||
delete day.mucus
|
||||
}
|
||||
// change format
|
||||
['bleeding', 'temperature', 'mucus'].forEach(symptomName => {
|
||||
if (day[symptomName]) day[symptomName] = day[symptomName].value
|
||||
@@ -120,3 +128,7 @@ function formatCycleForSympto(cycle) {
|
||||
formatted.reverse()
|
||||
return formatted
|
||||
}
|
||||
|
||||
function hasIncompleteCervixValue(day) {
|
||||
return day.cervix && (typeof day.cervix.opening != 'number' || typeof day.cervix.firmness != 'number')
|
||||
}
|
||||
Generated
+2957
-3410
File diff suppressed because it is too large
Load Diff
+23
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "drip",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1905.29-beta",
|
||||
"contributors": [
|
||||
"Julia Friesel <julia.friesel@gmail.com>",
|
||||
"Marie Kochsiek",
|
||||
@@ -9,50 +9,60 @@
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start",
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios --simulator=\"iPhone 8 Plus\"",
|
||||
"log": "react-native log-android",
|
||||
"test": "mocha --recursive --require @babel/register test && npm run lint",
|
||||
"test-watch": "mocha --recursive --require @babel/register --watch test",
|
||||
"lint": "eslint components lib test",
|
||||
"devtool": "adb shell input keyevent 82"
|
||||
"devtool": "adb shell input keyevent 82",
|
||||
"update-version": "./tools/update-version.js",
|
||||
"build-android-release": "cd android && ./gradlew clean && ./gradlew assembleRelease && cd ..",
|
||||
"commit-release": "./tools/commit-release",
|
||||
"update-changelog": "./tools/update-changelog.js",
|
||||
"release": "npm run update-version && npm run build-android-release && npm run commit-release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ptomasroos/react-native-multi-slider": "^1.0.0",
|
||||
"ajv": "^5.5.2",
|
||||
"assert": "^1.4.1",
|
||||
"csvtojson": "^2.0.8",
|
||||
"date-range": "0.0.2",
|
||||
"isobject": "^3.0.1",
|
||||
"js-base64": "^2.4.8",
|
||||
"js-joda": "^1.8.2",
|
||||
"moment": "^2.22.2",
|
||||
"nodejs-mobile-react-native": "^0.3.0",
|
||||
"nodejs-mobile-react-native": "^0.4.1",
|
||||
"object-path": "^0.11.4",
|
||||
"obv": "0.0.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.6.3",
|
||||
"react-native": "^0.57.8",
|
||||
"react": "16.6.1",
|
||||
"react-native": "0.58.0-rc.0",
|
||||
"react-native-calendars": "^1.19.3",
|
||||
"react-native-document-picker": "^2.1.0",
|
||||
"react-native-fs": "^2.13.3",
|
||||
"react-native-hyperlink": "0.0.14",
|
||||
"react-native-modal-datetime-picker-nevo": "^4.11.0",
|
||||
"react-native-push-notification": "^3.1.1",
|
||||
"react-native-push-notification": "github:jfr3000/react-native-push-notification",
|
||||
"react-native-restart": "0.0.7",
|
||||
"react-native-share": "^1.1.3",
|
||||
"react-native-vector-icons": "^5.0.0",
|
||||
"react-native-vector-icons": "^6.4.2",
|
||||
"realm": "^2.22.0",
|
||||
"sympto": "^1.0.0"
|
||||
"sympto": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.0.1",
|
||||
"@babel/register": "^7.0.0",
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/register": "^7.0.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"basic-changelog": "gitlab:bloodyhealth/basic-changelog",
|
||||
"chai": "^4.1.2",
|
||||
"dirty-chai": "^2.0.1",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-react": "^7.8.2",
|
||||
"jase": "^1.2.0",
|
||||
"left-pad": "^1.3.0",
|
||||
"metro-react-native-babel-preset": "^0.51.1",
|
||||
"mocha": "^5.2.0"
|
||||
"mocha": "^5.2.0",
|
||||
"react-native-version": "^3.1.0",
|
||||
"readline": "^1.3.0"
|
||||
},
|
||||
"description": "A menstrual cycle tracking app that's open-source and leaves your data on your phone. Use it to track your menstrual cycle or for fertility awareness!",
|
||||
"main": "index.js",
|
||||
|
||||
+99
-69
@@ -17,6 +17,7 @@ const headerFont = 'Prompt-ExtraLight'
|
||||
|
||||
const textFont = 'OpenSans-Light'
|
||||
const textFontBold = 'OpenSans-SemiBold'
|
||||
const textFontItalic = 'OpenSans-LightItalic'
|
||||
|
||||
const regularSize = 16
|
||||
const hintSize = 14
|
||||
@@ -26,6 +27,16 @@ const defaultIndentation = 10
|
||||
const defaultTopMargin = 10
|
||||
const colorInActive = '#666666'
|
||||
|
||||
export const calendarTheme = {
|
||||
textDayFontFamily: textFont,
|
||||
textMonthFontFamily: textFontBold,
|
||||
textDayHeaderFontFamily: textFont,
|
||||
textDayFontSize: regularSize,
|
||||
textMonthFontSize: regularSize,
|
||||
textDayHeaderFontSize: hintSize,
|
||||
textSectionTitleColor: 'grey'
|
||||
}
|
||||
|
||||
export default StyleSheet.create({
|
||||
appText: {
|
||||
color: 'black',
|
||||
@@ -33,12 +44,9 @@ export default StyleSheet.create({
|
||||
fontSize: regularSize,
|
||||
letterSpacing: 0.5
|
||||
},
|
||||
actionHint: {
|
||||
color: secondaryColor,
|
||||
fontFamily: textFont,
|
||||
hint: {
|
||||
fontFamily: textFontItalic,
|
||||
fontSize: hintSize,
|
||||
fontWeight: 'bold',
|
||||
margin: defaultIndentation
|
||||
},
|
||||
paragraph: {
|
||||
marginBottom: defaultBottomMargin
|
||||
@@ -94,9 +102,6 @@ export default StyleSheet.create({
|
||||
marginLeft: 10,
|
||||
marginTop: 6
|
||||
},
|
||||
homeView: {
|
||||
marginHorizontal: 50,
|
||||
},
|
||||
button: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
@@ -110,29 +115,26 @@ export default StyleSheet.create({
|
||||
homeButtonText: {
|
||||
color: fontOnPrimaryColor
|
||||
},
|
||||
homeIconElement: {
|
||||
alignItems: 'center',
|
||||
marginTop: 15
|
||||
homeView: {
|
||||
margin: 40,
|
||||
},
|
||||
homeDescriptionText: {
|
||||
width: 200,
|
||||
marginBottom: defaultBottomMargin,
|
||||
},
|
||||
homeElement: {
|
||||
marginBottom: 30,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
homeIconTextWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
wrapperCycle: {
|
||||
homeIconAndText: {
|
||||
justifyContent: 'center'
|
||||
},
|
||||
wrapperIcon: {
|
||||
width: 80,
|
||||
height: 77,
|
||||
position: 'absolute'
|
||||
},
|
||||
wrapperDrop: {
|
||||
width: 81,
|
||||
height: 85,
|
||||
marginTop: 20,
|
||||
position: 'absolute'
|
||||
},
|
||||
wrapperCircle: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
position: 'absolute'
|
||||
},
|
||||
homeCircle: {
|
||||
@@ -147,14 +149,6 @@ export default StyleSheet.create({
|
||||
iconText: {
|
||||
fontSize: 25
|
||||
},
|
||||
showMore: {
|
||||
transform: [{rotate: '90deg'}],
|
||||
position: 'absolute',
|
||||
},
|
||||
showLess: {
|
||||
transform: [{rotate: '270deg'}],
|
||||
position: 'absolute'
|
||||
},
|
||||
cycleDayNumber: {
|
||||
fontSize: 15,
|
||||
color: fontOnPrimaryColor,
|
||||
@@ -162,13 +156,11 @@ export default StyleSheet.create({
|
||||
fontFamily: headerFont
|
||||
},
|
||||
symptomViewHeading: {
|
||||
fontSize: 20,
|
||||
color: 'black',
|
||||
marginBottom: 5
|
||||
fontWeight: 'bold',
|
||||
fontFamily: textFontBold
|
||||
},
|
||||
symptomInfoIcon: {
|
||||
marginRight: 20,
|
||||
marginLeft: 20
|
||||
symptomSection: {
|
||||
marginBottom: 10
|
||||
},
|
||||
symptomBoxImage: {
|
||||
width: 50,
|
||||
@@ -226,16 +218,12 @@ export default StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
height: 80
|
||||
},
|
||||
headerCycleDay: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
navigationArrow: {
|
||||
padding: 20
|
||||
},
|
||||
hiddenIcon: {
|
||||
padding: 20
|
||||
padding: 20,
|
||||
position: 'absolute'
|
||||
},
|
||||
navigationArrowLeft: { left: 0 },
|
||||
navigationArrowRight: { right: 0 },
|
||||
menu: {
|
||||
backgroundColor: primaryColor,
|
||||
alignItems: 'center',
|
||||
@@ -259,16 +247,12 @@ export default StyleSheet.create({
|
||||
temperatureTextInput: {
|
||||
fontSize: 20,
|
||||
color: 'black',
|
||||
textAlign: 'center'
|
||||
textAlign: 'center',
|
||||
width: '30%'
|
||||
},
|
||||
temperatureTextInputSuggestion: {
|
||||
color: '#939393'
|
||||
},
|
||||
actionButtonRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-evenly',
|
||||
marginTop: 50
|
||||
},
|
||||
symptomEditButton: {
|
||||
width: 130
|
||||
},
|
||||
@@ -289,10 +273,40 @@ export default StyleSheet.create({
|
||||
fontWeight: 'bold',
|
||||
fontFamily: textFontBold
|
||||
},
|
||||
framedSegmentInlineChildren: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
infoPopUpWrapper: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
infoPopUp: {
|
||||
backgroundColor: 'white',
|
||||
padding: 15,
|
||||
marginHorizontal: 20,
|
||||
marginTop: 20,
|
||||
maxHeight: '92%'
|
||||
},
|
||||
dimmed: {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'black',
|
||||
opacity: 0.5,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
infoSymptomClose: {
|
||||
alignItems: 'flex-end'
|
||||
},
|
||||
infoSymptomText: {
|
||||
marginTop: 10
|
||||
},
|
||||
settingsButton: {
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
margin: 10
|
||||
margin: 10,
|
||||
borderRadius: 5,
|
||||
},
|
||||
settingsButtonAccent: {
|
||||
backgroundColor: secondaryColor
|
||||
@@ -333,10 +347,10 @@ export default StyleSheet.create({
|
||||
selectBoxSection: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginVertical: 10,
|
||||
marginTop: 7,
|
||||
},
|
||||
selectTabGroup: {
|
||||
marginVertical: 10,
|
||||
marginTop: 7,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
selectTab: {
|
||||
@@ -364,7 +378,8 @@ export default StyleSheet.create({
|
||||
borderLeftWidth: null
|
||||
},
|
||||
page: {
|
||||
marginHorizontal: 10
|
||||
marginHorizontal: 10,
|
||||
marginTop: 20,
|
||||
},
|
||||
calendarToday: {
|
||||
fontWeight: 'bold',
|
||||
@@ -373,13 +388,21 @@ export default StyleSheet.create({
|
||||
marginTop: 1
|
||||
},
|
||||
passwordField: {
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
marginHorizontal: 10,
|
||||
backgroundColor: 'white'
|
||||
marginTop: 10
|
||||
},
|
||||
textInputField: {
|
||||
padding: 10,
|
||||
marginVertical: 10,
|
||||
backgroundColor: 'white',
|
||||
borderColor: secondaryColor,
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
},
|
||||
passwordPromptPage: {
|
||||
padding: 30,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
passwordPromptField: {
|
||||
@@ -408,8 +431,16 @@ export default StyleSheet.create({
|
||||
marginTop: 20,
|
||||
color: 'grey'
|
||||
},
|
||||
infoButton: {
|
||||
paddingVertical: 20
|
||||
headerDeleteButton: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 20,
|
||||
position: 'absolute',
|
||||
right: 0
|
||||
},
|
||||
infoButtonSymptomView: {
|
||||
position: 'absolute',
|
||||
padding: 15,
|
||||
right: 0
|
||||
},
|
||||
licensePage: {
|
||||
paddingVertical: 20,
|
||||
@@ -441,6 +472,10 @@ export const iconStyles = {
|
||||
symptomBoxActive: {
|
||||
color: fontOnPrimaryColor
|
||||
},
|
||||
info: {
|
||||
color: secondaryColor,
|
||||
fontSize: 25
|
||||
},
|
||||
menuIcon: {
|
||||
size: 20,
|
||||
color: fontOnPrimaryColor
|
||||
@@ -448,12 +483,7 @@ export const iconStyles = {
|
||||
menuIconInactive: {
|
||||
color: colorInActive,
|
||||
},
|
||||
infoInHeading: {
|
||||
marginRight: 5,
|
||||
color: 'black'
|
||||
},
|
||||
hiddenIcon: {
|
||||
size: 20,
|
||||
display: 'none'
|
||||
infoPopUpClose: {
|
||||
size: 25
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ chai.use(dirtyChai)
|
||||
import getSensiplanMucus from '../lib/nfp-mucus'
|
||||
|
||||
describe('getSensiplanMucus', () => {
|
||||
it('returns null if there is no value for feeling or texture', () => {
|
||||
expect(getSensiplanMucus()).to.be.null()
|
||||
expect(getSensiplanMucus(undefined, 3)).to.be.null()
|
||||
expect(getSensiplanMucus(2, undefined)).to.be.null()
|
||||
})
|
||||
|
||||
describe('results in t for:', () => {
|
||||
it('dry feeling and no texture', function () {
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Any copyright is dedicated to the Public Domain.
|
||||
# http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
set -eEu -o pipefail
|
||||
shopt -s extdebug
|
||||
IFS=$'\n\t'
|
||||
trap 'onFailure $?' ERR
|
||||
|
||||
function onFailure() {
|
||||
echo "Unhandled script error $1 at ${BASH_SOURCE[0]}:${BASH_LINENO[0]}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
git add -A
|
||||
git commit -m "release: $(cat package.json | $(npm bin)/jase version)"
|
||||
git tag v$(cat package.json | $(npm bin)/jase version)
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const updateChangelog = require('basic-changelog')
|
||||
|
||||
const opts = {
|
||||
filterCommitsStartingWith: ['release:']
|
||||
}
|
||||
|
||||
updateChangelog('./CHANGELOG.md', opts, err => {
|
||||
if (err) {
|
||||
console.error('Something went wrong trying to update the changelog:')
|
||||
console.error(err)
|
||||
return
|
||||
}
|
||||
console.log('Changelog successfully updated')
|
||||
})
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// from https://gitlab.com/staltz/manyverse/blob/master/tools/update-version.js
|
||||
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const ReactNativeVersion = require('react-native-version')
|
||||
const readline = require('readline')
|
||||
const leftPad = require('left-pad')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const currentVersion = JSON.parse(fs.readFileSync('./package.json')).version
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
|
||||
function createTodaysVersion(attempt) {
|
||||
const today = new Date()
|
||||
const yy = today.getFullYear() - 2000 // So it's two digits
|
||||
const mm = leftPad(today.getMonth() + 1, 2, '0')
|
||||
const d = today.getDate()
|
||||
if (attempt === 0) {
|
||||
return `0.${yy}${mm}.${d}-beta`
|
||||
} else {
|
||||
const letter = String.fromCharCode(97 + attempt) // 0=a, 1=b, 2=c, ...
|
||||
return `0.${yy}${mm}.${d}-beta.${letter}`
|
||||
}
|
||||
}
|
||||
|
||||
let nextVersion
|
||||
for (let i = 0 /* letter a */; i <= 25 /* letter z */; i++) {
|
||||
nextVersion = createTodaysVersion(i)
|
||||
if (nextVersion !== currentVersion) break
|
||||
}
|
||||
if (nextVersion === currentVersion) {
|
||||
console.error('I dont know what else to generate beyong ' + nextVersion)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
rl.question('Next version will be `' + nextVersion + '`, okay? y/n ', yn => {
|
||||
if (yn !== 'y' && yn !== 'Y') {
|
||||
console.log('Release cancelled.\n')
|
||||
process.exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
const pkgJSON = JSON.parse(fs.readFileSync('./package.json'))
|
||||
const pkgLockJSON = JSON.parse(fs.readFileSync('./package-lock.json'))
|
||||
pkgJSON.version = nextVersion
|
||||
pkgLockJSON.version = nextVersion
|
||||
fs.writeFileSync('./package.json', JSON.stringify(pkgJSON, null, 2))
|
||||
fs.writeFileSync('./package-lock.json', JSON.stringify(pkgLockJSON, null, 2))
|
||||
|
||||
ReactNativeVersion.version(
|
||||
{
|
||||
neverAmend: true,
|
||||
target: 'android',
|
||||
},
|
||||
path.resolve(__dirname, '../'),
|
||||
).catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
rl.close()
|
||||
})
|
||||
Reference in New Issue
Block a user