Merge branch 'master' into '129-extract-text-from-stats-and-layout'

# Conflicts:
#   components/labels.js
This commit is contained in:
tina
2018-08-22 10:07:47 +00:00
44 changed files with 2928 additions and 1963 deletions
+1
View File
@@ -8,6 +8,7 @@
"eslint:recommended", "eslint:recommended",
"plugin:react/recommended" "plugin:react/recommended"
], ],
"parser": "babel-eslint",
"parserOptions": { "parserOptions": {
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": { "ecmaFeatures": {
+4 -1
View File
@@ -77,6 +77,7 @@ project.ext.react = [
] ]
apply from: "../../node_modules/react-native/react.gradle" apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
/** /**
* Set this to true to create two separate APKs instead of one: * Set this to true to create two separate APKs instead of one:
@@ -137,9 +138,11 @@ android {
} }
dependencies { dependencies {
compile project(':react-native-vector-icons')
compile project(':react-native-fs')
compile project(':react-native-document-picker')
compile project(':react-native-share') compile project(':react-native-share')
compile project(':realm') compile project(':realm')
compile project(':react-native-svg')
compile fileTree(dir: "libs", include: ["*.jar"]) compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
compile "com.facebook.react:react-native:+" // From node_modules compile "com.facebook.react:react-native:+" // From node_modules
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,10 +3,12 @@ package com.drip;
import android.app.Application; import android.app.Application;
import com.facebook.react.ReactApplication; import com.facebook.react.ReactApplication;
import com.oblador.vectoricons.VectorIconsPackage;
import com.rnfs.RNFSPackage;
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
import cl.json.RNSharePackage; import cl.json.RNSharePackage;
import cl.json.ShareApplication; import cl.json.ShareApplication;
import io.realm.react.RealmReactPackage; import io.realm.react.RealmReactPackage;
import com.horcrux.svg.SvgPackage;
import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage; import com.facebook.react.shell.MainReactPackage;
@@ -27,9 +29,11 @@ public class MainApplication extends Application implements ReactApplication, Sh
protected List<ReactPackage> getPackages() { protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList( return Arrays.<ReactPackage>asList(
new MainReactPackage(), new MainReactPackage(),
new VectorIconsPackage(),
new RNFSPackage(),
new ReactNativeDocumentPicker(),
new RNSharePackage(), new RNSharePackage(),
new RealmReactPackage(), new RealmReactPackage()
new SvgPackage()
); );
} }
+6 -2
View File
@@ -1,9 +1,13 @@
rootProject.name = 'drip' rootProject.name = 'drip'
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':react-native-document-picker'
project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android')
include ':react-native-share' include ':react-native-share'
project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android') project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android')
include ':realm' include ':realm'
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android') project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':app' include ':app'
+21 -9
View File
@@ -1,4 +1,4 @@
import { createStackNavigator } from 'react-navigation' import { createStackNavigator, createBottomTabNavigator } from 'react-navigation'
import Home from './components/home' import Home from './components/home'
import Calendar from './components/calendar' import Calendar from './components/calendar'
@@ -7,16 +7,28 @@ import Chart from './components/chart/chart'
import Settings from './components/settings' import Settings from './components/settings'
import Stats from './components/stats' import Stats from './components/stats'
import styles from './styles'
// this is until react native fixes this bugg, see // this is until react native fixes this bugg, see
// https://github.com/facebook/react-native/issues/18868#issuecomment-382671739 // https://github.com/facebook/react-native/issues/18868#issuecomment-382671739
import { YellowBox } from 'react-native' import { YellowBox } from 'react-native'
YellowBox.ignoreWarnings(['Warning: isMounted(...) is deprecated']) YellowBox.ignoreWarnings(['Warning: isMounted(...) is deprecated'])
export default createStackNavigator({ const routes = {
home: { screen: Home }, Home: createStackNavigator({Home, CycleDay}, {headerMode: 'none'}),
calendar: { screen: Calendar }, Calendar: createStackNavigator({Calendar, CycleDay}, {headerMode: 'none'}),
cycleDay: { screen: CycleDay }, Chart: createStackNavigator({Chart, CycleDay}, {headerMode: 'none'}),
chart: { screen: Chart }, Settings: { screen: Settings },
settings: { screen: Settings }, Stats: { screen: Stats}
stats: { screen: Stats} }
})
const config = {
labeled: true,
shifting: false,
tabBarOptions: {
style: {backgroundColor: '#ff7e5f'},
labelStyle: {fontSize: 15, color: 'white'}
},
}
export default createBottomTabNavigator(routes, config)
+3 -3
View File
@@ -1,6 +1,6 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { View } from 'react-native' import { View } from 'react-native'
import { Calendar } from 'react-native-calendars' import { CalendarList } from 'react-native-calendars'
import * as styles from '../styles' import * as styles from '../styles'
import { getOrCreateCycleDay, bleedingDaysSortedByDate } from '../db' import { getOrCreateCycleDay, bleedingDaysSortedByDate } from '../db'
@@ -29,13 +29,13 @@ export default class CalendarView extends Component {
passDateToDayView(result) { passDateToDayView(result) {
const cycleDay = getOrCreateCycleDay(result.dateString) const cycleDay = getOrCreateCycleDay(result.dateString)
const navigate = this.props.navigation.navigate const navigate = this.props.navigation.navigate
navigate('cycleDay', { cycleDay }) navigate('CycleDay', { cycleDay })
} }
render() { render() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Calendar <CalendarList
onDayPress={ this.passDateToDayView.bind(this) } onDayPress={ this.passDateToDayView.bind(this) }
markedDates = { this.state.bleedingDaysInCalFormat } markedDates = { this.state.bleedingDaysInCalFormat }
markingType = {'period'} markingType = {'period'}
+70 -274
View File
@@ -1,35 +1,34 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { Text as ReactNativeText, View, FlatList, ScrollView } from 'react-native' import { View, FlatList } from 'react-native'
import range from 'date-range' import range from 'date-range'
import Svg,{
G,
Rect,
Text,
Circle,
Line,
Path
} from 'react-native-svg'
import { LocalDate } from 'js-joda' import { LocalDate } from 'js-joda'
import { getCycleDay, getOrCreateCycleDay, cycleDaysSortedByDate } from '../../db' import { yAxis, normalizeToScale, horizontalGrid } from './y-axis'
import cycleModule from '../../lib/cycle' import setUpFertilityStatusFunc from './nfp-lines'
import DayColumn from './day-column'
import { getCycleDay, cycleDaysSortedByDate, getAmountOfCycleDays } from '../../db'
import styles from './styles' import styles from './styles'
import config from './config'
import { getCycleStatusForDay } from '../../lib/sympto-adapter'
const getCycleDayNumber = cycleModule().getCycleDayNumber const yAxisView = <View {...styles.yAxis}>{yAxis.labels}</View>
const yAxis = makeYAxis(config)
export default class CycleChart extends Component { export default class CycleChart extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
columns: makeColumnInfo(config.xAxisRangeInDays) columns: makeColumnInfo(setUpFertilityStatusFunc()),
}
this.renderColumn = ({item, index}) => {
return (
<DayColumn
{...item}
index={index}
navigate={this.props.navigation.navigate}
/>
)
} }
this.reCalculateChartInfo = (function(Chart) { this.reCalculateChartInfo = (function(Chart) {
return function() { return function() {
Chart.setState({columns: makeColumnInfo(config.xAxisRangeInDays)}) Chart.setState({columns: makeColumnInfo(setUpFertilityStatusFunc())})
} }
})(this) })(this)
@@ -40,159 +39,37 @@ export default class CycleChart extends Component {
cycleDaysSortedByDate.removeListener(this.reCalculateChartInfo) cycleDaysSortedByDate.removeListener(this.reCalculateChartInfo)
} }
passDateToDayView(dateString) {
const cycleDay = getOrCreateCycleDay(dateString)
this.props.navigation.navigate('cycleDay', { cycleDay })
}
placeHorizontalGrid() {
return yAxis.tickPositions.map(tick => {
return (
<Line
x1={0}
y1={tick}
x2={config.columnWidth}
y2={tick}
{...styles.horizontalGrid}
key={tick}
/>
)
})
}
makeDayColumn({ dateString, cycleDay, y }, index) {
const cycleDayNumber = getCycleDayNumber(dateString)
const label = styles.column.label
const dateLabel = dateString.split('-').slice(1).join('-')
const getFhmAndLtlInfo = setUpFertilityStatusFunc()
const nfpLineInfo = getFhmAndLtlInfo(dateString, cycleDay)
return (
<G onPress={() => this.passDateToDayView(dateString)}>
<Rect {...styles.column.rect} />
{nfpLineInfo.drawFhmLine ?
<Line
x1={0 + styles.nfpLine.strokeWidth / 2}
y1="20"
x2={0 + styles.nfpLine.strokeWidth / 2}
y2={config.chartHeight - 20}
{...styles.nfpLine}
/> : null}
{this.placeHorizontalGrid()}
<Text {...label.number} y={config.cycleDayNumberRowY}>
{cycleDayNumber}
</Text>
<Text {...label.date} y={config.dateRowY}>
{dateLabel}
</Text>
{cycleDay && cycleDay.bleeding ?
<Path {...styles.bleedingIcon}
d="M15 3
Q16.5 6.8 25 18
A12.8 12.8 0 1 1 5 18
Q13.5 6.8 15 3z" />
: null}
{nfpLineInfo.drawLtlAt ?
<Line
x1="0"
y1={nfpLineInfo.drawLtlAt}
x2={config.columnWidth}
y2={nfpLineInfo.drawLtlAt}
{...styles.nfpLine}
/> : null}
{y ?
this.drawDotAndLines(y, cycleDay.temperature.exclude, index)
: null
}
{cycleDay && cycleDay.mucus ?
<Circle
{...styles.mucusIcon}
fill={styles.mucusIconShades[cycleDay.mucus.value]}
/> : null}
{y ?
this.drawDotAndLines(y, cycleDay.temperature.exclude, index)
: null}
</G>
)
}
drawDotAndLines(currY, exclude, index) {
let lineToRight
let lineToLeft
const cols = this.state.columns
function makeLine(otherColY, x, excludeLine) {
const middleY = ((otherColY - currY) / 2) + currY
const target = [x, middleY]
const lineStyle = excludeLine ? styles.curveExcluded : styles.curve
return <Line
x1={config.columnMiddle}
y1={currY}
x2={target[0]}
y2={target[1]}
{...lineStyle}
/>
}
const thereIsADotToTheRight = index > 0 && cols[index - 1].y
const thereIsADotToTheLeft = index < cols.length - 1 && cols[index + 1].y
if (thereIsADotToTheRight) {
const otherDot = cols[index - 1]
const excludedLine = otherDot.cycleDay.temperature.exclude || exclude
lineToRight = makeLine(otherDot.y, config.columnWidth, excludedLine)
}
if (thereIsADotToTheLeft) {
const otherDot = cols[index + 1]
const excludedLine = otherDot.cycleDay.temperature.exclude || exclude
lineToLeft = makeLine(otherDot.y, 0, excludedLine)
}
const dotStyle = exclude ? styles.curveDotsExcluded : styles.curveDots
return (<G>
{lineToRight}
{lineToLeft}
<Circle
cx={config.columnMiddle}
cy={currY}
{...dotStyle}
/>
</G>)
}
render() { render() {
return ( return (
<ScrollView contentContainerStyle={{flexDirection: 'row'}}> <View style={{ flexDirection: 'row', marginTop: 50 }}>
<View {...styles.yAxis}>{yAxis.labels}</View> {yAxisView}
<FlatList {horizontalGrid}
{<FlatList
horizontal={true} horizontal={true}
inverted={true} inverted={true}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
data={this.state.columns} data={this.state.columns}
renderItem={({ item, index }) => { renderItem={this.renderColumn}
return (
<Svg width={config.columnWidth} height={config.chartHeight}>
{this.makeDayColumn(item, index)}
</Svg>
)
}}
keyExtractor={item => item.dateString} keyExtractor={item => item.dateString}
initialNumToRender={15}
maxToRenderPerBatch={5}
> >
</FlatList> </FlatList>}
</ScrollView> </View>
) )
} }
} }
function makeColumnInfo(n) { function makeColumnInfo(getFhmAndLtlInfo) {
const xAxisDates = getPreviousDays(n).map(jsDate => { let amountOfCycleDays = getAmountOfCycleDays()
// if there's not much data yet, we want to show at least 30 days on the chart
if (amountOfCycleDays < 30) {
amountOfCycleDays = 30
} else {
// we don't want the chart to end abruptly before the first data day
amountOfCycleDays += 5
}
const xAxisDates = getTodayAndPreviousDays(amountOfCycleDays).map(jsDate => {
return LocalDate.of( return LocalDate.of(
jsDate.getFullYear(), jsDate.getFullYear(),
jsDate.getMonth() + 1, jsDate.getMonth() + 1,
@@ -200,18 +77,29 @@ function makeColumnInfo(n) {
).toString() ).toString()
}) })
return xAxisDates.map(dateString => { const columns = xAxisDates.map(dateString => {
const cycleDay = getCycleDay(dateString) const cycleDay = getCycleDay(dateString)
const temp = cycleDay && cycleDay.temperature && cycleDay.temperature.value const symptoms = ['temperature', 'mucus', 'bleeding'].reduce((acc, symptom) => {
acc[symptom] = cycleDay && cycleDay[symptom] && cycleDay[symptom].value
acc[`${symptom}Exclude`] = cycleDay && cycleDay[symptom] && cycleDay[symptom].exclude
return acc
}, {})
return { return {
dateString, dateString,
cycleDay, y: symptoms.temperature ? normalizeToScale(symptoms.temperature) : null,
y: temp ? normalizeToScale(temp) : null ...symptoms,
...getFhmAndLtlInfo(dateString, symptoms.temperature)
} }
}) })
return columns.map((col, i) => {
const info = getInfoForNeighborColumns(i, columns)
return Object.assign(col, info)
})
} }
function getPreviousDays(n) { function getTodayAndPreviousDays(n) {
const today = new Date() const today = new Date()
today.setHours(0) today.setHours(0)
today.setMinutes(0) today.setMinutes(0)
@@ -222,114 +110,22 @@ function getPreviousDays(n) {
return range(earlierDate, today).reverse() return range(earlierDate, today).reverse()
} }
function normalizeToScale(temp) { function getInfoForNeighborColumns(index, cols) {
const scale = config.temperatureScale const ret = {
const valueRelativeToScale = (scale.high - temp) / (scale.high - scale.low) rightY: null,
const scaleHeight = config.chartHeight rightTemperatureExclude: null,
return scaleHeight * valueRelativeToScale leftY: null,
} leftTemperatureExclude: null
}
function makeYAxis() { const right = index > 0 ? cols[index - 1] : undefined
const scaleMin = config.temperatureScale.low const left = index < cols.length - 1 ? cols[index + 1] : undefined
const scaleMax = config.temperatureScale.high if (right && right.y) {
const numberOfTicks = (scaleMax - scaleMin) * 2 ret.rightY = right.y
const tickDistance = config.chartHeight / numberOfTicks ret.rightTemperatureExclude = right.temperatureExclude
}
const tickPositions = [] if (left && left.y) {
const labels = [] ret.leftY = left.y
// for style reasons, we don't want the first and last tick ret.leftTemperatureExclude = left.temperatureExclude
for (let i = 1; i < numberOfTicks - 1; i++) { }
const y = tickDistance * i return ret
const style = styles.yAxisLabel
// this eyeballing is sadly necessary because RN does not
// support percentage values for transforms, which we'd need
// to reliably place the label vertically centered to the grid
style.top = y - 8
labels.push(
<ReactNativeText
style={{...style}}
key={i}>
{scaleMax - i * 0.5}
</ReactNativeText>
)
tickPositions.push(y)
}
return {labels, tickPositions}
}
function setUpFertilityStatusFunc() {
let cycleStatus
let cycleStartDate
let noMoreCycles = false
function updateCurrentCycle(dateString) {
cycleStatus = getCycleStatusForDay(dateString)
if(!cycleStatus) {
noMoreCycles = true
return
}
if (cycleStatus.phases.preOvulatory) {
cycleStartDate = cycleStatus.phases.preOvulatory.start.date
} else {
cycleStartDate = cycleStatus.phases.periOvulatory.start.date
}
}
function dateIsInPeriOrPostPhase(dateString) {
return (
dateString >= cycleStatus.phases.periOvulatory.start.date
)
}
function precededByAnotherTempValue(dateString) {
return (
// we are only interested in days that have a preceding
// temp
Object.keys(cycleStatus.phases).some(phaseName => {
return cycleStatus.phases[phaseName].cycleDays.some(day => {
return day.temperature && day.date < dateString
})
})
// and also a following temp, so we don't draw the line
// longer than necessary
&&
cycleStatus.phases.postOvulatory.cycleDays.some(day => {
return day.temperature && day.date > dateString
})
)
}
function isInTempMeasuringPhase(cycleDay, dateString) {
return (
cycleDay && cycleDay.temperature
|| precededByAnotherTempValue(dateString)
)
}
return function(dateString, cycleDay) {
const ret = {}
if (!cycleStatus && !noMoreCycles) updateCurrentCycle(dateString)
if (noMoreCycles) return ret
if (dateString < cycleStartDate) updateCurrentCycle(dateString)
if (noMoreCycles) return ret
const tempShift = cycleStatus.temperatureShift
if (tempShift) {
if (tempShift.firstHighMeasurementDay.date === dateString) {
ret.drawFhmLine = true
}
if (
dateIsInPeriOrPostPhase(dateString) &&
isInTempMeasuringPhase(cycleDay, dateString)
) {
ret.drawLtlAt = normalizeToScale(tempShift.ltl)
}
}
return ret
}
} }
+5 -5
View File
@@ -1,11 +1,11 @@
const config = { const config = {
chartHeight: 350, chartHeight: 350,
columnWidth: 30, columnWidth: 25,
temperatureScale: { temperatureScale: {
low: 33, low: 35,
high: 40 high: 38,
}, units: 0.1
xAxisRangeInDays: 40 }
} }
const margin = 3 const margin = 3
+136
View File
@@ -0,0 +1,136 @@
import React, { Component } from 'react'
import {
Text, View, TouchableOpacity
} from 'react-native'
import Icon from 'react-native-vector-icons/Entypo'
import styles from './styles'
import config from './config'
import { getOrCreateCycleDay } from '../../db'
import cycleModule from '../../lib/cycle'
import DotAndLine from './dot-and-line'
const getCycleDayNumber = cycleModule().getCycleDayNumber
const label = styles.column.label
export default class DayColumn extends Component {
passDateToDayView(dateString) {
const cycleDay = getOrCreateCycleDay(dateString)
this.props.navigate('cycleDay', { cycleDay })
}
shouldComponentUpdate(newProps) {
return Object.keys(newProps).some(key => newProps[key] != this.props[key])
}
render() {
const {
dateString,
y,
temperatureExclude,
bleeding,
mucus,
drawFhmLine,
drawLtlAt,
rightY,
rightTemperatureExclude,
leftY,
leftTemperatureExclude
} = this.props
const columnElements = []
if (typeof bleeding === 'number') {
columnElements.push(
<Icon
name='drop'
position='absolute'
size={18}
color='#900'
style={{ marginTop: 10, marginLeft: 3 }}
key='bleeding'
/>
)
}
if (typeof mucus === 'number') {
const mucusIcon = (
<View
position='absolute'
top = {40}
left = {config.columnMiddle - styles.mucusIcon.width / 2}
{...styles.mucusIcon}
backgroundColor={styles.mucusIconShades[mucus]}
key='mucus'
/>
)
columnElements.push(mucusIcon)
}
if(drawFhmLine) {
const fhmLine = (<View
position = 'absolute'
top={100}
width={styles.nfpLine.strokeWidth}
height={200}
{...styles.nfpLine}
key='fhm'
/>)
columnElements.push(fhmLine)
}
if(drawLtlAt) {
const ltlLine = (<View
position = 'absolute'
width={'100%'}
top={drawLtlAt}
{...styles.nfpLine}
key='ltl'
/>)
columnElements.push(ltlLine)
}
if (y) {
columnElements.push(
<DotAndLine
y={y}
exclude={temperatureExclude}
rightY={rightY}
rightTemperatureExclude={rightTemperatureExclude}
leftY={leftY}
leftTemperatureExclude={leftTemperatureExclude}
key='dotandline'
/>
)
}
const cycleDayNumber = getCycleDayNumber(dateString)
const shortDate = dateString.split('-').slice(1).join('-')
const cycleDayLabel = (
<Text style={label.number} y={config.cycleDayNumberRowY}>
{cycleDayNumber}
</Text>)
const dateLabel = (
<Text style = {label.date} y={config.dateRowY}>
{shortDate}
</Text>
)
columnElements.push(
<View position='absolute' bottom={0} key='date'>
{cycleDayLabel}
{dateLabel}
</View>
)
return React.createElement(
TouchableOpacity,
{
style: styles.column.rect,
key: this.props.index.toString(),
onPress: () => {
this.passDateToDayView(dateString)
},
activeOpacity: 1
},
columnElements
)
}
}
+71
View File
@@ -0,0 +1,71 @@
import React, { Component } from 'react'
import { View } from 'react-native'
import styles from './styles'
import config from './config'
export default class DotAndLine extends Component {
shouldComponentUpdate(newProps) {
return Object.keys(newProps).some(key => newProps[key] != this.props[key])
}
render() {
const y = this.props.y
const exclude = this.props.exclude
let lineToRight
let lineToLeft
if (this.props.leftY) {
const middleY = ((this.props.leftY - y) / 2) + y
const excludedLine = this.props.leftTemperatureExclude || exclude
lineToLeft = makeLine(middleY, y, 'left', excludedLine)
}
if (this.props.rightY) {
const middleY = ((y - this.props.rightY) / 2) + this.props.rightY
const excludedLine = this.props.rightTemperatureExclude || exclude
lineToRight = makeLine(y, middleY, 'right', excludedLine)
}
const dotStyle = exclude ? styles.curveDotsExcluded : styles.curveDots
const dot = (
<View
position='absolute'
top={y - (dotStyle.height / 2)}
left={config.columnMiddle - (dotStyle.width / 2)}
style={dotStyle}
key='dot'
/>
)
return [lineToLeft, lineToRight, dot]
}
}
function makeLine(leftY, rightY, direction, excludeLine) {
const colWidth = config.columnWidth
const heightDiff = -leftY - -rightY
const angle = Math.atan2(heightDiff, colWidth / 2)
const lineStyle = excludeLine ? styles.curveExcluded : styles.curve
// hypotenuse, we add 3px for good measure, because otherwise the lines
// don't quite touch at the day border
const h = (colWidth / 2) / Math.cos(angle) + 10
// the rotation by default rotates from the middle of the line,
// but we want the transform origin to be at its beginning
// react native doesn't have transformOrigin, so we do this manually
// if it's the right line, we put the pivot at 3/4 of the column
// if it's to the left, at 1/4
const pivot = direction === 'right' ? colWidth / 4 : -(colWidth / 4)
const projectedX = -(h - colWidth) / 2 + pivot
return (<View
width={h}
position='absolute'
top={((leftY + rightY) / 2) - lineStyle.borderWidth / 2}
left={projectedX}
style={{
transform: [
{ rotateZ: `${angle}rad` }
],
}}
{...lineStyle}
key ={direction}
/>)
}
+12
View File
@@ -0,0 +1,12 @@
<svg version="1.1"
baseProfile="full"
width="300" height="200"
xmlns="http://www.w3.org/2000/svg">
<path d="
M15 3
Q16.5 6.8 25 18
A12.8 12.8 0 1 1 5 18
Q13.5 6.8 15 3z
" />
</svg>

After

Width:  |  Height:  |  Size: 217 B

+80
View File
@@ -0,0 +1,80 @@
import { getCycleStatusForDay } from '../../lib/sympto-adapter'
import { normalizeToScale } from './y-axis'
export default function () {
const cycle = {
status: null
}
function updateCurrentCycle(dateString) {
cycle.status = getCycleStatusForDay(dateString)
if(!cycle.status) {
cycle.noMoreCycles = true
return
}
if (cycle.status.phases.preOvulatory) {
cycle.startDate = cycle.status.phases.preOvulatory.start.date
} else {
cycle.startDate = cycle.status.phases.periOvulatory.start.date
}
}
function dateIsInPeriOrPostPhase(dateString) {
return (
dateString >= cycle.status.phases.periOvulatory.start.date
)
}
function precededByAnotherTempValue(dateString) {
return (
// we are only interested in days that have a preceding
// temp
Object.keys(cycle.status.phases).some(phaseName => {
return cycle.status.phases[phaseName].cycleDays.some(day => {
return day.temperature && day.date < dateString
})
})
// and also a following temp, so we don't draw the line
// longer than necessary
&&
cycle.status.phases.postOvulatory.cycleDays.some(day => {
return day.temperature && day.date > dateString
})
)
}
function isInTempMeasuringPhase(temperature, dateString) {
return (
temperature || precededByAnotherTempValue(dateString)
)
}
return function(dateString, temperature) {
const ret = {
drawLtlAt: null,
drawFhmLine: false
}
if (!cycle.status && !cycle.noMoreCycles) updateCurrentCycle(dateString)
if (cycle.noMoreCycles) return ret
if (dateString < cycle.startDate) updateCurrentCycle(dateString)
if (cycle.noMoreCycles) return ret
const tempShift = cycle.status.temperatureShift
if (tempShift) {
if (tempShift.firstHighMeasurementDay.date === dateString) {
ret.drawFhmLine = true
}
if (
dateIsInPeriOrPostPhase(dateString) &&
isInTempMeasuringPhase(temperature, dateString)
) {
ret.drawLtlAt = normalizeToScale(tempShift.ltl)
}
}
return ret
}
}
+40 -30
View File
@@ -2,44 +2,46 @@ import config from './config'
const styles = { const styles = {
curve: { curve: {
stroke: '#ffc425', borderStyle: 'solid',
strokeWidth: 2 borderColor: '#ffc425',
borderWidth: 2,
}, },
curveExcluded: { curveExcluded: {
stroke: 'lightgrey', borderColor: 'lightgrey',
strokeWidth: 2, borderWidth: 2,
strokeDashArray: [4] borderStyle: 'solid'
}, },
curveDots: { curveDots: {
fill: '#00aedb', backgroundColor: '#00aedb',
r: 6 width: 12,
height: 12,
borderRadius: 50
}, },
curveDotsExcluded: { curveDotsExcluded: {
fill: 'lightgrey', backgroundColor: 'lightgrey',
r: 6 width: 12,
height: 12,
borderRadius: 50
}, },
column: { column: {
label: { label: {
date: { date: {
stroke: 'grey', color: 'grey',
fontSize: 10, fontSize: 9,
x: 2,
fontWeight: '100' fontWeight: '100'
}, },
number: { number: {
stroke: '#00b159', color: '#00b159',
fontSize: 13, fontSize: 13,
x: config.columnMiddle - 1 textAlign: 'center'
} }
}, },
rect: { rect: {
fill: '#f9f9f9',
strokeWidth: 1,
stroke: 'grey',
x: 0,
y: 0,
width: config.columnWidth, width: config.columnWidth,
height: config.chartHeight height: config.chartHeight,
borderStyle: 'solid',
borderColor: 'grey',
borderWidth: 0.5
} }
}, },
bleedingIcon: { bleedingIcon: {
@@ -49,9 +51,9 @@ const styles = {
y: 3 y: 3
}, },
mucusIcon: { mucusIcon: {
cx: config.columnWidth / 2, width: 12,
cy: 50, height: 12,
r: 10 borderRadius: 50,
}, },
mucusIconShades: [ mucusIconShades: [
'#cc99cc', '#cc99cc',
@@ -63,21 +65,29 @@ const styles = {
yAxis: { yAxis: {
height: config.chartHeight, height: config.chartHeight,
width: config.columnWidth, width: config.columnWidth,
borderRightWidth: 0.5,
borderColor: 'lightgrey',
borderStyle: 'solid'
}, },
yAxisLabel: { yAxisLabel: {
position: 'absolute', position: 'absolute',
right: 3, left: 3,
color: 'grey', color: 'grey',
fontSize: 12, fontSize: 11,
fontWeight: 'bold' textAlign: 'left'
}, },
horizontalGrid: { horizontalGrid: {
stroke: 'lightgrey', position:'absolute',
strokeWidth: 1 borderColor: 'lightgrey',
borderWidth: 0.5,
width: '100%',
borderStyle: 'solid',
left: config.columnWidth
}, },
nfpLine: { nfpLine: {
stroke: '#00b159', borderColor: '#00b159',
strokeWidth: 3 borderWidth: 2,
borderStyle: 'solid'
} }
} }
+53
View File
@@ -0,0 +1,53 @@
import React from 'react'
import { Text, View } from 'react-native'
import config from './config'
import styles from './styles'
function makeYAxis() {
const scale = config.temperatureScale
const scaleMin = scale.low
const scaleMax = scale.high
const numberOfTicks = (scaleMax - scaleMin) * (1 / scale.units)
const tickDistance = config.chartHeight / numberOfTicks
const tickPositions = []
const labels = []
// for style reasons, we don't want the first and last tick
for (let i = 1; i < numberOfTicks - 1; i++) {
const y = tickDistance * i
const style = styles.yAxisLabel
// this eyeballing is sadly necessary because RN does not
// support percentage values for transforms, which we'd need
// to reliably place the label vertically centered to the grid
style.top = y - 8
labels.push(
<Text
style={{...style}}
key={i}>
{scaleMax - i * scale.units}
</Text>
)
tickPositions.push(y)
}
return {labels, tickPositions}
}
export const yAxis = makeYAxis()
export const horizontalGrid = yAxis.tickPositions.map(tick => {
return (
<View
top={tick}
{...styles.horizontalGrid}
key={tick}
/>
)
})
export function normalizeToScale(temp) {
const scale = config.temperatureScale
const valueRelativeToScale = (scale.high - temp) / (scale.high - scale.low)
const scaleHeight = config.chartHeight
return scaleHeight * valueRelativeToScale
}
+31 -4
View File
@@ -98,11 +98,20 @@ export default class DayView extends Component {
<Text style={styles.symptomDayView}>Desire</Text> <Text style={styles.symptomDayView}>Desire</Text>
<View style={styles.symptomEditButton}> <View style={styles.symptomEditButton}>
<Button <Button
onPress={() => this.showView('desireEditView')} onPress={() => this.showView('DesireEditView')}
title={getLabel('desire', cycleDay.desire)}> title={getLabel('desire', cycleDay.desire)}>
</Button> </Button>
</View> </View>
</View> </View>
<View style={styles.symptomViewRowInline}>
<Text style={styles.symptomDayView}>Sex</Text>
<View style={styles.symptomEditButton}>
<Button
onPress={() => this.showView('SexEditView')}
title={getLabel('sex', cycleDay.sex)}>
</Button>
</View>
</View>
</View > </View >
) )
} }
@@ -132,15 +141,22 @@ function getLabel(symptomName, symptom) {
typeof mucus.texture === 'number' && typeof mucus.texture === 'number' &&
typeof mucus.value === 'number' typeof mucus.value === 'number'
) { ) {
let mucusLabel = `${feelingLabels[mucus.feeling]} + ${textureLabels[mucus.texture]} ( ${computeSensiplanMucusLabels[mucus.value]} )` let mucusLabel =
`${feelingLabels[mucus.feeling]} +
${textureLabels[mucus.texture]}
( ${computeSensiplanMucusLabels[mucus.value]} )`
if (mucus.exclude) mucusLabel = "( " + mucusLabel + " )" if (mucus.exclude) mucusLabel = "( " + mucusLabel + " )"
return mucusLabel return mucusLabel
} }
}, },
cervix: cervix => { cervix: cervix => {
if (cervix.opening > -1 && cervix.firmness > -1) { if (cervix.opening > -1 && cervix.firmness > -1) {
let cervixLabel = `${openingLabels[cervix.opening]} + ${firmnessLabels[cervix.firmness]}` let cervixLabel =
if (cervix.position > -1) cervixLabel += `+ ${positionLabels[cervix.position]}` `${openingLabels[cervix.opening]} +
${firmnessLabels[cervix.firmness]}`
if (cervix.position > -1) {
cervixLabel += `+ ${positionLabels[cervix.position]}`
}
if (cervix.exclude) cervixLabel = "( " + cervixLabel + " )" if (cervix.exclude) cervixLabel = "( " + cervixLabel + " )"
return cervixLabel return cervixLabel
} }
@@ -153,6 +169,17 @@ function getLabel(symptomName, symptom) {
const desireLabel = `${intensityLabels[desire.value]}` const desireLabel = `${intensityLabels[desire.value]}`
return desireLabel return desireLabel
} }
},
sex: sex => {
let sexLabel = ''
if ( sex.solo || sex.partner ) {
sexLabel += 'Activity '
}
if (sex.condom || sex.pill || sex.iud ||
sex.patch || sex.ring || sex.implant || sex.other) {
sexLabel += 'Contraceptive'
}
return sexLabel ? sexLabel : 'edit'
} }
} }
+13
View File
@@ -6,6 +6,19 @@ export const cervixOpening = ['closed', 'medium', 'open']
export const cervixFirmness = ['hard', 'soft'] export const cervixFirmness = ['hard', 'soft']
export const cervixPosition = ['low', 'medium', 'high'] export const cervixPosition = ['low', 'medium', 'high']
export const intensity = ['low', 'medium', 'high'] export const intensity = ['low', 'medium', 'high']
export const sexActivity = {
solo: 'Solo',
partner: 'Partner'
}
export const contraceptives = {
condom: 'Condom',
pill: 'Pill',
iud: 'IUD',
patch: 'Patch',
ring: 'Ring',
implant: 'Implant',
other: 'Other'
}
export const fertilityStatus = { export const fertilityStatus = {
fertile: 'fertile', fertile: 'fertile',
+3 -1
View File
@@ -4,6 +4,7 @@ import MucusEditView from './mucus'
import CervixEditView from './cervix' import CervixEditView from './cervix'
import NoteEditView from './note' import NoteEditView from './note'
import DesireEditView from './desire' import DesireEditView from './desire'
import SexEditView from './sex'
export default { export default {
BleedingEditView, BleedingEditView,
@@ -11,5 +12,6 @@ export default {
MucusEditView, MucusEditView,
CervixEditView, CervixEditView,
NoteEditView, NoteEditView,
DesireEditView DesireEditView,
SexEditView
} }
+156
View File
@@ -0,0 +1,156 @@
import React, { Component } from 'react'
import {
CheckBox,
Text,
TextInput,
View
} from 'react-native'
import styles from '../../../styles'
import { saveSymptom } from '../../../db'
import {
sexActivity as activityLabels,
contraceptives as contraceptiveLabels
} from '../labels/labels'
export default class Sex extends Component {
constructor(props) {
super(props)
this.cycleDay = props.cycleDay
this.state = {}
if (this.cycleDay.sex !== null ) {
Object.assign(this.state, this.cycleDay.sex)
// We make sure other is always true when there is a note,
// e.g. when import is messed up.
if (this.cycleDay.sex && this.cycleDay.sex.note) {
this.state.other = true
}
}
}
render() {
return (
<View style={styles.symptomEditView}>
<Text style={styles.symptomDayView}>SEX</Text>
<View style={styles.symptomViewRowInline}>
<Text style={styles.symptomDayView}>{activityLabels.solo}</Text>
<CheckBox
value={this.state.solo}
onValueChange={(val) => {
this.setState({solo: val})
}}
/>
<Text style={styles.symptomDayView}>{activityLabels.partner}</Text>
<CheckBox
value={this.state.partner}
onValueChange={(val) => {
this.setState({partner: val})
}}
/>
</View>
<Text style={styles.symptomDayView}>CONTRACEPTIVES</Text>
<View style={styles.symptomViewRowInline}>
<Text style={styles.symptomDayView}>
{contraceptiveLabels.condom}
</Text>
<CheckBox
value={this.state.condom}
onValueChange={(val) => {
this.setState({condom: val})
}}
/>
<Text style={styles.symptomDayView}>
{contraceptiveLabels.pill}
</Text>
<CheckBox
value={this.state.pill}
onValueChange={(val) => {
this.setState({pill: val})
}}
/>
</View>
<View style={styles.symptomViewRowInline}>
<Text style={styles.symptomDayView}>
{contraceptiveLabels.iud}
</Text>
<CheckBox
value={this.state.iud}
onValueChange={(val) => {
this.setState({iud: val})
}}
/>
<Text style={styles.symptomDayView}>
{contraceptiveLabels.patch}
</Text>
<CheckBox
value={this.state.patch}
onValueChange={(val) => {
this.setState({patch: val})
}}
/>
</View>
<View style={styles.symptomViewRowInline}>
<Text style={styles.symptomDayView}>
{contraceptiveLabels.ring}
</Text>
<CheckBox
value={this.state.ring}
onValueChange={(val) => {
this.setState({ring: val})
}}
/>
<Text style={styles.symptomDayView}>
{contraceptiveLabels.implant}
</Text>
<CheckBox
value={this.state.implant}
onValueChange={(val) => {
this.setState({implant: val})
}}
/>
</View>
<View style={styles.symptomViewRowInline}>
<Text style={styles.symptomDayView}>
{contraceptiveLabels.other}
</Text>
<CheckBox
value={this.state.other}
onValueChange={(val) => {
this.setState({
other: val,
focusTextArea: true
})
}}
/>
</View>
{ this.state.other &&
<TextInput
autoFocus={this.state.focusTextArea}
multiline={true}
placeholder="Enter"
value={this.state.note}
onChangeText={(val) => {
this.setState({note: val})
}}
/>
}
<View style={styles.actionButtonRow}>
{this.props.makeActionButtons(
{
symptom: 'sex',
cycleDay: this.cycleDay,
saveAction: () => {
const copyOfState = Object.assign({}, this.state)
if (!copyOfState.other) {
copyOfState.note = null
}
saveSymptom('sex', this.cycleDay, copyOfState)
},
saveDisabled: Object.values(this.state).every(value => !value)
}
)}
</View>
</View>
)
}
}
+5 -1
View File
@@ -70,8 +70,12 @@ export default class Temp extends Component {
mode="time" mode="time"
isVisible={this.state.isTimePickerVisible} isVisible={this.state.isTimePickerVisible}
onConfirm={jsDate => { onConfirm={jsDate => {
let hours = jsDate.getHours()
if (hours < 10) hours = `0${hours}`
let minutes = jsDate.getMinutes()
if (minutes < 10) minutes = `0${minutes}`
this.setState({ this.setState({
time: `${jsDate.getHours()}:${jsDate.getMinutes()}`, time: `${hours}:${minutes}`,
isTimePickerVisible: false isTimePickerVisible: false
}) })
}} }}
+1 -26
View File
@@ -42,11 +42,10 @@ export default class Home extends Component {
const todayDateString = LocalDate.now().toString() const todayDateString = LocalDate.now().toString()
const cycleDay = getOrCreateCycleDay(todayDateString) const cycleDay = getOrCreateCycleDay(todayDateString)
const navigate = this.props.navigation.navigate const navigate = this.props.navigation.navigate
navigate('cycleDay', { cycleDay }) navigate('CycleDay', { cycleDay })
} }
render() { render() {
const navigate = this.props.navigation.navigate
return ( return (
<ScrollView> <ScrollView>
<Text style={styles.welcome}>{this.state.welcomeText}</Text> <Text style={styles.welcome}>{this.state.welcomeText}</Text>
@@ -57,24 +56,6 @@ export default class Home extends Component {
title="Edit symptoms for today"> title="Edit symptoms for today">
</Button> </Button>
</View> </View>
<View style={styles.homeButton}>
<Button
onPress={() => navigate('calendar')}
title="Go to calendar">
</Button>
</View>
<View style={styles.homeButton}>
<Button
onPress={() => navigate('chart')}
title="Go to chart">
</Button>
</View>
<View style={styles.homeButton}>
<Button
onPress={() => navigate('settings')}
title="Go to settings">
</Button>
</View>
<View style={styles.homeButton}> <View style={styles.homeButton}>
<Button <Button
onPress={() => fillWithDummyData()} onPress={() => fillWithDummyData()}
@@ -87,12 +68,6 @@ export default class Home extends Component {
title="delete everything"> title="delete everything">
</Button> </Button>
</View> </View>
<View style={styles.homeButton}>
<Button
onPress={() => navigate('stats')}
title="Go to stats">
</Button>
</View>
</View> </View>
</ScrollView> </ScrollView>
) )
+30 -7
View File
@@ -1,12 +1,35 @@
export const settings = { export const settings = {
errors: { shared: {
noData: 'There is no data to export', cancel: 'Cancel',
couldNotConvert: 'Could not convert data to CSV', errorTitle: 'Error',
problemSharing: 'There was a problem sharing the data export file' successTitle: 'Success'
}, },
exportTitle: 'My Drip data export', export: {
exportSubject: 'My Drip data export', errors: {
buttonLabel: 'Export data' noData: 'There is no data to export',
couldNotConvert: 'Could not convert data to CSV',
problemSharing: 'There was a problem sharing the data export file'
},
title: 'My Drip data export',
subject: 'My Drip data export',
button: 'Export data',
},
import: {
button: 'Import data',
title: 'Keep existing data?',
message: `There are two options for the import:
1. Keep existing cycle days and replace only the ones in the import file.
2. Delete all existing cycle days and import cycle days from file.`,
replaceOption: 'Import and replace',
deleteOption: 'Import and delete existing',
errors: {
couldNotOpenFile: 'Could not open file',
postFix: 'No data was imported or changed'
},
success: {
message: 'Data successfully imported'
}
}
} }
export const stats = { export const stats = {
+94 -27
View File
@@ -7,9 +7,12 @@ import {
} from 'react-native' } from 'react-native'
import Share from 'react-native-share' import Share from 'react-native-share'
import getDataAsCsvDataUri from '../lib/export-to-csv' import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker'
import rnfs from 'react-native-fs'
import styles from '../styles/index' import styles from '../styles/index'
import { settings as labels } from './labels' import { settings as labels } from './labels'
import getDataAsCsvDataUri from '../lib/import-export/export-to-csv'
import importCsv from '../lib/import-export/import-from-csv'
export default class Settings extends Component { export default class Settings extends Component {
render() { render() {
@@ -18,32 +21,14 @@ export default class Settings extends Component {
<View style={styles.homeButtons}> <View style={styles.homeButtons}>
<View style={styles.homeButton}> <View style={styles.homeButton}>
<Button <Button
onPress={async () => { onPress={ openShareDialogAndExport }
let data title={labels.export.button}>
try { </Button>
data = getDataAsCsvDataUri() </View>
if (!data) { <View style={styles.homeButton}>
return Alert.alert(labels.errors.noData) <Button
} title={labels.import.button}
} catch (err) { onPress={ openImportDialogAndImport }>
console.error(err)
return Alert.alert(labels.errors.couldNotConvert)
}
try {
await Share.open({
title: labels.exportTitle,
url: data,
subject: labels.exportSubject,
type: 'text/csv',
showAppsToView: true
})
} catch (err) {
console.error(err)
return Alert.alert(labels.errors.problemSharing)
}
}}
title={labels.buttonLabel}>
</Button> </Button>
</View> </View>
</View> </View>
@@ -51,3 +36,85 @@ export default class Settings extends Component {
) )
} }
} }
async function openShareDialogAndExport() {
let data
try {
data = getDataAsCsvDataUri()
if (!data) {
return alertError(labels.errors.noData)
}
} catch (err) {
console.error(err)
return alertError(labels.errors.couldNotConvert)
}
try {
await Share.open({
title: labels.export.title,
url: data,
subject: labels.export.subject,
type: 'text/csv',
showAppsToView: true
})
} catch (err) {
console.error(err)
return alertError(labels.export.errors.problemSharing)
}
}
function openImportDialogAndImport() {
Alert.alert(
labels.import.title,
labels.import.message,
[{
text: labels.import.replaceOption,
onPress: () => getFileContentAndImport({ deleteExisting: false })
}, {
text: labels.import.deleteOption,
onPress: () => getFileContentAndImport({ deleteExisting: true })
}, {
text: labels.shared.cancel, style: 'cancel', onPress: () => { }
}]
)
}
async function getFileContentAndImport({ deleteExisting }) {
let fileInfo
try {
fileInfo = await new Promise((resolve, reject) => {
DocumentPicker.show({
filetype: [DocumentPickerUtil.allFiles()],
}, (err, res) => {
if (err) return reject(err)
resolve(res)
})
})
} catch (err) {
// because cancelling also triggers an error, we do nothing here
return
}
let fileContent
try {
fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
} catch (err) {
return importError(labels.import.errors.couldNotOpenFile)
}
try {
await importCsv(fileContent, deleteExisting)
Alert.alert(labels.import.success.title, labels.import.success.message)
} catch(err) {
importError(err.message)
}
}
function alertError(msg) {
Alert.alert(labels.shared.errorTitle, msg)
}
function importError(msg) {
const postFixed = `${msg}\n\n${labels.import.errors.postFix}`
alertError(postFixed)
}
+63 -17
View File
@@ -1,5 +1,5 @@
import Realm from 'realm' import Realm from 'realm'
import { LocalDate } from 'js-joda' import { LocalDate, ChronoUnit } from 'js-joda'
import { import {
cycleWithTempAndNoMucusShift, cycleWithTempAndNoMucusShift,
cycleWithFhm, cycleWithFhm,
@@ -60,6 +60,22 @@ const DesireSchema = {
} }
} }
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 },
other: { type: 'bool', optional: true },
note: { type: 'string', optional: true }
}
}
const CycleDaySchema = { const CycleDaySchema = {
name: 'CycleDay', name: 'CycleDay',
primaryKey: 'date', primaryKey: 'date',
@@ -88,6 +104,10 @@ const CycleDaySchema = {
desire: { desire: {
type: 'Desire', type: 'Desire',
optional: true optional: true
},
sex: {
type: 'Sex',
optional: true
} }
} }
} }
@@ -100,7 +120,8 @@ const realmConfig = {
MucusSchema, MucusSchema,
CervixSchema, CervixSchema,
NoteSchema, NoteSchema,
DesireSchema DesireSchema,
SexSchema
], ],
// we only want this in dev mode // we only want this in dev mode
deleteRealmIfMigrationNeeded: true deleteRealmIfMigrationNeeded: true
@@ -175,24 +196,46 @@ function getPreviousTemperature(cycleDay) {
return winner.temperature.value return winner.temperature.value
} }
function getColumnNamesForCsv() { const schema = db.schema.reduce((acc, curr) => {
return getPrefixedKeys('CycleDay') acc[curr.name] = curr.properties
return acc
}, {})
function getPrefixedKeys(schemaName, prefix) { function tryToCreateCycleDay(day, i) {
const schema = db.schema.find(x => x.name === schemaName).properties try {
return Object.keys(schema).reduce((acc, key) => { db.create('CycleDay', day)
const prefixedKey = prefix ? [prefix, key].join('.') : key } catch (err) {
const childSchemaName = schema[key].objectType const msg = `Line ${i + 1}(${day.date}): ${err.message}`
if (!childSchemaName) { throw new Error(msg)
acc.push(prefixedKey)
return acc
}
acc.push(...getPrefixedKeys(childSchemaName, prefixedKey))
return acc
}, [])
} }
} }
function getAmountOfCycleDays() {
const amountOfCycleDays = cycleDaysSortedByDate.length
if (!amountOfCycleDays) return 0
const earliest = cycleDaysSortedByDate[amountOfCycleDays - 1]
const today = LocalDate.now()
const earliestAsLocalDate = LocalDate.parse(earliest.date)
return earliestAsLocalDate.until(today, ChronoUnit.DAYS)
}
function tryToImportWithDelete(cycleDays) {
db.write(() => {
db.delete(db.objects('CycleDay'))
cycleDays.forEach(tryToCreateCycleDay)
})
}
function tryToImportWithoutDelete(cycleDays) {
db.write(() => {
cycleDays.forEach((day, i) => {
const existing = getCycleDay(day.date)
if (existing) db.delete(existing)
tryToCreateCycleDay(day, i)
})
})
}
export { export {
saveSymptom, saveSymptom,
getOrCreateCycleDay, getOrCreateCycleDay,
@@ -203,5 +246,8 @@ export {
deleteAll, deleteAll,
getPreviousTemperature, getPreviousTemperature,
getCycleDay, getCycleDay,
getColumnNamesForCsv getAmountOfCycleDays,
schema,
tryToImportWithDelete,
tryToImportWithoutDelete
} }
+116 -15
View File
@@ -37,12 +37,27 @@
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; }; 5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */; }; ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */; };
EFD2F9E5E59248D9AD6EBABA /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F4A1722D79F04D5EB528E21A /* libRNSVG.a */; };
934282049FA3497D9062CEC1 /* libRNSVG-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5ADC07BB282A481EBD5FD2A2 /* libRNSVG-tvOS.a */; };
089A8A31B3244EB381D3BA67 /* libRealmReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A5827160B914D2B99C47381 /* libRealmReact.a */; }; 089A8A31B3244EB381D3BA67 /* libRealmReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A5827160B914D2B99C47381 /* libRealmReact.a */; };
62F2A4645AC84CDC9506FF27 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AEBF0735214455AAEDF56D5 /* libc++.tbd */; }; 62F2A4645AC84CDC9506FF27 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AEBF0735214455AAEDF56D5 /* libc++.tbd */; };
D91133DCE120440893E2FD2E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CD8C8B91E0A747B3883A0D56 /* libz.tbd */; }; D91133DCE120440893E2FD2E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CD8C8B91E0A747B3883A0D56 /* libz.tbd */; };
26DC04B498C64CE5AAA0C4F8 /* libRNShare.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8B59389C2FC4F19BD30ABC3 /* libRNShare.a */; }; 26DC04B498C64CE5AAA0C4F8 /* libRNShare.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8B59389C2FC4F19BD30ABC3 /* libRNShare.a */; };
AED64B7892744F21B3A156BB /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F5039D0A572B4BBCB7995891 /* libRNVectorIcons.a */; };
283136C9CE964E07BD52BE20 /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A2811C9225AC4EAC93FCD2DB /* Entypo.ttf */; };
87508EF4BE7548878981BE9E /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B41CDA1146DB4492B1796444 /* EvilIcons.ttf */; };
4E04B0A1FECA4915AD4A6CCF /* Feather.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F59A471BDE4144A1A41D4B52 /* Feather.ttf */; };
20146F2E2E9E4EB289472E81 /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EE8CBB8533DA48A9A78697C3 /* FontAwesome.ttf */; };
AF2EAEC3772C41A49A4AF117 /* FontAwesome5_Brands.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 044ECCD7D9B8470782A453CA /* FontAwesome5_Brands.ttf */; };
3F0F9C6368674C66AEAC3807 /* FontAwesome5_Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C783C08EEEB541D2A30FEE44 /* FontAwesome5_Regular.ttf */; };
7D48A7B9435741CCA9678C42 /* FontAwesome5_Solid.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0BE71E2D5A634744A662DF44 /* FontAwesome5_Solid.ttf */; };
8E4308D3D8614B0BACABF058 /* Foundation.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EA87C1DB160640E0907EE056 /* Foundation.ttf */; };
DB93C03E56074FB78F7F5B7C /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 8F2C7294680449AEA698AF76 /* Ionicons.ttf */; };
96687426D3D64D0F8E15FE4B /* MaterialCommunityIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5D8CEEF11CF346C7A641F4EC /* MaterialCommunityIcons.ttf */; };
DF31809C83AD48569741458C /* MaterialIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B39DD260535A4B0EB31A3E0A /* MaterialIcons.ttf */; };
DB91E6CCC3EB4A549D947797 /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4902D5DCD46748BD8DC403FD /* Octicons.ttf */; };
3DF2498A20844F298CD84CC3 /* SimpleLineIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E954835D62BD45F0A5FFC523 /* SimpleLineIcons.ttf */; };
A1410AC4C98A49B2820D9E45 /* Zocial.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B6F5078F7DEC470782757471 /* Zocial.ttf */; };
29DF0CCC1AEA4C92BCA0BCCD /* libRNDocumentPicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D211D71BE5A8436A978770A9 /* libRNDocumentPicker.a */; };
17AD822C42A44BADA96BD860 /* libRNFS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 84CCEBD3B2C44758853BC941 /* libRNFS.a */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -348,15 +363,32 @@
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTBlob.xcodeproj; path = "../node_modules/react-native/Libraries/Blob/RCTBlob.xcodeproj"; sourceTree = "<group>"; }; ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTBlob.xcodeproj; path = "../node_modules/react-native/Libraries/Blob/RCTBlob.xcodeproj"; sourceTree = "<group>"; };
8316A5AD64274E6FBA6C9FFE /* RNSVG.xcodeproj */ = {isa = PBXFileReference; name = "RNSVG.xcodeproj"; path = "../node_modules/react-native-svg/ios/RNSVG.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
F4A1722D79F04D5EB528E21A /* libRNSVG.a */ = {isa = PBXFileReference; name = "libRNSVG.a"; path = "libRNSVG.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
5ADC07BB282A481EBD5FD2A2 /* libRNSVG-tvOS.a */ = {isa = PBXFileReference; name = "libRNSVG-tvOS.a"; path = "libRNSVG-tvOS.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
7F6C9FA9B66B453CA602B334 /* RealmReact.xcodeproj */ = {isa = PBXFileReference; name = "RealmReact.xcodeproj"; path = "../node_modules/realm/react-native/ios/RealmReact.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; 7F6C9FA9B66B453CA602B334 /* RealmReact.xcodeproj */ = {isa = PBXFileReference; name = "RealmReact.xcodeproj"; path = "../node_modules/realm/react-native/ios/RealmReact.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
7A5827160B914D2B99C47381 /* libRealmReact.a */ = {isa = PBXFileReference; name = "libRealmReact.a"; path = "libRealmReact.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; 7A5827160B914D2B99C47381 /* libRealmReact.a */ = {isa = PBXFileReference; name = "libRealmReact.a"; path = "libRealmReact.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
9AEBF0735214455AAEDF56D5 /* libc++.tbd */ = {isa = PBXFileReference; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; fileEncoding = undefined; lastKnownFileType = sourcecode.text-based-dylib-definition; explicitFileType = undefined; includeInIndex = 0; }; 9AEBF0735214455AAEDF56D5 /* libc++.tbd */ = {isa = PBXFileReference; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; fileEncoding = undefined; lastKnownFileType = sourcecode.text-based-dylib-definition; explicitFileType = undefined; includeInIndex = 0; };
CD8C8B91E0A747B3883A0D56 /* libz.tbd */ = {isa = PBXFileReference; name = "libz.tbd"; path = "usr/lib/libz.tbd"; sourceTree = SDKROOT; fileEncoding = undefined; lastKnownFileType = sourcecode.text-based-dylib-definition; explicitFileType = undefined; includeInIndex = 0; }; CD8C8B91E0A747B3883A0D56 /* libz.tbd */ = {isa = PBXFileReference; name = "libz.tbd"; path = "usr/lib/libz.tbd"; sourceTree = SDKROOT; fileEncoding = undefined; lastKnownFileType = sourcecode.text-based-dylib-definition; explicitFileType = undefined; includeInIndex = 0; };
4E6AB77B55F2491487B6124E /* RNShare.xcodeproj */ = {isa = PBXFileReference; name = "RNShare.xcodeproj"; path = "../node_modules/react-native-share/ios/RNShare.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; 4E6AB77B55F2491487B6124E /* RNShare.xcodeproj */ = {isa = PBXFileReference; name = "RNShare.xcodeproj"; path = "../node_modules/react-native-share/ios/RNShare.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
A8B59389C2FC4F19BD30ABC3 /* libRNShare.a */ = {isa = PBXFileReference; name = "libRNShare.a"; path = "libRNShare.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; A8B59389C2FC4F19BD30ABC3 /* libRNShare.a */ = {isa = PBXFileReference; name = "libRNShare.a"; path = "libRNShare.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
D1E5ACC4B66345868F556374 /* RNVectorIcons.xcodeproj */ = {isa = PBXFileReference; name = "RNVectorIcons.xcodeproj"; path = "../node_modules/react-native-vector-icons/RNVectorIcons.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
F5039D0A572B4BBCB7995891 /* libRNVectorIcons.a */ = {isa = PBXFileReference; name = "libRNVectorIcons.a"; path = "libRNVectorIcons.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
A2811C9225AC4EAC93FCD2DB /* Entypo.ttf */ = {isa = PBXFileReference; name = "Entypo.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
B41CDA1146DB4492B1796444 /* EvilIcons.ttf */ = {isa = PBXFileReference; name = "EvilIcons.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
F59A471BDE4144A1A41D4B52 /* Feather.ttf */ = {isa = PBXFileReference; name = "Feather.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
EE8CBB8533DA48A9A78697C3 /* FontAwesome.ttf */ = {isa = PBXFileReference; name = "FontAwesome.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
044ECCD7D9B8470782A453CA /* FontAwesome5_Brands.ttf */ = {isa = PBXFileReference; name = "FontAwesome5_Brands.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
C783C08EEEB541D2A30FEE44 /* FontAwesome5_Regular.ttf */ = {isa = PBXFileReference; name = "FontAwesome5_Regular.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
0BE71E2D5A634744A662DF44 /* FontAwesome5_Solid.ttf */ = {isa = PBXFileReference; name = "FontAwesome5_Solid.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
EA87C1DB160640E0907EE056 /* Foundation.ttf */ = {isa = PBXFileReference; name = "Foundation.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
8F2C7294680449AEA698AF76 /* Ionicons.ttf */ = {isa = PBXFileReference; name = "Ionicons.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
5D8CEEF11CF346C7A641F4EC /* MaterialCommunityIcons.ttf */ = {isa = PBXFileReference; name = "MaterialCommunityIcons.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
B39DD260535A4B0EB31A3E0A /* MaterialIcons.ttf */ = {isa = PBXFileReference; name = "MaterialIcons.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
4902D5DCD46748BD8DC403FD /* Octicons.ttf */ = {isa = PBXFileReference; name = "Octicons.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/Octicons.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
E954835D62BD45F0A5FFC523 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; name = "SimpleLineIcons.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
B6F5078F7DEC470782757471 /* Zocial.ttf */ = {isa = PBXFileReference; name = "Zocial.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/Zocial.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
1F05FE29622E4F21AF70C2B7 /* RNDocumentPicker.xcodeproj */ = {isa = PBXFileReference; name = "RNDocumentPicker.xcodeproj"; path = "../node_modules/react-native-document-picker/ios/RNDocumentPicker.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
D211D71BE5A8436A978770A9 /* libRNDocumentPicker.a */ = {isa = PBXFileReference; name = "libRNDocumentPicker.a"; path = "libRNDocumentPicker.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
49089E09BFCF4F3DB209B6E9 /* RNFS.xcodeproj */ = {isa = PBXFileReference; name = "RNFS.xcodeproj"; path = "../node_modules/react-native-fs/RNFS.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
84CCEBD3B2C44758853BC941 /* libRNFS.a */ = {isa = PBXFileReference; name = "libRNFS.a"; path = "libRNFS.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -385,11 +417,13 @@
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */,
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
EFD2F9E5E59248D9AD6EBABA /* libRNSVG.a in Frameworks */,
089A8A31B3244EB381D3BA67 /* libRealmReact.a in Frameworks */, 089A8A31B3244EB381D3BA67 /* libRealmReact.a in Frameworks */,
62F2A4645AC84CDC9506FF27 /* libc++.tbd in Frameworks */, 62F2A4645AC84CDC9506FF27 /* libc++.tbd in Frameworks */,
D91133DCE120440893E2FD2E /* libz.tbd in Frameworks */, D91133DCE120440893E2FD2E /* libz.tbd in Frameworks */,
26DC04B498C64CE5AAA0C4F8 /* libRNShare.a in Frameworks */, 26DC04B498C64CE5AAA0C4F8 /* libRNShare.a in Frameworks */,
AED64B7892744F21B3A156BB /* libRNVectorIcons.a in Frameworks */,
29DF0CCC1AEA4C92BCA0BCCD /* libRNDocumentPicker.a in Frameworks */,
17AD822C42A44BADA96BD860 /* libRNFS.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -579,9 +613,11 @@
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */,
00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */,
139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */,
8316A5AD64274E6FBA6C9FFE /* RNSVG.xcodeproj */,
7F6C9FA9B66B453CA602B334 /* RealmReact.xcodeproj */, 7F6C9FA9B66B453CA602B334 /* RealmReact.xcodeproj */,
4E6AB77B55F2491487B6124E /* RNShare.xcodeproj */, 4E6AB77B55F2491487B6124E /* RNShare.xcodeproj */,
D1E5ACC4B66345868F556374 /* RNVectorIcons.xcodeproj */,
1F05FE29622E4F21AF70C2B7 /* RNDocumentPicker.xcodeproj */,
49089E09BFCF4F3DB209B6E9 /* RNFS.xcodeproj */,
); );
name = Libraries; name = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -603,6 +639,7 @@
00E356EF1AD99517003FC87E /* dripTests */, 00E356EF1AD99517003FC87E /* dripTests */,
83CBBA001A601CBA00E9B192 /* Products */, 83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */, 2D16E6871FA4F8E400B85C8A /* Frameworks */,
006C39A0B9774387BC5ACA43 /* Resources */,
); );
indentWidth = 2; indentWidth = 2;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -629,6 +666,28 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
006C39A0B9774387BC5ACA43 /* Resources */ = {
isa = "PBXGroup";
children = (
A2811C9225AC4EAC93FCD2DB /* Entypo.ttf */,
B41CDA1146DB4492B1796444 /* EvilIcons.ttf */,
F59A471BDE4144A1A41D4B52 /* Feather.ttf */,
EE8CBB8533DA48A9A78697C3 /* FontAwesome.ttf */,
044ECCD7D9B8470782A453CA /* FontAwesome5_Brands.ttf */,
C783C08EEEB541D2A30FEE44 /* FontAwesome5_Regular.ttf */,
0BE71E2D5A634744A662DF44 /* FontAwesome5_Solid.ttf */,
EA87C1DB160640E0907EE056 /* Foundation.ttf */,
8F2C7294680449AEA698AF76 /* Ionicons.ttf */,
5D8CEEF11CF346C7A641F4EC /* MaterialCommunityIcons.ttf */,
B39DD260535A4B0EB31A3E0A /* MaterialIcons.ttf */,
4902D5DCD46748BD8DC403FD /* Octicons.ttf */,
E954835D62BD45F0A5FFC523 /* SimpleLineIcons.ttf */,
B6F5078F7DEC470782757471 /* Zocial.ttf */,
);
name = Resources;
sourceTree = "<group>";
path = "";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -1075,6 +1134,20 @@
files = ( files = (
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
283136C9CE964E07BD52BE20 /* Entypo.ttf in Resources */,
87508EF4BE7548878981BE9E /* EvilIcons.ttf in Resources */,
4E04B0A1FECA4915AD4A6CCF /* Feather.ttf in Resources */,
20146F2E2E9E4EB289472E81 /* FontAwesome.ttf in Resources */,
AF2EAEC3772C41A49A4AF117 /* FontAwesome5_Brands.ttf in Resources */,
3F0F9C6368674C66AEAC3807 /* FontAwesome5_Regular.ttf in Resources */,
7D48A7B9435741CCA9678C42 /* FontAwesome5_Solid.ttf in Resources */,
8E4308D3D8614B0BACABF058 /* Foundation.ttf in Resources */,
DB93C03E56074FB78F7F5B7C /* Ionicons.ttf in Resources */,
96687426D3D64D0F8E15FE4B /* MaterialCommunityIcons.ttf in Resources */,
DF31809C83AD48569741458C /* MaterialIcons.ttf in Resources */,
DB91E6CCC3EB4A549D947797 /* Octicons.ttf in Resources */,
3DF2498A20844F298CD84CC3 /* SimpleLineIcons.ttf in Resources */,
A1410AC4C98A49B2820D9E45 /* Zocial.ttf in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -1209,12 +1282,16 @@
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
); );
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
"$(SRCROOT)/../node_modules/realm/src/**", "$(SRCROOT)/../node_modules/realm/src/**",
"$(SRCROOT)/../node_modules/react-native-share/ios", "$(SRCROOT)/../node_modules/react-native-share/ios",
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
"$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker",
"$(SRCROOT)/../node_modules/react-native-fs/**",
); );
}; };
name = Debug; name = Debug;
@@ -1236,12 +1313,16 @@
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
); );
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
"$(SRCROOT)/../node_modules/realm/src/**", "$(SRCROOT)/../node_modules/realm/src/**",
"$(SRCROOT)/../node_modules/react-native-share/ios", "$(SRCROOT)/../node_modules/react-native-share/ios",
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
"$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker",
"$(SRCROOT)/../node_modules/react-native-fs/**",
); );
}; };
name = Release; name = Release;
@@ -1263,9 +1344,11 @@
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
"$(SRCROOT)/../node_modules/realm/src/**", "$(SRCROOT)/../node_modules/realm/src/**",
"$(SRCROOT)/../node_modules/react-native-share/ios", "$(SRCROOT)/../node_modules/react-native-share/ios",
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
"$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker",
"$(SRCROOT)/../node_modules/react-native-fs/**",
); );
}; };
name = Debug; name = Debug;
@@ -1286,9 +1369,11 @@
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
"$(SRCROOT)/../node_modules/realm/src/**", "$(SRCROOT)/../node_modules/realm/src/**",
"$(SRCROOT)/../node_modules/react-native-share/ios", "$(SRCROOT)/../node_modules/react-native-share/ios",
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
"$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker",
"$(SRCROOT)/../node_modules/react-native-fs/**",
); );
}; };
name = Release; name = Release;
@@ -1319,12 +1404,16 @@
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
); );
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
"$(SRCROOT)/../node_modules/realm/src/**", "$(SRCROOT)/../node_modules/realm/src/**",
"$(SRCROOT)/../node_modules/react-native-share/ios", "$(SRCROOT)/../node_modules/react-native-share/ios",
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
"$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker",
"$(SRCROOT)/../node_modules/react-native-fs/**",
); );
}; };
name = Debug; name = Debug;
@@ -1355,12 +1444,16 @@
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
); );
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
"$(SRCROOT)/../node_modules/realm/src/**", "$(SRCROOT)/../node_modules/realm/src/**",
"$(SRCROOT)/../node_modules/react-native-share/ios", "$(SRCROOT)/../node_modules/react-native-share/ios",
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
"$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker",
"$(SRCROOT)/../node_modules/react-native-fs/**",
); );
}; };
name = Release; name = Release;
@@ -1390,12 +1483,16 @@
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
); );
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
"$(SRCROOT)/../node_modules/realm/src/**", "$(SRCROOT)/../node_modules/realm/src/**",
"$(SRCROOT)/../node_modules/react-native-share/ios", "$(SRCROOT)/../node_modules/react-native-share/ios",
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
"$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker",
"$(SRCROOT)/../node_modules/react-native-fs/**",
); );
}; };
name = Debug; name = Debug;
@@ -1425,12 +1522,16 @@
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
); );
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
"$(SRCROOT)/../node_modules/realm/src/**", "$(SRCROOT)/../node_modules/realm/src/**",
"$(SRCROOT)/../node_modules/react-native-share/ios", "$(SRCROOT)/../node_modules/react-native-share/ios",
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
"$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker",
"$(SRCROOT)/../node_modules/react-native-fs/**",
); );
}; };
name = Release; name = Release;
+18 -2
View File
@@ -39,9 +39,8 @@
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string></string> <string/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<!--See http://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/ -->
<dict> <dict>
<key>NSExceptionDomains</key> <key>NSExceptionDomains</key>
<dict> <dict>
@@ -52,5 +51,22 @@
</dict> </dict>
</dict> </dict>
</dict> </dict>
<key>UIAppFonts</key>
<array>
<string>Entypo.ttf</string>
<string>EvilIcons.ttf</string>
<string>Feather.ttf</string>
<string>FontAwesome.ttf</string>
<string>FontAwesome5_Brands.ttf</string>
<string>FontAwesome5_Regular.ttf</string>
<string>FontAwesome5_Solid.ttf</string>
<string>Foundation.ttf</string>
<string>Ionicons.ttf</string>
<string>MaterialCommunityIcons.ttf</string>
<string>MaterialIcons.ttf</string>
<string>Octicons.ttf</string>
<string>SimpleLineIcons.ttf</string>
<string>Zocial.ttf</string>
</array>
</dict> </dict>
</plist> </plist>
@@ -1,7 +1,7 @@
import objectPath from 'object-path' import objectPath from 'object-path'
import { Base64 } from 'js-base64' import { Base64 } from 'js-base64'
import { cycleDaysSortedByDate } from '../../db'
import { getColumnNamesForCsv, cycleDaysSortedByDate } from '../db' import getColumnNamesForCsv from './get-csv-column-names'
export default function makeDataURI() { export default function makeDataURI() {
if (!cycleDaysSortedByDate.length) return null if (!cycleDaysSortedByDate.length) return null
+19
View File
@@ -0,0 +1,19 @@
import { schema } from '../../db'
export default function getColumnNamesForCsv() {
return getPrefixedKeys('CycleDay')
function getPrefixedKeys(schemaName, prefix) {
const model = schema[schemaName]
return Object.keys(model).reduce((acc, key) => {
const prefixedKey = prefix ? [prefix, key].join('.') : key
const childSchemaName = model[key].objectType
if (!childSchemaName) {
acc.push(prefixedKey)
return acc
}
acc.push(...getPrefixedKeys(childSchemaName, prefixedKey))
return acc
}, [])
}
}
+82
View File
@@ -0,0 +1,82 @@
import csvParser from 'csvtojson'
import isObject from 'isobject'
import { schema, tryToImportWithDelete, tryToImportWithoutDelete } from '../../db'
import getColumnNamesForCsv from './get-csv-column-names'
export default async function importCsv(csv, deleteFirst) {
const parseFuncs = {
bool: val => {
if (val.toLowerCase() === 'true') return true
if (val.toLowerCase() === 'false') return false
return val
},
int: parseNumberIfPossible,
float: parseNumberIfPossible,
double: parseNumberIfPossible,
string: val => val
}
function parseNumberIfPossible(val) {
// Number and parseFloat catch different cases of weirdness,
// so we test them both
if (isNaN(Number(val)) || isNaN(parseFloat(val))) return val
return Number(val)
}
const config = {
ignoreEmpty: true,
colParser: getColumnNamesForCsv().reduce((acc, colName) => {
const path = colName.split('.')
const dbType = getDbType(schema.CycleDay, path)
acc[colName] = item => {
if (item === '') return null
return parseFuncs[dbType](item)
}
return acc
}, {})
}
const cycleDays = await csvParser(config)
.fromString(csv)
.on('header', validateHeaders)
//remove symptoms where all fields are null
putNullForEmptySymptoms(cycleDays)
if (deleteFirst) {
tryToImportWithDelete(cycleDays)
} else {
tryToImportWithoutDelete(cycleDays)
}
}
function validateHeaders(headers) {
const expectedHeaders = getColumnNamesForCsv()
if (!headers.every(header => {
return expectedHeaders.indexOf(header) > -1
})) {
const msg = `Expected CSV column titles to be ${expectedHeaders.join()}`
throw new Error(msg)
}
}
function putNullForEmptySymptoms(data) {
data.forEach(replaceWithNullIfAllPropertiesAreNull)
function replaceWithNullIfAllPropertiesAreNull(obj) {
Object.keys(obj).forEach((key) => {
if (!isObject(obj[key])) return
if (Object.values(obj[key]).every(val => val === null)) {
obj[key] = null
return
}
replaceWithNullIfAllPropertiesAreNull(obj[key])
})
}
}
function getDbType(modelProperties, path) {
if (path.length === 1) return modelProperties[path[0]].type
const modelName = modelProperties[path[0]].objectType
return getDbType(schema[modelName], path.slice(1))
}
+1782 -1535
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -16,7 +16,9 @@
}, },
"dependencies": { "dependencies": {
"assert": "^1.4.1", "assert": "^1.4.1",
"csvtojson": "^2.0.8",
"date-range": "0.0.2", "date-range": "0.0.2",
"isobject": "^3.0.1",
"js-base64": "^2.4.8", "js-base64": "^2.4.8",
"js-joda": "^1.8.2", "js-joda": "^1.8.2",
"moment": "^2.22.1", "moment": "^2.22.1",
@@ -24,16 +26,19 @@
"react": "16.4.1", "react": "16.4.1",
"react-native": "^0.56.0", "react-native": "^0.56.0",
"react-native-calendars": "^1.19.3", "react-native-calendars": "^1.19.3",
"react-native-document-picker": "^2.1.0",
"react-native-fs": "^2.10.14",
"react-native-modal-datetime-picker-nevo": "^4.11.0", "react-native-modal-datetime-picker-nevo": "^4.11.0",
"react-native-share": "^1.1.0", "react-native-share": "^1.1.0",
"react-native-simple-radio-button": "^2.7.1", "react-native-simple-radio-button": "^2.7.1",
"react-native-svg": "^6.3.1", "react-native-vector-icons": "^5.0.0",
"react-navigation": "^2.0.4", "react-navigation": "^2.0.4",
"realm": "^2.7.1", "realm": "^2.7.1",
"uuid": "^3.2.1" "uuid": "^3.2.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/register": "^7.0.0-beta.55", "@babel/register": "^7.0.0-beta.55",
"babel-eslint": "^8.2.6",
"babel-preset-react-native": "^5.0.0", "babel-preset-react-native": "^5.0.0",
"chai": "^4.1.2", "chai": "^4.1.2",
"dirty-chai": "^2.0.1", "dirty-chai": "^2.0.1",