diff --git a/components/settings.js b/components/settings.js deleted file mode 100644 index 125cd27176c8cd9aef2ad1fd907b5e511baaf1da..0000000000000000000000000000000000000000 --- a/components/settings.js +++ /dev/null @@ -1,283 +0,0 @@ -import React, { Component } from 'react' -import { - View, - TouchableOpacity, - ScrollView, - Alert, - Switch -} from 'react-native' -import DateTimePicker from 'react-native-modal-datetime-picker-nevo' -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, { secondaryColor } from '../styles/index' -import config from '../config' -import { settings as labels, 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, - tempReminderObservable, - saveTempReminder -} from '../local-storage' -import { AppText } from './app-text' - -export default class Settings extends Component { - constructor(props) { - super(props) - this.state = {} - } - - render() { - return ( - <ScrollView> - <TempReminderPicker/> - <View style={styles.settingsSegment}> - <AppText style={styles.settingsSegmentTitle}> - {labels.tempScale.segmentTitle} - </AppText> - <AppText>{labels.tempScale.segmentExplainer}</AppText> - <TempSlider/> - </View> - <View style={styles.settingsSegment}> - <AppText style={styles.settingsSegmentTitle}> - {labels.export.button} - </AppText> - <AppText>{labels.export.segmentExplainer}</AppText> - <TouchableOpacity - onPress={openShareDialogAndExport} - style={styles.settingsButton}> - <AppText style={styles.settingsButtonText}> - {labels.export.button} - </AppText> - </TouchableOpacity> - </View> - <View style={styles.settingsSegment}> - <AppText style={styles.settingsSegmentTitle}> - {labels.import.button} - </AppText> - <AppText>{labels.import.segmentExplainer}</AppText> - <TouchableOpacity - onPress={openImportDialogAndImport} - style={styles.settingsButton}> - <AppText style={styles.settingsButtonText}> - {labels.import.button} - </AppText> - </TouchableOpacity> - </View> - </ScrollView> - ) - } -} - -class TempReminderPicker extends Component { - constructor(props) { - super(props) - this.state = Object.assign({}, tempReminderObservable.value) - } - - render() { - return ( - <TouchableOpacity - style={styles.settingsSegment} - onPress={() => this.setState({ isTimePickerVisible: true })} - > - <AppText style={styles.settingsSegmentTitle}> - {labels.tempReminder.title} - </AppText> - <View style={{ flexDirection: 'row', alignItems: 'center' }}> - <View style={{ flex: 1 }}> - {this.state.time && this.state.enabled ? - <AppText>{labels.tempReminder.timeSet(this.state.time)}</AppText> - : - <AppText>{labels.tempReminder.noTimeSet}</AppText> - } - </View> - <Switch - value={this.state.enabled} - onValueChange={switchOn => { - this.setState({ enabled: switchOn }) - if (switchOn && !this.state.time) { - this.setState({ isTimePickerVisible: true }) - } - if (!switchOn) saveTempReminder({ enabled: false }) - }} - /> - <DateTimePicker - mode="time" - isVisible={this.state.isTimePickerVisible} - onConfirm={jsDate => { - const time = padWithZeros(`${jsDate.getHours()}:${jsDate.getMinutes()}`) - this.setState({ - time, - isTimePickerVisible: false, - enabled: true - }) - saveTempReminder({ - time, - enabled: true - }) - }} - onCancel={() => { - this.setState({ isTimePickerVisible: false }) - if (!this.state.time) this.setState({enabled: false}) - }} - /> - </View> - </TouchableOpacity> - ) - } -} - -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(labels.tempScale.saveError) - } - } - - render() { - return ( - <View style={{ alignItems: 'center' }}> - <AppText>{`${labels.tempScale.min} ${this.state.min}`}</AppText> - <AppText>{`${labels.tempScale.max} ${this.state.max}`}</AppText> - <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) - } - } 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: sharedLabels.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(sharedLabels.successTitle, labels.import.success.message) - } catch(err) { - importError(err.message) - } -} - -function alertError(msg) { - Alert.alert(sharedLabels.errorTitle, msg) -} - -function importError(msg) { - const postFixed = `${msg}\n\n${labels.import.errors.postFix}` - alertError(postFixed) -} - -function padWithZeros(time) { - const vals = time.split(':') - return vals.map(val => { - if (parseInt(val) < 10) { - val = `0${val}` - } - return val - }).join(':') -} \ No newline at end of file diff --git a/components/settings/alert-error.js b/components/settings/alert-error.js new file mode 100644 index 0000000000000000000000000000000000000000..ae375405ff0a3136a10622314224b19819053f5b --- /dev/null +++ b/components/settings/alert-error.js @@ -0,0 +1,6 @@ +import { Alert } from 'react-native' +import { shared as sharedLabels } from '../labels' + +export default function alertError(msg) { + Alert.alert(sharedLabels.errorTitle, msg) +} \ No newline at end of file diff --git a/components/settings/export-dialog.js b/components/settings/export-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..4e3c21f170265e35b203a6a94da81c24a452af01 --- /dev/null +++ b/components/settings/export-dialog.js @@ -0,0 +1,31 @@ +import Share from 'react-native-share' +import getDataAsCsvDataUri from '../../lib/import-export/export-to-csv' +import alertError from './alert-error' +import { settings as labels } from '../labels' + +export default 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) + } +} + diff --git a/components/settings/import-dialog.js b/components/settings/import-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..a07671780bbe98a854cefdea9126610fd71f567b --- /dev/null +++ b/components/settings/import-dialog.js @@ -0,0 +1,58 @@ +import { Alert } from 'react-native' +import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker' +import rnfs from 'react-native-fs' +import importCsv from '../../lib/import-export/import-from-csv' +import { settings as labels, shared as sharedLabels } from '../labels' +import alertError from './alert-error' + +export default 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: sharedLabels.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(sharedLabels.successTitle, labels.import.success.message) + } catch(err) { + importError(err.message) + } +} + +function importError(msg) { + const postFixed = `${msg}\n\n${labels.import.errors.postFix}` + alertError(postFixed) +} \ No newline at end of file diff --git a/components/settings/index.js b/components/settings/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5e729be93003a12cdceb6e645a8fbfdf5a6da1e0 --- /dev/null +++ b/components/settings/index.js @@ -0,0 +1,61 @@ +import React, { Component } from 'react' +import { + View, + TouchableOpacity, + ScrollView, +} from 'react-native' +import styles from '../../styles/index' +import { settings as labels } from '../labels' +import { AppText } from '../app-text' +import TempReminderPicker from './temp-reminder-picker' +import TempSlider from './temp-slider' +import openImportDialogAndImport from './import-dialog' +import openShareDialogAndExport from './export-dialog' + +export default class Settings extends Component { + constructor(props) { + super(props) + this.state = {} + } + + render() { + return ( + <ScrollView> + <TempReminderPicker/> + <View style={styles.settingsSegment}> + <AppText style={styles.settingsSegmentTitle}> + {labels.tempScale.segmentTitle} + </AppText> + <AppText>{labels.tempScale.segmentExplainer}</AppText> + <TempSlider/> + </View> + <View style={styles.settingsSegment}> + <AppText style={styles.settingsSegmentTitle}> + {labels.export.button} + </AppText> + <AppText>{labels.export.segmentExplainer}</AppText> + <TouchableOpacity + onPress={openShareDialogAndExport} + style={styles.settingsButton}> + <AppText style={styles.settingsButtonText}> + {labels.export.button} + </AppText> + </TouchableOpacity> + </View> + <View style={styles.settingsSegment}> + <AppText style={styles.settingsSegmentTitle}> + {labels.import.button} + </AppText> + <AppText>{labels.import.segmentExplainer}</AppText> + <TouchableOpacity + onPress={openImportDialogAndImport} + style={styles.settingsButton}> + <AppText style={styles.settingsButtonText}> + {labels.import.button} + </AppText> + </TouchableOpacity> + </View> + </ScrollView> + ) + } +} diff --git a/components/settings/temp-reminder-picker.js b/components/settings/temp-reminder-picker.js new file mode 100644 index 0000000000000000000000000000000000000000..8cfbf86edda1c4b3d1bcfbebda3238f5fe40f712 --- /dev/null +++ b/components/settings/temp-reminder-picker.js @@ -0,0 +1,83 @@ +import React, { Component } from 'react' +import { + View, + TouchableOpacity, + Switch +} from 'react-native' +import DateTimePicker from 'react-native-modal-datetime-picker-nevo' +import { AppText } from '../app-text' +import { + tempReminderObservable, + saveTempReminder +} from '../../local-storage' +import styles from '../../styles/index' +import { settings as labels } from '../labels' + +export default class TempReminderPicker extends Component { + constructor(props) { + super(props) + this.state = Object.assign({}, tempReminderObservable.value) + } + + render() { + return ( + <TouchableOpacity + style={styles.settingsSegment} + onPress={() => this.setState({ isTimePickerVisible: true })} + > + <AppText style={styles.settingsSegmentTitle}> + {labels.tempReminder.title} + </AppText> + <View style={{ flexDirection: 'row', alignItems: 'center' }}> + <View style={{ flex: 1 }}> + {this.state.time && this.state.enabled ? + <AppText>{labels.tempReminder.timeSet(this.state.time)}</AppText> + : + <AppText>{labels.tempReminder.noTimeSet}</AppText> + } + </View> + <Switch + value={this.state.enabled} + onValueChange={switchOn => { + this.setState({ enabled: switchOn }) + if (switchOn && !this.state.time) { + this.setState({ isTimePickerVisible: true }) + } + if (!switchOn) saveTempReminder({ enabled: false }) + }} + /> + <DateTimePicker + mode="time" + isVisible={this.state.isTimePickerVisible} + onConfirm={jsDate => { + const time = padWithZeros(`${jsDate.getHours()}:${jsDate.getMinutes()}`) + this.setState({ + time, + isTimePickerVisible: false, + enabled: true + }) + saveTempReminder({ + time, + enabled: true + }) + }} + onCancel={() => { + this.setState({ isTimePickerVisible: false }) + if (!this.state.time) this.setState({enabled: false}) + }} + /> + </View> + </TouchableOpacity> + ) + } +} + +function padWithZeros(time) { + const vals = time.split(':') + return vals.map(val => { + if (parseInt(val) < 10) { + val = `0${val}` + } + return val + }).join(':') +} \ No newline at end of file diff --git a/components/settings/temp-slider.js b/components/settings/temp-slider.js new file mode 100644 index 0000000000000000000000000000000000000000..f21fd74006ebe606417b26baebb75e25241af88d --- /dev/null +++ b/components/settings/temp-slider.js @@ -0,0 +1,71 @@ +import React, { Component } from 'react' +import { View } from 'react-native' +import Slider from '@ptomasroos/react-native-multi-slider' +import { AppText } from '../app-text' +import { + scaleObservable, + saveTempScale, +} from '../../local-storage' +import { secondaryColor } from '../../styles/index' +import { settings as labels } from '../labels' +import config from '../../config' +import alertError from './alert-error' + +export default 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(labels.tempScale.saveError) + } + } + + render() { + return ( + <View style={{ alignItems: 'center' }}> + <AppText>{`${labels.tempScale.min} ${this.state.min}`}</AppText> + <AppText>{`${labels.tempScale.max} ${this.state.max}`}</AppText> + <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> + ) + } +} \ No newline at end of file diff --git a/lib/import-export/export-to-csv.js b/lib/import-export/export-to-csv.js index c5bca2b3ffb4c4afd5669fa3d62c93e4ac1468ca..619c5e2b4bcc6c2e1a8bb5e497e8571543348985 100644 --- a/lib/import-export/export-to-csv.js +++ b/lib/import-export/export-to-csv.js @@ -1,9 +1,10 @@ import objectPath from 'object-path' import { Base64 } from 'js-base64' -import { cycleDaysSortedByDate } from '../../db' +import { getCycleDaysSortedByDate } from '../../db' import getColumnNamesForCsv from './get-csv-column-names' export default function makeDataURI() { + const cycleDaysSortedByDate = getCycleDaysSortedByDate() if (!cycleDaysSortedByDate.length) return null const csv = transformToCsv(cycleDaysSortedByDate) diff --git a/lib/import-export/get-csv-column-names.js b/lib/import-export/get-csv-column-names.js index 1b63943ab6b5a6b05eee6460684b62b3947f3075..3e8b425417cd212d442cb05bf2fbdeaece8b383c 100644 --- a/lib/import-export/get-csv-column-names.js +++ b/lib/import-export/get-csv-column-names.js @@ -1,9 +1,10 @@ -import { schema } from '../../db' +import { getSchema } from '../../db' export default function getColumnNamesForCsv() { return getPrefixedKeys('CycleDay') function getPrefixedKeys(schemaName, prefix) { + const schema = getSchema() const model = schema[schemaName] return Object.keys(model).reduce((acc, key) => { const prefixedKey = prefix ? [prefix, key].join('.') : key diff --git a/lib/import-export/import-from-csv.js b/lib/import-export/import-from-csv.js index 77c2444430072f4441340dcef4e732ebcf2fbf9a..c34ebc9b21b9e1f0c1860abb453b6defcccd3350 100644 --- a/lib/import-export/import-from-csv.js +++ b/lib/import-export/import-from-csv.js @@ -1,6 +1,6 @@ import csvParser from 'csvtojson' import isObject from 'isobject' -import { schema, tryToImportWithDelete, tryToImportWithoutDelete } from '../../db' +import { getSchema, tryToImportWithDelete, tryToImportWithoutDelete } from '../../db' import getColumnNamesForCsv from './get-csv-column-names' export default async function importCsv(csv, deleteFirst) { @@ -23,6 +23,7 @@ export default async function importCsv(csv, deleteFirst) { return Number(val) } + const schema = getSchema() const config = { ignoreEmpty: true, colParser: getColumnNamesForCsv().reduce((acc, colName) => { @@ -76,6 +77,7 @@ function putNullForEmptySymptoms(data) { } function getDbType(modelProperties, path) { + const schema = getSchema() if (path.length === 1) return modelProperties[path[0]].type const modelName = modelProperties[path[0]].objectType return getDbType(schema[modelName], path.slice(1))