Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a783cf99d | |||
| 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 | |||
| c22bff5948 |
@@ -1,24 +1,55 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
## 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
|
||||
|
||||
## [0.0.2] - 2019-04-16
|
||||
## Second updated beta release version
|
||||
### 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
|
||||
|
||||
## [0.0.1] - 2019-02-15
|
||||
## 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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -10,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'
|
||||
@@ -133,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>
|
||||
|
||||
@@ -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 = {
|
||||
@@ -44,7 +45,7 @@ const styles = {
|
||||
},
|
||||
stroke: {
|
||||
color: gridColor,
|
||||
width: gridLineWidth,
|
||||
width: gridLineWidthVertical,
|
||||
}
|
||||
},
|
||||
symptomIcon: {
|
||||
@@ -110,10 +111,10 @@ const styles = {
|
||||
},
|
||||
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
|
||||
@@ -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>
|
||||
|
||||
@@ -1,94 +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 ||
|
||||
(Object.keys(currentSymptomValue).length === 0 && currentSymptomValue.constructor === Object) ||
|
||||
(Object.values(currentSymptomValue).every(x => !x) && currentSymptomValue.constructor === Object)
|
||||
),
|
||||
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,45 +41,30 @@ 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}
|
||||
explainer={bleeding.heaviness.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={bleedingRadioProps}
|
||||
active={this.state.currentValue}
|
||||
onSelect={val => this.setState({ currentValue: val })}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={bleeding.exclude.header}
|
||||
explainer={bleeding.exclude.explainer}
|
||||
inline={true}
|
||||
>
|
||||
<Switch
|
||||
onValueChange={(val) => {
|
||||
this.setState({ exclude: val })
|
||||
}}
|
||||
value={this.state.exclude}
|
||||
/>
|
||||
</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>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header={bleeding.heaviness.header}
|
||||
explainer={bleeding.heaviness.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={bleedingRadioProps}
|
||||
active={this.state.currentValue}
|
||||
onSelect={val => this.setState({ currentValue: val })}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={bleeding.exclude.header}
|
||||
explainer={bleeding.exclude.explainer}
|
||||
inline={true}
|
||||
>
|
||||
<Switch
|
||||
onValueChange={(val) => {
|
||||
this.setState({ exclude: val })
|
||||
}}
|
||||
value={this.state.exclude}
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,70 +49,53 @@ 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"
|
||||
explainer={labels.opening.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={cervixOpeningRadioProps}
|
||||
active={this.state.opening}
|
||||
onSelect={val => this.setState({ opening: val })}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header="Firmness"
|
||||
explainer={labels.firmness.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={cervixFirmnessRadioProps}
|
||||
active={this.state.firmness}
|
||||
onSelect={val => this.setState({ firmness: val })}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header="Position"
|
||||
explainer={labels.position.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={cervixPositionRadioProps}
|
||||
active={this.state.position}
|
||||
onSelect={val => this.setState({ position: val })}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header="Exclude"
|
||||
explainer="You can exclude this value if you don't want to use it for fertility detection"
|
||||
inline={true}
|
||||
>
|
||||
<Switch
|
||||
onValueChange={(val) => {
|
||||
this.setState({ exclude: val })
|
||||
}}
|
||||
value={this.state.exclude}
|
||||
/>
|
||||
</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>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header="Opening"
|
||||
explainer={labels.opening.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={cervixOpeningRadioProps}
|
||||
active={this.state.opening}
|
||||
onSelect={val => this.setState({ opening: val })}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header="Firmness"
|
||||
explainer={labels.firmness.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={cervixFirmnessRadioProps}
|
||||
active={this.state.firmness}
|
||||
onSelect={val => this.setState({ firmness: val })}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header="Position"
|
||||
explainer={labels.position.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={cervixPositionRadioProps}
|
||||
active={this.state.position}
|
||||
onSelect={val => this.setState({ position: val })}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header="Exclude"
|
||||
explainer="You can exclude this value if you don't want to use it for fertility detection"
|
||||
inline={true}
|
||||
>
|
||||
<Switch
|
||||
onValueChange={(val) => {
|
||||
this.setState({ exclude: val })
|
||||
}}
|
||||
value={this.state.exclude}
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,51 @@
|
||||
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}
|
||||
explainer={desire.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={desireRadioProps}
|
||||
active={this.state.currentValue}
|
||||
onSelect={val => this.setState({ currentValue: val })}
|
||||
/>
|
||||
</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>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header={desire.header}
|
||||
explainer={desire.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={desireRadioProps}
|
||||
active={this.state.currentValue}
|
||||
onSelect={val => this.setState({ currentValue: val })}
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
import styles, {iconStyles} 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]
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<FramedSegment
|
||||
style={styles.framedSegmentLast}
|
||||
title={labels[currentSymptom].title}
|
||||
>
|
||||
<AppText>{labels[currentSymptom].text}</AppText>
|
||||
</FramedSegment>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
export default function InfoSymptom(props) {
|
||||
return (
|
||||
<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,19 +45,18 @@ export default class Mood extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
renderContent() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
explainer={labels.explainer}
|
||||
>
|
||||
<SelectBoxGroup
|
||||
labels={labels.categories}
|
||||
onSelect={this.toggleState}
|
||||
optionsState={this.state}
|
||||
/>
|
||||
{ this.state.other &&
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
explainer={labels.explainer}
|
||||
>
|
||||
<SelectBoxGroup
|
||||
labels={labels.categories}
|
||||
onSelect={this.toggleState}
|
||||
optionsState={this.state}
|
||||
/>
|
||||
{ this.state.other &&
|
||||
<TextInput
|
||||
autoFocus={this.state.focusTextArea}
|
||||
multiline={true}
|
||||
@@ -55,24 +66,9 @@ export default class Mood extends Component {
|
||||
this.setState({note: val})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</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>
|
||||
}
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,62 +49,43 @@ 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'
|
||||
explainer={labels.feeling.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={mucusFeeling}
|
||||
onSelect={val => this.setState({ feeling: val })}
|
||||
active={this.state.feeling}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header='Texture'
|
||||
explainer={labels.texture.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={mucusTexture}
|
||||
onSelect={val => this.setState({ texture: val })}
|
||||
active={this.state.texture}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header="Exclude"
|
||||
explainer={labels.excludeExplainer}
|
||||
inline={true}
|
||||
>
|
||||
<Switch
|
||||
onValueChange={(val) => {
|
||||
this.setState({ exclude: val })
|
||||
}}
|
||||
value={this.state.exclude}
|
||||
/>
|
||||
</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>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header='Feeling'
|
||||
explainer={labels.feeling.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={mucusFeeling}
|
||||
onSelect={val => this.setState({ feeling: val })}
|
||||
active={this.state.feeling}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header='Texture'
|
||||
explainer={labels.texture.explainer}
|
||||
>
|
||||
<SelectTabGroup
|
||||
buttons={mucusTexture}
|
||||
onSelect={val => this.setState({ texture: val })}
|
||||
active={this.state.texture}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header="Exclude"
|
||||
explainer={labels.excludeExplainer}
|
||||
inline={true}
|
||||
>
|
||||
<Switch
|
||||
onValueChange={(val) => {
|
||||
this.setState({ exclude: val })
|
||||
}}
|
||||
value={this.state.exclude}
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,55 @@
|
||||
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}
|
||||
>
|
||||
<TextInput
|
||||
autoFocus={!this.state.currentValue}
|
||||
multiline={true}
|
||||
placeholder={sharedLabels.enter}
|
||||
onChangeText={(val) => {
|
||||
this.setState({ currentValue: val })
|
||||
}}
|
||||
value={this.state.currentValue}
|
||||
/>
|
||||
</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>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
explainer={noteExplainer}
|
||||
>
|
||||
<TextInput
|
||||
autoFocus={!this.state.currentValue}
|
||||
multiline={true}
|
||||
placeholder={sharedLabels.enter}
|
||||
onChangeText={(val) => {
|
||||
this.setState({ currentValue: val })
|
||||
}}
|
||||
value={this.state.currentValue}
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,19 +48,18 @@ export default class Pain extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
renderContent() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
explainer={labels.explainer}
|
||||
>
|
||||
<SelectBoxGroup
|
||||
labels={labels.categories}
|
||||
onSelect={this.toggleState}
|
||||
optionsState={this.state}
|
||||
/>
|
||||
{ this.state.other &&
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
explainer={labels.explainer}
|
||||
>
|
||||
<SelectBoxGroup
|
||||
labels={labels.categories}
|
||||
onSelect={this.toggleState}
|
||||
optionsState={this.state}
|
||||
/>
|
||||
{ this.state.other &&
|
||||
<TextInput
|
||||
autoFocus={this.state.focusTextArea}
|
||||
multiline={true}
|
||||
@@ -56,24 +69,8 @@ export default class Pain extends Component {
|
||||
this.setState({note: val})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
</SymptomSection>
|
||||
</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,32 +48,31 @@ export default class Sex extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
renderContent() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header={sexLabels.header}
|
||||
explainer={sexLabels.explainer}
|
||||
>
|
||||
<SelectBoxGroup
|
||||
labels={sexLabels.categories}
|
||||
onSelect={this.toggleState}
|
||||
optionsState={this.state}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={contraceptivesLabels.header}
|
||||
explainer={contraceptivesLabels.explainer}
|
||||
>
|
||||
<SelectBoxGroup
|
||||
labels={contraceptivesLabels.categories}
|
||||
onSelect={this.toggleState}
|
||||
optionsState={this.state}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header={sexLabels.header}
|
||||
explainer={sexLabels.explainer}
|
||||
>
|
||||
<SelectBoxGroup
|
||||
labels={sexLabels.categories}
|
||||
onSelect={this.toggleState}
|
||||
optionsState={this.state}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={contraceptivesLabels.header}
|
||||
explainer={contraceptivesLabels.explainer}
|
||||
>
|
||||
<SelectBoxGroup
|
||||
labels={contraceptivesLabels.categories}
|
||||
onSelect={this.toggleState}
|
||||
optionsState={this.state}
|
||||
/>
|
||||
</SymptomSection>
|
||||
|
||||
{this.state.other &&
|
||||
{this.state.other &&
|
||||
<TextInput
|
||||
autoFocus={this.state.focusTextArea}
|
||||
multiline={true}
|
||||
@@ -69,23 +82,8 @@ export default class Sex extends Component {
|
||||
this.setState({ note: val })
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</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>
|
||||
}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
<SymptomSectionHeader flex={1}>{p.header}</SymptomSectionHeader>
|
||||
<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}
|
||||
>
|
||||
<View flex={1}>
|
||||
<AppText>{p.explainer}</AppText>
|
||||
</View>
|
||||
{ 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,170 +42,154 @@ 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}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={labels.time}
|
||||
inline={true}
|
||||
>
|
||||
<TextInput
|
||||
style={styles.temperatureTextInput}
|
||||
onFocus={() => {
|
||||
Keyboard.dismiss()
|
||||
this.setState({ isTimePickerVisible: true })
|
||||
}}
|
||||
value={this.state.time}
|
||||
/>
|
||||
<DateTimePicker
|
||||
mode="time"
|
||||
isVisible={this.state.isTimePickerVisible}
|
||||
onConfirm={jsDate => {
|
||||
this.setState({
|
||||
time: padWithZeros(jsDate),
|
||||
isTimePickerVisible: false
|
||||
})
|
||||
}}
|
||||
onCancel={() => this.setState({ isTimePickerVisible: false })}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={labels.note.header}
|
||||
explainer={labels.note.explainer}
|
||||
>
|
||||
<TextInput
|
||||
multiline={true}
|
||||
autoFocus={this.state.focusTextArea}
|
||||
placeholder={sharedLabels.enter}
|
||||
value={this.state.note}
|
||||
onChangeText={(val) => {
|
||||
this.setState({ note: val })
|
||||
}}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={labels.exclude.header}
|
||||
explainer={labels.exclude.explainer}
|
||||
inline={true}
|
||||
>
|
||||
<Switch
|
||||
onValueChange={(val) => {
|
||||
this.setState({ exclude: val })
|
||||
}}
|
||||
value={this.state.exclude}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<ScrollView style={styles.page}>
|
||||
<SymptomSection
|
||||
header={labels.temperature.header}
|
||||
explainer={labels.temperature.explainer}
|
||||
>
|
||||
<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>
|
||||
</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)
|
||||
{this.state.outOfRangeWarning &&
|
||||
<AppText style={styles.hint}>
|
||||
{this.state.outOfRangeWarning}
|
||||
</AppText>
|
||||
}
|
||||
navigate={this.props.navigate}
|
||||
autoShowDayView={false}
|
||||
/>
|
||||
</View>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={labels.time}
|
||||
>
|
||||
<View style={styles.framedSegmentInlineChildren}>
|
||||
<AppTextInput
|
||||
style={[styles.temperatureTextInput]}
|
||||
onFocus={this.showTimePicker}
|
||||
value={this.state.time}
|
||||
/>
|
||||
<DateTimePicker
|
||||
mode="time"
|
||||
isVisible={this.state.isTimePickerVisible}
|
||||
onConfirm={jsDate => {
|
||||
this.setState({
|
||||
time: padWithZeros(jsDate),
|
||||
isTimePickerVisible: false
|
||||
})
|
||||
}}
|
||||
onCancel={() => this.setState({ isTimePickerVisible: false })}
|
||||
/>
|
||||
</View>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={labels.note.header}
|
||||
explainer={labels.note.explainer}
|
||||
>
|
||||
<AppTextInput
|
||||
multiline={true}
|
||||
autoFocus={this.state.focusTextArea}
|
||||
placeholder={sharedLabels.enter}
|
||||
value={this.state.note}
|
||||
onChangeText={this.setNote}
|
||||
/>
|
||||
</SymptomSection>
|
||||
<SymptomSection
|
||||
header={labels.exclude.header}
|
||||
explainer={labels.exclude.explainer}
|
||||
inline={true}
|
||||
>
|
||||
<Switch
|
||||
onValueChange={(val) => {
|
||||
this.setState({ exclude: val })
|
||||
}}
|
||||
value={this.state.exclude}
|
||||
/>
|
||||
</SymptomSection>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 >
|
||||
<TouchableOpacity
|
||||
onPress={() => props.goToSymptomInfo()}
|
||||
style={styles.infoButton}
|
||||
>
|
||||
<FeatherIcon
|
||||
name="info"
|
||||
style={styles.symptomInfoIcon}
|
||||
{...iconStyles.symptomHeaderIcons}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{ props.deleteIconActive &&
|
||||
<TouchableOpacity
|
||||
onPress={props.deleteEntry}
|
||||
style={[
|
||||
styles.headerDeleteButton,
|
||||
]}
|
||||
>
|
||||
<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')
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { ChronoUnit, LocalDate } from 'js-joda'
|
||||
import React, { Component } from 'react'
|
||||
import { Dimensions, ScrollView, TouchableHighlight, View } from 'react-native'
|
||||
import Icon from 'react-native-vector-icons/Entypo'
|
||||
import { ScrollView, View } from 'react-native'
|
||||
|
||||
import DripHomeIcon from '../assets/drip-home-icons'
|
||||
import { getCycleDay } from '../db'
|
||||
import {
|
||||
bleedingPrediction as predictLabels,
|
||||
home as labels,
|
||||
shared,
|
||||
home as labels
|
||||
} from '../i18n/en/labels'
|
||||
import links from '../i18n/en/links'
|
||||
import cycleModule from '../lib/cycle'
|
||||
@@ -16,25 +14,7 @@ import { getFertilityStatusForDay } from '../lib/sympto-adapter'
|
||||
import styles, { cycleDayColor, periodColor, secondaryColor } from '../styles'
|
||||
import AppText from './app-text'
|
||||
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 (
|
||||
@@ -50,15 +30,22 @@ const HomeElement = ({ children, onPress, buttonColor, buttonLabel }) => {
|
||||
return (
|
||||
<View
|
||||
onPress={ onPress }
|
||||
style={ styles.homeIconElement }
|
||||
style={ styles.homeElement }
|
||||
>
|
||||
{ children }
|
||||
<Button
|
||||
style={styles.homeButton}
|
||||
onPress={ onPress }
|
||||
backgroundColor={ buttonColor }>
|
||||
{ buttonLabel }
|
||||
</Button>
|
||||
<View style={styles.homeIconAndText}>
|
||||
{children[0]}
|
||||
{children[1]}
|
||||
</View>
|
||||
|
||||
<View style={{paddingLeft: 15}}>
|
||||
{children.slice(2)}
|
||||
<Button
|
||||
style={styles.homeButton}
|
||||
onPress={ onPress }
|
||||
backgroundColor={ buttonColor }>
|
||||
{ buttonLabel }
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -74,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),
|
||||
@@ -90,12 +76,8 @@ export default class Home extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
toggleShowingMore = () => {
|
||||
this.setState({ isShowingMore: !this.state.isShowingMore })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isShowingMore, cycleDayNumber, phase, status } = this.state
|
||||
const { cycleDayNumber, phase, status } = this.state
|
||||
const { navigate } = this.props
|
||||
const cycleDayMoreText = cycleDayNumber ?
|
||||
labels.cycleDayKnown(cycleDayNumber) :
|
||||
@@ -116,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
|
||||
@@ -130,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}>
|
||||
{this.state.predictionText}
|
||||
</AppText>
|
||||
}
|
||||
<AppText style={styles.homeDescriptionText}>
|
||||
{this.state.predictionText}
|
||||
</AppText>
|
||||
</HomeElement>
|
||||
|
||||
<HomeElement
|
||||
@@ -152,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.wiki.url}.` }
|
||||
</AppText>
|
||||
</View>
|
||||
<AppText style={styles.homeDescriptionText}>
|
||||
{`${labels.phase(phase)} (${status})`}
|
||||
</AppText>
|
||||
}
|
||||
<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) {
|
||||
@@ -208,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,37 +1,91 @@
|
||||
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 = () => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<FramedSegment title={labels.export.button}>
|
||||
<AppText>{labels.export.segmentExplainer}</AppText>
|
||||
<SettingsButton onPress={openShareDialogAndExport}>
|
||||
{labels.export.button}
|
||||
</SettingsButton>
|
||||
</FramedSegment>
|
||||
<FramedSegment title={labels.import.button}>
|
||||
<AppText>{labels.import.segmentExplainer}</AppText>
|
||||
<SettingsButton onPress={openImportDialogAndImport}>
|
||||
{labels.import.button}
|
||||
</SettingsButton>
|
||||
</FramedSegment>
|
||||
<FramedSegment
|
||||
title={labels.deleteSegment.title}
|
||||
last
|
||||
>
|
||||
<AppText>{labels.deleteSegment.explainer}</AppText>
|
||||
<DeleteData />
|
||||
</FramedSegment>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
export default class DataManagement extends Component {
|
||||
|
||||
export default DataManagement
|
||||
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={this.startExport}>
|
||||
{labels.export.button}
|
||||
</SettingsButton>
|
||||
</FramedSegment>
|
||||
<FramedSegment title={labels.import.button}>
|
||||
<AppText>{labels.import.segmentExplainer}</AppText>
|
||||
<SettingsButton
|
||||
onPress= {this.startImport}
|
||||
>
|
||||
{labels.import.button}
|
||||
</SettingsButton>
|
||||
</FramedSegment>
|
||||
<FramedSegment
|
||||
title={labels.deleteSegment.title}
|
||||
last
|
||||
>
|
||||
<AppText>{labels.deleteSegment.explainer}</AppText>
|
||||
<DeleteData
|
||||
isDeletingData = { currentAction === 'delete' }
|
||||
onStartDeletion = {() => this.setCurrentAction('delete')}
|
||||
/>
|
||||
</FramedSegment>
|
||||
</View>
|
||||
</ScrollView>
|
||||
}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,8 +28,8 @@ 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>
|
||||
</FramedSegment>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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]
|
||||
@@ -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",
|
||||
|
||||
@@ -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. Please find more information on NFP rules here:",
|
||||
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.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,4 +14,8 @@ export default {
|
||||
website: {
|
||||
url: 'https://bloodyhealth.gitlab.io/'
|
||||
},
|
||||
donate: {
|
||||
url: 'https://ko-fi.com/dripapp',
|
||||
text: 'here'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
@@ -123,5 +123,9 @@ You can contact us by bloodyhealth@mailbox.org.`
|
||||
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.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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.`,
|
||||
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.`,
|
||||
@@ -85,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}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
|
||||
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
|
||||
@@ -72,6 +71,8 @@
|
||||
E09F3B05A4F84E9883101CC7 /* libRNNodeJsMobile.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F992F2D99E614DD79FAD6565 /* libRNNodeJsMobile.a */; };
|
||||
E43EF009AC8C4698AB322190 /* NodeMobile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C225FC4966694B9FBD32E946 /* NodeMobile.framework */; };
|
||||
E4584E55EEC24302A3E84A23 /* nodejs-project in Resources */ = {isa = PBXBuildFile; fileRef = 6466AE2461BE4FA88B8372F0 /* nodejs-project */; };
|
||||
77500FAD5ADD402AAD2D9972 /* AntDesign.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3102FB76D69C42938E0E126D /* AntDesign.ttf */; };
|
||||
AA800A96BB73482AA90E29B8 /* OpenSans-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 885BDE0B3896402F99D0C860 /* OpenSans-LightItalic.ttf */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -515,6 +516,8 @@
|
||||
F5039D0A572B4BBCB7995891 /* libRNVectorIcons.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNVectorIcons.a; sourceTree = "<group>"; };
|
||||
F59A471BDE4144A1A41D4B52 /* Feather.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Feather.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = "<group>"; };
|
||||
F992F2D99E614DD79FAD6565 /* libRNNodeJsMobile.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNNodeJsMobile.a; sourceTree = "<group>"; };
|
||||
3102FB76D69C42938E0E126D /* AntDesign.ttf */ = {isa = PBXFileReference; name = "AntDesign.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
885BDE0B3896402F99D0C860 /* OpenSans-LightItalic.ttf */ = {isa = PBXFileReference; name = "OpenSans-LightItalic.ttf"; path = "../assets/fonts/OpenSans-LightItalic.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -606,6 +609,8 @@
|
||||
5E7B0A75F8004C6699B70F86 /* Prompt-ExtraLight.ttf */,
|
||||
673C016DDDD74C2F89050279 /* OpenSans-Light.ttf */,
|
||||
644690BCCEBF41789960B9A2 /* OpenSans-SemiBold.ttf */,
|
||||
3102FB76D69C42938E0E126D /* AntDesign.ttf */,
|
||||
885BDE0B3896402F99D0C860 /* OpenSans-LightItalic.ttf */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
@@ -949,9 +954,9 @@
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
2B572382D4504B8FB4B9D251 /* Embed Frameworks */,
|
||||
FF1D4199225D4DA692E5AEB6 /* Build NodeJS Mobile Native Modules */,
|
||||
BF4F9DB28A984C43A497C8E6 /* Sign NodeJS Mobile Native Modules */,
|
||||
6292723A374D49FDB7CF4114 /* Remove NodeJS Mobile Framework Simulator Strips */,
|
||||
D6AE48E8124449EE863F342D /* Build NodeJS Mobile Native Modules */,
|
||||
7D43D3729FAE42C0859CDC36 /* Sign NodeJS Mobile Native Modules */,
|
||||
54483AF1DA474A71931D6D6A /* Remove NodeJS Mobile Framework Simulator Strips */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -1510,6 +1515,8 @@
|
||||
B9A5B9946C4C456C823B7641 /* Prompt-ExtraLight.ttf in Resources */,
|
||||
5D921C348AC14944835A4D82 /* OpenSans-Light.ttf in Resources */,
|
||||
71D0BCE4666A4AB8A0874B5A /* OpenSans-SemiBold.ttf in Resources */,
|
||||
77500FAD5ADD402AAD2D9972 /* AntDesign.ttf in Resources */,
|
||||
AA800A96BB73482AA90E29B8 /* OpenSans-LightItalic.ttf in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1559,47 +1566,159 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh";
|
||||
};
|
||||
6292723A374D49FDB7CF4114 /* Remove NodeJS Mobile Framework Simulator Strips */ = {
|
||||
D6AE48E8124449EE863F342D /* Build NodeJS Mobile Native Modules */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Remove NodeJS Mobile Framework Simulator Strips";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\nset -e\nFRAMEWORK_BINARY_PATH=\"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/NodeMobile.framework/NodeMobile\"\nFRAMEWORK_STRIPPED_PATH=\"$FRAMEWORK_BINARY_PATH-strip\"\nif [ \"$PLATFORM_NAME\" != \"iphonesimulator\" ]; then\n if $(lipo \"$FRAMEWORK_BINARY_PATH\" -verify_arch \"x86_64\") ; then\n lipo -output \"$FRAMEWORK_STRIPPED_PATH\" -remove \"x86_64\" \"$FRAMEWORK_BINARY_PATH\"\n rm \"$FRAMEWORK_BINARY_PATH\"\n mv \"$FRAMEWORK_STRIPPED_PATH\" \"$FRAMEWORK_BINARY_PATH\"\n echo \"Removed simulator strip from NodeMobile.framework\"\n fi\nfi\n";
|
||||
};
|
||||
BF4F9DB28A984C43A497C8E6 /* Sign NodeJS Mobile Native Modules */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Sign NodeJS Mobile Native Modules";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\nset -e\nif [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then\n# If build native modules preference is not set, look for it in the project's\n#nodejs-assets/BUILD_NATIVE_MODULES.txt file.\nNODEJS_ASSETS_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../nodejs-assets/ && pwd )\"\nPREFERENCE_FILE_PATH=\"$NODEJS_ASSETS_DIR/BUILD_NATIVE_MODULES.txt\"\n if [ -f \"$PREFERENCE_FILE_PATH\" ]; then\n NODEJS_MOBILE_BUILD_NATIVE_MODULES=\"$(cat $PREFERENCE_FILE_PATH | xargs)\"\n fi\nfi\nif [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then\n# If build native modules preference is not set, try to find .gyp files\n#to turn it on.\n gypfiles=($(find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -type f -name \"*.gyp\"))\n if [ ${#gypfiles[@]} -gt 0 ]; then\n NODEJS_MOBILE_BUILD_NATIVE_MODULES=1\n else\n NODEJS_MOBILE_BUILD_NATIVE_MODULES=0\n fi\nfi\nif [ \"1\" != \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then exit 0; fi\n# Delete object files\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.o\" -type f -delete\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.a\" -type f -delete\n# Create Info.plist for each framework built and loader override.\nPATCH_SCRIPT_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/scripts/ && pwd )\"\nNODEJS_PROJECT_DIR=\"$( cd \"$CODESIGNING_FOLDER_PATH\" && cd nodejs-project/ && pwd )\"\nnode \"$PATCH_SCRIPT_DIR\"/ios-create-plists-and-dlopen-override.js $NODEJS_PROJECT_DIR\n# Embed every resulting .framework in the application and delete them afterwards.\nembed_framework()\n{\n FRAMEWORK_NAME=\"$(basename \"$1\")\"\n cp -r \"$1\" \"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/\"\n \n /usr/bin/codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY --preserve-metadata=identifier,entitlements,flags --timestamp=none \"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/$FRAMEWORK_NAME\"\n}\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.framework\" -type d | while read frmwrk_path; do embed_framework \"$frmwrk_path\"; done\n\n#Delete gyp temporary .deps dependency folders from the project structure.\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/.deps/*\" -delete\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \".deps\" -type d -delete\n\n#Delete frameworks from their build paths\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/*.framework/*\" -delete\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.framework\" -type d -delete\n";
|
||||
};
|
||||
FF1D4199225D4DA692E5AEB6 /* Build NodeJS Mobile Native Modules */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Build NodeJS Mobile Native Modules";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\nset -e\nif [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then\n# If build native modules preference is not set, look for it in the project's\n#nodejs-assets/BUILD_NATIVE_MODULES.txt file.\nNODEJS_ASSETS_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../nodejs-assets/ && pwd )\"\nPREFERENCE_FILE_PATH=\"$NODEJS_ASSETS_DIR/BUILD_NATIVE_MODULES.txt\"\n if [ -f \"$PREFERENCE_FILE_PATH\" ]; then\n NODEJS_MOBILE_BUILD_NATIVE_MODULES=\"$(cat $PREFERENCE_FILE_PATH | xargs)\"\n fi\nfi\nif [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then\n# If build native modules preference is not set, try to find .gyp files\n#to turn it on.\n gypfiles=($(find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -type f -name \"*.gyp\"))\n if [ ${#gypfiles[@]} -gt 0 ]; then\n NODEJS_MOBILE_BUILD_NATIVE_MODULES=1\n else\n NODEJS_MOBILE_BUILD_NATIVE_MODULES=0\n fi\nfi\nif [ \"1\" != \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then exit 0; fi\n# Delete object files that may already come from within the npm package.\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.o\" -type f -delete\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.a\" -type f -delete\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.node\" -type f -delete\n# Delete bundle contents that may be there from previous builds.\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/*.node/*\" -delete\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.node\" -type d -delete\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/*.framework/*\" -delete\nfind \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.framework\" -type d -delete\n# Apply patches to the modules package.json\nif [ -d \"$CODESIGNING_FOLDER_PATH\"/nodejs-project/node_modules/ ]; then\n PATCH_SCRIPT_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/scripts/ && pwd )\"\n NODEJS_PROJECT_MODULES_DIR=\"$( cd \"$CODESIGNING_FOLDER_PATH\" && cd nodejs-project/node_modules/ && pwd )\"\n node \"$PATCH_SCRIPT_DIR\"/patch-package.js $NODEJS_PROJECT_MODULES_DIR\nfi\n# Get the nodejs-mobile-gyp location\nif [ -d \"$PROJECT_DIR/../node_modules/nodejs-mobile-gyp/\" ]; then\n NODEJS_MOBILE_GYP_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-gyp/ && pwd )\"\nelse\n NODEJS_MOBILE_GYP_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/node_modules/nodejs-mobile-gyp/ && pwd )\"\nfi\nNODEJS_MOBILE_GYP_BIN_FILE=\"$NODEJS_MOBILE_GYP_DIR\"/bin/node-gyp.js\n# Rebuild modules with right environment\nNODEJS_HEADERS_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/ios/libnode/ && pwd )\"\npushd $CODESIGNING_FOLDER_PATH/nodejs-project/\nif [ \"$PLATFORM_NAME\" == \"iphoneos\" ]\nthen\n GYP_DEFINES=\"OS=ios\" npm_config_nodedir=\"$NODEJS_HEADERS_DIR\" npm_config_node_gyp=\"$NODEJS_MOBILE_GYP_BIN_FILE\" npm_config_platform=\"ios\" npm_config_format=\"make-ios\" npm_config_node_engine=\"chakracore\" npm_config_arch=\"arm64\" npm --verbose rebuild --build-from-source\nelse\n GYP_DEFINES=\"OS=ios\" npm_config_nodedir=\"$NODEJS_HEADERS_DIR\" npm_config_node_gyp=\"$NODEJS_MOBILE_GYP_BIN_FILE\" npm_config_platform=\"ios\" npm_config_format=\"make-ios\" npm_config_node_engine=\"chakracore\" npm_config_arch=\"x64\" npm --verbose rebuild --build-from-source\nfi\npopd\n";
|
||||
shellScript = "
|
||||
set -e
|
||||
if [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then
|
||||
# If build native modules preference is not set, look for it in the project's
|
||||
#nodejs-assets/BUILD_NATIVE_MODULES.txt file.
|
||||
NODEJS_ASSETS_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../nodejs-assets/ && pwd )\"
|
||||
PREFERENCE_FILE_PATH=\"$NODEJS_ASSETS_DIR/BUILD_NATIVE_MODULES.txt\"
|
||||
if [ -f \"$PREFERENCE_FILE_PATH\" ]; then
|
||||
NODEJS_MOBILE_BUILD_NATIVE_MODULES=\"$(cat $PREFERENCE_FILE_PATH | xargs)\"
|
||||
fi
|
||||
fi
|
||||
if [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then
|
||||
# If build native modules preference is not set, try to find .gyp files
|
||||
#to turn it on.
|
||||
gypfiles=($(find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -type f -name \"*.gyp\"))
|
||||
if [ ${#gypfiles[@]} -gt 0 ]; then
|
||||
NODEJS_MOBILE_BUILD_NATIVE_MODULES=1
|
||||
else
|
||||
NODEJS_MOBILE_BUILD_NATIVE_MODULES=0
|
||||
fi
|
||||
fi
|
||||
if [ \"1\" != \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then exit 0; fi
|
||||
# Delete object files that may already come from within the npm package.
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.o\" -type f -delete
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.a\" -type f -delete
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.node\" -type f -delete
|
||||
# Delete bundle contents that may be there from previous builds.
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/*.node/*\" -delete
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.node\" -type d -delete
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/*.framework/*\" -delete
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.framework\" -type d -delete
|
||||
# Apply patches to the modules package.json
|
||||
if [ -d \"$CODESIGNING_FOLDER_PATH\"/nodejs-project/node_modules/ ]; then
|
||||
PATCH_SCRIPT_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/scripts/ && pwd )\"
|
||||
NODEJS_PROJECT_MODULES_DIR=\"$( cd \"$CODESIGNING_FOLDER_PATH\" && cd nodejs-project/node_modules/ && pwd )\"
|
||||
node \"$PATCH_SCRIPT_DIR\"/patch-package.js $NODEJS_PROJECT_MODULES_DIR
|
||||
fi
|
||||
# Get the nodejs-mobile-gyp location
|
||||
if [ -d \"$PROJECT_DIR/../node_modules/nodejs-mobile-gyp/\" ]; then
|
||||
NODEJS_MOBILE_GYP_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-gyp/ && pwd )\"
|
||||
else
|
||||
NODEJS_MOBILE_GYP_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/node_modules/nodejs-mobile-gyp/ && pwd )\"
|
||||
fi
|
||||
NODEJS_MOBILE_GYP_BIN_FILE=\"$NODEJS_MOBILE_GYP_DIR\"/bin/node-gyp.js
|
||||
# Rebuild modules with right environment
|
||||
NODEJS_HEADERS_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/ios/libnode/ && pwd )\"
|
||||
pushd $CODESIGNING_FOLDER_PATH/nodejs-project/
|
||||
if [ \"$PLATFORM_NAME\" == \"iphoneos\" ]
|
||||
then
|
||||
GYP_DEFINES=\"OS=ios\" npm_config_nodedir=\"$NODEJS_HEADERS_DIR\" npm_config_node_gyp=\"$NODEJS_MOBILE_GYP_BIN_FILE\" npm_config_platform=\"ios\" npm_config_format=\"make-ios\" npm_config_node_engine=\"chakracore\" npm_config_arch=\"arm64\" npm --verbose rebuild --build-from-source
|
||||
else
|
||||
GYP_DEFINES=\"OS=ios\" npm_config_nodedir=\"$NODEJS_HEADERS_DIR\" npm_config_node_gyp=\"$NODEJS_MOBILE_GYP_BIN_FILE\" npm_config_platform=\"ios\" npm_config_format=\"make-ios\" npm_config_node_engine=\"chakracore\" npm_config_arch=\"x64\" npm --verbose rebuild --build-from-source
|
||||
fi
|
||||
popd
|
||||
";
|
||||
};
|
||||
7D43D3729FAE42C0859CDC36 /* Sign NodeJS Mobile Native Modules */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Sign NodeJS Mobile Native Modules";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
set -e
|
||||
if [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then
|
||||
# If build native modules preference is not set, look for it in the project's
|
||||
#nodejs-assets/BUILD_NATIVE_MODULES.txt file.
|
||||
NODEJS_ASSETS_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../nodejs-assets/ && pwd )\"
|
||||
PREFERENCE_FILE_PATH=\"$NODEJS_ASSETS_DIR/BUILD_NATIVE_MODULES.txt\"
|
||||
if [ -f \"$PREFERENCE_FILE_PATH\" ]; then
|
||||
NODEJS_MOBILE_BUILD_NATIVE_MODULES=\"$(cat $PREFERENCE_FILE_PATH | xargs)\"
|
||||
fi
|
||||
fi
|
||||
if [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then
|
||||
# If build native modules preference is not set, try to find .gyp files
|
||||
#to turn it on.
|
||||
gypfiles=($(find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -type f -name \"*.gyp\"))
|
||||
if [ ${#gypfiles[@]} -gt 0 ]; then
|
||||
NODEJS_MOBILE_BUILD_NATIVE_MODULES=1
|
||||
else
|
||||
NODEJS_MOBILE_BUILD_NATIVE_MODULES=0
|
||||
fi
|
||||
fi
|
||||
if [ \"1\" != \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then exit 0; fi
|
||||
# Delete object files
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.o\" -type f -delete
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.a\" -type f -delete
|
||||
# Create Info.plist for each framework built and loader override.
|
||||
PATCH_SCRIPT_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/scripts/ && pwd )\"
|
||||
NODEJS_PROJECT_DIR=\"$( cd \"$CODESIGNING_FOLDER_PATH\" && cd nodejs-project/ && pwd )\"
|
||||
node \"$PATCH_SCRIPT_DIR\"/ios-create-plists-and-dlopen-override.js $NODEJS_PROJECT_DIR
|
||||
# Embed every resulting .framework in the application and delete them afterwards.
|
||||
embed_framework()
|
||||
{
|
||||
FRAMEWORK_NAME=\"$(basename \"$1\")\"
|
||||
cp -r \"$1\" \"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/\"
|
||||
|
||||
/usr/bin/codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY --preserve-metadata=identifier,entitlements,flags --timestamp=none \"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/$FRAMEWORK_NAME\"
|
||||
}
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.framework\" -type d | while read frmwrk_path; do embed_framework \"$frmwrk_path\"; done
|
||||
|
||||
#Delete gyp temporary .deps dependency folders from the project structure.
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/.deps/*\" -delete
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \".deps\" -type d -delete
|
||||
|
||||
#Delete frameworks from their build paths
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/*.framework/*\" -delete
|
||||
find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.framework\" -type d -delete
|
||||
";
|
||||
};
|
||||
54483AF1DA474A71931D6D6A /* Remove NodeJS Mobile Framework Simulator Strips */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove NodeJS Mobile Framework Simulator Strips";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
set -e
|
||||
FRAMEWORK_BINARY_PATH=\"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/NodeMobile.framework/NodeMobile\"
|
||||
FRAMEWORK_STRIPPED_PATH=\"$FRAMEWORK_BINARY_PATH-strip\"
|
||||
if [ \"$PLATFORM_NAME\" != \"iphonesimulator\" ]; then
|
||||
if $(lipo \"$FRAMEWORK_BINARY_PATH\" -verify_arch \"x86_64\") ; then
|
||||
lipo -output \"$FRAMEWORK_STRIPPED_PATH\" -remove \"x86_64\" \"$FRAMEWORK_BINARY_PATH\"
|
||||
rm \"$FRAMEWORK_BINARY_PATH\"
|
||||
mv \"$FRAMEWORK_STRIPPED_PATH\" \"$FRAMEWORK_BINARY_PATH\"
|
||||
echo \"Removed simulator strip from NodeMobile.framework\"
|
||||
fi
|
||||
fi
|
||||
";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
Key Features
|
||||
• Track your bleeding, fertility, sex, mood, pain, and more if you want
|
||||
• Graphs to analyze cycle and period duration as well as other symptoms
|
||||
• Get notified about your next period and required temperature measurements
|
||||
• Easily import, export and password protect your data
|
||||
|
||||
What makes drip special
|
||||
• Your data, your choice. Everything stays on your device.
|
||||
• Not another cute, pink app. drip is designed with gender inclusivity in mind.
|
||||
• Your body is not a black box. drip is transparent in its calculations and encourages you to think for yourself.
|
||||
• Based on science. drip detects your fertility using the symptothermal method.
|
||||
• Track what you like. Just your period or fertility symptoms, and more.
|
||||
• Open source. Contribute to the code, the documentation, translations and get involved with the community.
|
||||
• Non-commercial. drip doesn’t sell your data, no ads.
|
||||
|
||||
SPECIAL THANKS TO:
|
||||
• All the condriputors!
|
||||
• The Prototype Fund
|
||||
• The Feminist Tech Fellowship
|
||||
• The Mozilla Foundation
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 75 KiB |
@@ -0,0 +1 @@
|
||||
Menstrual cycle tracking helps you understand your cycle data and gives you insights into yourself and your body. Use drip to track your menstrual cycle and/or for fertility awareness. Unlike other menstrual cycle tracking apps, drip is open-source and leaves your data on your phone, meaning you are in control.
|
||||
@@ -0,0 +1 @@
|
||||
drip. menstrual cycle and fertility tracking
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "drip",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1905.29-beta",
|
||||
"contributors": [
|
||||
"Julia Friesel <julia.friesel@gmail.com>",
|
||||
"Marie Kochsiek",
|
||||
@@ -14,7 +14,12 @@
|
||||
"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",
|
||||
@@ -25,12 +30,12 @@
|
||||
"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",
|
||||
@@ -39,20 +44,25 @@
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
@@ -43,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
|
||||
@@ -104,9 +102,6 @@ export default StyleSheet.create({
|
||||
marginLeft: 10,
|
||||
marginTop: 6
|
||||
},
|
||||
homeView: {
|
||||
marginHorizontal: 50,
|
||||
},
|
||||
button: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
@@ -120,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: {
|
||||
@@ -157,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,
|
||||
@@ -173,12 +157,10 @@ export default StyleSheet.create({
|
||||
},
|
||||
symptomViewHeading: {
|
||||
fontWeight: 'bold',
|
||||
fontFamily: textFontBold,
|
||||
marginTop: 10
|
||||
fontFamily: textFontBold
|
||||
},
|
||||
symptomInfoIcon: {
|
||||
marginRight: 20,
|
||||
marginLeft: 20
|
||||
symptomSection: {
|
||||
marginBottom: 10
|
||||
},
|
||||
symptomBoxImage: {
|
||||
width: 50,
|
||||
@@ -236,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',
|
||||
@@ -269,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
|
||||
},
|
||||
@@ -299,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
|
||||
@@ -374,7 +378,8 @@ export default StyleSheet.create({
|
||||
borderLeftWidth: null
|
||||
},
|
||||
page: {
|
||||
marginHorizontal: 10
|
||||
marginHorizontal: 10,
|
||||
marginTop: 20,
|
||||
},
|
||||
calendarToday: {
|
||||
fontWeight: 'bold',
|
||||
@@ -383,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: {
|
||||
@@ -418,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,
|
||||
@@ -451,6 +472,10 @@ export const iconStyles = {
|
||||
symptomBoxActive: {
|
||||
color: fontOnPrimaryColor
|
||||
},
|
||||
info: {
|
||||
color: secondaryColor,
|
||||
fontSize: 25
|
||||
},
|
||||
menuIcon: {
|
||||
size: 20,
|
||||
color: fontOnPrimaryColor
|
||||
@@ -458,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 () {
|
||||
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||