diff --git a/messages/renderer/en.json b/messages/renderer/en.json index 78ba6e8df..ebcf72392 100644 --- a/messages/renderer/en.json +++ b/messages/renderer/en.json @@ -1,4 +1,48 @@ { + "renderer.components.BackgroundMaps.BackgroundMapInfo.createOfflineArea": { + "description": "Button to create an offline area", + "message": "Create Offline Area" + }, + "renderer.components.BackgroundMaps.BackgroundMapInfo.deleteErrorDescription": { + "description": "Description for error message when deleting style,", + "message": "There was an error deleting the style" + }, + "renderer.components.BackgroundMaps.BackgroundMapInfo.deleteErrorTitle": { + "description": "Title for error message when deleting style", + "message": "Error Deleting Style" + }, + "renderer.components.BackgroundMaps.BackgroundMapInfo.deleteStyle": { + "description": "Button to delete style", + "message": "Delete Style" + }, + "renderer.components.BackgroundMaps.BackgroundMapInfo.mb": { + "description": "abbreviation for megabyte", + "message": "MB" + }, + "renderer.components.BackgroundMaps.BackgroundMapInfo.offlineAreas": { + "description": "Title for Offline Areas", + "message": "Offline Areas" + }, + "renderer.components.BackgroundMaps.BackgroundMapInfo.zoomLevel": { + "description": "Zoom Level Title", + "message": "Zoom Level: {zoom}" + }, + "renderer.components.BackgroundMaps.MapCard.areas": { + "description": "indicates how many offline areas", + "message": "offline areas" + }, + "renderer.components.BackgroundMaps.MapCard.mb": { + "description": "Abbreviation for megabytes", + "message": "MB" + }, + "renderer.components.BackgroundMaps.SidePanel.addMap": { + "description": "Button to add map background", + "message": "Add Map Background" + }, + "renderer.components.BackgroundMaps.SidePanel.backToSettings": { + "description": "button to go back to settings", + "message": "Back to Settings" + }, "renderer.components.Home.mapeditor": { "description": "MapEditor tab label", "message": "Territory" @@ -474,12 +518,40 @@ "renderer.components.SettingsView.AboutMapeo.mapeoVersion": { "message": "Mapeo Version" }, + "renderer.components.SettingsView.BackgroundMaps.mapBackgroundTitle": { + "description": "Title for description of offline maps", + "message": "Managing Map Backgrounds and Offline Areas" + }, + "renderer.components.SettingsView.ExperimentsMenu.backgroundMaps": { + "message": "Background Maps" + }, + "renderer.components.SettingsView.SettingsItem.off": { + "message": "Off" + }, + "renderer.components.SettingsView.SettingsItem.on": { + "message": "On" + }, + "renderer.components.SettingsView.SettingsList.off": { + "message": "Off" + }, + "renderer.components.SettingsView.SettingsList.on": { + "message": "On" + }, "renderer.components.SettingsView.index.aboutMapeo": { "message": "About Mapeo" }, "renderer.components.SettingsView.index.aboutMapeoSubtitle": { "message": "Version and build number" }, + "renderer.components.SettingsView.index.backgroundMaps": { + "message": "Background maps" + }, + "renderer.components.SettingsView.index.experiments": { + "message": "Experiments" + }, + "renderer.components.SettingsView.index.experimentsSubtitle": { + "message": "Turn on experimental new features" + }, "renderer.components.SyncView.Searching.searchingHint": { "description": "Hint on sync screen when searching on wifi for devices", "message": "Make sure devices are turned on and connected to the same wifi network" @@ -636,6 +708,26 @@ "renderer.components.dialogs.Error.openLog": { "message": "Open log..." }, + "renderer.components.dialogs.ImportMapStyle.addMap": { + "description": "Title of screen used to add a new background map", + "message": "Add Map Background" + }, + "renderer.components.dialogs.ImportMapStyle.cancel": { + "description": "button to cancel the import of a background map", + "message": "Cancel" + }, + "renderer.components.dialogs.ImportMapStyle.importErrorDescription": { + "description": "Description of map import error", + "message": "There was an error importing the background maps. Please try again. Error message:" + }, + "renderer.components.dialogs.ImportMapStyle.importErrorTitle": { + "description": "Title for import errot pop up dialog,", + "message": "Background Maps Import Error" + }, + "renderer.components.dialogs.ImportMapStyle.importFile": { + "description": "Label of 'Import File' button", + "message": "Import File" + }, "renderer.components.dialogs.LatLon.button-submit": { "message": "Submit" }, diff --git a/src/main/ipc.js b/src/main/ipc.js index ec2d97fa0..d4bb341f9 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -95,6 +95,19 @@ module.exports = function (ipcSend) { return i18n.t('closing-screen') }) + ipcMain.handle('select-mb-tile-file', async function () { + const result = await dialog.showOpenDialog({ + filters: [{ name: 'MbTiles', extensions: ['mbtiles'] }], + properties: ['openFile'] + }) + + return result + }) + + ipcMain.on('show-error-dialog', function (event, title, content) { + dialog.showErrorBox(title, content) + }) + ipcMain.on('save-file', function () { var metadata = userConfig.getSettings('metadata') var ext = metadata ? metadata.dataset_id : 'mapeodata' diff --git a/src/renderer/components/BackgroundMaps/BackgroundMapInfo.js b/src/renderer/components/BackgroundMaps/BackgroundMapInfo.js new file mode 100644 index 000000000..69206c437 --- /dev/null +++ b/src/renderer/components/BackgroundMaps/BackgroundMapInfo.js @@ -0,0 +1,152 @@ +// @ts-check +import { Button, Fade, makeStyles, Paper, Typography } from '@material-ui/core' +import * as React from 'react' +import { defineMessages, useIntl } from 'react-intl' +import DeleteIcon from '@material-ui/icons/DeleteForeverOutlined' + +import Loading from '../Loading' +import { useMapServerQuery } from '../../hooks/useMapServerQuery' +import { MapboxPrevOnly } from './MapCard' +import { convertKbToMb } from '../SettingsView/BackgroundMaps' +import { useMapServerMutation } from '../../hooks/useMapServerMutation' + +const m = defineMessages({ + // Title for Offline Areas + offlineAreas: 'Offline Areas', + // Button to create an offline area + createOfflineArea: 'Create Offline Area', + // Button to delete style + deleteStyle: 'Delete Style', + // Title for error message when deleting style + deleteErrorTitle: 'Error Deleting Style', + // Description for error message when deleting style, + deleteErrorDescription: 'There was an error deleting the style', + // Zoom Level Title + zoomLevel: 'Zoom Level: {zoom}', + // abbreviation for megabyte + mb: 'MB' +}) + +/** + * @typedef BackgroundMapInfoProps + * @prop {string} id + * @prop {string} url + * @prop {number} size + * @prop {()=>void} unsetMapValue + */ + +/** @param {BackgroundMapInfoProps} props */ +export const BackgroundMapInfo = ({ id, unsetMapValue, url, size }) => { + const { formatMessage: t } = useIntl() + + const { data } = useMapServerQuery(`/styles/${id}`) + + return ( + + + {!data ? ( + + ) : ( + <> + + {/* Text */} +
+ + {data.name} + + {data.zoom && ( + + {t(m.zoomLevel, { zoom: data.zoom })} + + )} + {`${Math.round( + convertKbToMb(size) + )} ${t(m.mb)}`} +
+ + )} +
+
+ ) +} + +/** + * @typedef MapInfoProps + * @prop {string|undefined} name + * @prop {string} id + * @prop {()=>void} unsetMapValue + * @prop {string} url + */ + +/** @param {MapInfoProps} props */ +const MapInfo = ({ name, id, unsetMapValue, url }) => { + const classes = useStyles() + + const { formatMessage: t } = useIntl() + + const mutation = useMapServerMutation('delete', `/styles/${id}`) + + async function deleteMap () { + await mutation.mutateAsync(null) // tc complains if no arg is passed... + } + + return ( + <> + {/* Banner */} + + {name} + +
+ +
+
+ + {/* Map */} + + + ) +} + +const useStyles = makeStyles({ + buttonContainer: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center' + }, + banner: { + width: '100%', + display: 'flex', + justifyContent: 'space-between', + padding: '10px 20px' + }, + textBanner: { + display: 'flex', + justifyContent: 'space-evenly' + }, + offlineCardContainer: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-evenly' + } +}) diff --git a/src/renderer/components/BackgroundMaps/MapCard.js b/src/renderer/components/BackgroundMaps/MapCard.js new file mode 100644 index 000000000..a018bd2a0 --- /dev/null +++ b/src/renderer/components/BackgroundMaps/MapCard.js @@ -0,0 +1,93 @@ +// @ts-check +import * as React from 'react' +import Button from '@material-ui/core/Button' +import { makeStyles, Typography } from '@material-ui/core' +import { useIntl, defineMessages } from 'react-intl' +import ReactMapboxGl from 'react-mapbox-gl' + +import { MAPBOX_ACCESS_TOKEN } from '../../../../config' +import { convertKbToMb } from '../SettingsView/BackgroundMaps' + +const m = defineMessages({ + // Abbreviation for megabytes + mb: 'MB', + // indicates how many offline areas + areas: 'offline areas', +}) + +export const MapboxPrevOnly = ReactMapboxGl({ + accessToken: MAPBOX_ACCESS_TOKEN, + dragRotate: false, + pitchWithRotate: false, + attributionControl: false, + injectCSS: false, +}) + +/** + * @typedef MapCardProps + * @prop {import('../Settings/BackgroundMaps').MapServerStyleInfo} offlineMap + * @prop {React.Dispatch>} setMap + * @prop {boolean } isBeingViewed + */ + +/** @param {MapCardProps} param */ +export const MapCard = ({ offlineMap, setMap, isBeingViewed }) => { + const classes = useStyles() + const { formatMessage: t } = useIntl() + + return ( + + ) +} + +const useStyles = makeStyles({ + root: { + height: 90, + width: '90%', + marginBottom: 20, + textTransform: 'none', + padding: 0, + '& .MuiButton-root': { + padding: 0, + }, + '& .MuiButton-outlined': { + padding: 0, + }, + '& .MuiButton-label': { + height: '100%', + }, + }, + inner: { + display: 'flex', + flex: 1, + height: '100%', + }, + text: { + alignItems: 'flex-start', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + flex: 1, + height: '100%', + marginLeft: 10, + }, +}) diff --git a/src/renderer/components/BackgroundMaps/SidePanel.js b/src/renderer/components/BackgroundMaps/SidePanel.js new file mode 100644 index 000000000..8ae3a7fc6 --- /dev/null +++ b/src/renderer/components/BackgroundMaps/SidePanel.js @@ -0,0 +1,107 @@ +// @ts-check +import { Button, makeStyles } from '@material-ui/core' +import ChevronLeft from '@material-ui/icons/ChevronLeft' +import * as React from 'react' +import { defineMessages, useIntl } from 'react-intl' +import { useMapServerQuery } from '../../hooks/useMapServerQuery' +import { ImportMapStyleDialog } from '../dialogs/ImportMapStyle' +import Loader from '../Loader' +import { MapCard } from './MapCard' + +const m = defineMessages({ + // Button to add map background + addMap: 'Add Map Background', + // button to go back to settings + backToSettings: 'Back to Settings' +}) +/** + * @typedef SidePanelProps + * @prop {()=>void} openSettings + * @prop {string|false} mapValue + * @prop {React.Dispatch>} setMapValue + */ + +/** @param {SidePanelProps} param */ +export const SidePanel = ({ openSettings, mapValue, setMapValue }) => { + const { formatMessage: t } = useIntl() + + const classes = useStyles() + const [open, setOpen] = React.useState(false) + + const { data, isLoading, refetch } = useMapServerQuery('/styles', false) + + return ( + <> +
+ +
+ +
+ + {isLoading ? ( + + ) : data ? ( + data.map(offlineMap => ( + + )) + ) : null} +
+ setOpen(false)} + refetch={refetch} + /> + + ) +} + +const useStyles = makeStyles({ + sidePanel: { + width: 'auto', + borderRight: '1px solid #E0E0E0', + height: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + minWidth: '35%' + }, + buttonContainer: { + display: 'flex', + justifyContent: 'space-between', + padding: 20 + }, + button: { + textTransform: 'none', + fontSize: 12 + }, + firstButton: { + marginRight: 10 + }, + backHeader: { + justifyContent: 'flex-start', + alignSelf: 'flex-start', + paddingLeft: 20, + paddingTop: 20, + paddingBottom: 20, + width: '100%', + display: 'flex', + textTransform: 'none', + '& :first-child': { + marginRight: 20 + } + } +}) diff --git a/src/renderer/components/Home.js b/src/renderer/components/Home.js index 06b6f3ee5..23934889c 100644 --- a/src/renderer/components/Home.js +++ b/src/renderer/components/Home.js @@ -180,7 +180,7 @@ const focusStates = { exited: 'blurred' } -function TabPanel (props) { +function TabPanel(props) { const { value, index, diff --git a/src/renderer/components/SettingsView/BackgroundMaps.js b/src/renderer/components/SettingsView/BackgroundMaps.js new file mode 100644 index 000000000..a61672342 --- /dev/null +++ b/src/renderer/components/SettingsView/BackgroundMaps.js @@ -0,0 +1,85 @@ +// @ts-check +import * as React from 'react' +import { defineMessages, useIntl } from 'react-intl' +import Typography from '@material-ui/core/Typography' + +import { BackgroundMapInfo } from '../BackgroundMaps/BackgroundMapInfo' +import { SidePanel } from '../BackgroundMaps/SidePanel' +import { useMapServerQuery } from '../../hooks/useMapServerQuery' + +const m = defineMessages({ + // Title for description of offline maps + mapBackgroundTitle: 'Managing Map Backgrounds and Offline Areas' +}) + +/** @typedef {import('../../hooks/useMapServerQuery').MapServerStyleInfo} MapServerStyleInfo */ + +/** + * @typedef BackgroundMapsProps + * @prop {()=>void} returnToSettings + */ + +/** @param {BackgroundMapsProps} param */ +export const BackgroundMaps = ({ returnToSettings }) => { + const { formatMessage: t } = useIntl() + + /** @type {MapServerStyleInfo['id']|false} */ + const initialMapId = /** {const} */ false + + const [mapValue, setMapValue] = React.useState(initialMapId) + + const { data } = useMapServerQuery('/styles') + + function unsetMapValue() { + setMapValue(false) + } + + const offlineMap = React.useMemo( + () => data && data.find(m => m.id === mapValue), + [data, mapValue] + ) + + return ( + <> + + + {!mapValue || !data ? ( +
+ {t(m.mapBackgroundTitle)} + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum. +
+
+
+ ) : offlineMap ? ( + + ) : null} + + ) +} + +/** + * + * @param {number} kilobyte + * @returns {number} + */ +export const convertKbToMb = kilobyte => { + return kilobyte / 2 ** 20 +} diff --git a/src/renderer/components/SettingsView/ExperimentsMenu.js b/src/renderer/components/SettingsView/ExperimentsMenu.js new file mode 100644 index 000000000..6c2920e6a --- /dev/null +++ b/src/renderer/components/SettingsView/ExperimentsMenu.js @@ -0,0 +1,33 @@ +import React from 'react' +import { defineMessages } from 'react-intl' +import { Map as MapIcon } from '@material-ui/icons' +import { SettingsList } from './SettingsList' +import { useExperimentsFlagsStore } from '../../hooks/store' + +const m = defineMessages({ + backgroundMaps: 'Background Maps' +}) + +export const ExperiementsMenu = () => { + const [backgroundMaps, setBackgroundMaps] = useExperimentsFlagsStore( + store => [store.backgroundMaps, store.setBackgroundMapsFlag] + ) + + const toggleBackgroundMaps = () => { + setBackgroundMaps(!backgroundMaps) + } + + /** @type {import('./SettingsList').option[]} */ + const options = [ + { + id: 'BackgroundMaps', + icon: MapIcon, + label: m.backgroundMaps, + checked: backgroundMaps, + onClick: toggleBackgroundMaps, + type: 'toggle' + } + ] + + return +} diff --git a/src/renderer/components/SettingsView/SettingsItem.js b/src/renderer/components/SettingsView/SettingsItem.js new file mode 100644 index 000000000..05fef594e --- /dev/null +++ b/src/renderer/components/SettingsView/SettingsItem.js @@ -0,0 +1,99 @@ +import React from 'react' + +import { Typography, useTheme } from '@material-ui/core' +import ChevronRightIcon from '@material-ui/icons/ChevronRight' + +import { defineMessages, useIntl } from 'react-intl' +import styled from 'styled-components' + +const m = defineMessages({ + on: 'On', + off: 'Off' +}) + +export const SettingsItem = React.forwardRef( + ({ type, label, subtitle, icon: Icon, active, onClick, ...rest }, ref) => { + const theme = useTheme() + const { formatMessage: t } = useIntl() + + return ( + + + {Icon ? : null} + + + + {typeof label === 'string' ? label : t(label)} + + + {type === 'toggle' ? ( + + ) : ( + + )} + + + {type === 'menuItem' && ( + + )} + + ) + } +) + +const Subtitle = ({ label }) => { + const { formatMessage: t } = useIntl() + return typeof label === 'string' ? label : t(label) +} + +const ToggleSubtitle = ({ on }) => { + const { formatMessage: t } = useIntl() + + return t(on ? m.on : m.off) +} + +const Row = styled.div` + display: flex; + flex-direction: row; +` + +const Column = styled.div` + display: flex; + flex-direction: column; +` + +const WrapperRow = styled(Row)` + padding: 20px; + align-items: center; + justify-content: space-between; + cursor: pointer; +` + +const IconContainer = styled.div` + flex: 1; + padding: 10px 30px 10px 10px; + display: flex; + align-items: center; +` + +const TitleContainer = styled(Column)` + justify-content: flex-start; + flex: 8; +` diff --git a/src/renderer/components/SettingsView/SettingsList.js b/src/renderer/components/SettingsView/SettingsList.js new file mode 100644 index 000000000..c15b97242 --- /dev/null +++ b/src/renderer/components/SettingsView/SettingsList.js @@ -0,0 +1,150 @@ +import React from 'react' +import styled from 'styled-components' + +import { Switch, Typography, Paper, useTheme } from '@material-ui/core' +import ChevronRightIcon from '@material-ui/icons/ChevronRight' + +import { defineMessages, useIntl } from 'react-intl' + +const m = defineMessages({ + on: 'On', + off: 'Off', +}) + +/** + * @typedef {object} option + * @prop {string} option.id + * @prop {boolean} option.checked + * @prop {() => void} option.onClick + * @prop {string | import('react-intl').MessageDescriptor} option.label + * @prop {string | import('react-intl').MessageDescriptor} option.subtitle + * @prop {'menuItem' | 'toggle'} option.type + * @prop {import('@material-ui/core/OverridableComponent').OverridableComponent>} tab.icon + * @typedef SettingsListProps + * @prop {option[]} options + */ + +/** @param {SettingsListProps} props */ +export const SettingsList = ({ options }) => { + return ( + + {options + ? options.map(({ id, onClick, ...rest }) => { + return + }) + : null} + + ) +} + +/** + * @typedef SettingsItemProps + * @prop {'menuItem' | 'toggle'} type + * @prop {string | import('react-intl').MessageDescriptor} label + * @prop {string | import('react-intl').MessageDescriptor} subtitle + * @prop {import('@material-ui/core/OverridableComponent').OverridableComponent>} icon + * @prop {boolean} checked + * @prop {boolean} active + * @prop {string} option.id + * @prop {() => void} onClick + */ + +export const SettingsItem = React.forwardRef( + /** @param {SettingsItemProps} props */ + ({ type, label, subtitle, icon: Icon, active, checked, onClick, ...rest }, ref) => { + const theme = useTheme() + const { formatMessage: t } = useIntl() + + return ( + + {Icon ? : null} + + + {typeof label === 'string' ? label : t(label)} + + + {type === 'toggle' ? : } + + + {type === 'menuItem' && } + {type === 'toggle' && } + + ) + }, +) + +const Stack = styled(Paper)` + display: flex; + flex-direction: column; + gap: ${props => props.gap || '10px'}; + + &.MuiPaper-rounded { + border-radius: 0; + } +` +const Subtitle = ({ label }) => { + const { formatMessage: t } = useIntl() + if (!label) return null + + return typeof label === 'string' ? label : t(label) +} + +const ToggleSubtitle = ({ on }) => { + const { formatMessage: t } = useIntl() + + return t(on ? m.on : m.off) +} + +const Row = styled.div` + display: flex; + flex-direction: row; +` + +const Column = styled.div` + display: flex; + flex-direction: column; +` + +const WrapperRow = styled(Row)` + padding: 20px; + align-items: center; + justify-content: space-between; + cursor: pointer; + + .MuiSwitch-root { + margin-left: 30px; + } +` + +const IconContainer = styled.div` + flex: 1; + padding: 10px 30px 10px 10px; + display: flex; + align-items: center; +` + +const TitleContainer = styled(Column)` + justify-content: flex-start; + flex: 8; + + * { + user-select: none; + } +` diff --git a/src/renderer/components/SettingsView/SettingsMenu.js b/src/renderer/components/SettingsView/SettingsMenu.js index 7bad924ea..5a85b0be7 100644 --- a/src/renderer/components/SettingsView/SettingsMenu.js +++ b/src/renderer/components/SettingsView/SettingsMenu.js @@ -1,18 +1,18 @@ import * as React from 'react' import Tabs from '@material-ui/core/Tabs' import Tab from '@material-ui/core/Tab' -import { useIntl } from 'react-intl' -import ChevronRightIcon from '@material-ui/icons/ChevronRight' import styled from 'styled-components' -import { Paper, Typography, useTheme } from '@material-ui/core' +import { Paper } from '@material-ui/core' +import { SettingsItem } from './SettingsList' -/** @typedef {'AboutMapeo'} tabId */ +/** @typedef {'AboutMapeo'| 'Experiments' | 'BackgroundMaps'} tabId */ /** * @typedef {object} tab * @prop {tabId} tab.tabId * @prop {string | import('react-intl').MessageDescriptor} tab.label * @prop {string | import('react-intl').MessageDescriptor} tab.subtitle + * @prop {'menuItem' | 'toggle'} tab.type * @prop {import('@material-ui/core/OverridableComponent').OverridableComponent>} tab.icon * @typedef SettingsMenuProps * @prop {tab[]} tabs @@ -25,7 +25,7 @@ export const SettingsMenu = ({ tabs, currentTab, onTabChange }) => { return ( { ) } -const RenderTab = React.forwardRef( - ( - { tab: { icon: Icon, label, subtitle }, active, children, ...rest }, - ref - ) => { - const { formatMessage: t } = useIntl() - const theme = useTheme() - - return ( - - - {Icon ? : null} - - - - {typeof label === 'string' ? label : t(label)} - - - {typeof subtitle === 'string' ? subtitle : t(subtitle)} - - - - - ) - } +/** + * @typedef RenderTabProps + * @prop {tab} tab + * @prop {number} currentTab + * @prop {(e: React.Dispatch) => number} onTabChange + */ +/** @param {RenderTabProps} props */ +export const RenderTab = React.forwardRef( + ({ tab: { icon, label, subtitle, type }, ...rest }, ref) => ( + + ) ) const StyledTabs = styled(Tabs)` @@ -108,31 +83,3 @@ const StyledTabs = styled(Tabs)` max-width: 100%; } ` - -const Row = styled.div` - display: flex; - flex-direction: row; -` - -const Column = styled.div` - display: flex; - flex-direction: column; -` - -const WrapperRow = styled(Row)` - padding: 20px; - align-items: center; - justify-content: space-between; -` - -const IconContainer = styled.div` - flex: 1; - padding: 10px 30px 10px 10px; - display: flex; - align-items: center; -` - -const TitleContainer = styled(Column)` - justify-content: flex-start; - flex: 8; -` diff --git a/src/renderer/components/SettingsView/index.js b/src/renderer/components/SettingsView/index.js index 8ccca6ef3..326d6adcc 100644 --- a/src/renderer/components/SettingsView/index.js +++ b/src/renderer/components/SettingsView/index.js @@ -1,34 +1,75 @@ import React, { useState } from 'react' import InfoIcon from '@material-ui/icons/InfoOutlined' +import FlagIcon from '@material-ui/icons/Flag' +import MapIcon from '@material-ui/icons/Map' import { SettingsMenu } from './SettingsMenu' import { defineMessages } from 'react-intl' import { AboutMapeoMenu } from './AboutMapeo' import styled from 'styled-components' +import { ExperiementsMenu } from './ExperimentsMenu' +import { BackgroundMaps } from './BackgroundMaps' +import { useExperimentsFlagsStore } from '../../hooks/store' const m = defineMessages({ aboutMapeo: 'About Mapeo', aboutMapeoSubtitle: 'Version and build number', + experiments: 'Experiments', + experimentsSubtitle: 'Turn on experimental new features', + backgroundMaps: 'Background maps' }) -const tabs = [ - { - /** @type {import('./SettingsMenu').tabId} */ - tabId: 'AboutMapeo', - icon: InfoIcon, - label: m.aboutMapeo, - subtitle: m.aboutMapeoSubtitle, - }, -] - export const SettingsView = () => { - /** @type {import('./SettingsMenu').tabId | null} */ - const initialMenuState = /** {const} */ null + const [backgroundMaps, setBackgroundMaps] = useExperimentsFlagsStore( + store => [store.backgroundMaps, store.backgroundMaps] + ) + const tabs = /** @type {import('./SettingsMenu').tabs} */ [ + { + tabId: 'AboutMapeo', + icon: InfoIcon, + label: m.aboutMapeo, + subtitle: m.aboutMapeoSubtitle + }, + { + tabId: 'Experiments', + icon: FlagIcon, + label: m.experiments, + subtitle: m.experimentsSubtitle + }, + ...(backgroundMaps + ? [ + { + tabId: 'BackgroundMaps', + icon: MapIcon, + label: m.backgroundMaps + } + ] + : []) + ] + const initialMenuState = /** {null | number} */ null const [menuItem, setMenuItem] = useState(initialMenuState) return ( - - {menuItem === 'AboutMapeo' && } + {menuItem === 'BackgroundMaps' ? ( + setMenuItem(initialMenuState)} + /> + ) : ( + <> + + {menuItem === 'AboutMapeo' && } + {menuItem === 'Experiments' && ( + + )} + + )} ) } @@ -36,4 +77,5 @@ export const SettingsView = () => { const Container = styled.div` width: 100%; display: flex; + height: 100%; ` diff --git a/src/renderer/components/dialogs/ImportMapStyle.js b/src/renderer/components/dialogs/ImportMapStyle.js new file mode 100644 index 000000000..d69e1d60e --- /dev/null +++ b/src/renderer/components/dialogs/ImportMapStyle.js @@ -0,0 +1,132 @@ +// @ts-check +import * as React from 'react' +import { + Button, + CardActionArea, + makeStyles, + Typography +} from '@material-ui/core' +import Dialog from '@material-ui/core/Dialog' +import DialogTitle from '@material-ui/core/DialogTitle' +import DialogActions from '@material-ui/core/DialogActions' +import DialogContent from '@material-ui/core/DialogContent' +import { defineMessages, useIntl } from 'react-intl' +import { useMapServerMutation } from '../../hooks/useMapServerMutation' +import Card from '@material-ui/core/Card' +import CardContent from '@material-ui/core/CardContent' +import SystemUpdateAltIcon from '@material-ui/icons/SystemUpdateAlt' +import CloseIcon from '@material-ui/icons/Close' +import IconButton from '@material-ui/core/IconButton' +import { ipcRenderer } from 'electron' + +const m = defineMessages({ + // Title of screen used to add a new background map + addMap: 'Add Map Background', + // button to cancel the import of a background map + cancel: 'Cancel', + // Title for import errot pop up dialog, + importErrorTitle: 'Background Maps Import Error', + // Description of map import error + importErrorDescription: + 'There was an error importing the background maps. Please try again. Error message:', + // Label of 'Import File' button + importFile: 'Import File' +}) + +/** + * @typedef ImportMapStyleDialogProps + * @prop {boolean} open + * @prop {()=>void} close + * @prop {()=>void} refetch + */ + +/** @param {ImportMapStyleDialogProps} importMapStyleDialogProps */ +export const ImportMapStyleDialog = ({ open, close, refetch }) => { + const { formatMessage: t } = useIntl() + const mutation = useMapServerMutation('post', '/tilesets/import') + + const classes = useStyles() + + async function selectMbTileFile () { + const result = await ipcRenderer.invoke('select-mb-tile-file') + + if (result.canceled) return + if (!result.filePaths || !result.filePaths.length) return + + try { + const filePath = result.filePaths[0] + + await mutation.mutateAsync({ filePath }) + close() + refetch() + } catch (err) { + close() + onError(err) + } + + async function onError (err) { + console.log({ err }) + ipcRenderer.send( + 'show-error-dialog', + t(m.importErrorTitle), + `${t(m.importErrorDescription)} ${err}` + ) + } + } + + return ( + + + + {t(m.addMap)} + + + + + + + + + + + + {t(m.importFile)} + + + {'(.mbtiles)'} + + + + + + + + + + ) +} + +const useStyles = makeStyles({ + titleContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' + }, + title: { + fontSize: 24, + fontWeight: 500 + } +})