diff options
Diffstat (limited to 'src/Components')
-rw-r--r-- | src/Components/Login.tsx | 71 | ||||
-rw-r--r-- | src/Components/Map.tsx | 180 | ||||
-rw-r--r-- | src/Components/MapObjects.tsx | 90 | ||||
-rw-r--r-- | src/Components/MapOverlay.tsx | 91 | ||||
-rw-r--r-- | src/Components/PortalPanel.tsx | 70 |
5 files changed, 502 insertions, 0 deletions
diff --git a/src/Components/Login.tsx b/src/Components/Login.tsx new file mode 100644 index 0000000..6fe7957 --- /dev/null +++ b/src/Components/Login.tsx @@ -0,0 +1,71 @@ +import React, { Component } from 'react'; +import { StyleSheet, Text, View, WebView } from 'react-native'; + +import { connect } from 'redux-su'; +import { getBottomSpace, getStatusBarHeight } from '../helper'; +import actions from '../Actions/actions'; + +const LOGIN_URL = "https://accounts.google.com/ServiceLogin?service=ah&passive=true&continue=https://appengine.google.com/_ah/conflogin%3Fcontinue%3Dhttps://intel.ingress.com/"; +const HOME_URL = "https://intel.ingress.com/"; + +var styles = StyleSheet.create({ + container: { + flexGrow: 1, + } +}); + +type Props = { + login(csrf: string, v: string): void + actions: any + auth: any +} + +type State = { + v: string + onIngress: boolean +} + +class Login extends Component<Props, State> { + webview!: WebView; + constructor(props: Props) { + super(props); + this.state = { v: "", onIngress: false } + } + + onNavigationStateChange(navState: WebViewNavigation) { + if (navState.url == HOME_URL) { + this.setState({ onIngress: true }) + this.props.actions.login() + } + } + + renderLogin() { + if (this.state.onIngress) { + return (<Text>Пожалуйста, подождите...</Text>); + } + return ( + <> + <WebView + ref={r => r && (this.webview = r)} + automaticallyAdjustContentInsets={false} + thirdPartyCookiesEnabled + useWebKit + style={styles.container} + source={{ uri: LOGIN_URL }} + javaScriptEnabled={true} + onNavigationStateChange={this.onNavigationStateChange.bind(this)} + startInLoadingState={true} + /> + </> + ); + } + + render() { + console.log(this.props) + return (<View style={{ flexGrow: 1, paddingTop: getStatusBarHeight(true), paddingBottom: getBottomSpace() }}> + {this.renderLogin()} + </View>); + } +} + +export default connect({ 'auth': 'auth' }, actions)(Login)
\ No newline at end of file diff --git a/src/Components/Map.tsx b/src/Components/Map.tsx new file mode 100644 index 0000000..3e878f8 --- /dev/null +++ b/src/Components/Map.tsx @@ -0,0 +1,180 @@ +import React, { Component } from 'react'; +import { StyleSheet, View, Dimensions, ActivityIndicator } from 'react-native'; +import MapView, { Marker, Region, UrlTile } from 'react-native-maps'; +import { connect } from 'redux-su'; +import { NavigationActions } from 'react-navigation'; + +import MapOverlay from './MapOverlay'; +import MapObjects from './MapObjects'; +import PortalPanel from './PortalPanel'; +import { getBottomSpace } from '../helper'; +import actions from '../Actions/actions'; +import { LatLng } from '../Api/interfaces'; + +const { width, height } = Dimensions.get("screen") +const draggableRange = { + top: height / 1.75, + bottom: 120 + getBottomSpace() +} + +type Props = any +type State = any +class Map extends Component<Props, State> { + refresh: number | undefined + map!: MapView; + constructor(props: Props) { + super(props) + this.state = { + user: undefined, + followUser: true, + zoom: 15, + region: null, + loading: false, + objects: { links: {}, fields: {}, portals: {}, loaded: false }, + } + } + componentWillMount() { + navigator.geolocation.getCurrentPosition( + position => { + this.setState({ + followUser: true, + }); + }, + error => alert(error.message), + { enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 } + ); + navigator.geolocation.watchPosition( + position => { + this.setState({ + user: { + latitude: position.coords.latitude, + longitude: position.coords.longitude + }, + }); + }, + error => alert(error.message), + { enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 } + ) + } + onRegionChange = (region: Region) => { + const zoom = this.getZoomByRegion(width, region) + this.setState({ region, zoom }) + setImmediate(() => this.load(false)) + } + getZoomByRegion(width: number, region: Region): number { + return Math.ceil(Math.log2(360 * ((width / 256) / region.longitudeDelta))) + 1 + } + load = async (refresh: boolean) => { + if (this.state.region != null && this.props.entities.loading == 0) { + this.props.actions.update(this.state.region, width, refresh) + } + return null + } + + refreshByTime = () => { + setTimeout(() => { + this.load(true).then(this.refreshByTime) + }, 30000) + } + + componentDidMount() { + this.refreshByTime() + } + + onPortalClick = (guid: string, coords: LatLng) => { + this.setState({ selectedPortal: { guid, coords } }) + } + + onOpenPortal = (guid: string, coords: LatLng) => { + const navigateAction = NavigationActions.navigate({ + routeName: 'Portal', + params: { guid, coords }, + }); + this.props.navigation.dispatch(navigateAction); + } + + render() { + if (!this.state.user) { + return <View style={styles.spinnerContainer}><ActivityIndicator size={'large'} /></View> + } + const camera = { + center: this.state.user, + altitude: 1000, + heading: 0, + pitch: 30, + zoom: 15, + } + const goToMe = () => { + this.map.animateToCoordinate(this.state.user) + } + const refresh = () => { + setImmediate(() => this.load(true)) + } + return ( + <> + <MapView + ref={r => (r != null) ? this.map = r : null} + style={styles.container} + initialRegion={{ ...this.state.user, latitudeDelta: 0.002, longitudeDelta: 0.002 }} + onRegionChangeComplete={this.onRegionChange} + showsCompass={false} + showsScale + showsUserLocation + showsMyLocationButton + loadingEnabled + type={'hybrid'} + > + <MapObjects onPortalClick={this.onPortalClick} zoom={this.state.zoom} /> + {this.state.selectedPortal && <Marker coordinate={this.state.selectedPortal.coords} />} + </MapView> + <MapOverlay + goToMe={goToMe} + refresh={refresh} + loading={this.props.entities.loading} + selectedPortal={this.state.selectedPortal} + onOpenPortal={this.onOpenPortal} + /> + </> + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + spinnerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + panel: { + flex: 1, + backgroundColor: '#fff', + position: 'relative', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + //ios + shadowOpacity: 0.3, + shadowRadius: 3, + shadowOffset: { + height: 0, + width: 0 + }, + //android + elevation: 1 + }, + panelHeader: { + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + dash: { + backgroundColor: 'rgba(200,200,200,0.9)', + height: 6, + width: 30, + borderRadius: 3 + } +}); + +export default connect({ 'entities': 'entities' }, actions)(Map)
\ No newline at end of file diff --git a/src/Components/MapObjects.tsx b/src/Components/MapObjects.tsx new file mode 100644 index 0000000..1d2fb12 --- /dev/null +++ b/src/Components/MapObjects.tsx @@ -0,0 +1,90 @@ +import React, { ReactChild } from 'react'; +import { View, StyleSheet, Text } from 'react-native'; +import { Polyline, Polygon, Marker, Region } from 'react-native-maps'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import { Link, Portal, Field } from '../Api/types'; +import { connect } from 'react-redux'; +import { COLORS, COLORS_LVL } from '../colors'; + +const fillPortalColor = { "R": "rgba(2, 140, 227, 0.5)", "E": "rgba(38, 205, 30, 0.5)", "N": "rgba(139, 0, 255, 0.5)" } +// const fillPortalColor = { "R": COLORS[1], "E": COLORS[2], "N": COLORS[0] } +const fractColor = { "R": "#028ce3", "E": "#26cd1e", "N": "#8b00ff" } +// const fractColor = { "R": COLORS[1], "E": COLORS[2], "N": COLORS[0] } +const fieldColor = { "R": "rgba(2, 140, 227, 0.1)", "E": "rgba(38, 205, 30, 0.1)", "N": "rgba(139, 0, 255, 0.1)" } + +type Props = { + portals: { [guid: string]: Portal } + links: { [guid: string]: Link } + fields: { [guid: string]: Field } + onPortalClick: Function + zoom: number +} + +const MapObjects = (props: Props) => { + const renderPortal = Object.keys(props.portals).map(guid => drawPortal(guid, props.portals[guid], props.zoom, props.onPortalClick)) + const renderField = Object.keys(props.fields).map(guid => drawField(guid, props.fields[guid])) + const renderLink = Object.keys(props.links).map(guid => drawLink(guid, props.links[guid])) + + if (props.zoom <= 14) { + return [...renderField, ...renderLink] + } else { + return [...renderField, ...renderLink, ...renderPortal] + } +} +const drawPortal = (guid: string, entity: Portal, zoom: number, onPortalClick: Function) => { + const size = (zoom) * 1.5 + return (<Marker + key={guid} + coordinate={entity.coords} + onPress={() => onPortalClick(guid, entity.coords)} + > + {/* <Icon name={entity.fraction == "N" ? "circle-o" : "circle"} color={fillPortalColor[entity.fraction]} size={size} /> */} + <View style={{ + borderWidth: 2, + height: size, + width: size, + borderRadius: size / 2, + borderColor: COLORS_LVL[entity.level], + backgroundColor: fillPortalColor[entity.fraction], + justifyContent: 'center', + alignItems: 'center', + }}> + <Text style={{ fontWeight: 'bold' }}>{entity.level}</Text> + </View> + </Marker>); +} +const drawField = (guid: string, entity: Field) => { + return <Polygon + key={guid} + coordinates={entity.coords} + fillColor={fieldColor[entity.fraction]} + strokeColor={fieldColor[entity.fraction]} + strokeWidth={StyleSheet.hairlineWidth} + /> +} +const drawLink = (guid: string, entity: Link) => { + return <Polyline + key={guid} + coordinates={entity.coords} + strokeColor={fractColor[entity.fraction]} + strokeWidth={1} + /> +} + + +const styles = StyleSheet.create({ + +}); + +export default connect((store) => { + const display = store.entities.display + const portals = {} + const fields = {} + const links = {} + display.portals.forEach(guid => { portals[guid] = store.entities.portals[guid] }) + display.fields.forEach(guid => { fields[guid] = store.entities.fields[guid] }) + display.links.forEach(guid => { links[guid] = store.entities.links[guid] }) + return { + portals, fields, links + } +}, {})(MapObjects)
\ No newline at end of file diff --git a/src/Components/MapOverlay.tsx b/src/Components/MapOverlay.tsx new file mode 100644 index 0000000..30160c9 --- /dev/null +++ b/src/Components/MapOverlay.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { StyleSheet, View, Text, GestureResponderEvent, ActivityIndicator, Linking } from 'react-native'; +import { FontAwesome } from '@expo/vector-icons'; +import { getStatusBarHeight } from '../helper'; +import { LatLng } from '../Api/interfaces'; + +type Props = { + goToMe: (e: GestureResponderEvent) => void; + refresh: (e: GestureResponderEvent) => void; + onOpenPortal: (guid: string, coords: LatLng) => void; + loading: Number; + selectedPortal: any; +} + +export default (props: Props) => ( + <View style={styles.overlay}> + <View style={styles.button}> + <FontAwesome.Button + name={'crosshairs'} + onPress={props.goToMe} + iconStyle={styles.icon} + color={"#000"} + size={24} + backgroundColor={"#e3e3e3"} + /> + </View> + <View style={styles.button}> + <FontAwesome.Button + name={'refresh'} + onPress={props.refresh} + iconStyle={styles.icon} + color={"#000"} + size={24} + backgroundColor={"#e3e3e3"} + /> + </View> + {props.selectedPortal ? ( + <> + <View style={styles.button}> + <FontAwesome.Button + name={'info'} + onPress={() => { + props.onOpenPortal(props.selectedPortal.guid, props.selectedPortal.coords) + }} + iconStyle={styles.icon} + color={"#000"} + size={24} + backgroundColor={"#e3e3e3"} + /> + </View> + <View style={styles.button}> + <FontAwesome.Button + name={'location-arrow'} + onPress={() => Linking.openURL(`geo:${props.selectedPortal.coords.latitude},${props.selectedPortal.coords.longitude}`)} + iconStyle={styles.icon} + color={"#000"} + size={24} + backgroundColor={"#e3e3e3"} + /> + </View> + </> + ) : null} + + <View style={styles.button}> + <Text>{props.loading}</Text> + </View> + {props.loading > 0 ? ( + <View style={styles.loader}> + <ActivityIndicator color={"#000"} /> + </View> + ) : null} + </View> +); + +const styles = StyleSheet.create({ + overlay: { + position: 'absolute', + top: 8 + getStatusBarHeight(true), + right: 8, + }, + button: { + margin: 8, + }, + icon: { + marginRight: 0, + }, + loader: { + margin: 8, + paddingHorizontal: 8, + } +});
\ No newline at end of file diff --git a/src/Components/PortalPanel.tsx b/src/Components/PortalPanel.tsx new file mode 100644 index 0000000..6cf19c2 --- /dev/null +++ b/src/Components/PortalPanel.tsx @@ -0,0 +1,70 @@ +import React, { Component, PureComponent } from 'react'; +import { StyleSheet, View, Text, GestureResponderEvent, ActivityIndicator } from 'react-native'; +// import { Button } from 'react-native-vector-icons/FontAwesome'; +import Reactotron from 'reactotron-react-native' +import { getStatusBarHeight } from '../helper'; +import { connect } from 'react-redux'; +import actions from '../Actions/actions'; +import { Portal } from '../Api/types'; +import { bindActionCreators } from 'redux'; + +type Props = { + guid: string + portal?: Portal +} + +class PortalPanel extends PureComponent<Props> { + static navigationOptions = ({ navigation }) => { + return { + title: navigation.getParam('title', 'Загрузка...'), + }; + }; + componentWillMount() { + this.props.navigation.setParams({title: this.props.portal.name}) + } + componentDidMount() { + this.props.getPortalDetails(this.props.guid) + } + componentWillReceiveProps(next: Props) { + if (next.guid != this.props.guid) { + this.props.navigation.setParams({title: this.props.portal.name}) + this.props.getPortalDetails(next.guid) + } + } + render() { + const { portal } = this.props + if (!portal) { + return <ActivityIndicator /> + } + return ( + <View style={styles.overlay}> + <Text style={styles.title}>{portal.name}</Text> + <Text style={styles.subtitle}>Уровeнь: {portal.level}, здоровье: {portal.power}</Text> + <Text>{JSON.stringify(this.props)}</Text> + </View> + ); + } +} + +export default connect((store, props: Props) => { + const guid = props.navigation.getParam('guid') + return { + portal: store.entities.portals[guid], + guid + } +}, (dispatch) => bindActionCreators(actions, dispatch))(PortalPanel) + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + padding: 8, + }, + title: { + fontWeight: 'bold', + fontSize: 22, + }, + subtitle: { + fontWeight: 'normal', + fontSize: 18, + } +});
\ No newline at end of file |