Merge branch '133-make-temp-scale-a-config-setting-and-read-it-in-chart-and-temp-screen' into 'master'

Resolve "make temp scale a config setting and read it in chart and temp screen"

Closes #133

See merge request bloodyhealth/drip!59
This commit is contained in:
Julia Friesel
2018-08-27 15:49:04 +00:00
18 changed files with 2031 additions and 1723 deletions
+8 -1
View File
@@ -49,6 +49,13 @@
"prefer-const": "error", "prefer-const": "error",
"no-trailing-spaces": "error", "no-trailing-spaces": "error",
"react/prop-types": 0, "react/prop-types": 0,
"max-len": [1, {"ignoreStrings": true}] "max-len": [
1,
{
"ignoreStrings": true,
"ignoreComments": true,
"ignoreTemplateLiterals": true
}
]
} }
} }
+94 -60
View File
@@ -1,51 +1,123 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { View, FlatList, ScrollView } from 'react-native' import { View, FlatList, Text } from 'react-native'
import range from 'date-range' import range from 'date-range'
import { LocalDate } from 'js-joda' import { LocalDate } from 'js-joda'
import { yAxis, normalizeToScale, horizontalGrid } from './y-axis' import { makeYAxisLabels, normalizeToScale, makeHorizontalGrid } from './y-axis'
import setUpFertilityStatusFunc from './nfp-lines' import nfpLines from './nfp-lines'
import DayColumn from './day-column' import DayColumn from './day-column'
import { getCycleDay, cycleDaysSortedByDate, getAmountOfCycleDays } from '../../db' import { getCycleDay, cycleDaysSortedByDate, getAmountOfCycleDays } from '../../db'
import styles from './styles' import styles from './styles'
import { scaleObservable } from '../../local-storage'
const yAxisView = <View {...styles.yAxis}>{yAxis.labels}</View> import config from '../../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(setUpFertilityStatusFunc()),
}
this.renderColumn = ({item, index}) => { this.renderColumn = ({item, index}) => {
return ( return (
<DayColumn <DayColumn
{...item} {...item}
index={index} index={index}
navigate={this.props.navigate} navigate={this.props.navigate}
chartHeight={this.state.chartHeight}
/> />
) )
} }
}
this.reCalculateChartInfo = (function(Chart) { onLayout = ({ nativeEvent }) => {
return function() { if (this.state.chartHeight) return
Chart.setState({columns: makeColumnInfo(setUpFertilityStatusFunc())})
} const height = nativeEvent.layout.height
})(this) this.setState({ chartHeight: height })
this.reCalculateChartInfo = () => {
this.setState({ columns: this.makeColumnInfo(nfpLines(height)) })
}
cycleDaysSortedByDate.addListener(this.reCalculateChartInfo) cycleDaysSortedByDate.addListener(this.reCalculateChartInfo)
this.removeObvListener = scaleObservable(this.reCalculateChartInfo, false)
} }
componentWillUnmount() { componentWillUnmount() {
cycleDaysSortedByDate.removeListener(this.reCalculateChartInfo) cycleDaysSortedByDate.removeListener(this.reCalculateChartInfo)
this.removeObvListener()
}
makeColumnInfo(getFhmAndLtlInfo) {
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 jsDates = getTodayAndPreviousDays(amountOfCycleDays)
const xAxisDates = jsDates.map(jsDate => {
return LocalDate.of(
jsDate.getFullYear(),
jsDate.getMonth() + 1,
jsDate.getDate()
).toString()
})
const columns = xAxisDates.map(dateString => {
const cycleDay = getCycleDay(dateString)
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
}, {})
const temp = symptoms.temperature
const columnHeight = this.state.chartHeight * config.columnHeightPercentage
return {
dateString,
y: temp ? normalizeToScale(temp, columnHeight) : null,
...symptoms,
...getFhmAndLtlInfo(dateString, temp)
}
})
return columns.map((col, i) => {
const info = getInfoForNeighborColumns(i, columns)
return Object.assign(col, info)
})
} }
render() { render() {
let columnHeight
let symptomRowHeight
if (this.state.chartHeight) {
columnHeight = this.state.chartHeight * config.columnHeightPercentage
symptomRowHeight = this.state.chartHeight * config.symptomRowHeightPercentage
}
return ( return (
<ScrollView> <View
<View style={{ flexDirection: 'row', marginTop: 50 }}> onLayout={this.onLayout}
{yAxisView} style={{ flexDirection: 'row', flex: 1 }}
{horizontalGrid} >
{<FlatList {!this.state.chartLoaded &&
<View style={{width: '100%', justifyContent: 'center', alignItems: 'center'}}>
<Text>Loading...</Text>
</View>
}
{this.state.chartHeight && this.state.chartLoaded &&
<View
style={[styles.yAxis, {
height: columnHeight,
marginTop: symptomRowHeight
}]}
>
{makeYAxisLabels(columnHeight)}
</View>}
{this.state.chartHeight && this.state.chartLoaded && makeHorizontalGrid(columnHeight, symptomRowHeight)}
{this.state.chartHeight &&
<FlatList
horizontal={true} horizontal={true}
inverted={true} inverted={true}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
@@ -54,52 +126,14 @@ export default class CycleChart extends Component {
keyExtractor={item => item.dateString} keyExtractor={item => item.dateString}
initialNumToRender={15} initialNumToRender={15}
maxToRenderPerBatch={5} maxToRenderPerBatch={5}
> onLayout={() => this.setState({chartLoaded: true})}
</FlatList>} />
</View> }
</ScrollView> </View>
) )
} }
} }
function makeColumnInfo(getFhmAndLtlInfo) {
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(
jsDate.getFullYear(),
jsDate.getMonth() + 1,
jsDate.getDate()
).toString()
})
const columns = xAxisDates.map(dateString => {
const cycleDay = getCycleDay(dateString)
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 {
dateString,
y: symptoms.temperature ? normalizeToScale(symptoms.temperature) : null,
...symptoms,
...getFhmAndLtlInfo(dateString, symptoms.temperature)
}
})
return columns.map((col, i) => {
const info = getInfoForNeighborColumns(i, columns)
return Object.assign(col, info)
})
}
function getTodayAndPreviousDays(n) { function getTodayAndPreviousDays(n) {
const today = new Date() const today = new Date()
-16
View File
@@ -1,16 +0,0 @@
const config = {
chartHeight: 350,
columnWidth: 25,
temperatureScale: {
low: 35,
high: 38,
units: 0.1
}
}
const margin = 3
config.columnMiddle = config.columnWidth / 2,
config.dateRowY = config.chartHeight - 15 - margin
config.cycleDayNumberRowY = config.chartHeight - margin
export default config
+52 -50
View File
@@ -4,7 +4,7 @@ import {
} from 'react-native' } from 'react-native'
import Icon from 'react-native-vector-icons/Entypo' import Icon from 'react-native-vector-icons/Entypo'
import styles from './styles' import styles from './styles'
import config from './config' import config from '../../config'
import { getOrCreateCycleDay } from '../../db' import { getOrCreateCycleDay } from '../../db'
import cycleModule from '../../lib/cycle' import cycleModule from '../../lib/cycle'
import DotAndLine from './dot-and-line' import DotAndLine from './dot-and-line'
@@ -34,48 +34,15 @@ export default class DayColumn extends Component {
rightY, rightY,
rightTemperatureExclude, rightTemperatureExclude,
leftY, leftY,
leftTemperatureExclude leftTemperatureExclude,
chartHeight
} = this.props } = this.props
const columnHeight = chartHeight * config.columnHeightPercentage
const xAxisHeight = chartHeight * config.xAxisHeightPercentage
const symptomHeight = chartHeight * config.symptomRowHeightPercentage
const columnElements = [] 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) { if(drawLtlAt) {
const ltlLine = (<View const ltlLine = (<View
@@ -105,25 +72,31 @@ export default class DayColumn extends Component {
const cycleDayNumber = getCycleDayNumber(dateString) const cycleDayNumber = getCycleDayNumber(dateString)
const shortDate = dateString.split('-').slice(1).join('-') const shortDate = dateString.split('-').slice(1).join('-')
const cycleDayLabel = ( const cycleDayLabel = (
<Text style={label.number} y={config.cycleDayNumberRowY}> <Text style={label.number}>
{cycleDayNumber} {cycleDayNumber}
</Text>) </Text>)
const dateLabel = ( const dateLabel = (
<Text style = {label.date} y={config.dateRowY}> <Text style = {label.date}>
{shortDate} {shortDate}
</Text> </Text>
) )
columnElements.push(
<View position='absolute' bottom={0} key='date'>
{cycleDayLabel}
{dateLabel}
</View>
)
return React.createElement( // we merge the colors here instead of from the stylesheet because of a RN
// bug that doesn't apply borderLeftColor otherwise
const customStyle = {
height: columnHeight,
borderLeftColor: 'grey',
borderRightColor: 'grey'
}
if (drawFhmLine) {
customStyle.borderLeftColor = styles.nfpLine.borderColor
customStyle.borderLeftWidth = 3
}
const column = React.createElement(
TouchableOpacity, TouchableOpacity,
{ {
style: styles.column.rect, style: [styles.column.rect, customStyle],
key: this.props.index.toString(), key: this.props.index.toString(),
onPress: () => { onPress: () => {
this.passDateToDayView(dateString) this.passDateToDayView(dateString)
@@ -132,5 +105,34 @@ export default class DayColumn extends Component {
}, },
columnElements columnElements
) )
return (
<View>
<View style={[styles.symptomRow, {height: symptomHeight}]}>
{typeof mucus === 'number' &&
<View
{...styles.mucusIcon}
backgroundColor={styles.mucusIconShades[mucus]}
key='mucus'
/>
}
{typeof bleeding === 'number' &&
<Icon
name='drop'
size={18}
color='#900'
key='bleeding'
/>
}
</View>
{column}
<View style={{height: xAxisHeight}}>
{cycleDayLabel}
{dateLabel}
</View>
</View>
)
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { View } from 'react-native' import { View } from 'react-native'
import styles from './styles' import styles from './styles'
import config from './config' import config from '../../config'
export default class DotAndLine extends Component { export default class DotAndLine extends Component {
shouldComponentUpdate(newProps) { shouldComponentUpdate(newProps) {
+2 -2
View File
@@ -1,7 +1,7 @@
import { getCycleStatusForDay } from '../../lib/sympto-adapter' import { getCycleStatusForDay } from '../../lib/sympto-adapter'
import { normalizeToScale } from './y-axis' import { normalizeToScale } from './y-axis'
export default function () { export default function (chartHeight) {
const cycle = { const cycle = {
status: null status: null
} }
@@ -71,7 +71,7 @@ export default function () {
dateIsInPeriOrPostPhase(dateString) && dateIsInPeriOrPostPhase(dateString) &&
isInTempMeasuringPhase(temperature, dateString) isInTempMeasuringPhase(temperature, dateString)
) { ) {
ret.drawLtlAt = normalizeToScale(tempShift.ltl) ret.drawLtlAt = normalizeToScale(tempShift.ltl, chartHeight)
} }
} }
+7 -5
View File
@@ -1,4 +1,4 @@
import config from './config' import config from '../../config'
const styles = { const styles = {
curve: { curve: {
@@ -38,10 +38,9 @@ const styles = {
}, },
rect: { rect: {
width: config.columnWidth, width: config.columnWidth,
height: config.chartHeight,
borderStyle: 'solid', borderStyle: 'solid',
borderColor: 'grey', borderLeftWidth: 0.5,
borderWidth: 0.5 borderRightWidth: 0.5,
} }
}, },
bleedingIcon: { bleedingIcon: {
@@ -63,7 +62,6 @@ const styles = {
'#993299' '#993299'
], ],
yAxis: { yAxis: {
height: config.chartHeight,
width: config.columnWidth, width: config.columnWidth,
borderRightWidth: 0.5, borderRightWidth: 0.5,
borderColor: 'lightgrey', borderColor: 'lightgrey',
@@ -88,6 +86,10 @@ const styles = {
borderColor: '#00b159', borderColor: '#00b159',
borderWidth: 2, borderWidth: 2,
borderStyle: 'solid' borderStyle: 'solid'
},
symptomRow: {
alignItems: 'center',
justifyContent: 'center'
} }
} }
+51 -36
View File
@@ -1,53 +1,68 @@
import React from 'react' import React from 'react'
import { Text, View } from 'react-native' import { Text, View } from 'react-native'
import config from './config' import config from '../../config'
import styles from './styles' import styles from './styles'
import { scaleObservable } from '../../local-storage'
function makeYAxis() { export function makeYAxisLabels(columnHeight) {
const scale = config.temperatureScale const units = config.temperatureScale.units
const scaleMin = scale.low const scaleMax = scaleObservable.value.max
const scaleMax = scale.high const style = styles.yAxisLabel
const numberOfTicks = (scaleMax - scaleMin) * (1 / scale.units)
const tickDistance = config.chartHeight / numberOfTicks
const tickPositions = [] return getTickPositions(columnHeight).map((y, i) => {
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 // this eyeballing is sadly necessary because RN does not
// support percentage values for transforms, which we'd need // support percentage values for transforms, which we'd need
// to reliably place the label vertically centered to the grid // to reliably place the label vertically centered to the grid
style.top = y - 8 if (scaleMax - i * units === 37) console.log(y)
labels.push( return (
<Text <Text
style={{...style}} style={[style, {top: y - 8}]}
key={i}> key={i}>
{scaleMax - i * scale.units} {scaleMax - i * units}
</Text> </Text>
) )
tickPositions.push(y) })
}
return {labels, tickPositions}
} }
export const yAxis = makeYAxis() export function makeHorizontalGrid(columnHeight, symptomRowHeight) {
return getTickPositions(columnHeight).map(tick => {
return (
<View
top={tick + symptomRowHeight}
{...styles.horizontalGrid}
key={tick}
/>
)
})
}
export const horizontalGrid = yAxis.tickPositions.map(tick => { function getTickPositions(columnHeight) {
return ( const units = config.temperatureScale.units
<View const scaleMin = scaleObservable.value.min
top={tick} const scaleMax = scaleObservable.value.max
{...styles.horizontalGrid} const numberOfTicks = (scaleMax - scaleMin) * (1 / units) + 1
key={tick} const tickDistance = 1 / (numberOfTicks - 1)
/>
) const tickPositions = []
}) for (let i = 0; i < numberOfTicks; i++) {
const position = getAbsoluteValue(tickDistance * i, columnHeight)
tickPositions.push(position)
}
return tickPositions
}
export function normalizeToScale(temp, columnHeight) {
const scale = scaleObservable.value
const valueRelativeToScale = (scale.max - temp) / (scale.max - scale.min)
return getAbsoluteValue(valueRelativeToScale, columnHeight)
}
function getAbsoluteValue(relative, columnHeight) {
// we add some height to have some breathing room
const verticalPadding = columnHeight * config.temperatureScale.verticalPadding
const scaleHeight = columnHeight - 2 * verticalPadding
console.log(scaleHeight)
console.log(columnHeight)
return scaleHeight * relative + verticalPadding
export function normalizeToScale(temp) {
const scale = config.temperatureScale
const valueRelativeToScale = (scale.high - temp) / (scale.high - scale.low)
const scaleHeight = config.chartHeight
return scaleHeight * valueRelativeToScale
} }
+6
View File
@@ -26,3 +26,9 @@ export const fertilityStatus = {
fertileUntilEvening: 'Fertile phase ends in the evening', fertileUntilEvening: 'Fertile phase ends in the evening',
unknown: 'We cannot show any cycle information because no menses has been entered' unknown: 'We cannot show any cycle information because no menses has been entered'
} }
export const temperature = {
outOfRangeWarning: 'This temperature value is out of the current range for the temperature chart. You can change the range in the settings.',
outOfAbsoluteRangeWarning: 'This temperature value is too high or low to be shown on the temperature chart.',
saveAnyway: 'Save anyway'
}
@@ -8,7 +8,14 @@ import styles, {iconStyles} from '../../../styles'
export default class ActionButtonFooter extends Component { export default class ActionButtonFooter extends Component {
render() { render() {
const { symptom, cycleDay, saveAction, saveDisabled, navigate} = this.props const {
symptom,
cycleDay,
saveAction,
saveDisabled,
navigate,
autoShowDayView = true}
= this.props
const navigateToOverView = () => navigate('CycleDay', {cycleDay}) const navigateToOverView = () => navigate('CycleDay', {cycleDay})
const buttons = [ const buttons = [
{ {
@@ -28,7 +35,7 @@ export default class ActionButtonFooter extends Component {
title: 'Save', title: 'Save',
action: () => { action: () => {
saveAction() saveAction()
navigateToOverView() if (autoShowDayView) navigateToOverView()
}, },
disabledCondition: saveDisabled, disabledCondition: saveDisabled,
icon: 'content-save-outline' icon: 'content-save-outline'
+96 -30
View File
@@ -5,6 +5,7 @@ import {
TextInput, TextInput,
Switch, Switch,
Keyboard, Keyboard,
Alert,
ScrollView ScrollView
} from 'react-native' } from 'react-native'
import DateTimePicker from 'react-native-modal-datetime-picker-nevo' import DateTimePicker from 'react-native-modal-datetime-picker-nevo'
@@ -12,35 +13,84 @@ import DateTimePicker from 'react-native-modal-datetime-picker-nevo'
import { getPreviousTemperature, saveSymptom } from '../../../db' import { getPreviousTemperature, saveSymptom } from '../../../db'
import styles from '../../../styles' import styles from '../../../styles'
import { LocalTime, ChronoUnit } from 'js-joda' import { LocalTime, ChronoUnit } from 'js-joda'
import { temperature as tempLabels } from '../labels/labels'
import { scaleObservable } from '../../../local-storage'
import { shared } from '../../labels'
import ActionButtonFooter from './action-button-footer' import ActionButtonFooter from './action-button-footer'
import config from '../../../config'
const MINUTES = ChronoUnit.MINUTES const minutes = ChronoUnit.MINUTES
export default class Temp extends Component { export default class Temp extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.cycleDay = props.cycleDay this.cycleDay = props.cycleDay
this.makeActionButtons = props.makeActionButtons this.makeActionButtons = props.makeActionButtons
let initialValue
const temp = this.cycleDay.temperature const temp = this.cycleDay.temperature
if (temp) { this.state = {
initialValue = temp.value.toString() exclude: temp ? temp.exclude : false,
this.time = temp.time time: temp ? temp.time : LocalTime.now().truncatedTo(minutes).toString(),
} else { isTimePickerVisible: false,
const prevTemp = getPreviousTemperature(this.cycleDay) outOfRange: null
initialValue = prevTemp ? prevTemp.toString() : ''
} }
this.state = { if (temp) {
currentValue: initialValue, this.state.temperature = temp.value.toString()
exclude: temp ? temp.exclude : false, if (temp.value === Math.floor(temp.value)) {
time: this.time || LocalTime.now().truncatedTo(MINUTES).toString(), this.state.temperature = `${this.state.temperature}.0`
isTimePickerVisible: false }
} else {
const prevTemp = getPreviousTemperature(this.cycleDay)
if (prevTemp) {
this.state.temperature = prevTemp.toString()
this.state.isSuggestion = true
}
} }
} }
saveTemperature = () => {
const dataToSave = {
value: Number(this.state.temperature),
exclude: this.state.exclude,
time: this.state.time
}
saveSymptom('temperature', this.cycleDay, dataToSave)
this.props.navigate('CycleDay', {cycleDay: this.cycleDay})
}
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 = tempLabels.outOfAbsoluteRangeWarning
} else if (value < scale.min || value > scale.max) {
warningMsg = tempLabels.outOfRangeWarning
}
if (warningMsg) {
Alert.alert(
shared.warning,
warningMsg,
[
{ text: shared.cancel },
{ text: shared.save, onPress: this.saveTemperature}
]
)
} else {
this.saveTemperature()
}
}
render() { render() {
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
@@ -48,14 +98,10 @@ export default class Temp extends Component {
<View> <View>
<View style={styles.symptomViewRowInline}> <View style={styles.symptomViewRowInline}>
<Text style={styles.symptomDayView}>Temperature (°C)</Text> <Text style={styles.symptomDayView}>Temperature (°C)</Text>
<TextInput <TempInput
style={styles.temperatureTextInput} value={this.state.temperature}
placeholder="Enter" setState={(val) => this.setState(val)}
onChangeText={(val) => { isSuggestion={this.state.isSuggestion}
this.setState({ currentValue: val })
}}
keyboardType='numeric'
value={this.state.currentValue}
/> />
</View> </View>
<View style={styles.symptomViewRowInline}> <View style={styles.symptomViewRowInline}>
@@ -94,22 +140,42 @@ export default class Temp extends Component {
<ActionButtonFooter <ActionButtonFooter
symptom='temperature' symptom='temperature'
cycleDay={this.cycleDay} cycleDay={this.cycleDay}
saveAction={() => { saveAction={() => this.checkRangeAndSave()}
const dataToSave = { saveDisabled={
value: Number(this.state.currentValue), this.state.temperature === '' ||
exclude: this.state.exclude, isNaN(Number(this.state.temperature)) ||
time: this.state.time isInvalidTime(this.state.time)
} }
saveSymptom('temperature', this.props.cycleDay, dataToSave)
}}
saveDisabled={this.state.currentValue === '' || isInvalidTime(this.state.time)}
navigate={this.props.navigate} navigate={this.props.navigate}
autoShowDayView={false}
/> />
</View> </View>
) )
} }
} }
class TempInput extends Component {
render() {
const style = [styles.temperatureTextInput]
if (this.props.isSuggestion) {
style.push(styles.temperatureTextInputSuggestion)
}
return (
<TextInput
style={style}
onChangeText={(val) => {
if (isNaN(Number(val))) return
this.props.setState({ temperature: val, isSuggestion: false })
}}
keyboardType='numeric'
value={this.props.value}
onBlur={this.checkRange}
autoFocus={true}
/>
)
}
}
function isInvalidTime(timeString) { function isInvalidTime(timeString) {
try { try {
LocalTime.parse(timeString) LocalTime.parse(timeString)
+19 -6
View File
@@ -1,9 +1,12 @@
export const shared = {
cancel: 'Cancel',
save: 'Save',
errorTitle: 'Error',
successTitle: 'Success',
warning: 'Warning'
}
export const settings = { export const settings = {
shared: {
cancel: 'Cancel',
errorTitle: 'Error',
successTitle: 'Success'
},
export: { export: {
errors: { errors: {
noData: 'There is no data to export', noData: 'There is no data to export',
@@ -13,6 +16,7 @@ export const settings = {
title: 'My Drip data export', title: 'My Drip data export',
subject: 'My Drip data export', subject: 'My Drip data export',
button: 'Export data', button: 'Export data',
segmentExplainer: 'Export data in CSV format for backup or so you can use it elsewhere'
}, },
import: { import: {
button: 'Import data', button: 'Import data',
@@ -28,7 +32,16 @@ export const settings = {
}, },
success: { success: {
message: 'Data successfully imported' message: 'Data successfully imported'
} },
segmentExplainer: 'Import data in CSV format'
},
tempScale: {
segmentTitle: 'Temperature scale',
segmentExplainer: 'Change the minimum and maximum value for the temperature chart',
min: 'Min',
max: 'Max',
loadError: 'Could not load saved temperature scale settings',
saveError: 'Could not save temperature scale settings'
} }
} }
+114 -33
View File
@@ -1,80 +1,161 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { import {
View, View,
Button, TouchableOpacity,
ScrollView, ScrollView,
Alert Alert,
Text
} from 'react-native' } from 'react-native'
import Slider from '@ptomasroos/react-native-multi-slider'
import Share from 'react-native-share' import Share from 'react-native-share'
import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker' import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker'
import rnfs from 'react-native-fs' import rnfs from 'react-native-fs'
import styles from '../styles/index' import styles, { secondaryColor } from '../styles/index'
import { settings as labels } from './labels' import config from '../config'
import { settings as settingsLabels, shared as sharedLabels } from './labels'
import getDataAsCsvDataUri from '../lib/import-export/export-to-csv' import getDataAsCsvDataUri from '../lib/import-export/export-to-csv'
import importCsv from '../lib/import-export/import-from-csv' import importCsv from '../lib/import-export/import-from-csv'
import { scaleObservable, saveTempScale } from '../local-storage'
export default class Settings extends Component { export default class Settings extends Component {
render() { render() {
return ( return (
<ScrollView> <ScrollView>
<View style={styles.homeButtons}> <View style={styles.settingsSegment}>
<View style={styles.homeButton}> <Text style={styles.settingsSegmentTitle}>
<Button {settingsLabels.tempScale.segmentTitle}
onPress={ openShareDialogAndExport } </Text>
title={labels.export.button}> <Text>{settingsLabels.tempScale.segmentExplainer}</Text>
</Button> <TempSlider/>
</View> </View>
<View style={styles.homeButton}> <View style={styles.settingsSegment}>
<Button <Text style={styles.settingsSegmentTitle}>
title={labels.import.button} {settingsLabels.export.button}
onPress={ openImportDialogAndImport }> </Text>
</Button> <Text>{settingsLabels.export.segmentExplainer}</Text>
</View> <TouchableOpacity
onPress={openShareDialogAndExport}
style={styles.settingsButton}>
<Text style={styles.settingsButtonText}>
{settingsLabels.export.button}
</Text>
</TouchableOpacity>
</View>
<View style={styles.settingsSegment}>
<Text style={styles.settingsSegmentTitle}>
{settingsLabels.import.button}
</Text>
<Text>{settingsLabels.import.segmentExplainer}</Text>
<TouchableOpacity
onPress={openImportDialogAndImport}
style={styles.settingsButton}>
<Text style={styles.settingsButtonText}>
{settingsLabels.import.button}
</Text>
</TouchableOpacity>
</View> </View>
</ScrollView> </ScrollView>
) )
} }
} }
class TempSlider extends Component {
constructor(props) {
super(props)
this.state = Object.assign({}, scaleObservable.value)
}
onValuesChange = (values) => {
this.setState({
min: values[0],
max: values[1]
})
}
onValuesChangeFinish = (values) => {
this.setState({
min: values[0],
max: values[1]
})
try {
saveTempScale(this.state)
} catch(err) {
alertError(settingsLabels.tempScale.saveError)
}
}
render() {
return (
<View style={{alignItems: 'center'}}>
<Text>{`${settingsLabels.tempScale.min} ${this.state.min}`}</Text>
<Text>{`${settingsLabels.tempScale.max} ${this.state.max}`}</Text>
<Slider
values={[this.state.min, this.state.max]}
min={config.temperatureScale.min}
max={config.temperatureScale.max}
step={0.5}
onValuesChange={this.onValuesChange}
onValuesChangeFinish={this.onValuesChangeFinish}
selectedStyle={{
backgroundColor: 'darkgrey',
}}
unselectedStyle={{
backgroundColor: 'silver',
}}
trackStyle={{
height:10,
}}
markerStyle={{
backgroundColor: secondaryColor,
height: 20,
width: 20,
borderRadius: 100,
marginTop: 10
}}
/>
</View>
)
}
}
async function openShareDialogAndExport() { async function openShareDialogAndExport() {
let data let data
try { try {
data = getDataAsCsvDataUri() data = getDataAsCsvDataUri()
if (!data) { if (!data) {
return alertError(labels.errors.noData) return alertError(settingsLabels.errors.noData)
} }
} catch (err) { } catch (err) {
console.error(err) console.error(err)
return alertError(labels.errors.couldNotConvert) return alertError(settingsLabels.errors.couldNotConvert)
} }
try { try {
await Share.open({ await Share.open({
title: labels.export.title, title: settingsLabels.export.title,
url: data, url: data,
subject: labels.export.subject, subject: settingsLabels.export.subject,
type: 'text/csv', type: 'text/csv',
showAppsToView: true showAppsToView: true
}) })
} catch (err) { } catch (err) {
console.error(err) console.error(err)
return alertError(labels.export.errors.problemSharing) return alertError(settingsLabels.export.errors.problemSharing)
} }
} }
function openImportDialogAndImport() { function openImportDialogAndImport() {
Alert.alert( Alert.alert(
labels.import.title, settingsLabels.import.title,
labels.import.message, settingsLabels.import.message,
[{ [{
text: labels.import.replaceOption, text: settingsLabels.import.replaceOption,
onPress: () => getFileContentAndImport({ deleteExisting: false }) onPress: () => getFileContentAndImport({ deleteExisting: false })
}, { }, {
text: labels.import.deleteOption, text: settingsLabels.import.deleteOption,
onPress: () => getFileContentAndImport({ deleteExisting: true }) onPress: () => getFileContentAndImport({ deleteExisting: true })
}, { }, {
text: labels.shared.cancel, style: 'cancel', onPress: () => { } text: sharedLabels.cancel, style: 'cancel', onPress: () => { }
}] }]
) )
} }
@@ -99,22 +180,22 @@ async function getFileContentAndImport({ deleteExisting }) {
try { try {
fileContent = await rnfs.readFile(fileInfo.uri, 'utf8') fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
} catch (err) { } catch (err) {
return importError(labels.import.errors.couldNotOpenFile) return importError(settingsLabels.import.errors.couldNotOpenFile)
} }
try { try {
await importCsv(fileContent, deleteExisting) await importCsv(fileContent, deleteExisting)
Alert.alert(labels.import.success.title, labels.import.success.message) Alert.alert(sharedLabels.successTitle, settingsLabels.import.success.message)
} catch(err) { } catch(err) {
importError(err.message) importError(err.message)
} }
} }
function alertError(msg) { function alertError(msg) {
Alert.alert(labels.shared.errorTitle, msg) Alert.alert(sharedLabels.errorTitle, msg)
} }
function importError(msg) { function importError(msg) {
const postFixed = `${msg}\n\n${labels.import.errors.postFix}` const postFixed = `${msg}\n\n${settingsLabels.import.errors.postFix}`
alertError(postFixed) alertError(postFixed)
} }
+18
View File
@@ -0,0 +1,18 @@
const config = {
columnWidth: 25,
columnHeightPercentage: 0.84,
xAxisHeightPercentage: 0.08,
symptomRowHeightPercentage: 0.08,
temperatureScale: {
defaultLow: 35,
defaultHigh: 38,
min: 34,
max: 40,
units: 0.1,
verticalPadding: 0.03
}
}
config.columnMiddle = config.columnWidth / 2
export default config
+26
View File
@@ -0,0 +1,26 @@
import { AsyncStorage } from 'react-native'
import Observable from 'obv'
import config from '../config'
export const scaleObservable = Observable()
setTempScaleInitially()
async function setTempScaleInitially() {
const result = await AsyncStorage.getItem('tempScale')
let value
if (result) {
value = JSON.parse(result)
} else {
value = {
min: config.temperatureScale.defaultLow,
max: config.temperatureScale.defaultHigh
}
}
scaleObservable.set(value)
}
export async function saveTempScale(scale) {
await AsyncStorage.setItem('tempScale', JSON.stringify(scale))
scaleObservable.set(scale)
}
+1498 -1474
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -15,6 +15,7 @@
"lint": "eslint components lib test" "lint": "eslint components lib test"
}, },
"dependencies": { "dependencies": {
"@ptomasroos/react-native-multi-slider": "^1.0.0",
"assert": "^1.4.1", "assert": "^1.4.1",
"csvtojson": "^2.0.8", "csvtojson": "^2.0.8",
"date-range": "0.0.2", "date-range": "0.0.2",
@@ -23,6 +24,7 @@
"js-joda": "^1.8.2", "js-joda": "^1.8.2",
"moment": "^2.22.1", "moment": "^2.22.1",
"object-path": "^0.11.4", "object-path": "^0.11.4",
"obv": "0.0.1",
"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",
+28 -7
View File
@@ -92,16 +92,17 @@ export default StyleSheet.create({
}, },
header: { header: {
backgroundColor: primaryColor, backgroundColor: primaryColor,
paddingVertical: 18,
paddingHorizontal: 15, paddingHorizontal: 15,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center' justifyContent: 'center',
height: '10%'
}, },
menu: { menu: {
backgroundColor: primaryColor, backgroundColor: primaryColor,
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
flexDirection: 'row' flexDirection: 'row',
height: '12%'
}, },
menuItem: { menuItem: {
alignItems: 'center', alignItems: 'center',
@@ -116,7 +117,8 @@ export default StyleSheet.create({
}, },
headerCycleDay: { headerCycleDay: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between' justifyContent: 'space-between',
height: '15%'
}, },
navigationArrow: { navigationArrow: {
fontSize: 60, fontSize: 60,
@@ -129,9 +131,11 @@ export default StyleSheet.create({
marginBottom: 15 marginBottom: 15
}, },
temperatureTextInput: { temperatureTextInput: {
width: 80, fontSize: 20,
textAlign: 'center', color: 'black'
fontSize: 20 },
temperatureTextInputSuggestion: {
color: '#939393'
}, },
actionButtonRow: { actionButtonRow: {
flexDirection: 'row', flexDirection: 'row',
@@ -152,6 +156,23 @@ export default StyleSheet.create({
textAlign: 'left', textAlign: 'left',
textAlignVertical: 'center' textAlignVertical: 'center'
}, },
settingsSegment: {
backgroundColor: 'lightgrey',
padding: 10,
marginBottom: 10,
},
settingsSegmentTitle: {
fontWeight: 'bold'
},
settingsButton: {
backgroundColor: secondaryColor,
padding: 10,
alignItems: 'center',
margin: 10
},
settingsButtonText: {
color: fontOnPrimaryColor
},
statsRow: { statsRow: {
flexDirection: 'row', flexDirection: 'row',
width: '100%' width: '100%'