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:
@@ -49,6 +49,13 @@
|
||||
"prefer-const": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"react/prop-types": 0,
|
||||
"max-len": [1, {"ignoreStrings": true}]
|
||||
"max-len": [
|
||||
1,
|
||||
{
|
||||
"ignoreStrings": true,
|
||||
"ignoreComments": true,
|
||||
"ignoreTemplateLiterals": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+74
-40
@@ -1,68 +1,50 @@
|
||||
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 { LocalDate } from 'js-joda'
|
||||
import { yAxis, normalizeToScale, horizontalGrid } from './y-axis'
|
||||
import setUpFertilityStatusFunc from './nfp-lines'
|
||||
import { makeYAxisLabels, normalizeToScale, makeHorizontalGrid } from './y-axis'
|
||||
import nfpLines from './nfp-lines'
|
||||
import DayColumn from './day-column'
|
||||
import { getCycleDay, cycleDaysSortedByDate, getAmountOfCycleDays } from '../../db'
|
||||
import styles from './styles'
|
||||
|
||||
const yAxisView = <View {...styles.yAxis}>{yAxis.labels}</View>
|
||||
import { scaleObservable } from '../../local-storage'
|
||||
import config from '../../config'
|
||||
|
||||
export default class CycleChart extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
columns: makeColumnInfo(setUpFertilityStatusFunc()),
|
||||
}
|
||||
this.state = {}
|
||||
this.renderColumn = ({item, index}) => {
|
||||
return (
|
||||
<DayColumn
|
||||
{...item}
|
||||
index={index}
|
||||
navigate={this.props.navigate}
|
||||
chartHeight={this.state.chartHeight}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
this.reCalculateChartInfo = (function(Chart) {
|
||||
return function() {
|
||||
Chart.setState({columns: makeColumnInfo(setUpFertilityStatusFunc())})
|
||||
}
|
||||
})(this)
|
||||
|
||||
onLayout = ({ nativeEvent }) => {
|
||||
if (this.state.chartHeight) return
|
||||
|
||||
const height = nativeEvent.layout.height
|
||||
this.setState({ chartHeight: height })
|
||||
this.reCalculateChartInfo = () => {
|
||||
this.setState({ columns: this.makeColumnInfo(nfpLines(height)) })
|
||||
}
|
||||
|
||||
cycleDaysSortedByDate.addListener(this.reCalculateChartInfo)
|
||||
this.removeObvListener = scaleObservable(this.reCalculateChartInfo, false)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
cycleDaysSortedByDate.removeListener(this.reCalculateChartInfo)
|
||||
this.removeObvListener()
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={{ flexDirection: 'row', marginTop: 50 }}>
|
||||
{yAxisView}
|
||||
{horizontalGrid}
|
||||
{<FlatList
|
||||
horizontal={true}
|
||||
inverted={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
data={this.state.columns}
|
||||
renderItem={this.renderColumn}
|
||||
keyExtractor={item => item.dateString}
|
||||
initialNumToRender={15}
|
||||
maxToRenderPerBatch={5}
|
||||
>
|
||||
</FlatList>}
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function makeColumnInfo(getFhmAndLtlInfo) {
|
||||
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) {
|
||||
@@ -71,7 +53,8 @@ function makeColumnInfo(getFhmAndLtlInfo) {
|
||||
// we don't want the chart to end abruptly before the first data day
|
||||
amountOfCycleDays += 5
|
||||
}
|
||||
const xAxisDates = getTodayAndPreviousDays(amountOfCycleDays).map(jsDate => {
|
||||
const jsDates = getTodayAndPreviousDays(amountOfCycleDays)
|
||||
const xAxisDates = jsDates.map(jsDate => {
|
||||
return LocalDate.of(
|
||||
jsDate.getFullYear(),
|
||||
jsDate.getMonth() + 1,
|
||||
@@ -87,11 +70,13 @@ function makeColumnInfo(getFhmAndLtlInfo) {
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const temp = symptoms.temperature
|
||||
const columnHeight = this.state.chartHeight * config.columnHeightPercentage
|
||||
return {
|
||||
dateString,
|
||||
y: symptoms.temperature ? normalizeToScale(symptoms.temperature) : null,
|
||||
y: temp ? normalizeToScale(temp, columnHeight) : null,
|
||||
...symptoms,
|
||||
...getFhmAndLtlInfo(dateString, symptoms.temperature)
|
||||
...getFhmAndLtlInfo(dateString, temp)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -99,8 +84,57 @@ function makeColumnInfo(getFhmAndLtlInfo) {
|
||||
const info = getInfoForNeighborColumns(i, columns)
|
||||
return Object.assign(col, info)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
let columnHeight
|
||||
let symptomRowHeight
|
||||
if (this.state.chartHeight) {
|
||||
columnHeight = this.state.chartHeight * config.columnHeightPercentage
|
||||
symptomRowHeight = this.state.chartHeight * config.symptomRowHeightPercentage
|
||||
}
|
||||
return (
|
||||
<View
|
||||
onLayout={this.onLayout}
|
||||
style={{ flexDirection: 'row', flex: 1 }}
|
||||
>
|
||||
{!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}
|
||||
inverted={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
data={this.state.columns}
|
||||
renderItem={this.renderColumn}
|
||||
keyExtractor={item => item.dateString}
|
||||
initialNumToRender={15}
|
||||
maxToRenderPerBatch={5}
|
||||
onLayout={() => this.setState({chartLoaded: true})}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getTodayAndPreviousDays(n) {
|
||||
const today = new Date()
|
||||
today.setHours(0)
|
||||
|
||||
@@ -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
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from 'react-native'
|
||||
import Icon from 'react-native-vector-icons/Entypo'
|
||||
import styles from './styles'
|
||||
import config from './config'
|
||||
import config from '../../config'
|
||||
import { getOrCreateCycleDay } from '../../db'
|
||||
import cycleModule from '../../lib/cycle'
|
||||
import DotAndLine from './dot-and-line'
|
||||
@@ -34,48 +34,15 @@ export default class DayColumn extends Component {
|
||||
rightY,
|
||||
rightTemperatureExclude,
|
||||
leftY,
|
||||
leftTemperatureExclude
|
||||
leftTemperatureExclude,
|
||||
chartHeight
|
||||
} = this.props
|
||||
|
||||
const columnHeight = chartHeight * config.columnHeightPercentage
|
||||
const xAxisHeight = chartHeight * config.xAxisHeightPercentage
|
||||
const symptomHeight = chartHeight * config.symptomRowHeightPercentage
|
||||
|
||||
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
|
||||
@@ -105,25 +72,31 @@ export default class DayColumn extends Component {
|
||||
const cycleDayNumber = getCycleDayNumber(dateString)
|
||||
const shortDate = dateString.split('-').slice(1).join('-')
|
||||
const cycleDayLabel = (
|
||||
<Text style={label.number} y={config.cycleDayNumberRowY}>
|
||||
<Text style={label.number}>
|
||||
{cycleDayNumber}
|
||||
</Text>)
|
||||
const dateLabel = (
|
||||
<Text style = {label.date} y={config.dateRowY}>
|
||||
<Text style = {label.date}>
|
||||
{shortDate}
|
||||
</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,
|
||||
{
|
||||
style: styles.column.rect,
|
||||
style: [styles.column.rect, customStyle],
|
||||
key: this.props.index.toString(),
|
||||
onPress: () => {
|
||||
this.passDateToDayView(dateString)
|
||||
@@ -132,5 +105,34 @@ export default class DayColumn extends Component {
|
||||
},
|
||||
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,7 +1,7 @@
|
||||
import React, { Component } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import styles from './styles'
|
||||
import config from './config'
|
||||
import config from '../../config'
|
||||
|
||||
export default class DotAndLine extends Component {
|
||||
shouldComponentUpdate(newProps) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getCycleStatusForDay } from '../../lib/sympto-adapter'
|
||||
import { normalizeToScale } from './y-axis'
|
||||
|
||||
export default function () {
|
||||
export default function (chartHeight) {
|
||||
const cycle = {
|
||||
status: null
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export default function () {
|
||||
dateIsInPeriOrPostPhase(dateString) &&
|
||||
isInTempMeasuringPhase(temperature, dateString)
|
||||
) {
|
||||
ret.drawLtlAt = normalizeToScale(tempShift.ltl)
|
||||
ret.drawLtlAt = normalizeToScale(tempShift.ltl, chartHeight)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import config from './config'
|
||||
import config from '../../config'
|
||||
|
||||
const styles = {
|
||||
curve: {
|
||||
@@ -38,10 +38,9 @@ const styles = {
|
||||
},
|
||||
rect: {
|
||||
width: config.columnWidth,
|
||||
height: config.chartHeight,
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'grey',
|
||||
borderWidth: 0.5
|
||||
borderLeftWidth: 0.5,
|
||||
borderRightWidth: 0.5,
|
||||
}
|
||||
},
|
||||
bleedingIcon: {
|
||||
@@ -63,7 +62,6 @@ const styles = {
|
||||
'#993299'
|
||||
],
|
||||
yAxis: {
|
||||
height: config.chartHeight,
|
||||
width: config.columnWidth,
|
||||
borderRightWidth: 0.5,
|
||||
borderColor: 'lightgrey',
|
||||
@@ -88,6 +86,10 @@ const styles = {
|
||||
borderColor: '#00b159',
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid'
|
||||
},
|
||||
symptomRow: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+47
-32
@@ -1,53 +1,68 @@
|
||||
import React from 'react'
|
||||
import { Text, View } from 'react-native'
|
||||
import config from './config'
|
||||
import config from '../../config'
|
||||
import styles from './styles'
|
||||
import { scaleObservable } from '../../local-storage'
|
||||
|
||||
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
|
||||
export function makeYAxisLabels(columnHeight) {
|
||||
const units = config.temperatureScale.units
|
||||
const scaleMax = scaleObservable.value.max
|
||||
const style = styles.yAxisLabel
|
||||
|
||||
return getTickPositions(columnHeight).map((y, i) => {
|
||||
// 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(
|
||||
if (scaleMax - i * units === 37) console.log(y)
|
||||
return (
|
||||
<Text
|
||||
style={{...style}}
|
||||
style={[style, {top: y - 8}]}
|
||||
key={i}>
|
||||
{scaleMax - i * scale.units}
|
||||
{scaleMax - i * units}
|
||||
</Text>
|
||||
)
|
||||
tickPositions.push(y)
|
||||
}
|
||||
|
||||
return {labels, tickPositions}
|
||||
})
|
||||
}
|
||||
|
||||
export const yAxis = makeYAxis()
|
||||
|
||||
export const horizontalGrid = yAxis.tickPositions.map(tick => {
|
||||
export function makeHorizontalGrid(columnHeight, symptomRowHeight) {
|
||||
return getTickPositions(columnHeight).map(tick => {
|
||||
return (
|
||||
<View
|
||||
top={tick}
|
||||
top={tick + symptomRowHeight}
|
||||
{...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
|
||||
})
|
||||
}
|
||||
|
||||
function getTickPositions(columnHeight) {
|
||||
const units = config.temperatureScale.units
|
||||
const scaleMin = scaleObservable.value.min
|
||||
const scaleMax = scaleObservable.value.max
|
||||
const numberOfTicks = (scaleMax - scaleMin) * (1 / units) + 1
|
||||
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
|
||||
|
||||
}
|
||||
@@ -26,3 +26,9 @@ export const fertilityStatus = {
|
||||
fertileUntilEvening: 'Fertile phase ends in the evening',
|
||||
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 {
|
||||
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 buttons = [
|
||||
{
|
||||
@@ -28,7 +35,7 @@ export default class ActionButtonFooter extends Component {
|
||||
title: 'Save',
|
||||
action: () => {
|
||||
saveAction()
|
||||
navigateToOverView()
|
||||
if (autoShowDayView) navigateToOverView()
|
||||
},
|
||||
disabledCondition: saveDisabled,
|
||||
icon: 'content-save-outline'
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
TextInput,
|
||||
Switch,
|
||||
Keyboard,
|
||||
Alert,
|
||||
ScrollView
|
||||
} from 'react-native'
|
||||
import DateTimePicker from 'react-native-modal-datetime-picker-nevo'
|
||||
@@ -12,34 +13,83 @@ import DateTimePicker from 'react-native-modal-datetime-picker-nevo'
|
||||
import { getPreviousTemperature, saveSymptom } from '../../../db'
|
||||
import styles from '../../../styles'
|
||||
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 config from '../../../config'
|
||||
|
||||
const MINUTES = ChronoUnit.MINUTES
|
||||
const minutes = ChronoUnit.MINUTES
|
||||
|
||||
export default class Temp extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.cycleDay = props.cycleDay
|
||||
this.makeActionButtons = props.makeActionButtons
|
||||
let initialValue
|
||||
|
||||
const temp = this.cycleDay.temperature
|
||||
|
||||
if (temp) {
|
||||
initialValue = temp.value.toString()
|
||||
this.time = temp.time
|
||||
} else {
|
||||
const prevTemp = getPreviousTemperature(this.cycleDay)
|
||||
initialValue = prevTemp ? prevTemp.toString() : ''
|
||||
this.state = {
|
||||
exclude: temp ? temp.exclude : false,
|
||||
time: temp ? temp.time : LocalTime.now().truncatedTo(minutes).toString(),
|
||||
isTimePickerVisible: false,
|
||||
outOfRange: null
|
||||
}
|
||||
|
||||
this.state = {
|
||||
currentValue: initialValue,
|
||||
exclude: temp ? temp.exclude : false,
|
||||
time: this.time || LocalTime.now().truncatedTo(MINUTES).toString(),
|
||||
isTimePickerVisible: false
|
||||
if (temp) {
|
||||
this.state.temperature = temp.value.toString()
|
||||
if (temp.value === Math.floor(temp.value)) {
|
||||
this.state.temperature = `${this.state.temperature}.0`
|
||||
}
|
||||
} 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() {
|
||||
return (
|
||||
@@ -48,14 +98,10 @@ export default class Temp extends Component {
|
||||
<View>
|
||||
<View style={styles.symptomViewRowInline}>
|
||||
<Text style={styles.symptomDayView}>Temperature (°C)</Text>
|
||||
<TextInput
|
||||
style={styles.temperatureTextInput}
|
||||
placeholder="Enter"
|
||||
onChangeText={(val) => {
|
||||
this.setState({ currentValue: val })
|
||||
}}
|
||||
keyboardType='numeric'
|
||||
value={this.state.currentValue}
|
||||
<TempInput
|
||||
value={this.state.temperature}
|
||||
setState={(val) => this.setState(val)}
|
||||
isSuggestion={this.state.isSuggestion}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.symptomViewRowInline}>
|
||||
@@ -94,22 +140,42 @@ export default class Temp extends Component {
|
||||
<ActionButtonFooter
|
||||
symptom='temperature'
|
||||
cycleDay={this.cycleDay}
|
||||
saveAction={() => {
|
||||
const dataToSave = {
|
||||
value: Number(this.state.currentValue),
|
||||
exclude: this.state.exclude,
|
||||
time: this.state.time
|
||||
saveAction={() => this.checkRangeAndSave()}
|
||||
saveDisabled={
|
||||
this.state.temperature === '' ||
|
||||
isNaN(Number(this.state.temperature)) ||
|
||||
isInvalidTime(this.state.time)
|
||||
}
|
||||
saveSymptom('temperature', this.props.cycleDay, dataToSave)
|
||||
}}
|
||||
saveDisabled={this.state.currentValue === '' || isInvalidTime(this.state.time)}
|
||||
navigate={this.props.navigate}
|
||||
autoShowDayView={false}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class TempInput extends Component {
|
||||
render() {
|
||||
const style = [styles.temperatureTextInput]
|
||||
if (this.props.isSuggestion) {
|
||||
style.push(styles.temperatureTextInputSuggestion)
|
||||
}
|
||||
return (
|
||||
<TextInput
|
||||
style={style}
|
||||
onChangeText={(val) => {
|
||||
if (isNaN(Number(val))) return
|
||||
this.props.setState({ temperature: val, isSuggestion: false })
|
||||
}}
|
||||
keyboardType='numeric'
|
||||
value={this.props.value}
|
||||
onBlur={this.checkRange}
|
||||
autoFocus={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function isInvalidTime(timeString) {
|
||||
try {
|
||||
LocalTime.parse(timeString)
|
||||
|
||||
+18
-5
@@ -1,9 +1,12 @@
|
||||
export const settings = {
|
||||
shared: {
|
||||
export const shared = {
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
errorTitle: 'Error',
|
||||
successTitle: 'Success'
|
||||
},
|
||||
successTitle: 'Success',
|
||||
warning: 'Warning'
|
||||
}
|
||||
|
||||
export const settings = {
|
||||
export: {
|
||||
errors: {
|
||||
noData: 'There is no data to export',
|
||||
@@ -13,6 +16,7 @@ export const settings = {
|
||||
title: 'My Drip data export',
|
||||
subject: 'My Drip data export',
|
||||
button: 'Export data',
|
||||
segmentExplainer: 'Export data in CSV format for backup or so you can use it elsewhere'
|
||||
},
|
||||
import: {
|
||||
button: 'Import data',
|
||||
@@ -28,7 +32,16 @@ export const settings = {
|
||||
},
|
||||
success: {
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+111
-30
@@ -1,80 +1,161 @@
|
||||
import React, { Component } from 'react'
|
||||
import {
|
||||
View,
|
||||
Button,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Alert
|
||||
Alert,
|
||||
Text
|
||||
} from 'react-native'
|
||||
|
||||
import Slider from '@ptomasroos/react-native-multi-slider'
|
||||
import Share from 'react-native-share'
|
||||
import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker'
|
||||
import rnfs from 'react-native-fs'
|
||||
import styles from '../styles/index'
|
||||
import { settings as labels } from './labels'
|
||||
import styles, { secondaryColor } from '../styles/index'
|
||||
import config from '../config'
|
||||
import { settings as settingsLabels, shared as sharedLabels } from './labels'
|
||||
import getDataAsCsvDataUri from '../lib/import-export/export-to-csv'
|
||||
import importCsv from '../lib/import-export/import-from-csv'
|
||||
import { scaleObservable, saveTempScale } from '../local-storage'
|
||||
|
||||
export default class Settings extends Component {
|
||||
render() {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.homeButtons}>
|
||||
<View style={styles.homeButton}>
|
||||
<Button
|
||||
onPress={ openShareDialogAndExport }
|
||||
title={labels.export.button}>
|
||||
</Button>
|
||||
<View style={styles.settingsSegment}>
|
||||
<Text style={styles.settingsSegmentTitle}>
|
||||
{settingsLabels.tempScale.segmentTitle}
|
||||
</Text>
|
||||
<Text>{settingsLabels.tempScale.segmentExplainer}</Text>
|
||||
<TempSlider/>
|
||||
</View>
|
||||
<View style={styles.homeButton}>
|
||||
<Button
|
||||
title={labels.import.button}
|
||||
onPress={ openImportDialogAndImport }>
|
||||
</Button>
|
||||
<View style={styles.settingsSegment}>
|
||||
<Text style={styles.settingsSegmentTitle}>
|
||||
{settingsLabels.export.button}
|
||||
</Text>
|
||||
<Text>{settingsLabels.export.segmentExplainer}</Text>
|
||||
<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>
|
||||
</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() {
|
||||
let data
|
||||
try {
|
||||
data = getDataAsCsvDataUri()
|
||||
if (!data) {
|
||||
return alertError(labels.errors.noData)
|
||||
return alertError(settingsLabels.errors.noData)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return alertError(labels.errors.couldNotConvert)
|
||||
return alertError(settingsLabels.errors.couldNotConvert)
|
||||
}
|
||||
|
||||
try {
|
||||
await Share.open({
|
||||
title: labels.export.title,
|
||||
title: settingsLabels.export.title,
|
||||
url: data,
|
||||
subject: labels.export.subject,
|
||||
subject: settingsLabels.export.subject,
|
||||
type: 'text/csv',
|
||||
showAppsToView: true
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return alertError(labels.export.errors.problemSharing)
|
||||
return alertError(settingsLabels.export.errors.problemSharing)
|
||||
}
|
||||
}
|
||||
|
||||
function openImportDialogAndImport() {
|
||||
Alert.alert(
|
||||
labels.import.title,
|
||||
labels.import.message,
|
||||
settingsLabels.import.title,
|
||||
settingsLabels.import.message,
|
||||
[{
|
||||
text: labels.import.replaceOption,
|
||||
text: settingsLabels.import.replaceOption,
|
||||
onPress: () => getFileContentAndImport({ deleteExisting: false })
|
||||
}, {
|
||||
text: labels.import.deleteOption,
|
||||
text: settingsLabels.import.deleteOption,
|
||||
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 {
|
||||
fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
|
||||
} catch (err) {
|
||||
return importError(labels.import.errors.couldNotOpenFile)
|
||||
return importError(settingsLabels.import.errors.couldNotOpenFile)
|
||||
}
|
||||
|
||||
try {
|
||||
await importCsv(fileContent, deleteExisting)
|
||||
Alert.alert(labels.import.success.title, labels.import.success.message)
|
||||
Alert.alert(sharedLabels.successTitle, settingsLabels.import.success.message)
|
||||
} catch(err) {
|
||||
importError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function alertError(msg) {
|
||||
Alert.alert(labels.shared.errorTitle, msg)
|
||||
Alert.alert(sharedLabels.errorTitle, msg)
|
||||
}
|
||||
|
||||
function importError(msg) {
|
||||
const postFixed = `${msg}\n\n${labels.import.errors.postFix}`
|
||||
const postFixed = `${msg}\n\n${settingsLabels.import.errors.postFix}`
|
||||
alertError(postFixed)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Generated
+1498
-1474
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
||||
"lint": "eslint components lib test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ptomasroos/react-native-multi-slider": "^1.0.0",
|
||||
"assert": "^1.4.1",
|
||||
"csvtojson": "^2.0.8",
|
||||
"date-range": "0.0.2",
|
||||
@@ -23,6 +24,7 @@
|
||||
"js-joda": "^1.8.2",
|
||||
"moment": "^2.22.1",
|
||||
"object-path": "^0.11.4",
|
||||
"obv": "0.0.1",
|
||||
"react": "16.4.1",
|
||||
"react-native": "^0.56.0",
|
||||
"react-native-calendars": "^1.19.3",
|
||||
|
||||
+28
-7
@@ -92,16 +92,17 @@ export default StyleSheet.create({
|
||||
},
|
||||
header: {
|
||||
backgroundColor: primaryColor,
|
||||
paddingVertical: 18,
|
||||
paddingHorizontal: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
height: '10%'
|
||||
},
|
||||
menu: {
|
||||
backgroundColor: primaryColor,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexDirection: 'row'
|
||||
flexDirection: 'row',
|
||||
height: '12%'
|
||||
},
|
||||
menuItem: {
|
||||
alignItems: 'center',
|
||||
@@ -116,7 +117,8 @@ export default StyleSheet.create({
|
||||
},
|
||||
headerCycleDay: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
justifyContent: 'space-between',
|
||||
height: '15%'
|
||||
},
|
||||
navigationArrow: {
|
||||
fontSize: 60,
|
||||
@@ -129,9 +131,11 @@ export default StyleSheet.create({
|
||||
marginBottom: 15
|
||||
},
|
||||
temperatureTextInput: {
|
||||
width: 80,
|
||||
textAlign: 'center',
|
||||
fontSize: 20
|
||||
fontSize: 20,
|
||||
color: 'black'
|
||||
},
|
||||
temperatureTextInputSuggestion: {
|
||||
color: '#939393'
|
||||
},
|
||||
actionButtonRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -152,6 +156,23 @@ export default StyleSheet.create({
|
||||
textAlign: 'left',
|
||||
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: {
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
|
||||
Reference in New Issue
Block a user