summaryrefslogtreecommitdiff
path: root/src/Components
diff options
context:
space:
mode:
Diffstat (limited to 'src/Components')
-rw-r--r--src/Components/Login.tsx71
-rw-r--r--src/Components/Map.tsx180
-rw-r--r--src/Components/MapObjects.tsx90
-rw-r--r--src/Components/MapOverlay.tsx91
-rw-r--r--src/Components/PortalPanel.tsx70
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