├── .watchmanconfig
├── screenshots
├── pw-1.PNG
├── pw-2.PNG
├── pw-3.PNG
├── pw-4.PNG
├── rw-1.PNG
├── rw-2.PNG
├── rw-3.PNG
└── rw-4.PNG
├── js
├── components
│ ├── img
│ │ ├── fog.png
│ │ ├── cloudy.png
│ │ ├── rain.png
│ │ ├── snow.png
│ │ ├── sunny.gif
│ │ ├── sunny.png
│ │ ├── rain_light.png
│ │ ├── partly_cloudy.png
│ │ ├── rain_s_cloudy.png
│ │ ├── thunderstorms.png
│ │ ├── sunny_s_cloudy.png
│ │ └── header-background.png
│ ├── settings
│ │ ├── userguide.js
│ │ ├── separator.js
│ │ ├── styles.js
│ │ ├── section.js
│ │ ├── sectiontitle.js
│ │ ├── settings.js
│ │ ├── navigationbuttonrow.js
│ │ ├── about.js
│ │ ├── locations.js
│ │ └── addlocation.js
│ ├── loading.js
│ ├── pager.js
│ ├── forecastimage.js
│ ├── forecast.js
│ ├── forecastitem.js
│ ├── footer.js
│ ├── weather.js
│ └── header.js
├── reducers
│ ├── index.js
│ ├── postcode.js
│ ├── location.js
│ └── weather.js
├── actions
│ ├── index.js
│ ├── postcode.js
│ ├── types.js
│ ├── weather.js
│ └── location.js
├── realm
│ ├── index.js
│ ├── schema.js
│ ├── configure.js
│ ├── schema-v1.js
│ └── schema-v2.js
├── store
│ ├── promise.js
│ └── configure.js
├── config.js
├── app.js
├── dependencies
│ ├── parallaxview
│ │ ├── styles.js
│ │ └── index.js
│ ├── swipeout
│ │ ├── styles.js
│ │ └── index.js
│ └── swiper
│ │ └── index.js
├── models
│ └── view.js
├── services
│ ├── postcode.js
│ ├── location.js
│ ├── weather.js
│ └── testdata.js
├── main.js
└── navigators
│ ├── application.js
│ └── settings.js
├── android
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── app
│ ├── src
│ │ └── main
│ │ │ ├── assets
│ │ │ └── fonts
│ │ │ │ ├── Entypo.ttf
│ │ │ │ ├── Zocial.ttf
│ │ │ │ ├── EvilIcons.ttf
│ │ │ │ ├── Foundation.ttf
│ │ │ │ ├── Ionicons.ttf
│ │ │ │ ├── Octicons.ttf
│ │ │ │ ├── FontAwesome.ttf
│ │ │ │ └── MaterialIcons.ttf
│ │ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ └── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ └── com
│ │ │ └── reactweather
│ │ │ └── MainActivity.java
│ ├── BUCK
│ ├── proguard-rules.pro
│ ├── build.gradle
│ └── app.iml
├── keystores
│ ├── debug.keystore.properties
│ └── BUCK
├── settings.gradle
├── build.gradle
├── gradle.properties
├── ReactWeather.iml
├── gradlew.bat
└── gradlew
├── jsconfig.json
├── tsconfig.json
├── .buckconfig
├── index.ios.js
├── index.android.js
├── ios
├── ReactWeather
│ ├── AppDelegate.h
│ ├── main.m
│ ├── Images.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── AppDelegate.m
│ └── Base.lproj
│ │ └── LaunchScreen.xib
├── ReactWeatherTests
│ ├── Info.plist
│ └── ReactWeatherTests.m
└── ReactWeather.xcodeproj
│ └── xcshareddata
│ └── xcschemes
│ └── ReactWeather.xcscheme
├── package.json
├── api
├── suburbs.json
├── location.js
├── weather.json
├── forecast-daily.json
└── postcode.js
├── .gitignore
├── LICENSE
├── .flowconfig
└── README.md
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/screenshots/pw-1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/screenshots/pw-1.PNG
--------------------------------------------------------------------------------
/screenshots/pw-2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/screenshots/pw-2.PNG
--------------------------------------------------------------------------------
/screenshots/pw-3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/screenshots/pw-3.PNG
--------------------------------------------------------------------------------
/screenshots/pw-4.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/screenshots/pw-4.PNG
--------------------------------------------------------------------------------
/screenshots/rw-1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/screenshots/rw-1.PNG
--------------------------------------------------------------------------------
/screenshots/rw-2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/screenshots/rw-2.PNG
--------------------------------------------------------------------------------
/screenshots/rw-3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/screenshots/rw-3.PNG
--------------------------------------------------------------------------------
/screenshots/rw-4.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/screenshots/rw-4.PNG
--------------------------------------------------------------------------------
/js/components/img/fog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/fog.png
--------------------------------------------------------------------------------
/js/components/img/cloudy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/cloudy.png
--------------------------------------------------------------------------------
/js/components/img/rain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/rain.png
--------------------------------------------------------------------------------
/js/components/img/snow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/snow.png
--------------------------------------------------------------------------------
/js/components/img/sunny.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/sunny.gif
--------------------------------------------------------------------------------
/js/components/img/sunny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/sunny.png
--------------------------------------------------------------------------------
/js/components/img/rain_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/rain_light.png
--------------------------------------------------------------------------------
/js/components/img/partly_cloudy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/partly_cloudy.png
--------------------------------------------------------------------------------
/js/components/img/rain_s_cloudy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/rain_s_cloudy.png
--------------------------------------------------------------------------------
/js/components/img/thunderstorms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/thunderstorms.png
--------------------------------------------------------------------------------
/js/components/img/sunny_s_cloudy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/sunny_s_cloudy.png
--------------------------------------------------------------------------------
/js/components/img/header-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/js/components/img/header-background.png
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Entypo.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/assets/fonts/Entypo.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Zocial.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/assets/fonts/Zocial.ttf
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ReactWeather
6 |
7 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6"
4 | },
5 | "exclude": [
6 | "node_modules"
7 | ]
8 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true
4 | },
5 | "exclude": [
6 | "node_modules"
7 | ]
8 | }
--------------------------------------------------------------------------------
/.buckconfig:
--------------------------------------------------------------------------------
1 |
2 | [android]
3 | target = Google Inc.:Google APIs:23
4 |
5 | [maven_repositories]
6 | central = https://repo1.maven.org/maven2
7 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/EvilIcons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/assets/fonts/EvilIcons.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Foundation.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/assets/fonts/Foundation.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Ionicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/assets/fonts/Ionicons.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Octicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/assets/fonts/Octicons.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/FontAwesome.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/assets/fonts/FontAwesome.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/MaterialIcons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/assets/fonts/MaterialIcons.ttf
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stage88/react-weather/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/keystores/debug.keystore.properties:
--------------------------------------------------------------------------------
1 | key.store=debug.keystore
2 | key.alias=androiddebugkey
3 | key.store.password=android
4 | key.alias.password=android
5 |
--------------------------------------------------------------------------------
/android/keystores/BUCK:
--------------------------------------------------------------------------------
1 | keystore(
2 | name = 'debug',
3 | store = 'debug.keystore',
4 | properties = 'debug.keystore.properties',
5 | visibility = [
6 | 'PUBLIC',
7 | ],
8 | )
9 |
--------------------------------------------------------------------------------
/index.ios.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | const { AppRegistry } = require('react-native');
8 | const setup = require('./js/main');
9 |
10 | AppRegistry.registerComponent('ReactWeather', setup);
11 |
--------------------------------------------------------------------------------
/index.android.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | const { AppRegistry } = require('react-native');
8 | const setup = require('./js/main');
9 |
10 | AppRegistry.registerComponent('ReactWeather', setup);
11 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jun 16 22:28:01 EST 2016
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
7 |
--------------------------------------------------------------------------------
/js/reducers/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | var { combineReducers } = require('redux');
8 |
9 | module.exports = combineReducers({
10 | weather: require('./weather'),
11 | location: require('./location'),
12 | postcode: require('./postcode')
13 | });
14 |
--------------------------------------------------------------------------------
/js/actions/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | const weather = require('./weather');
8 | const location = require('./location');
9 | const postcode = require('./postcode');
10 |
11 | module.exports = {
12 | ...weather,
13 | ...location,
14 | ...postcode
15 | };
16 |
--------------------------------------------------------------------------------
/js/realm/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import Realm from 'realm';
8 | import Schema from './schema';
9 |
10 | function getCurrent() {
11 | var schema = new Schema();
12 | var current = schema.current();
13 |
14 | return new Realm(current);
15 | }
16 |
17 | module.exports = {
18 | current: getCurrent
19 | }
20 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'ReactWeather'
2 |
3 | include ':app'
4 | include ':realm'
5 | project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
6 | include ':react-native-vector-icons'
7 | project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
8 |
--------------------------------------------------------------------------------
/js/store/promise.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | function warn(error) {
8 | console.warn(error.message || error);
9 | throw error; // To let the caller handle the rejection
10 | }
11 |
12 | module.exports = (store: any) => (next: any) => (action: any) =>
13 | typeof action.then === 'function'
14 | ? Promise.resolve(action).then(next, warn)
15 | : next(action);
16 |
--------------------------------------------------------------------------------
/js/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | // TODO: find out how to handle errors from require
8 | try {
9 | var { weatherApiKey } = require('../release/keys');
10 | } catch (e) {}
11 |
12 | module.exports = {
13 | weatherApiKey: weatherApiKey,
14 | weatherApiUrl: 'http://api.openweathermap.org/data/2.5',
15 | postcodeApiUrl: 'http://v0.postcodeapi.com.au/suburbs.json',
16 | isDebugData: false
17 | };
18 |
--------------------------------------------------------------------------------
/js/realm/schema.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import schema_v1 from './schema-v1';
8 | import schema_v2 from './schema-v2';
9 |
10 | class Schema {
11 | schemas: Array;
12 |
13 | constructor() {
14 | this.schemas = [
15 | schema_v1,
16 | schema_v2
17 | ];
18 | }
19 |
20 | current() {
21 | return this.schemas[this.schemas.length - 1];
22 | }
23 | }
24 |
25 | module.exports = Schema;
26 |
--------------------------------------------------------------------------------
/js/components/settings/userguide.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | View,
11 | Text
12 | } from 'react-native';
13 |
14 | class UserGuide extends Component {
15 | render() {
16 | return (
17 |
18 | User Guide
19 |
20 | );
21 | }
22 | }
23 |
24 | module.exports = UserGuide;
25 |
--------------------------------------------------------------------------------
/ios/ReactWeather/AppDelegate.h:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015-present, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | */
9 |
10 | #import
11 |
12 | @interface AppDelegate : UIResponder
13 |
14 | @property (nonatomic, strong) UIWindow *window;
15 |
16 | @end
17 |
--------------------------------------------------------------------------------
/js/components/settings/separator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | View,
11 | } from 'react-native';
12 |
13 | class Separator extends Component {
14 | render() {
15 | return (
16 |
17 | );
18 | }
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | separator: {
23 | height: StyleSheet.hairlineWidth,
24 | backgroundColor: '#C8C7CC',
25 | marginLeft: 14
26 | },
27 | });
28 |
29 | module.exports = Separator;
--------------------------------------------------------------------------------
/ios/ReactWeather/main.m:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015-present, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | */
9 |
10 | #import
11 |
12 | #import "AppDelegate.h"
13 |
14 | int main(int argc, char * argv[]) {
15 | @autoreleasepool {
16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/js/actions/postcode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import type { Action } from './types';
8 |
9 | import PostcodeService from '../services/postcode';
10 | const service = new PostcodeService();
11 |
12 | async function searchPostcodes(text: string) {
13 | var result = await service.getPostcodes(text);
14 | return {
15 | type: 'POSTCODE_SEARCH',
16 | data: result
17 | };
18 | }
19 |
20 | function clearPostcodes() {
21 | return {
22 | type: 'POSTCODE_CLEAR'
23 | }
24 | }
25 |
26 | module.exports = {
27 | searchPostcodes,
28 | clearPostcodes
29 | };
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ReactWeather",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "start": "node node_modules/react-native/local-cli/cli.js start"
7 | },
8 | "dependencies": {
9 | "react": "15.0.2",
10 | "react-native": "^0.26.3",
11 | "react-native-vector-icons": "^2.0.3",
12 | "react-redux": "^4.4.5",
13 | "react-tween-state": "^0.1.5",
14 | "realm": "^0.13.2",
15 | "redux": "^3.5.2",
16 | "redux-logger": "^2.6.1",
17 | "redux-thunk": "^2.1.0"
18 | },
19 | "devDependencies": {
20 | "rnpm-plugin-upgrade": "^0.26.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/js/actions/types.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import type { WeatherModel, Location, Postcode } from '../models/view';
8 |
9 | export type Action =
10 | { type: 'WEATHER_SET_LOADING' }
11 | | { type: 'WEATHER_SET_REFRESHING' }
12 | | { type: 'WEATHER_GET_ALL', data: Array }
13 | | { type: 'LOCATION_INITIALISED' }
14 | | { type: 'LOCATION_GET_ALL', data: Array }
15 | | { type: 'LOCATION_CLEAR_ALL_DATA' }
16 | | { type: 'LOCATION_DELETE' }
17 | | { type: 'LOCATION_ADD' }
18 | | { type: 'POSTCODE_SEARCH', data: Array }
19 | | { type: 'POSTCODE_CLEAR' };
20 |
--------------------------------------------------------------------------------
/js/realm/configure.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import Realm from 'realm';
8 | import Schema from './schema';
9 |
10 | function configureRealm() {
11 | var schema = new Schema();
12 |
13 | var next = Realm.schemaVersion(Realm.defaultPath);
14 | if (next > 0) {
15 | while (next < schema.schemas.length) {
16 | var migratedSchema = schema.schemas[next++];
17 | var migratedRealm = new Realm(migratedSchema);
18 | migratedRealm.close();
19 | }
20 | }
21 |
22 | var current = schema.current();
23 | var realm = new Realm(current);
24 | realm.close();
25 | }
26 |
27 | module.exports = configureRealm;
28 |
--------------------------------------------------------------------------------
/api/suburbs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Queanbeyan East",
4 | "postcode": 2620,
5 | "state": {
6 | "name": "New South Wales",
7 | "abbreviation": "NSW"
8 | },
9 | "locality": "CANBERRA",
10 | "latitude": -35.342799999999997,
11 | "longitude": 149.24440000000001
12 | },
13 | {
14 | "name": "Queanbeyan",
15 | "postcode": 2620,
16 | "state": {
17 | "name": "New South Wales",
18 | "abbreviation": "NSW"
19 | },
20 | "locality": "CANBERRA",
21 | "latitude": -35.354399999999998,
22 | "longitude": 149.2321
23 | }
24 | ]
--------------------------------------------------------------------------------
/js/reducers/postcode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import type { Action } from '../actions/types';
8 | import type { Postcode } from '../models/view';
9 |
10 | export type State = {
11 | data: Array;
12 | };
13 |
14 | const initial: State = {
15 | data: []
16 | };
17 |
18 | function postcode(state: State = initial, action: Action): State {
19 | switch (action.type) {
20 | case 'POSTCODE_SEARCH':
21 | return {
22 | data: action.data
23 | };
24 | case 'POSTCODE_CLEAR':
25 | return {
26 | data: []
27 | };
28 | default:
29 | return state;
30 | }
31 | }
32 |
33 | module.exports = postcode;
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | project.xcworkspace
24 |
25 | # Android/IJ
26 | #
27 | .idea
28 | .gradle
29 | local.properties
30 |
31 | # node.js
32 | #
33 | node_modules/
34 | npm-debug.log
35 |
36 | # BUCK
37 | buck-out/
38 | \.buckd/
39 | android/app/libs
40 | android/keystores/debug.keystore
41 |
42 | # Custom
43 | #
44 | /release
45 | /logs
46 | /.vscode
47 | /ios/main.jsbundle
48 |
--------------------------------------------------------------------------------
/js/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import StatusBar from 'StatusBar';
8 | import React, { Component } from 'react';
9 | import {
10 | StyleSheet,
11 | View,
12 | } from 'react-native';
13 |
14 | import ApplicationNavigator from './navigators/application';
15 |
16 | class App extends Component {
17 | render() {
18 | return (
19 |
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
27 | const styles = StyleSheet.create({
28 | container: {
29 | flex: 1
30 | }
31 | });
32 |
33 | module.exports = App;
34 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.1.2'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | mavenLocal()
18 | jcenter()
19 | maven {
20 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
21 | url "$projectDir/../../node_modules/react-native/android"
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/js/store/configure.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import { applyMiddleware, createStore } from 'redux';
8 | import thunk from 'redux-thunk';
9 | import createLogger from 'redux-logger';
10 |
11 | import reducers from '../reducers';
12 | import promise from './promise';
13 |
14 | var isDebuggingInChrome = __DEV__ && !!window.navigator.userAgent;
15 |
16 | var logger = createLogger({
17 | predicate: (getState, action) => isDebuggingInChrome,
18 | collapsed: true,
19 | duration: true,
20 | });
21 |
22 | var store = applyMiddleware(thunk, promise, logger)(createStore)(reducers);
23 |
24 | function configureStore() {
25 | if (isDebuggingInChrome) {
26 | window.store = store;
27 | }
28 |
29 | return store;
30 | }
31 |
32 | module.exports = configureStore;
33 |
--------------------------------------------------------------------------------
/js/reducers/location.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import type { Action } from '../actions/types';
8 | import type { Location } from '../models/view';
9 |
10 | export type State = {
11 | isInitalised: bool;
12 | data: Array;
13 | };
14 |
15 | const initial: State = {
16 | isInitalised: false,
17 | data: []
18 | };
19 |
20 | function location(state: State = initial, action: Action): State {
21 | switch (action.type) {
22 | case 'LOCATION_INITIALISED':
23 | return {
24 | ...state,
25 | isInitalised: true
26 | };
27 | case 'LOCATION_GET_ALL':
28 | return {
29 | ...state,
30 | data: action.data
31 | }
32 | default:
33 | return state;
34 | }
35 | }
36 |
37 | module.exports = location;
38 |
--------------------------------------------------------------------------------
/ios/ReactWeather/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/js/dependencies/parallaxview/styles.js:
--------------------------------------------------------------------------------
1 | const StyleSheet = require('react-native').StyleSheet;
2 |
3 | const styles = StyleSheet.create({
4 | container: {
5 | flex: 1,
6 | backgroundColor: 'transparent'
7 | },
8 | parallaxHeaderContainer: {
9 | backgroundColor: 'transparent',
10 | overflow: 'hidden'
11 | },
12 | parallaxHeader: {
13 | backgroundColor: 'transparent',
14 | overflow: 'hidden'
15 | },
16 | backgroundImage: {
17 | position: 'absolute',
18 | backgroundColor: 'transparent',
19 | overflow: 'hidden',
20 | top: 0
21 | },
22 | stickyHeader: {
23 | backgroundColor: 'transparent',
24 | position: 'absolute',
25 | overflow: 'hidden',
26 | top: 0,
27 | left: 0
28 | },
29 | scrollView: {
30 | backgroundColor: 'transparent'
31 | }
32 | });
33 |
34 | module.exports = styles;
35 |
--------------------------------------------------------------------------------
/js/components/settings/styles.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import { StyleSheet } from 'react-native';
8 |
9 | const styles = StyleSheet.create({
10 | button: {
11 | width: 200,
12 | height: 35,
13 | backgroundColor: '#007AFF',
14 | borderRadius: 2,
15 | flexDirection: 'row',
16 | justifyContent: 'center',
17 | alignItems: 'center'
18 | },
19 | redButton: {
20 | width: 200,
21 | height: 35,
22 | backgroundColor: '#D0021B',
23 | borderRadius: 2,
24 | flexDirection: 'row',
25 | justifyContent: 'center',
26 | alignItems: 'center'
27 | },
28 | buttonText: {
29 | color: '#fff'
30 | },
31 | listItem: {
32 | backgroundColor: '#fff',
33 | borderColor: '#C8C7CC',
34 | borderBottomWidth: StyleSheet.hairlineWidth,
35 | }
36 | });
37 |
38 | module.exports = styles;
39 |
--------------------------------------------------------------------------------
/js/models/view.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | export type WeatherForecast = {
8 | day: string;
9 | forecast: string;
10 | low: string;
11 | high: string;
12 | icon: string;
13 | };
14 |
15 | export type WeatherObservation = {
16 | location: string;
17 | forecast: string;
18 | feelsLike: string;
19 | current: string;
20 | low: string;
21 | high: string;
22 | icon: string;
23 | };
24 |
25 | export type WeatherModel = {
26 | freshness: Date;
27 | observation: WeatherObservation;
28 | forecast: Array;
29 | };
30 |
31 | export type Location = {
32 | name: string;
33 | postcode: string;
34 | state: string;
35 | openWeatherId: string;
36 | observation: WeatherObservation;
37 | };
38 |
39 | export type Postcode = {
40 | name: string;
41 | postcode: number;
42 | state: string;
43 | }
44 |
--------------------------------------------------------------------------------
/js/components/settings/section.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | View,
11 | } from 'react-native';
12 |
13 | type Props = {
14 | children: any;
15 | style: any;
16 | };
17 |
18 | class Section extends Component {
19 | props: Props;
20 |
21 | constructor(props: Props) {
22 | super(props);
23 | }
24 |
25 | render() {
26 | return (
27 |
28 | { this.props.children }
29 |
30 | );
31 | }
32 | }
33 |
34 | const styles = StyleSheet.create({
35 | section: {
36 | backgroundColor: '#fff',
37 | borderColor: '#C8C7CC',
38 | borderTopWidth: StyleSheet.hairlineWidth,
39 | borderBottomWidth: StyleSheet.hairlineWidth
40 | },
41 | });
42 |
43 | module.exports = Section;
--------------------------------------------------------------------------------
/ios/ReactWeatherTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/api/location.js:
--------------------------------------------------------------------------------
1 | {
2 | "message":"accurate",
3 | "cod":"200",
4 | "count":1,
5 | "list":[
6 | {
7 | "id":2147714,
8 | "name":"Sydney",
9 | "coord":{
10 | "lon":151.207321,
11 | "lat":-33.867851
12 | },
13 | "main":{
14 | "temp":13.84,
15 | "humidity":58,
16 | "pressure":1029,
17 | "temp_min":11.67,
18 | "temp_max":16.67
19 | },
20 | "dt":1465979431,
21 | "wind":{
22 | "speed":1.03,
23 | "gust":1.54,
24 | "deg":0
25 | },
26 | "sys":{
27 | "country":"AU"
28 | },
29 | "clouds":{
30 | "all":0
31 | },
32 | "weather":[
33 | {
34 | "id":800,
35 | "main":"Clear",
36 | "description":"Sky is Clear",
37 | "icon":"01n"
38 | }
39 | ]
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/js/components/settings/sectiontitle.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | View,
11 | Text,
12 | } from 'react-native';
13 |
14 | type Props = {
15 | text: string;
16 | };
17 |
18 | class SectionTitle extends Component {
19 | props: Props;
20 |
21 | constructor(props: Props) {
22 | super(props);
23 | }
24 |
25 | render() {
26 | return (
27 |
28 | { this.props.text }
29 |
30 | );
31 | }
32 | }
33 |
34 | const styles = StyleSheet.create({
35 | sectionTitle: {
36 | paddingLeft: 14,
37 | paddingBottom: 8
38 | },
39 | sectionTitleText: {
40 | paddingTop: 36,
41 | color: '#6D6D72',
42 | fontSize: 12
43 | },
44 | });
45 |
46 | module.exports = SectionTitle;
--------------------------------------------------------------------------------
/api/weather.json:
--------------------------------------------------------------------------------
1 | {
2 | "coord": {
3 | "lon": 149.11,
4 | "lat": -35.22
5 | },
6 | "weather": [
7 | {
8 | "id": 804,
9 | "main": "Clouds",
10 | "description": "overcast clouds",
11 | "icon": "04d"
12 | }
13 | ],
14 | "base": "stations",
15 | "main": {
16 | "temp": 21.34,
17 | "pressure": 1016,
18 | "humidity": 88,
19 | "temp_min": 19,
20 | "temp_max": 24
21 | },
22 | "visibility": 16093,
23 | "wind": {
24 | "speed": 2.6,
25 | "deg": 200
26 | },
27 | "clouds": {
28 | "all": 90
29 | },
30 | "dt": 1463812680,
31 | "sys": {
32 | "type": 1,
33 | "id": 1688,
34 | "message": 0.014,
35 | "country": "AU",
36 | "sunrise": 1463777765,
37 | "sunset": 1463814240
38 | },
39 | "id": 6619483,
40 | "name": "Kaleen",
41 | "cod": 200
42 | }
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | android.useDeprecatedNdk=true
21 |
--------------------------------------------------------------------------------
/js/components/loading.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | View,
11 | Image,
12 | Text,
13 | } from 'react-native';
14 |
15 | class Loading extends Component {
16 | render() {
17 | return (
18 |
19 |
20 | Loading...
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | const styles = StyleSheet.create({
29 | loadingView: {
30 | backgroundColor: '#fff',
31 | flex: 1
32 | },
33 | loadingHeader: {
34 | height: 290,
35 | backgroundColor: '#589BC7',
36 | justifyContent: 'center',
37 | alignItems: 'center',
38 | },
39 | loadingText: {
40 | color: '#fff',
41 | marginBottom: 18
42 | }
43 | });
44 |
45 | module.exports = Loading;
--------------------------------------------------------------------------------
/android/ReactWeather.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/js/dependencies/swipeout/styles.js:
--------------------------------------------------------------------------------
1 | var React = require('react-native')
2 | var {StyleSheet} = React
3 |
4 | var styles = StyleSheet.create({
5 | swipeout: {
6 | backgroundColor: '#dbddde',
7 | flex: 1,
8 | overflow: 'hidden',
9 | },
10 | swipeoutBtnTouchable: {
11 | flex: 1,
12 | },
13 | swipeoutBtn: {
14 | alignItems: 'center',
15 | backgroundColor: '#b6bec0',
16 | flex: 1,
17 | justifyContent: 'center',
18 | overflow: 'hidden',
19 | },
20 | swipeoutBtnText: {
21 | color: '#fff',
22 | textAlign: 'center',
23 | },
24 | swipeoutBtns: {
25 | bottom: 0,
26 | flex: 1,
27 | flexDirection: 'row',
28 | position: 'absolute',
29 | right: 0,
30 | top: 0,
31 | },
32 | swipeoutContent: {
33 | flex: 1,
34 | },
35 | colorDelete: {
36 | backgroundColor: '#fb3d38',
37 | },
38 | colorPrimary: {
39 | backgroundColor: '#006fff'
40 | },
41 | colorSecondary: {
42 | backgroundColor: '#fd9427'
43 | },
44 | })
45 |
46 | module.exports = styles
--------------------------------------------------------------------------------
/js/realm/schema-v1.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | const Observation = {
8 | name: 'Observation',
9 | properties: {
10 | forecast: 'string',
11 | feelsLike: 'string',
12 | current: 'string',
13 | low: 'string',
14 | high: 'string',
15 | icon: 'string'
16 | }
17 | };
18 |
19 | const Forecast = {
20 | name: 'Forecast',
21 | properties: {
22 | day: 'string',
23 | forecast: 'string',
24 | low: 'string',
25 | high: 'string',
26 | icon: 'string'
27 | }
28 | };
29 |
30 | const Location = {
31 | name: 'Location',
32 | properties: {
33 | name: 'string',
34 | postcode: 'string',
35 | state: 'string',
36 | openWeatherId: 'string',
37 | freshness: 'date',
38 | observation: 'Observation',
39 | forecast: {
40 | type: 'list',
41 | objectType: 'Forecast'
42 | }
43 | }
44 | };
45 |
46 | module.exports = {
47 | schema: [Observation, Forecast, Location],
48 | schemaVersion: 1,
49 | migration: () => {}
50 | };
51 |
--------------------------------------------------------------------------------
/js/reducers/weather.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import type { Action } from '../actions/types';
8 | import type { WeatherModel } from '../models/view';
9 |
10 | export type State = {
11 | isLoading: bool;
12 | isRefreshing: bool;
13 | data: Array;
14 | current: number;
15 | };
16 |
17 | const initial: State = {
18 | isLoading: false,
19 | isRefreshing: false,
20 | data: [],
21 | current: 0
22 | };
23 |
24 | function weather(state: State = initial, action: Action): State {
25 | switch (action.type) {
26 | case 'WEATHER_SET_LOADING':
27 | return {
28 | ...state,
29 | isLoading: true
30 | };
31 | case 'WEATHER_SET_REFRESHING':
32 | return {
33 | ...state,
34 | isRefreshing: true
35 | };
36 | case 'WEATHER_GET_ALL':
37 | return {
38 | ...state,
39 | isLoading: false,
40 | isRefreshing: false,
41 | data: action.data,
42 | };
43 | default:
44 | return state;
45 | }
46 | }
47 |
48 | module.exports = weather;
49 |
--------------------------------------------------------------------------------
/js/components/pager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | View
11 | } from 'react-native';
12 |
13 | import Icon from 'react-native-vector-icons/FontAwesome';
14 |
15 | type Props = {
16 | current: number;
17 | count: number;
18 | };
19 |
20 | class Pager extends Component {
21 | props: Props;
22 |
23 | render() {
24 | var items = new Array(this.props.count).fill(0);
25 | var dots = items.map((item, index) => {
26 | var key = `pager-dot-${index}`;
27 | var colour = index === this.props.current ? '#000000' : '#c7c7c7';
28 |
29 | return (
30 |
31 | );
32 | });
33 |
34 | return (
35 | { dots }
36 | );
37 | }
38 | }
39 |
40 | const styles = StyleSheet.create({
41 | pager: {
42 | flexDirection: 'row'
43 | },
44 | dot: {
45 | marginLeft: 3,
46 | marginRight: 3
47 | }
48 | });
49 |
50 | module.exports = Pager;
51 |
--------------------------------------------------------------------------------
/js/services/postcode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import realm from '../realm';
8 | import { postcodeApiUrl } from '../config';
9 |
10 | class PostcodeService {
11 | async getPostcodes(text: string) {
12 | var data = [];
13 | var postcodes = await this.getPostcodesFromApi(text);
14 |
15 | if (postcodes) {
16 | for (var i = 0; i < postcodes.length; i++) {
17 | var postcode = postcodes[i];
18 | var item = {
19 | name: postcode.name,
20 | postcode: postcode.postcode,
21 | state: postcode.state.abbreviation
22 | }
23 |
24 | data.push(item);
25 | }
26 | }
27 |
28 | return data;
29 | }
30 |
31 | async getPostcodesFromApi(text: string) {
32 | var url = `${postcodeApiUrl}?q=${text}`;
33 |
34 | try {
35 | let response = await fetch(url);
36 | const result = await response.json();
37 |
38 | return result;
39 | } catch(error) {
40 | // Handle error
41 | global.log(error);
42 | }
43 | }
44 | }
45 |
46 | module.exports = PostcodeService;
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Sam Ilic
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/js/realm/schema-v2.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | const Observation = {
8 | name: 'Observation',
9 | properties: {
10 | forecast: 'string',
11 | feelsLike: 'string',
12 | current: 'string',
13 | low: 'string',
14 | high: 'string',
15 | icon: 'string'
16 | }
17 | };
18 |
19 | const Forecast = {
20 | name: 'Forecast',
21 | properties: {
22 | day: 'string',
23 | forecast: 'string',
24 | low: 'string',
25 | high: 'string',
26 | icon: 'string'
27 | }
28 | };
29 |
30 | const Weather = {
31 | name: 'Weather',
32 | properties: {
33 | freshness: 'date',
34 | observation: 'Observation',
35 | forecast: {
36 | type: 'list',
37 | objectType: 'Forecast'
38 | }
39 | }
40 | }
41 |
42 | const Location = {
43 | name: 'Location',
44 | properties: {
45 | name: 'string',
46 | postcode: 'string',
47 | state: 'string',
48 | openWeatherId: 'string',
49 | weather: 'Weather'
50 | }
51 | };
52 |
53 | module.exports = {
54 | schema: [Observation, Forecast, Weather, Location],
55 | schemaVersion: 2,
56 | migration: () => {}
57 | };
58 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/js/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import { Provider } from 'react-redux';
9 | import App from './app';
10 |
11 | import configureRealm from './realm/configure';
12 | import configureStore from './store/configure';
13 | import { setWeatherLoading, initaliseLocations } from './actions';
14 |
15 | type State = {
16 | store: any;
17 | };
18 |
19 | function setup() {
20 |
21 | class Root extends Component {
22 | state: State;
23 |
24 | constructor() {
25 | super();
26 | configureRealm();
27 |
28 | this.state = {
29 | store: configureStore()
30 | };
31 |
32 | this.state.store.dispatch(setWeatherLoading());
33 | this.state.store.dispatch(initaliseLocations());
34 | }
35 |
36 | render() {
37 | return (
38 |
39 |
40 |
41 | );
42 | }
43 | }
44 |
45 | return Root;
46 | }
47 |
48 | global.log = (...args) => {
49 | console.log('------------------------------');
50 | console.log(...args);
51 | console.log('------------------------------');
52 | return args[args.length - 1];
53 | };
54 |
55 | module.exports = setup;
56 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/reactweather/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.reactweather;
2 |
3 | import com.facebook.react.ReactActivity;
4 | import io.realm.react.RealmReactPackage;
5 | import com.oblador.vectoricons.VectorIconsPackage;
6 | import com.facebook.react.ReactPackage;
7 | import com.facebook.react.shell.MainReactPackage;
8 |
9 | import java.util.Arrays;
10 | import java.util.List;
11 |
12 | public class MainActivity extends ReactActivity {
13 |
14 | /**
15 | * Returns the name of the main component registered from JavaScript.
16 | * This is used to schedule rendering of the component.
17 | */
18 | @Override
19 | protected String getMainComponentName() {
20 | return "ReactWeather";
21 | }
22 |
23 | /**
24 | * Returns whether dev mode should be enabled.
25 | * This enables e.g. the dev menu.
26 | */
27 | @Override
28 | protected boolean getUseDeveloperSupport() {
29 | return BuildConfig.DEBUG;
30 | }
31 |
32 | /**
33 | * A list of packages used by the app. If the app uses additional views
34 | * or modules besides the default ones, add more packages here.
35 | */
36 | @Override
37 | protected List getPackages() {
38 | return Arrays.asList(
39 | new MainReactPackage(),
40 | new RealmReactPackage(),
41 | new VectorIconsPackage()
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/js/actions/weather.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import type { Action } from './types';
8 |
9 | import WeatherService from '../services/weather';
10 | const service = new WeatherService();
11 |
12 | const maxAgeInSeconds = (10 * 60); // 10 minutes
13 |
14 | function getAllWeather() {
15 | return (dispatch: any) => {
16 | service.getAllWeather(false).then(
17 | (result) => {
18 | dispatch({
19 | type: 'WEATHER_GET_ALL',
20 | data: result
21 | });
22 |
23 | var dates = result.map((item) => item.freshness);
24 | var freshness = new Date(Math.min(...dates));
25 | if (getAgeInSeconds(freshness) > maxAgeInSeconds) {
26 | dispatch(setWeatherRefreshing());
27 | dispatch(forceWeatherUpdate());
28 | }
29 | }
30 | );
31 | };
32 | }
33 |
34 | async function forceWeatherUpdate() {
35 | var result = await service.getAllWeather(true);
36 | return {
37 | type: 'WEATHER_GET_ALL',
38 | data: result
39 | };
40 | }
41 |
42 | function getAgeInSeconds(freshness: Date) {
43 | return Math.floor((Date.now() - freshness.getTime()) / 1000);
44 | }
45 |
46 | function setWeatherLoading() {
47 | return {
48 | type: 'WEATHER_SET_LOADING'
49 | };
50 | }
51 |
52 | function setWeatherRefreshing() {
53 | return {
54 | type: 'WEATHER_SET_REFRESHING'
55 | };
56 | }
57 |
58 | module.exports = {
59 | getAllWeather,
60 | setWeatherLoading,
61 | setWeatherRefreshing
62 | }
63 |
--------------------------------------------------------------------------------
/js/components/forecastimage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import { Image } from 'react-native';
9 |
10 | function renderForecastImage(icon: string, width: number, height: number) {
11 | var image: number;
12 | switch (icon) {
13 | case '01d':
14 | case '01n':
15 | image = require('./img/sunny.png');
16 | break;
17 | case '02d':
18 | case '02n':
19 | image = require('./img/sunny_s_cloudy.png');
20 | break;
21 | case '03d':
22 | case '03n':
23 | image = require('./img/partly_cloudy.png');
24 | break;
25 | case '04d':
26 | case '04n':
27 | image = require('./img/cloudy.png');
28 | break;
29 | case '09d':
30 | case '09n':
31 | image = require('./img/rain.png');
32 | break;
33 | case '10d':
34 | case '10n':
35 | image = require('./img/rain_s_cloudy.png');
36 | break;
37 | case '11d':
38 | case '11n':
39 | image = require('./img/thunderstorms.png');
40 | break;
41 | case '13d':
42 | case '13n':
43 | image = require('./img/snow.png');
44 | break;
45 | case '50d':
46 | case '50n':
47 | image = require('./img/fog.png');
48 | break;
49 | }
50 |
51 | const imageStyle = {
52 | width: width,
53 | height: height
54 | };
55 |
56 | return (
57 |
58 | );
59 | }
60 |
61 | module.exports = renderForecastImage;
62 |
--------------------------------------------------------------------------------
/js/components/settings/settings.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | View,
11 | StatusBar,
12 | } from 'react-native';
13 |
14 | import Icon from 'react-native-vector-icons/Ionicons';
15 |
16 | import SectionTitle from './sectiontitle';
17 | import Section from './section';
18 | import Separator from './separator';
19 | import NavigationButtonRow from './navigationbuttonrow';
20 | import AboutSettings from './about';
21 | import UserGuide from './userguide';
22 |
23 | type Props = {
24 | navigator: any;
25 | };
26 |
27 | class Settings extends Component {
28 | props: Props;
29 |
30 | constructor(props: Props) {
31 | super(props);
32 |
33 | (this: any).close = this.close.bind(this);
34 | }
35 |
36 | render() {
37 | return (
38 |
39 |
40 |
41 |
42 |
47 |
48 | );
49 | }
50 |
51 | close() {
52 | this.props.navigator.close();
53 | }
54 | }
55 |
56 | const styles = StyleSheet.create({
57 | container: {
58 | flex: 1,
59 | flexDirection: 'column',
60 | backgroundColor: '#f8f8f8',
61 | marginTop: 64,
62 | }
63 | });
64 |
65 | module.exports = Settings;
66 |
--------------------------------------------------------------------------------
/js/components/settings/navigationbuttonrow.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | View,
11 | Text,
12 | StatusBar,
13 | TouchableHighlight
14 | } from 'react-native';
15 |
16 | import Icon from 'react-native-vector-icons/Ionicons';
17 |
18 | type Props = {
19 | text: string;
20 | navigator: any;
21 | component: any;
22 | style: any;
23 | };
24 |
25 | class NavigationButtonRow extends Component {
26 | props: Props;
27 |
28 | constructor(props: Props) {
29 | super(props);
30 | }
31 |
32 | render() {
33 | return (
34 | this.props.navigator.navigateTo({
38 | title: this.props.text,
39 | component: this.props.component
40 | })}>
41 |
42 | { this.props.text }
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | const styles = StyleSheet.create({
51 | navigationButtonRow: {
52 | height: 44,
53 | flexDirection: 'column',
54 | justifyContent: 'center',
55 | backgroundColor: '#fff'
56 | },
57 | navigationButtonView: {
58 | flexDirection: 'row',
59 | justifyContent: 'space-between',
60 | paddingRight: 14,
61 | paddingLeft: 14
62 | },
63 | navigationButtonText: {
64 | fontSize: 16
65 | },
66 | });
67 |
68 | module.exports = NavigationButtonRow;
--------------------------------------------------------------------------------
/js/components/settings/about.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | View,
11 | Text,
12 | TouchableHighlight,
13 | ActionSheetIOS
14 | } from 'react-native';
15 |
16 | import { connect } from 'react-redux';
17 |
18 | import defaultStyles from './styles';
19 | import { clearAllLocationData } from '../../actions';
20 |
21 | type Props = {
22 | dispatch: any;
23 | navigator: any;
24 | };
25 |
26 | class AboutSettings extends Component {
27 | props: any;
28 |
29 | constructor(props: any) {
30 | super(props);
31 |
32 | (this: any).clearAllData = this.clearAllData.bind(this);
33 | }
34 |
35 | render() {
36 | return (
37 |
38 | About
39 |
40 | Clear all data
41 |
42 |
43 | );
44 | }
45 |
46 | clearAllData() {
47 | ActionSheetIOS.showActionSheetWithOptions({
48 | message: 'Are you sure you want to delete all app data?',
49 | options: [
50 | 'Delete',
51 | 'Cancel'
52 | ],
53 | cancelButtonIndex: 1,
54 | destructiveButtonIndex: 0,
55 | },
56 | (buttonIndex) => {
57 | if(buttonIndex === 0) {
58 | this.props.dispatch(clearAllLocationData());
59 | this.props.navigator.close();
60 | }
61 | });
62 | }
63 | }
64 |
65 | const styles = StyleSheet.create({
66 | container: {
67 | }
68 | });
69 |
70 | module.exports = connect()(AboutSettings);
71 |
--------------------------------------------------------------------------------
/android/app/BUCK:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | # To learn about Buck see [Docs](https://buckbuild.com/).
4 | # To run your application with Buck:
5 | # - install Buck
6 | # - `npm start` - to start the packager
7 | # - `cd android`
8 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US`
9 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
10 | # - `buck install -r android/app` - compile, install and run application
11 | #
12 |
13 | lib_deps = []
14 | for jarfile in glob(['libs/*.jar']):
15 | name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
16 | lib_deps.append(':' + name)
17 | prebuilt_jar(
18 | name = name,
19 | binary_jar = jarfile,
20 | )
21 |
22 | for aarfile in glob(['libs/*.aar']):
23 | name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
24 | lib_deps.append(':' + name)
25 | android_prebuilt_aar(
26 | name = name,
27 | aar = aarfile,
28 | )
29 |
30 | android_library(
31 | name = 'all-libs',
32 | exported_deps = lib_deps
33 | )
34 |
35 | android_library(
36 | name = 'app-code',
37 | srcs = glob([
38 | 'src/main/java/**/*.java',
39 | ]),
40 | deps = [
41 | ':all-libs',
42 | ':build_config',
43 | ':res',
44 | ],
45 | )
46 |
47 | android_build_config(
48 | name = 'build_config',
49 | package = 'com.reactweather',
50 | )
51 |
52 | android_resource(
53 | name = 'res',
54 | res = 'src/main/res',
55 | package = 'com.reactweather',
56 | )
57 |
58 | android_binary(
59 | name = 'app',
60 | package_type = 'debug',
61 | manifest = 'src/main/AndroidManifest.xml',
62 | keystore = '//android/keystores:debug',
63 | deps = [
64 | ':app-code',
65 | ],
66 | )
67 |
--------------------------------------------------------------------------------
/js/actions/location.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import type { Action } from './types';
8 | import { getAllWeather } from './weather';
9 |
10 | import LocationService from '../services/location';
11 | const service = new LocationService();
12 |
13 | function initaliseLocations() {
14 | return (dispatch: any) => {
15 | service.initialise().then(
16 | (result) => {
17 | dispatch({
18 | type: 'LOCATION_INITIALISED'
19 | });
20 | dispatch(getAllWeather());
21 | }
22 | );
23 | };
24 | }
25 |
26 | function getAllLocations() {
27 | var result = service.getAllLocations();
28 | return {
29 | type: 'LOCATION_GET_ALL',
30 | data: result
31 | };
32 | }
33 |
34 | function clearAllLocationData() {
35 | return (dispatch: any) => {
36 | service.clearAllData()
37 | dispatch({
38 | type: 'LOCATION_CLEAR_ALL_DATA'
39 | });
40 | dispatch(initaliseLocations());
41 | };
42 | }
43 |
44 | function deleteLocation(openWeatherId: string) {
45 | return (dispatch: any) => {
46 | service.deleteLocation(openWeatherId);
47 | dispatch({
48 | type: 'LOCATION_DELETE'
49 | });
50 | dispatch(getAllLocations());
51 | dispatch(getAllWeather());
52 | };
53 | }
54 |
55 | function addLocation(name: string, postcode: string, state: string) {
56 | return (dispatch: any) => {
57 | service.addLocation(name, postcode, state).then(
58 | (result) => {
59 | dispatch({
60 | type: 'LOCATION_ADD'
61 | });
62 | dispatch(getAllLocations());
63 | dispatch(getAllWeather());
64 | }
65 | );
66 | };
67 | }
68 |
69 | module.exports = {
70 | initaliseLocations,
71 | getAllLocations,
72 | clearAllLocationData,
73 | deleteLocation,
74 | addLocation
75 | };
76 |
--------------------------------------------------------------------------------
/ios/ReactWeather/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | NSAppTransportSecurity
26 |
27 | NSAllowsArbitraryLoads
28 |
29 |
30 | NSLocationWhenInUseUsageDescription
31 |
32 | UIAppFonts
33 |
34 | Entypo.ttf
35 | EvilIcons.ttf
36 | FontAwesome.ttf
37 | Foundation.ttf
38 | Ionicons.ttf
39 | MaterialIcons.ttf
40 | Octicons.ttf
41 | Zocial.ttf
42 |
43 | UILaunchStoryboardName
44 | LaunchScreen
45 | UIRequiredDeviceCapabilities
46 |
47 | armv7
48 |
49 | UISupportedInterfaceOrientations
50 |
51 | UIInterfaceOrientationPortrait
52 |
53 | UIViewControllerBasedStatusBarAppearance
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/js/components/forecast.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | Text,
11 | View,
12 | } from 'react-native';
13 |
14 | import ForecastItem from './forecastitem';
15 | import type { WeatherForecast } from '../models/view';
16 |
17 | type Props = {
18 | forecast: Array;
19 | };
20 |
21 | class Forecast extends Component {
22 | props: Props;
23 |
24 | constructor(props: Props) {
25 | super(props);
26 |
27 | (this: any).renderForecastItems = this.renderForecastItems.bind(this);
28 | }
29 |
30 | render() {
31 | return (
32 |
33 |
34 | { this.renderForecastItems() }
35 |
36 |
37 | );
38 | }
39 |
40 | renderForecastItems() {
41 | return (
42 | this.props.forecast.map((item, index) => {
43 | if (index === 0) {
44 | return null;
45 | };
46 |
47 | if (index < this.props.forecast.length - 1) {
48 | var separator = {
49 | borderColor: '#F4F4F4',
50 | borderBottomWidth: StyleSheet.hairlineWidth,
51 | };
52 | }
53 |
54 | return (
55 |
56 | );
57 | })
58 | );
59 | }
60 | }
61 |
62 | const styles = StyleSheet.create({
63 | forecastView: {
64 | marginLeft: 5,
65 | marginRight: 5,
66 | flexDirection: 'row',
67 | borderColor: '#e2e2e2',
68 | backgroundColor: '#fff',
69 | borderWidth: 1,
70 | borderRadius: 3
71 | },
72 | forecastList: {
73 | flex: 1,
74 | borderColor: '#E2E2E2',
75 | paddingLeft: 12,
76 | paddingRight: 12
77 | },
78 | });
79 |
80 | module.exports = Forecast;
81 |
--------------------------------------------------------------------------------
/js/navigators/application.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | Navigator
11 | } from 'react-native';
12 |
13 | import Weather from '../components/weather';
14 | import Settings from '../components/settings/settings';
15 | import Locations from '../components/settings/locations';
16 | import SettingsNavigator from '../navigators/settings';
17 |
18 | type Props = {};
19 |
20 | class ApplicationNavigator extends Component {
21 | constructor(props: Props) {
22 | super(props);
23 |
24 | (this: any).renderScene = this.renderScene.bind(this);
25 | }
26 |
27 | render() {
28 | return (
29 | {
33 | return Navigator.SceneConfigs.FloatFromBottom;
34 | }}
35 | initialRoute={{}}
36 | renderScene={this.renderScene}
37 | />
38 | );
39 | }
40 |
41 | push(route: any) {
42 | this.refs.navigator.push(route);
43 | }
44 |
45 | pop() {
46 | this.refs.navigator.pop();
47 | }
48 |
49 | renderScene(route: any, navigator: Navigator) {
50 | if (route.settings) {
51 | return (
52 |
56 | );
57 | }
58 |
59 | if (route.locations) {
60 | return (
61 |
65 | );
66 | }
67 |
68 | return (
69 |
70 | );
71 | }
72 | }
73 |
74 | const styles = StyleSheet.create({
75 | container: {
76 | flex: 1,
77 | backgroundColor: '#000',
78 | }
79 | });
80 |
81 | module.exports = ApplicationNavigator;
82 |
--------------------------------------------------------------------------------
/js/components/forecastitem.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | Text,
11 | View,
12 | Image,
13 | } from 'react-native';
14 |
15 | const renderForecastImage = require('./forecastimage');
16 |
17 | type Props = {
18 | index: number;
19 | day: string;
20 | icon: string;
21 | low: string;
22 | high: string;
23 | separator: any;
24 | };
25 |
26 | class ForecastItem extends Component {
27 | props: Props;
28 |
29 | constructor(props: Props) {
30 | super(props);
31 | }
32 |
33 | render() {
34 | var day = this.props.index === 1 ? 'Tomorrow' : this.props.day;
35 | return (
36 |
37 |
38 | { day }
39 |
40 |
41 | { renderForecastImage(this.props.icon, 22, 22) }
42 | { this.props.low }
43 | { this.props.high }
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | const styles = StyleSheet.create({
51 | forecastItem: {
52 | paddingTop: 14,
53 | paddingBottom: 12,
54 | flexDirection: 'row'
55 | },
56 | forecastItemDayView: {
57 | flex: 1
58 | },
59 | forecastItemDataView: {
60 | flex: 1,
61 | flexDirection: 'row',
62 | justifyContent: 'flex-end'
63 | },
64 | dayText: {
65 | fontSize: 16
66 | },
67 | forecastItemTempLow: {
68 | textAlign: 'right',
69 | marginLeft: 16,
70 | width: 20,
71 | color: '#B0B5BF',
72 | fontSize: 16
73 | },
74 | forecastItemTempHigh: {
75 | textAlign: 'right',
76 | marginLeft: 16,
77 | width: 20,
78 | fontSize: 16
79 | }
80 | });
81 |
82 | module.exports = ForecastItem;
--------------------------------------------------------------------------------
/js/navigators/settings.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | NavigatorIOS,
11 | View
12 | } from 'react-native';
13 |
14 | import Icon from 'react-native-vector-icons/Ionicons';
15 | import Settings from '../components/settings/settings';
16 |
17 | type Props = {
18 | route: any;
19 | navigator: any;
20 | };
21 | type State = {
22 | backIcon: any;
23 | };
24 |
25 | class SettingsNavigator extends Component {
26 | props: Props;
27 | state: State;
28 |
29 | constructor(props: Props) {
30 | super(props);
31 |
32 | this.state = {
33 | backIcon: null
34 | };
35 | }
36 |
37 | componentWillMount() {
38 | Icon.getImageSource('ios-close', 40, '#007AFF').then(
39 | (source) => this.setState({ backIcon: source })
40 | );
41 | }
42 |
43 | render() {
44 | if (!this.state.backIcon) {
45 | return null;
46 | }
47 |
48 | return (
49 |
62 | );
63 | }
64 |
65 | navigateTo(route: any) {
66 | this.push({
67 | ...route,
68 | passProps: {
69 | navigator: this
70 | }
71 | });
72 | }
73 |
74 | push(route: any) {
75 | this.refs.navigator.push(route);
76 | }
77 |
78 | pop() {
79 | this.refs.navigator.pop();
80 | }
81 |
82 | close() {
83 | this.props.navigator.pop();
84 | }
85 | }
86 |
87 | const styles = StyleSheet.create({
88 | container: {
89 | flex: 1
90 | },
91 | content: {
92 | flex: 1
93 | }
94 | });
95 |
96 | module.exports = SettingsNavigator;
97 |
--------------------------------------------------------------------------------
/ios/ReactWeather/AppDelegate.m:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015-present, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | */
9 |
10 | #import "AppDelegate.h"
11 |
12 | #import "RCTRootView.h"
13 |
14 | @implementation AppDelegate
15 |
16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
17 | {
18 | NSURL *jsCodeLocation;
19 |
20 | /**
21 | * Loading JavaScript code - uncomment the one you want.
22 | *
23 | * OPTION 1
24 | * Load from development server. Start the server from the repository root:
25 | *
26 | * $ npm start
27 | *
28 | * To run on device, change `localhost` to the IP address of your computer
29 | * (you can get this by typing `ifconfig` into the terminal and selecting the
30 | * `inet` value under `en0:`) and make sure your computer and iOS device are
31 | * on the same Wi-Fi network.
32 | */
33 |
34 | //jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
35 |
36 | /**
37 | * OPTION 2
38 | * Load from pre-bundled file on disk. The static bundle is automatically
39 | * generated by the "Bundle React Native code and images" build step when
40 | * running the project on an actual device or running the project on the
41 | * simulator in the "Release" build configuration.
42 | */
43 |
44 | jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
45 |
46 | RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
47 | moduleName:@"ReactWeather"
48 | initialProperties:nil
49 | launchOptions:launchOptions];
50 |
51 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
52 | UIViewController *rootViewController = [UIViewController new];
53 | rootViewController.view = rootView;
54 | self.window.rootViewController = rootViewController;
55 | [self.window makeKeyAndVisible];
56 | return YES;
57 | }
58 |
59 | @end
60 |
--------------------------------------------------------------------------------
/ios/ReactWeatherTests/ReactWeatherTests.m:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015-present, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | */
9 |
10 | #import
11 | #import
12 |
13 | #import "RCTLog.h"
14 | #import "RCTRootView.h"
15 |
16 | #define TIMEOUT_SECONDS 600
17 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!"
18 |
19 | @interface ReactWeatherTests : XCTestCase
20 |
21 | @end
22 |
23 | @implementation ReactWeatherTests
24 |
25 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test
26 | {
27 | if (test(view)) {
28 | return YES;
29 | }
30 | for (UIView *subview in [view subviews]) {
31 | if ([self findSubviewInView:subview matching:test]) {
32 | return YES;
33 | }
34 | }
35 | return NO;
36 | }
37 |
38 | - (void)testRendersWelcomeScreen
39 | {
40 | UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
41 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
42 | BOOL foundElement = NO;
43 |
44 | __block NSString *redboxError = nil;
45 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
46 | if (level >= RCTLogLevelError) {
47 | redboxError = message;
48 | }
49 | });
50 |
51 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
52 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
53 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
54 |
55 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) {
56 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
57 | return YES;
58 | }
59 | return NO;
60 | }];
61 | }
62 |
63 | RCTSetLogFunction(RCTDefaultLogFunction);
64 |
65 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
66 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
67 | }
68 |
69 |
70 | @end
71 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Disabling obfuscation is useful if you collect stack traces from production crashes
20 | # (unless you are using a system that supports de-obfuscate the stack traces).
21 | -dontobfuscate
22 |
23 | # React Native
24 |
25 | # Keep our interfaces so they can be used by other ProGuard rules.
26 | # See http://sourceforge.net/p/proguard/bugs/466/
27 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
28 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
29 |
30 | # Do not strip any method/class that is annotated with @DoNotStrip
31 | -keep @com.facebook.proguard.annotations.DoNotStrip class *
32 | -keepclassmembers class * {
33 | @com.facebook.proguard.annotations.DoNotStrip *;
34 | }
35 |
36 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
37 | void set*(***);
38 | *** get*();
39 | }
40 |
41 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
42 | -keep class * extends com.facebook.react.bridge.NativeModule { *; }
43 | -keepclassmembers,includedescriptorclasses class * { native ; }
44 | -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; }
45 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; }
46 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; }
47 |
48 | -dontwarn com.facebook.react.**
49 |
50 | # okhttp
51 |
52 | -keepattributes Signature
53 | -keepattributes *Annotation*
54 | -keep class com.squareup.okhttp.** { *; }
55 | -keep interface com.squareup.okhttp.** { *; }
56 | -dontwarn com.squareup.okhttp.**
57 |
58 | # okio
59 |
60 | -keep class sun.misc.Unsafe { *; }
61 | -dontwarn java.nio.file.*
62 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
63 | -dontwarn okio.**
64 |
--------------------------------------------------------------------------------
/js/components/footer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | Text,
11 | View,
12 | Image,
13 | TouchableHighlight
14 | } from 'react-native';
15 |
16 | import Icon from 'react-native-vector-icons/Ionicons';
17 |
18 | import Pager from './pager';
19 | import Settings from './settings/settings';
20 |
21 | type Props = {
22 | current: number;
23 | count: number;
24 | navigator: any;
25 | };
26 |
27 | class Footer extends Component {
28 | constructor(props: Props) {
29 | super(props);
30 |
31 | (this: any).navigateToSettings = this.navigateToSettings.bind(this);
32 | (this: any).navigateToLocations = this.navigateToLocations.bind(this);
33 | }
34 |
35 | render() {
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
55 | navigateToSettings() {
56 | this.props.navigator.push({
57 | settings: true
58 | });
59 | }
60 |
61 | navigateToLocations() {
62 | this.props.navigator.push({
63 | locations: true
64 | });
65 | }
66 | }
67 |
68 | const styles = StyleSheet.create({
69 | footer: {
70 | position: 'absolute',
71 | left: 0,
72 | right: 0,
73 | bottom: 0,
74 | height: 40,
75 | paddingLeft: 15,
76 | paddingRight: 15,
77 | flexDirection: 'row',
78 | justifyContent: 'center',
79 | alignItems: 'center',
80 | backgroundColor: '#f8f8f8',
81 | borderTopWidth: StyleSheet.hairlineWidth,
82 | borderColor: '#c4c4c4'
83 | },
84 | left: {
85 | flex: 1,
86 | },
87 | center: {
88 | flex: 1,
89 | justifyContent: 'center',
90 | alignItems: 'center'
91 | },
92 | right: {
93 | flex: 1,
94 | justifyContent: 'flex-end',
95 | flexDirection: 'row'
96 | }
97 | });
98 |
99 | module.exports = Footer;
100 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/js/components/weather.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import StatusBar from 'StatusBar';
8 | import React, { Component } from 'react';
9 | import {
10 | StyleSheet,
11 | View,
12 | Animated,
13 | Image,
14 | Text,
15 | Dimensions
16 | } from 'react-native';
17 |
18 | import { connect } from 'react-redux';
19 | import Swiper from '../dependencies/swiper';
20 |
21 | import Header from './header';
22 | import Footer from './footer';
23 | import Forecast from './forecast';
24 | import Loading from './loading';
25 |
26 | import type { WeatherModel } from '../models/view'
27 |
28 | const SCREEN_WIDTH = Dimensions.get('window').width;
29 |
30 | type Props = {
31 | dispatch: any;
32 | isLoading: bool;
33 | weather: Array;
34 | count: number;
35 | navigator: any;
36 | };
37 |
38 | type State = {
39 | shift: Animated.Value;
40 | current: number;
41 | };
42 |
43 | class Weather extends Component {
44 | props: Props;
45 | state: State;
46 |
47 | constructor(props: Props) {
48 | super(props);
49 |
50 | this.state = {
51 | shift: new Animated.Value(0),
52 | current: 0
53 | };
54 |
55 | (this: any).onScroll = this.onScroll.bind(this);
56 | (this: any).onSelectedIndexChange = this.onSelectedIndexChange.bind(this);
57 | }
58 |
59 | render() {
60 | if (this.props.isLoading === true) {
61 | return (
62 |
63 | );
64 | }
65 |
66 | var forecastItems = this.props.weather.map((item, index) => {
67 | return (
68 |
69 | );
70 | });
71 |
72 | var swiper = (
73 |
79 | { forecastItems }
80 |
81 | );
82 |
83 | return (
84 |
85 |
88 |
92 |
93 | );
94 | }
95 |
96 | onSelectedIndexChange(index, offset) {
97 | this.setState({current: index});
98 | }
99 |
100 | onScroll(e) {
101 | this.state.shift.setValue(e.nativeEvent.contentOffset.x);
102 | }
103 | }
104 |
105 | const styles = StyleSheet.create({
106 | container: {
107 | flex: 1,
108 | backgroundColor: '#F9F9F9'
109 | }
110 | });
111 |
112 | function select(store: any, props: Props) {
113 | return {
114 | isLoading: store.weather.isLoading,
115 | weather: store.weather.data,
116 | count: store.weather.data.length,
117 | ...props
118 | };
119 | }
120 |
121 | module.exports = connect(select)(Weather);
122 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | # We fork some components by platform.
4 | .*/*.web.js
5 | .*/*.android.js
6 |
7 | # Some modules have their own node_modules with overlap
8 | .*/node_modules/node-haste/.*
9 |
10 | # Ugh
11 | .*/node_modules/babel.*
12 | .*/node_modules/babylon.*
13 | .*/node_modules/invariant.*
14 |
15 | # Ignore react and fbjs where there are overlaps, but don't ignore
16 | # anything that react-native relies on
17 | .*/node_modules/fbjs/lib/Map.js
18 | .*/node_modules/fbjs/lib/ErrorUtils.js
19 |
20 | # Flow has a built-in definition for the 'react' module which we prefer to use
21 | # over the currently-untyped source
22 | .*/node_modules/react/react.js
23 | .*/node_modules/react/lib/React.js
24 | .*/node_modules/react/lib/ReactDOM.js
25 |
26 | .*/__mocks__/.*
27 | .*/__tests__/.*
28 |
29 | .*/commoner/test/source/widget/share.js
30 |
31 | # Ignore commoner tests
32 | .*/node_modules/commoner/test/.*
33 |
34 | # See https://github.com/facebook/flow/issues/442
35 | .*/react-tools/node_modules/commoner/lib/reader.js
36 |
37 | # Ignore jest
38 | .*/node_modules/jest-cli/.*
39 |
40 | # Ignore Website
41 | .*/website/.*
42 |
43 | # Ignore generators
44 | .*/local-cli/generator.*
45 |
46 | # Ignore BUCK generated folders
47 | .*\.buckd/
48 |
49 | .*/node_modules/is-my-json-valid/test/.*\.json
50 | .*/node_modules/iconv-lite/encodings/tables/.*\.json
51 | .*/node_modules/y18n/test/.*\.json
52 | .*/node_modules/spdx-license-ids/spdx-license-ids.json
53 | .*/node_modules/spdx-exceptions/index.json
54 | .*/node_modules/resolve/test/subdirs/node_modules/a/b/c/x.json
55 | .*/node_modules/resolve/lib/core.json
56 | .*/node_modules/jsonparse/samplejson/.*\.json
57 | .*/node_modules/json5/test/.*\.json
58 | .*/node_modules/ua-parser-js/test/.*\.json
59 | .*/node_modules/builtin-modules/builtin-modules.json
60 | .*/node_modules/binary-extensions/binary-extensions.json
61 | .*/node_modules/url-regex/tlds.json
62 | .*/node_modules/joi/.*\.json
63 | .*/node_modules/isemail/.*\.json
64 | .*/node_modules/tr46/.*\.json
65 |
66 | # Also ignore these
67 | .*/node_modules/react-native/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderTitle.js
68 |
69 | [include]
70 |
71 | [libs]
72 | node_modules/react-native/Libraries/react-native/react-native-interface.js
73 | node_modules/react-native/flow
74 | flow/
75 |
76 | [options]
77 | module.system=haste
78 |
79 | esproposal.class_static_fields=enable
80 | esproposal.class_instance_fields=enable
81 |
82 | munge_underscores=true
83 |
84 | module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub'
85 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
86 |
87 | suppress_type=$FlowIssue
88 | suppress_type=$FlowFixMe
89 | suppress_type=$FixMe
90 |
91 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-4]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
92 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-4]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
93 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
94 |
95 | [version]
96 | 0.26.0
97 |
--------------------------------------------------------------------------------
/ios/ReactWeather/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/js/services/location.js:
--------------------------------------------------------------------------------
1 | /**
2 | * flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import realm from '../realm';
8 | import { weatherApiKey, weatherApiUrl } from '../config';
9 |
10 | const defaultLocations = [
11 | {name: 'Canberra', postcode: '2600', state: 'ACT'},
12 | {name: 'Sydney', postcode: '2000', state: 'NSW'},
13 | {name: 'Melbourne', postcode: '3000', state: 'VIC'},
14 | {name: 'Brisbane', postcode: '4000', state: 'QLD'},
15 | {name: 'Perth', postcode: '6000', state: 'WA'},
16 | {name: 'Adelaide', postcode: '5000', state: 'SA'},
17 | {name: 'Hobart', postcode: '7000', state: 'TAS'},
18 | {name: 'Darwin', postcode: '0800', state: 'NT'},
19 | ];
20 |
21 | class LocationService {
22 | async initialise() {
23 | let context = realm.current();
24 | try {
25 | let locations = context.objects('Location');
26 | if (locations.length > 0) {
27 | return;
28 | }
29 |
30 | for (var i = 0; i < defaultLocations.length; i++) {
31 | var location = defaultLocations[i];
32 | var openWeatherId = await this.getLocationIdFromApi(location.name);
33 |
34 | context.write(() => {
35 | context.create('Location', {
36 | name: location.name,
37 | postcode: location.postcode,
38 | state: location.state,
39 | openWeatherId: openWeatherId.toString()
40 | });
41 | });
42 | }
43 | } finally {
44 | context.close();
45 | }
46 | }
47 |
48 | getAllLocations() {
49 | let context = realm.current();
50 |
51 | var data = [];
52 | try {
53 | let locations = context.objects('Location');
54 |
55 | for (var i = 0; i < locations.length; i++) {
56 | var location = locations[i];
57 | var item = {
58 | name: location.name,
59 | postcode: location.postcode,
60 | state: location.state,
61 | openWeatherId: location.openWeatherId,
62 | }
63 |
64 | if (location.weather) {
65 | item.observation = {
66 | current: location.weather.observation.current,
67 | low: location.weather.observation.low,
68 | high: location.weather.observation.high,
69 | icon: location.weather.observation.icon
70 | }
71 | }
72 |
73 | data.push(item);
74 | }
75 | } finally {
76 | context.close();
77 | }
78 |
79 | return data;
80 | }
81 |
82 | deleteLocation(openWeatherId: string) {
83 | let context = realm.current();
84 | try {
85 | let location = context
86 | .objects('Location')
87 | .filtered(`openWeatherId = "${openWeatherId}"`);;
88 |
89 | context.write(() => {
90 | context.delete(location);
91 | });
92 | } finally {
93 | context.close();
94 | }
95 | }
96 |
97 | async addLocation(name: string, postcode: string, state: string) {
98 | let context = realm.current();
99 | try {
100 | var openWeatherId = await this.getLocationIdFromApi(name);
101 | let location = context
102 | .objects('Location')
103 | .filtered(`openWeatherId = "${openWeatherId}"`);
104 |
105 | if (location.length > 0) {
106 | return;
107 | }
108 |
109 | context.write(() => {
110 | context.create('Location', {
111 | name: name,
112 | postcode: postcode,
113 | state: state,
114 | openWeatherId: openWeatherId.toString()
115 | });
116 | });
117 | } finally {
118 | context.close();
119 | }
120 | }
121 |
122 | clearAllData() {
123 | let context = realm.current();
124 | try {
125 | let locations = context.objects('Location');
126 | context.write(() => {
127 | context.delete(locations);
128 | });
129 | } finally {
130 | context.close();
131 | }
132 | }
133 |
134 | async getLocationIdFromApi(location: string) {
135 | var url = `${weatherApiUrl}/find?q=${location},AU&type=accurate&units=metric&appid=${weatherApiKey}`;
136 |
137 | try {
138 | let response = await fetch(url);
139 | const result = await response.json();
140 |
141 | return result.list[0].id;
142 | } catch(error) {
143 | // Handle error
144 | global.log(error);
145 | }
146 | }
147 | }
148 |
149 | module.exports = LocationService;
150 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Weather
2 | A simple weather app built with React Native
3 |
4 | # Inspiration
5 | I wanted to learn how to make apps using React Native. Instead of inventing an app, I wanted to try and see how hard would it be to implement one of my favourite weather apps: [Pocket Weather Australia](https://itunes.apple.com/au/app/pocket-weather-australia/id546266910?mt=8). Full credits for user experience goes to them.
6 |
7 | The app architecture/structure is mostly based on ideas I've seen in the Facebook's F8 app. You can read more about it on their web site: http://makeitopen.com/.
8 |
9 | # Development stack
10 | + [Flow](http://flowtype.org/) was enabled to catch typing errors in React Native JavaScript code
11 | + [Realm](https://realm.io/) for React Native is used to persist data
12 | + I used [Nuclide](http://nuclide.io/) and [Visual Studio Code](https://code.visualstudio.com/) on OSX, both have great support for React Native app development
13 | + I used git for version control, and stored progress on GitHub.
14 | + Currently only tested on an iOS device
15 |
16 | # APIs
17 | + Weather data is retrieved from http://openweathermap.org/
18 | + Australia postcode and suburb data is retrieved from http://postcodeapi.com.au/
19 | + You can see examples of API data in [react-weather/api](https://github.com/stage88/react-weather/tree/master/api)
20 |
21 | # Note about API data
22 | The weather data retrived from Open Weather is not very accurate. Most of the time, the weather forecast is not correct.
23 | The most reliable Australian weather data is hosted by Australian Bureau of Meteorology, however it is not very API friendly.
24 |
25 | # External packages
26 | + [react-native-parallax-scroll-view](https://github.com/jaysoo/react-native-parallax-scroll-view): A ScrollView-like component with parallax and sticky header support. I use a customised version of the component to modify behaviour of animations.
27 | + [react-swipeable-views](https://github.com/oliviertassinari/react-swipeable-views): A React component for swipeable views
28 |
29 | # Weather images
30 | Images are used _without permission_ from Google Weather, for example: [Canberra Weather](https://www.google.com.au/?gws_rd=ssl#safe=active&q=Canberra+weather).
31 |
32 | # Current progress
33 | - [x] App skeleton
34 | - [x] Basic UI prototype
35 | - [x] Call weather APIs for data
36 | - [x] Add React Redux, implement store
37 | - [x] Create first actions, reducers
38 | - [x] Implement weather service
39 | - [x] Add scroll view animations
40 | - [x] Add pull to refresh
41 | - [x] Implement basic settings UI prototype with navigation
42 | - [x] Create location and weather repositories backed by Realm
43 | - [ ] Create settings repositories backed by Realm
44 | - [ ] Call Australian postcode APIs when adding locations
45 | - [ ] Add support for Android devices
46 |
47 | Pocket Weather | React Weather
48 | -------------- | --------------
49 |
|
50 |
|
51 |
|
52 |
|
53 |
54 | # Running
55 |
56 | ## Clone & install
57 |
58 | + Clone this repo `git clone git@github.com:stage88/react-weather.git`
59 | + `cd react-weather`
60 | + run `npm install`
61 |
62 | ## API keys
63 | + Get your API key from http://openweathermap.org/
64 | + No key is required to use http://postcodeapi.com.au/
65 | + Create a new file `release/keys.js`:
66 | ```jsx
67 | module.exports = {
68 | weatherApiKey: 'YOUR_KEY_HERE'
69 | };
70 | ```
71 |
72 | ## iOS
73 |
74 | + Open `ReactWeather.xcodeproj` in `XCode`
75 | + Press `cmd+r` to build it
76 |
77 | ## Android
78 |
79 | + Run `android avd` and start an emulator
80 | + Run `react-native run-android`
81 |
82 | ## License
83 |
84 | Released under the [MIT License](http://opensource.org/licenses/MIT).
85 |
86 | # Feedback
87 |
88 | Feedback is always welcome. Feel free to contact me, I would love to know if you notice something that can be done better. Please be nice, this is my first React Native app.
89 |
--------------------------------------------------------------------------------
/js/components/settings/locations.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import { connect } from 'react-redux';
8 |
9 | import React, { Component } from 'react';
10 | import {
11 | StyleSheet,
12 | View,
13 | Text,
14 | StatusBar,
15 | ScrollView,
16 | LayoutAnimation
17 | } from 'react-native';
18 |
19 | import Icon from 'react-native-vector-icons/Ionicons';
20 | import Swipeout from '../../dependencies/swipeout';
21 |
22 | import defaultStyles from './styles';
23 | import { getAllLocations, deleteLocation } from '../../actions/location';
24 | import type { Location } from '../../models/view';
25 |
26 | import Section from './section';
27 | import NavigationButtonRow from './navigationbuttonrow';
28 | import AddLocation from './addlocation';
29 |
30 | const renderForecastImage = require('../forecastimage')
31 |
32 | type Props = {
33 | navigator: any;
34 | dispatch: any;
35 | locations: Array;
36 | count: number;
37 | };
38 |
39 | class Locations extends Component {
40 | props: Props;
41 |
42 | constructor(props: Props) {
43 | super(props);
44 |
45 | (this: any).close = this.close.bind(this);
46 | }
47 |
48 | componentWillMount() {
49 | this.props.dispatch(getAllLocations());
50 | }
51 |
52 | render() {
53 | var locations = this.props.locations.map((item) => {
54 | var current = item.observation ? item.observation.current + '\u00B0' : "-";
55 | var low = item.observation ? item.observation.low : "-";
56 | var high = item.observation ? item.observation.high : "-";
57 | var icon = item.observation ? renderForecastImage(item.observation.icon, 20, 20) : null;
58 |
59 | return (
60 | {
64 | this.props.dispatch(deleteLocation(item.openWeatherId))
65 | }}]}>
66 |
67 |
68 | { item.name }
69 | { current }
70 |
71 |
72 |
73 | { icon }
74 | { low }
75 | { high }
76 |
77 |
78 |
79 |
80 | );
81 | });
82 |
83 | return (
84 |
85 |
86 |
87 |
88 | { locations }
89 |
90 |
96 |
97 |
98 | );
99 | }
100 |
101 | close() {
102 | this.props.navigator.close();
103 | }
104 | }
105 |
106 | const styles = StyleSheet.create({
107 | container: {
108 | flex: 1,
109 | flexDirection: 'column',
110 | backgroundColor: '#f8f8f8'
111 | },
112 | locationRow: {
113 | flexDirection: 'row',
114 | justifyContent: 'center',
115 | alignItems: 'stretch',
116 | backgroundColor: '#fff',
117 | paddingLeft: 14,
118 | paddingRight: 14,
119 | height: 72,
120 | borderColor: '#C8C7CC',
121 | borderBottomWidth: 0.3
122 | },
123 | locationLeft: {
124 | flex: 1,
125 | justifyContent: 'center'
126 | },
127 | locationNameText: {
128 | fontSize: 16
129 | },
130 | locationCurrentText: {
131 | fontSize: 16,
132 | color: '#B0B5BF'
133 | },
134 | locationRight: {
135 | flex: 1,
136 | justifyContent: 'flex-end',
137 | alignItems: 'center',
138 | flexDirection: 'row'
139 | },
140 | locationTextLow: {
141 | textAlign: 'right',
142 | marginLeft: 14,
143 | width: 20,
144 | color: '#B0B5BF',
145 | fontSize: 16
146 | },
147 | locationTextHigh: {
148 | textAlign: 'right',
149 | marginLeft: 14,
150 | width: 20,
151 | fontSize: 16
152 | }
153 | });
154 |
155 | function select(store: any, props: Props) {
156 | return {
157 | isRefreshing: store.weather.isRefreshing,
158 | locations: store.location.data,
159 | count: store.location.data.length,
160 | ...props
161 | };
162 | }
163 |
164 | module.exports = connect(select)(Locations);
165 |
--------------------------------------------------------------------------------
/ios/ReactWeather.xcodeproj/xcshareddata/xcschemes/ReactWeather.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
75 |
77 |
83 |
84 |
85 |
86 |
87 |
88 |
94 |
96 |
102 |
103 |
104 |
105 |
107 |
108 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/api/forecast-daily.json:
--------------------------------------------------------------------------------
1 | {
2 | "city": {
3 | "id": 6619483,
4 | "name": "Kaleen",
5 | "coord": {
6 | "lon": 149.105164,
7 | "lat": -35.21814
8 | },
9 | "country": "AU",
10 | "population": 0
11 | },
12 | "cod": "200",
13 | "message": 0.0171,
14 | "cnt": 7,
15 | "list": [
16 | {
17 | "dt": 1463796000,
18 | "temp": {
19 | "day": 21.24,
20 | "min": 10.21,
21 | "max": 21.24,
22 | "night": 10.21,
23 | "eve": 16.36,
24 | "morn": 21.24
25 | },
26 | "pressure": 972.91,
27 | "humidity": 84,
28 | "weather": [
29 | {
30 | "id": 803,
31 | "main": "Clouds",
32 | "description": "broken clouds",
33 | "icon": "04d"
34 | }
35 | ],
36 | "speed": 1.37,
37 | "deg": 356,
38 | "clouds": 56
39 | },
40 | {
41 | "dt": 1463882400,
42 | "temp": {
43 | "day": 16.76,
44 | "min": 5.7,
45 | "max": 16.91,
46 | "night": 13.72,
47 | "eve": 12.92,
48 | "morn": 5.7
49 | },
50 | "pressure": 971.91,
51 | "humidity": 94,
52 | "weather": [
53 | {
54 | "id": 800,
55 | "main": "Clear",
56 | "description": "clear sky",
57 | "icon": "01d"
58 | }
59 | ],
60 | "speed": 2.97,
61 | "deg": 333,
62 | "clouds": 0
63 | },
64 | {
65 | "dt": 1463968800,
66 | "temp": {
67 | "day": 11.98,
68 | "min": 7.06,
69 | "max": 13.6,
70 | "night": 7.06,
71 | "eve": 8.78,
72 | "morn": 13.07
73 | },
74 | "pressure": 966.33,
75 | "humidity": 100,
76 | "weather": [
77 | {
78 | "id": 501,
79 | "main": "Rain",
80 | "description": "moderate rain",
81 | "icon": "10d"
82 | }
83 | ],
84 | "speed": 6.28,
85 | "deg": 275,
86 | "clouds": 80,
87 | "rain": 5.1
88 | },
89 | {
90 | "dt": 1464055200,
91 | "temp": {
92 | "day": 9.21,
93 | "min": 4.47,
94 | "max": 11.37,
95 | "night": 4.47,
96 | "eve": 11.37,
97 | "morn": 8.54
98 | },
99 | "pressure": 956.43,
100 | "humidity": 0,
101 | "weather": [
102 | {
103 | "id": 500,
104 | "main": "Rain",
105 | "description": "light rain",
106 | "icon": "10d"
107 | }
108 | ],
109 | "speed": 3.53,
110 | "deg": 210,
111 | "clouds": 0
112 | },
113 | {
114 | "dt": 1464141600,
115 | "temp": {
116 | "day": 9.08,
117 | "min": 1.5,
118 | "max": 12.77,
119 | "night": 3.98,
120 | "eve": 12.77,
121 | "morn": 1.5
122 | },
123 | "pressure": 959.12,
124 | "humidity": 0,
125 | "weather": [
126 | {
127 | "id": 800,
128 | "main": "Clear",
129 | "description": "clear sky",
130 | "icon": "01d"
131 | }
132 | ],
133 | "speed": 0.58,
134 | "deg": 147,
135 | "clouds": 13
136 | },
137 | {
138 | "dt": 1464228000,
139 | "temp": {
140 | "day": 10.09,
141 | "min": 2.43,
142 | "max": 13.52,
143 | "night": 4.7,
144 | "eve": 13.52,
145 | "morn": 2.43
146 | },
147 | "pressure": 958.57,
148 | "humidity": 0,
149 | "weather": [
150 | {
151 | "id": 500,
152 | "main": "Rain",
153 | "description": "light rain",
154 | "icon": "10d"
155 | }
156 | ],
157 | "speed": 0.92,
158 | "deg": 88,
159 | "clouds": 71
160 | },
161 | {
162 | "dt": 1464314400,
163 | "temp": {
164 | "day": 9.07,
165 | "min": 2.33,
166 | "max": 14.01,
167 | "night": 10.76,
168 | "eve": 14.01,
169 | "morn": 2.33
170 | },
171 | "pressure": 953.62,
172 | "humidity": 0,
173 | "weather": [
174 | {
175 | "id": 500,
176 | "main": "Rain",
177 | "description": "light rain",
178 | "icon": "10d"
179 | }
180 | ],
181 | "speed": 1.05,
182 | "deg": 37,
183 | "clouds": 70
184 | }
185 | ]
186 | }
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/js/services/weather.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import realm from '../realm';
8 | import { weatherApiKey, weatherApiUrl } from '../config';
9 | import type { WeatherModel, WeatherObservation, WeatherForecast } from '../models/view';
10 | import { isDebugData } from '../config';
11 |
12 | import dateFormat from 'dateformat';
13 |
14 | import TestDataService from '../services/testdata';
15 | const testdata = new TestDataService();
16 |
17 | class WeatherService {
18 | async getAllWeather(forceUpdate: bool) {
19 | if (isDebugData) {
20 | return testdata.getAll();
21 | }
22 |
23 | var data = [];
24 | let context = realm.current();
25 | try {
26 | let locations = context.objects('Location');
27 |
28 | for (var i = 0; i < locations.length; i++) {
29 | var location = locations[i];
30 |
31 | var result;
32 | if (forceUpdate === false && location.weather) {
33 | result = this.getWeatherFromContext(location);
34 | } else {
35 | result = await this.getWeatherFromApiAsync(location.openWeatherId);
36 | this.updateWeatherInContext(location, result, context)
37 | }
38 |
39 | data.push(result);
40 | }
41 | } finally {
42 | context.close();
43 | }
44 |
45 | return data;
46 | }
47 |
48 | getWeatherFromContext(location: any) {
49 | return {
50 | id: location.openWeatherId,
51 | freshness: location.weather.freshness,
52 | observation: {
53 | location: location.name,
54 | forecast: location.weather.observation.forecast,
55 | feelsLike: location.weather.observation.feelsLike,
56 | current: location.weather.observation.current,
57 | low: location.weather.observation.low,
58 | high: location.weather.observation.high,
59 | icon: location.weather.observation.icon
60 | },
61 | forecast: location.weather.forecast.map((item) => {
62 | return {
63 | day: item.day,
64 | forecast: item.forecast,
65 | low: item.low,
66 | high: item.high,
67 | icon: item.icon
68 | }
69 | })
70 | }
71 | }
72 |
73 | updateWeatherInContext(location: any, weather: any, context: any) {
74 | context.write(() => {
75 | location.weather = {
76 | freshness: weather.freshness,
77 | observation: {
78 | forecast: weather.observation.forecast,
79 | feelsLike: weather.observation.feelsLike.toString(),
80 | current: weather.observation.current.toString(),
81 | low: weather.observation.low.toString(),
82 | high: weather.observation.high.toString(),
83 | icon: weather.observation.icon
84 | }
85 | }
86 |
87 | while(location.weather.forecast.length > 0) {
88 | location.weather.forecast.pop();
89 | }
90 |
91 | weather.forecast.forEach((item) => {
92 | location.weather.forecast.push({
93 | day: item.day,
94 | forecast: item.forecast,
95 | low: item.low.toString(),
96 | high: item.high.toString(),
97 | icon: item.icon
98 | })
99 | });
100 | });
101 | }
102 |
103 | async getWeatherArrayFromApiAsync(locationIds: Array) {
104 | var data = [];
105 | for (var index = 0; index < locationIds.length; ++index) {
106 | var locationId = locationIds[index];
107 | var result = await this.getWeatherFromApiAsync(locationId);
108 |
109 | data.push(result);
110 | }
111 |
112 | return data;
113 | }
114 |
115 | async getWeatherFromApiAsync(locationId: string) {
116 | var observation = await this.getWeatherObservationFromApiAsync(locationId);
117 | var forecast = await this.getWeatherForecastFromApiAsync(locationId);
118 |
119 | if (observation && forecast) {
120 | observation.low = forecast[0].low;
121 | observation.high = forecast[0].high;
122 | observation.icon = forecast[0].icon;
123 | }
124 |
125 | var result = {
126 | id: locationId,
127 | freshness: new Date(),
128 | observation: observation,
129 | forecast: forecast
130 | };
131 |
132 | return result;
133 | }
134 |
135 | async getWeatherObservationFromApiAsync(locationId: string) {
136 | var url = `${weatherApiUrl}/weather?id=${locationId}&units=metric&appid=${weatherApiKey}`;
137 |
138 | try {
139 | let response = await fetch(url);
140 | const result = await response.json();
141 |
142 | return {
143 | location: result.name,
144 | forecast: result.weather[0].main,
145 | feelsLike: (result.main.temp_min | 0),
146 | current: (result.main.temp | 0),
147 | low: '',
148 | high: '',
149 | icon: ''
150 | };
151 | } catch(error) {
152 | global.log(error);
153 | }
154 | }
155 |
156 | async getWeatherForecastFromApiAsync(locationId: string) {
157 | var url = `${weatherApiUrl}/forecast/daily?id=${locationId}&cnt=7&units=metric&appid=${weatherApiKey}`;
158 |
159 | try {
160 | let response = await fetch(url);
161 | const result = await response.json();
162 |
163 | return result.list.map((item, index) => {
164 | return {
165 | day: this.getDayFromUtcDate(item.dt),
166 | forecast: item.weather[0].main,
167 | low: (item.temp.min | 0),
168 | high: (item.temp.max | 0),
169 | icon: item.weather[0].icon
170 | }
171 | });
172 | } catch(error) {
173 | global.log(error);
174 | }
175 | }
176 |
177 | getDayFromUtcDate(date: number): string {
178 | var value = new Date(date * 1000);
179 | var day = dateFormat(value, 'dddd');
180 |
181 | return day;
182 | }
183 | }
184 |
185 | module.exports = WeatherService;
186 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 |
3 | import com.android.build.OutputFile
4 |
5 | /**
6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
7 | * and bundleReleaseJsAndAssets).
8 | * These basically call `react-native bundle` with the correct arguments during the Android build
9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
10 | * bundle directly from the development server. Below you can see all the possible configurations
11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the
12 | * `apply from: "../../node_modules/react-native/react.gradle"` line.
13 | *
14 | * project.ext.react = [
15 | * // the name of the generated asset file containing your JS bundle
16 | * bundleAssetName: "index.android.bundle",
17 | *
18 | * // the entry file for bundle generation
19 | * entryFile: "index.android.js",
20 | *
21 | * // whether to bundle JS and assets in debug mode
22 | * bundleInDebug: false,
23 | *
24 | * // whether to bundle JS and assets in release mode
25 | * bundleInRelease: true,
26 | *
27 | * // whether to bundle JS and assets in another build variant (if configured).
28 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
29 | * // The configuration property can be in the following formats
30 | * // 'bundleIn${productFlavor}${buildType}'
31 | * // 'bundleIn${buildType}'
32 | * // bundleInFreeDebug: true,
33 | * // bundleInPaidRelease: true,
34 | * // bundleInBeta: true,
35 | *
36 | * // the root of your project, i.e. where "package.json" lives
37 | * root: "../../",
38 | *
39 | * // where to put the JS bundle asset in debug mode
40 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
41 | *
42 | * // where to put the JS bundle asset in release mode
43 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release",
44 | *
45 | * // where to put drawable resources / React Native assets, e.g. the ones you use via
46 | * // require('./image.png')), in debug mode
47 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
48 | *
49 | * // where to put drawable resources / React Native assets, e.g. the ones you use via
50 | * // require('./image.png')), in release mode
51 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
52 | *
53 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means
54 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
55 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle
56 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
57 | * // for example, you might want to remove it from here.
58 | * inputExcludes: ["android/**", "ios/**"]
59 | * ]
60 | */
61 |
62 | apply from: "../../node_modules/react-native/react.gradle"
63 |
64 | /**
65 | * Set this to true to create two separate APKs instead of one:
66 | * - An APK that only works on ARM devices
67 | * - An APK that only works on x86 devices
68 | * The advantage is the size of the APK is reduced by about 4MB.
69 | * Upload all the APKs to the Play Store and people will download
70 | * the correct one based on the CPU architecture of their device.
71 | */
72 | def enableSeparateBuildPerCPUArchitecture = false
73 |
74 | /**
75 | * Run Proguard to shrink the Java bytecode in release builds.
76 | */
77 | def enableProguardInReleaseBuilds = false
78 |
79 | android {
80 | compileSdkVersion 23
81 | buildToolsVersion "23.0.1"
82 |
83 | defaultConfig {
84 | applicationId "com.reactweather"
85 | minSdkVersion 16
86 | targetSdkVersion 22
87 | versionCode 1
88 | versionName "1.0"
89 | ndk {
90 | abiFilters "armeabi-v7a", "x86"
91 | }
92 | }
93 | splits {
94 | abi {
95 | reset()
96 | enable enableSeparateBuildPerCPUArchitecture
97 | universalApk false // If true, also generate a universal APK
98 | include "armeabi-v7a", "x86"
99 | }
100 | }
101 | buildTypes {
102 | release {
103 | minifyEnabled enableProguardInReleaseBuilds
104 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
105 | }
106 | }
107 | // applicationVariants are e.g. debug, release
108 | applicationVariants.all { variant ->
109 | variant.outputs.each { output ->
110 | // For each separate APK per architecture, set a unique version code as described here:
111 | // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
112 | def versionCodes = ["armeabi-v7a":1, "x86":2]
113 | def abi = output.getFilter(OutputFile.ABI)
114 | if (abi != null) { // null for the universal-debug, universal-release variants
115 | output.versionCodeOverride =
116 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
117 | }
118 | }
119 | }
120 | }
121 |
122 | dependencies {
123 | compile project(':realm')
124 | compile project(':react-native-vector-icons')
125 | compile fileTree(dir: "libs", include: ["*.jar"])
126 | compile "com.android.support:appcompat-v7:23.0.1"
127 | compile "com.facebook.react:react-native:+" // From node_modules
128 | }
129 |
130 | // Run this once to be able to run the application with BUCK
131 | // puts all compile dependencies into folder libs for BUCK to use
132 | task copyDownloadableDepsToLibs(type: Copy) {
133 | from configurations.compile
134 | into 'libs'
135 | }
136 |
--------------------------------------------------------------------------------
/api/postcode.js:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name":"West Chatswood",
4 | "postcode":1515,
5 | "state":{
6 | "name":"New South Wales",
7 | "abbreviation":"NSW"
8 | },
9 | "locality":"CHATSWOOD",
10 | "latitude":-33.820500000000003,
11 | "longitude":151.18389999999999
12 | },
13 | {
14 | "name":"Hornsby Westfield",
15 | "postcode":1635,
16 | "state":{
17 | "name":"New South Wales",
18 | "abbreviation":"NSW"
19 | },
20 | "locality":"GOSFORD",
21 | "latitude":-33.700000000000003,
22 | "longitude":151.09999999999999
23 | },
24 | {
25 | "name":"West Ryde",
26 | "postcode":1685,
27 | "state":{
28 | "name":"New South Wales",
29 | "abbreviation":"NSW"
30 | },
31 | "locality":"GOSFORD",
32 | "latitude":-33.816699999999997,
33 | "longitude":151.09999999999999
34 | },
35 | {
36 | "name":"Clovelly West",
37 | "postcode":2031,
38 | "state":{
39 | "name":"New South Wales",
40 | "abbreviation":"NSW"
41 | },
42 | "locality":"BONDI",
43 | "latitude":-33.911099999999998,
44 | "longitude":151.24160000000001
45 | },
46 | {
47 | "name":"Westgate",
48 | "postcode":2048,
49 | "state":{
50 | "name":"New South Wales",
51 | "abbreviation":"NSW"
52 | },
53 | "locality":"LEICHHARDT",
54 | "latitude":-33.893099999999997,
55 | "longitude":151.1662
56 | },
57 | {
58 | "name":"Lane Cove West",
59 | "postcode":2066,
60 | "state":{
61 | "name":"New South Wales",
62 | "abbreviation":"NSW"
63 | },
64 | "locality":"CHATSWOOD",
65 | "latitude":-33.816299999999998,
66 | "longitude":151.1618
67 | },
68 | {
69 | "name":"Chatswood West",
70 | "postcode":2067,
71 | "state":{
72 | "name":"New South Wales",
73 | "abbreviation":"NSW"
74 | },
75 | "locality":"CHATSWOOD",
76 | "latitude":-33.796500000000002,
77 | "longitude":151.17179999999999
78 | },
79 | {
80 | "name":"Lindfield West",
81 | "postcode":2070,
82 | "state":{
83 | "name":"New South Wales",
84 | "abbreviation":"NSW"
85 | },
86 | "locality":"GOSFORD",
87 | "latitude":-33.776600000000002,
88 | "longitude":151.16999999999999
89 | },
90 | {
91 | "name":"West Pymble",
92 | "postcode":2073,
93 | "state":{
94 | "name":"New South Wales",
95 | "abbreviation":"NSW"
96 | },
97 | "locality":"GOSFORD",
98 | "latitude":-33.7667,
99 | "longitude":151.13329999999999
100 | },
101 | {
102 | "name":"Belrose West",
103 | "postcode":2085,
104 | "state":{
105 | "name":"New South Wales",
106 | "abbreviation":"NSW"
107 | },
108 | "locality":"WARRINGAH",
109 | "latitude":-33.717300000000002,
110 | "longitude":151.232
111 | },
112 | {
113 | "name":"Denistone West",
114 | "postcode":2114,
115 | "state":{
116 | "name":"New South Wales",
117 | "abbreviation":"NSW"
118 | },
119 | "locality":"GOSFORD",
120 | "latitude":-33.807099999999998,
121 | "longitude":151.08410000000001
122 | },
123 | {
124 | "name":"West Ryde",
125 | "postcode":2114,
126 | "state":{
127 | "name":"New South Wales",
128 | "abbreviation":"NSW"
129 | },
130 | "locality":"GOSFORD",
131 | "latitude":-33.807099999999998,
132 | "longitude":151.08410000000001
133 | },
134 | {
135 | "name":"Westleigh",
136 | "postcode":2120,
137 | "state":{
138 | "name":"New South Wales",
139 | "abbreviation":"NSW"
140 | },
141 | "locality":"GOSFORD",
142 | "latitude":-33.726799999999997,
143 | "longitude":151.07579999999999
144 | },
145 | {
146 | "name":"West Pennant Hills",
147 | "postcode":2125,
148 | "state":{
149 | "name":"New South Wales",
150 | "abbreviation":"NSW"
151 | },
152 | "locality":"RICHMOND",
153 | "latitude":-33.748600000000003,
154 | "longitude":151.03380000000001
155 | },
156 | {
157 | "name":"Concord West",
158 | "postcode":2138,
159 | "state":{
160 | "name":"New South Wales",
161 | "abbreviation":"NSW"
162 | },
163 | "locality":"PARRAMATTA",
164 | "latitude":-33.840400000000002,
165 | "longitude":151.08959999999999
166 | },
167 | {
168 | "name":"Westmead",
169 | "postcode":2145,
170 | "state":{
171 | "name":"New South Wales",
172 | "abbreviation":"NSW"
173 | },
174 | "locality":"RICHMOND",
175 | "latitude":-33.816699999999997,
176 | "longitude":150.98330000000001
177 | },
178 | {
179 | "name":"Seven Hills West",
180 | "postcode":2147,
181 | "state":{
182 | "name":"New South Wales",
183 | "abbreviation":"NSW"
184 | },
185 | "locality":"RICHMOND",
186 | "latitude":-33.7684,
187 | "longitude":150.9365
188 | },
189 | {
190 | "name":"Blacktown Westpoint",
191 | "postcode":2148,
192 | "state":{
193 | "name":"New South Wales",
194 | "abbreviation":"NSW"
195 | },
196 | "locality":"RICHMOND",
197 | "latitude":-33.7896,
198 | "longitude":150.90000000000001
199 | },
200 | {
201 | "name":"Merrylands West",
202 | "postcode":2160,
203 | "state":{
204 | "name":"New South Wales",
205 | "abbreviation":"NSW"
206 | },
207 | "locality":"BANKSTOWN",
208 | "latitude":-33.836100000000002,
209 | "longitude":150.98320000000001
210 | },
211 | {
212 | "name":"Guildford West",
213 | "postcode":2161,
214 | "state":{
215 | "name":"New South Wales",
216 | "abbreviation":"NSW"
217 | },
218 | "locality":"BANKSTOWN",
219 | "latitude":-33.851100000000002,
220 | "longitude":150.97229999999999
221 | },
222 | {
223 | "name":"Smithfield West",
224 | "postcode":2164,
225 | "state":{
226 | "name":"New South Wales",
227 | "abbreviation":"NSW"
228 | },
229 | "locality":"BANKSTOWN",
230 | "latitude":-33.844799999999999,
231 | "longitude":150.9161
232 | },
233 | {
234 | "name":"Fairfield West",
235 | "postcode":2165,
236 | "state":{
237 | "name":"New South Wales",
238 | "abbreviation":"NSW"
239 | },
240 | "locality":"BANKSTOWN",
241 | "latitude":-33.870100000000001,
242 | "longitude":150.9453
243 | },
244 | {
245 | "name":"West Hoxton",
246 | "postcode":2171,
247 | "state":{
248 | "name":"New South Wales",
249 | "abbreviation":"NSW"
250 | },
251 | "locality":"CAMPBELLTOWN",
252 | "latitude":-33.916699999999999,
253 | "longitude":150.84999999999999
254 | },
255 | {
256 | "name":"Yagoona West",
257 | "postcode":2199,
258 | "state":{
259 | "name":"New South Wales",
260 | "abbreviation":"NSW"
261 | },
262 | "locality":"BANKSTOWN",
263 | "latitude":-33.902099999999997,
264 | "longitude":151.0223
265 | },
266 | {
267 | "name":"Kingsway West",
268 | "postcode":2208,
269 | "state":{
270 | "name":"New South Wales",
271 | "abbreviation":"NSW"
272 | },
273 | "locality":"ST GEORGE",
274 | "latitude":-33.938899999999997,
275 | "longitude":151.09970000000001
276 | }
277 | ]
278 |
--------------------------------------------------------------------------------
/js/components/settings/addlocation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | View,
11 | Text,
12 | TouchableHighlight,
13 | TouchableOpacity,
14 | TextInput,
15 | LayoutAnimation,
16 | ListView
17 | } from 'react-native';
18 |
19 | import { connect } from 'react-redux';
20 |
21 | import defaultStyles from './styles';
22 | import { searchPostcodes, clearPostcodes } from '../../actions/postcode';
23 | import { addLocation } from '../../actions/location';
24 | import type { Postcode } from '../../models/view';
25 |
26 | import Icon from 'react-native-vector-icons/Ionicons';
27 |
28 | type Props = {
29 | postcodes: Array;
30 | dispatch: any;
31 | navigator: any;
32 | };
33 |
34 | type State = {
35 | isSearchActive: bool;
36 | isSearchTextEntered: bool;
37 | postcodeDataSource: any;
38 | };
39 |
40 | class AddLocation extends Component {
41 | props: Props;
42 | state: State;
43 |
44 | constructor(props: Props) {
45 | super(props);
46 |
47 | let dataSource = new ListView.DataSource({
48 | rowHasChanged: (row1, row2) => row1 !== row2
49 | });
50 |
51 | this.state = {
52 | isSearchActive: false,
53 | isSearchTextEntered: false,
54 | postcodeDataSource: this.cloneWithData(dataSource, props.postcodes)
55 | };
56 |
57 | (this: any).onSearchBarPressed = this.onSearchBarPressed.bind(this);
58 | (this: any).onSearchBarCancelPressed = this.onSearchBarCancelPressed.bind(this);
59 | (this: any).onSearchTextChange = this.onSearchTextChange.bind(this);
60 | (this: any).renderListViewRowItem = this.renderListViewRowItem.bind(this);
61 | }
62 |
63 | componentWillReceiveProps(nextProps: Props) {
64 | if (this.props.postcodes !== nextProps.postcodes) {
65 | this.setState({
66 | postcodeDataSource: this.cloneWithData(
67 | this.state.postcodeDataSource,
68 | nextProps.postcodes
69 | )
70 | });
71 | }
72 | }
73 |
74 | render() {
75 | var iconStyle = {};
76 | var textInputStyle = {width: 56};
77 | var isTextInputEditable = false;
78 | var cancelTouchableStyle = {width: 0, height: 0};
79 | var postcodeListView;
80 |
81 | if (this.state.isSearchActive) {
82 | iconStyle = {marginLeft: 8};
83 | textInputStyle = {flex: 1};
84 | isTextInputEditable = true;
85 | cancelTouchableStyle = {marginLeft: 8};
86 |
87 | if (this.state.isSearchTextEntered) {
88 | postcodeListView = (
89 |
94 | );
95 | }
96 | }
97 |
98 | return (
99 |
100 |
101 |
105 |
106 |
107 |
114 |
115 |
116 |
119 | Cancel
120 |
121 |
122 |
123 | { postcodeListView }
124 |
125 | );
126 | }
127 |
128 | renderListViewRowItem(row) {
129 | if (row.isNoResults) {
130 | return (
131 |
132 |
133 |
134 |
135 | No locations found
136 |
137 |
138 |
139 |
140 | );
141 | }
142 |
143 | return (
144 | {
147 | this.onSearchBarCancelPressed();
148 |
149 | this.props.dispatch(addLocation(row.name, row.postcode.toString(), row.state));
150 | this.props.navigator.pop();
151 | }}>
152 |
153 |
154 |
155 |
156 | { row.name }
157 |
158 |
159 | { row.postcode }
160 |
161 |
162 |
163 | { row.state }
164 |
165 |
166 |
167 |
168 | );
169 | }
170 |
171 | onSearchTextChange(text: string) {
172 | if (text.length >= 3) {
173 | this.setState({
174 | isSearchTextEntered: true
175 | });
176 |
177 | this.props.dispatch(searchPostcodes(text));
178 | } else {
179 | this.setState({
180 | isSearchTextEntered: false
181 | });
182 | }
183 | }
184 |
185 | onSearchBarPressed() {
186 | this.props.dispatch(clearPostcodes());
187 |
188 | LayoutAnimation.spring();
189 | this.setState({
190 | isSearchActive: true
191 | });
192 |
193 | this.refs.searchTextInput.focus();
194 | }
195 |
196 | onSearchBarCancelPressed() {
197 | LayoutAnimation.spring();
198 | this.setState({
199 | isSearchActive: false
200 | });
201 |
202 | this.refs.searchTextInput.clear();
203 | }
204 |
205 | cloneWithData(dataSource: ListView.DataSource, data: any) {
206 | if (!data) {
207 | return dataSource.cloneWithRows([]);
208 | }
209 |
210 | if (data.length <= 0) {
211 | return dataSource.cloneWithRows([{isNoResults: true}]);
212 | }
213 |
214 | if (Array.isArray(data)) {
215 | return dataSource.cloneWithRows(data);
216 | }
217 |
218 | return dataSource.cloneWithRowsAndSections(data);
219 | }
220 | }
221 |
222 | const styles = StyleSheet.create({
223 | container: {
224 | flex: 1,
225 | flexDirection: 'column',
226 | backgroundColor: '#f8f8f8',
227 | marginTop: 64,
228 | },
229 | searchView: {
230 | backgroundColor: '#C9C9CE',
231 | height: 44,
232 | padding: 8,
233 | flexDirection: 'row'
234 | },
235 | searchInnerView: {
236 | flex: 1,
237 | backgroundColor: '#fff',
238 | borderRadius: 4,
239 | flexDirection: 'row',
240 | justifyContent: 'center',
241 | alignItems: 'center'
242 | },
243 | searchBarTouchable: {
244 | flex: 1
245 | },
246 | searchBarTextInput: {
247 | marginLeft: 8,
248 | fontSize: 14
249 | },
250 | searchBarCancelTouchable: {
251 | flexDirection: 'column',
252 | justifyContent: 'center'
253 | },
254 | searchBarCancelText: {
255 | color: '#0078FF',
256 | fontSize: 16
257 | }
258 | });
259 |
260 | function select(store: any, props: Props): Props {
261 | return {
262 | postcodes: store.postcode.data,
263 | ...props
264 | };
265 | }
266 |
267 | module.exports = connect(select)(AddLocation);
268 |
--------------------------------------------------------------------------------
/js/services/testdata.js:
--------------------------------------------------------------------------------
1 | /**
2 | * flow
3 | */
4 |
5 | 'use strict';
6 |
7 | class TestDataService {
8 | getAll() {
9 | return [
10 | {
11 | "id":"2172517",
12 | "observation":{
13 | "location":"Canberra",
14 | "forecast":"Clouds",
15 | "feelsLike":10,
16 | "current":17,
17 | "low":11,
18 | "high":17,
19 | "icon":"10d"
20 | },
21 | "forecast":[
22 | {
23 | "day":"Monday",
24 | "forecast":"Rain",
25 | "low":11,
26 | "high":17,
27 | "icon":"10d"
28 | },
29 | {
30 | "day":"Tuesday",
31 | "forecast":"Rain",
32 | "low":7,
33 | "high":8,
34 | "icon":"10d"
35 | },
36 | {
37 | "day":"Wednesday",
38 | "forecast":"Rain",
39 | "low":8,
40 | "high":10,
41 | "icon":"10d"
42 | },
43 | {
44 | "day":"Thursday",
45 | "forecast":"Rain",
46 | "low":8,
47 | "high":10,
48 | "icon":"10d"
49 | },
50 | {
51 | "day":"Friday",
52 | "forecast":"Rain",
53 | "low":5,
54 | "high":10,
55 | "icon":"10d"
56 | },
57 | {
58 | "day":"Saturday",
59 | "forecast":"Rain",
60 | "low":3,
61 | "high":10,
62 | "icon":"10d"
63 | },
64 | {
65 | "day":"Sunday",
66 | "forecast":"Rain",
67 | "low":2,
68 | "high":9,
69 | "icon":"10d"
70 | }
71 | ]
72 | },
73 | {
74 | "id":"2147714",
75 | "observation":{
76 | "location":"Sydney",
77 | "forecast":"Clear",
78 | "feelsLike":12,
79 | "current":13,
80 | "low":13,
81 | "high":14,
82 | "icon":"10n"
83 | },
84 | "forecast":[
85 | {
86 | "day":"Monday",
87 | "forecast":"Rain",
88 | "low":13,
89 | "high":14,
90 | "icon":"10n"
91 | },
92 | {
93 | "day":"Tuesday",
94 | "forecast":"Clear",
95 | "low":14,
96 | "high":17,
97 | "icon":"01d"
98 | },
99 | {
100 | "day":"Wednesday",
101 | "forecast":"Clear",
102 | "low":15,
103 | "high":17,
104 | "icon":"01d"
105 | },
106 | {
107 | "day":"Thursday",
108 | "forecast":"Rain",
109 | "low":14,
110 | "high":18,
111 | "icon":"10d"
112 | },
113 | {
114 | "day":"Friday",
115 | "forecast":"Rain",
116 | "low":14,
117 | "high":16,
118 | "icon":"10d"
119 | },
120 | {
121 | "day":"Saturday",
122 | "forecast":"Rain",
123 | "low":13,
124 | "high":15,
125 | "icon":"10d"
126 | },
127 | {
128 | "day":"Sunday",
129 | "forecast":"Rain",
130 | "low":13,
131 | "high":16,
132 | "icon":"10d"
133 | }
134 | ]
135 | },
136 | {
137 | "id":"2174003",
138 | "observation":{
139 | "location":"Brisbane",
140 | "forecast":"Clear",
141 | "feelsLike":12,
142 | "current":14,
143 | "low":14,
144 | "high":15,
145 | "icon":"01n"
146 | },
147 | "forecast":[
148 | {
149 | "day":"Monday",
150 | "forecast":"Clear",
151 | "low":14,
152 | "high":15,
153 | "icon":"01n"
154 | },
155 | {
156 | "day":"Tuesday",
157 | "forecast":"Clear",
158 | "low":11,
159 | "high":17,
160 | "icon":"01d"
161 | },
162 | {
163 | "day":"Wednesday",
164 | "forecast":"Clear",
165 | "low":10,
166 | "high":19,
167 | "icon":"01d"
168 | },
169 | {
170 | "day":"Thursday",
171 | "forecast":"Clear",
172 | "low":13,
173 | "high":23,
174 | "icon":"01d"
175 | },
176 | {
177 | "day":"Friday",
178 | "forecast":"Clear",
179 | "low":17,
180 | "high":23,
181 | "icon":"01d"
182 | },
183 | {
184 | "day":"Saturday",
185 | "forecast":"Clear",
186 | "low":15,
187 | "high":18,
188 | "icon":"01d"
189 | },
190 | {
191 | "day":"Sunday",
192 | "forecast":"Rain",
193 | "low":16,
194 | "high":20,
195 | "icon":"10d"
196 | }
197 | ]
198 | },
199 | {
200 | "id":"2158177",
201 | "observation":{
202 | "location":"Melbourne",
203 | "forecast":"Clouds",
204 | "feelsLike":10,
205 | "current":10,
206 | "low":9,
207 | "high":10,
208 | "icon":"10d"
209 | },
210 | "forecast":[
211 | {
212 | "day":"Monday",
213 | "forecast":"Rain",
214 | "low":9,
215 | "high":10,
216 | "icon":"10d"
217 | },
218 | {
219 | "day":"Tuesday",
220 | "forecast":"Rain",
221 | "low":10,
222 | "high":11,
223 | "icon":"10d"
224 | },
225 | {
226 | "day":"Wednesday",
227 | "forecast":"Rain",
228 | "low":7,
229 | "high":12,
230 | "icon":"10d"
231 | },
232 | {
233 | "day":"Thursday",
234 | "forecast":"Rain",
235 | "low":11,
236 | "high":13,
237 | "icon":"10d"
238 | },
239 | {
240 | "day":"Friday",
241 | "forecast":"Rain",
242 | "low":10,
243 | "high":12,
244 | "icon":"10d"
245 | },
246 | {
247 | "day":"Saturday",
248 | "forecast":"Rain",
249 | "low":9,
250 | "high":12,
251 | "icon":"10d"
252 | },
253 | {
254 | "day":"Sunday",
255 | "forecast":"Rain",
256 | "low":9,
257 | "high":14,
258 | "icon":"10d"
259 | }
260 | ]
261 | },
262 | {
263 | "id":"2063523",
264 | "observation":{
265 | "location":"Perth",
266 | "forecast":"Rain",
267 | "feelsLike":15,
268 | "current":15,
269 | "low":12,
270 | "high":16,
271 | "icon":"10d"
272 | },
273 | "forecast":[
274 | {
275 | "day":"Monday",
276 | "forecast":"Rain",
277 | "low":12,
278 | "high":16,
279 | "icon":"10d"
280 | },
281 | {
282 | "day":"Tuesday",
283 | "forecast":"Rain",
284 | "low":10,
285 | "high":11,
286 | "icon":"10d"
287 | },
288 | {
289 | "day":"Wednesday",
290 | "forecast":"Clouds",
291 | "low":10,
292 | "high":15,
293 | "icon":"03d"
294 | },
295 | {
296 | "day":"Thursday",
297 | "forecast":"Clear",
298 | "low":7,
299 | "high":16,
300 | "icon":"01d"
301 | },
302 | {
303 | "day":"Friday",
304 | "forecast":"Clear",
305 | "low":7,
306 | "high":18,
307 | "icon":"01d"
308 | },
309 | {
310 | "day":"Saturday",
311 | "forecast":"Rain",
312 | "low":10,
313 | "high":18,
314 | "icon":"10d"
315 | },
316 | {
317 | "day":"Sunday",
318 | "forecast":"Rain",
319 | "low":14,
320 | "high":18,
321 | "icon":"10d"
322 | }
323 | ]
324 | },
325 | {
326 | "id":"2073124",
327 | "observation":{
328 | "location":"Darwin",
329 | "forecast":"Clouds",
330 | "feelsLike":30,
331 | "current":30,
332 | "low":29,
333 | "high":30,
334 | "icon":"01d"
335 | },
336 | "forecast":[
337 | {
338 | "day":"Monday",
339 | "forecast":"Clear",
340 | "low":29,
341 | "high":30,
342 | "icon":"01d"
343 | },
344 | {
345 | "day":"Tuesday",
346 | "forecast":"Clear",
347 | "low":28,
348 | "high":29,
349 | "icon":"01d"
350 | },
351 | {
352 | "day":"Wednesday",
353 | "forecast":"Clear",
354 | "low":27,
355 | "high":29,
356 | "icon":"01d"
357 | },
358 | {
359 | "day":"Thursday",
360 | "forecast":"Clear",
361 | "low":27,
362 | "high":32,
363 | "icon":"01d"
364 | },
365 | {
366 | "day":"Friday",
367 | "forecast":"Rain",
368 | "low":27,
369 | "high":33,
370 | "icon":"10d"
371 | },
372 | {
373 | "day":"Saturday",
374 | "forecast":"Rain",
375 | "low":27,
376 | "high":32,
377 | "icon":"10d"
378 | },
379 | {
380 | "day":"Sunday",
381 | "forecast":"Clear",
382 | "low":27,
383 | "high":31,
384 | "icon":"01d"
385 | }
386 | ]
387 | }
388 | ];
389 | }
390 | }
391 |
392 | module.exports = TestDataService;
393 |
--------------------------------------------------------------------------------
/js/dependencies/swipeout/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | var tweenState = require('react-tween-state')
3 | import {PanResponder, TouchableHighlight, StyleSheet, Text, View} from 'react-native'
4 | var styles = require('./styles.js')
5 |
6 | var SwipeoutBtn = React.createClass({
7 | getDefaultProps: function() {
8 | return {
9 | backgroundColor: null,
10 | color: null,
11 | component: null,
12 | underlayColor: null,
13 | height: 0,
14 | key: null,
15 | onPress: null,
16 | text: 'Click me',
17 | type: '',
18 | width: 0,
19 | }
20 | }
21 | , render: function() {
22 | var btn = this.props
23 |
24 | var styleSwipeoutBtn = [styles.swipeoutBtn]
25 |
26 | // apply "type" styles (delete || primary || secondary)
27 | if (btn.type === 'delete') styleSwipeoutBtn.push(styles.colorDelete)
28 | else if (btn.type === 'primary') styleSwipeoutBtn.push(styles.colorPrimary)
29 | else if (btn.type === 'secondary') styleSwipeoutBtn.push(styles.colorSecondary)
30 |
31 | // apply background color
32 | if (btn.backgroundColor) styleSwipeoutBtn.push([{ backgroundColor: btn.backgroundColor }])
33 |
34 | styleSwipeoutBtn.push([{
35 | height: btn.height,
36 | width: btn.width,
37 | }])
38 |
39 | var styleSwipeoutBtnComponent = []
40 |
41 | // set button dimensions
42 | styleSwipeoutBtnComponent.push([{
43 | height: btn.height,
44 | width: btn.width,
45 | }])
46 |
47 | var styleSwipeoutBtnText = [styles.swipeoutBtnText]
48 |
49 | // apply text color
50 | if (btn.color) styleSwipeoutBtnText.push([{ color: btn.color }])
51 |
52 | return (
53 |
58 |
59 | {btn.component ?
60 | {btn.component}
61 | : {btn.text}
62 | }
63 |
64 |
65 | )
66 | }
67 | })
68 |
69 | var Swipeout = React.createClass({
70 | mixins: [tweenState.Mixin]
71 | , getDefaultProps: function() {
72 | return {
73 | onOpen: function(sectionID, rowID) {console.log('onOpen: '+sectionID+" "+rowID)},
74 | rowID: -1,
75 | sectionID: -1,
76 | }
77 | }
78 | , getInitialState: function() {
79 | return {
80 | autoClose: this.props.autoClose || false,
81 | btnWidth: 0,
82 | btnsLeftWidth: 0,
83 | btnsRightWidth: 0,
84 | contentHeight: 0,
85 | contentPos: 0,
86 | contentWidth: 0,
87 | openedRight: false,
88 | swiping: false,
89 | tweenDuration: 160,
90 | timeStart: null,
91 | }
92 | }
93 | , componentWillMount: function() {
94 | this._panResponder = PanResponder.create({
95 | onStartShouldSetPanResponder: (event, gestureState) => true,
96 | onMoveShouldSetPanResponder: (event, gestureState) => !(gestureState.dx === 0 || gestureState.dy === 0),
97 | onPanResponderGrant: this._handlePanResponderGrant,
98 | onPanResponderMove: this._handlePanResponderMove,
99 | onPanResponderRelease: this._handlePanResponderEnd,
100 | onPanResponderTerminate: this._handlePanResponderEnd,
101 | onShouldBlockNativeResponder: (event, gestureState) => true,
102 | });
103 | }
104 | , componentWillReceiveProps: function(nextProps) {
105 | if (nextProps.close) this._close()
106 | }
107 | , _handlePanResponderGrant: function(e: Object, gestureState: Object) {
108 | this.props.onOpen(this.props.sectionID, this.props.rowID)
109 | this.refs.swipeoutContent.measure((ox, oy, width, height) => {
110 | this.setState({
111 | btnWidth: (width/5),
112 | btnsLeftWidth: this.props.left ? (width/5)*this.props.left.length : 0,
113 | btnsRightWidth: this.props.right ? (width/5)*this.props.right.length : 0,
114 | swiping: true,
115 | timeStart: (new Date()).getTime(),
116 | })
117 | })
118 | }
119 | , _handlePanResponderMove: function(e: Object, gestureState: Object) {
120 | var posX = gestureState.dx
121 | var posY = gestureState.dy
122 | var leftWidth = this.state.btnsLeftWidth
123 | var rightWidth = this.state.btnsRightWidth
124 | if (this.state.openedRight) var posX = gestureState.dx - rightWidth
125 | else if (this.state.openedLeft) var posX = gestureState.dx + leftWidth
126 |
127 | // prevent scroll if moveX is true
128 | var moveX = Math.abs(posX) > Math.abs(posY)
129 | if (this.props.scroll) {
130 | if (moveX) this.props.scroll(false)
131 | else this.props.scroll(true)
132 | }
133 | if (this.state.swiping) {
134 | // move content to reveal swipeout
135 | if (posX < 0 && this.props.right) this.setState({ contentPos: Math.min(posX, 0) })
136 | else if (posX > 0 && this.props.left) this.setState({ contentPos: Math.max(posX, 0) })
137 | }
138 | }
139 | , _handlePanResponderEnd: function(e: Object, gestureState: Object) {
140 | var posX = gestureState.dx
141 | var contentPos = this.state.contentPos
142 | var contentWidth = this.state.contentWidth
143 | var btnsLeftWidth = this.state.btnsLeftWidth
144 | var btnsRightWidth = this.state.btnsRightWidth
145 |
146 | // minimum threshold to open swipeout
147 | var openX = contentWidth*0.33
148 |
149 | // should open swipeout
150 | var openLeft = posX > openX || posX > btnsLeftWidth/2
151 | var openRight = posX < -openX || posX < -btnsRightWidth/2
152 |
153 | // account for open swipeouts
154 | if (this.state.openedRight) var openRight = posX-openX < -openX
155 | if (this.state.openedLeft) var openLeft = posX+openX > openX
156 |
157 | // reveal swipeout on quick swipe
158 | var timeDiff = (new Date()).getTime() - this.state.timeStart < 200
159 | if (timeDiff) {
160 | var openRight = posX < -openX/10 && !this.state.openedLeft
161 | var openLeft = posX > openX/10 && !this.state.openedRight
162 | }
163 |
164 | if (this.state.swiping) {
165 | if (openRight && contentPos < 0 && posX < 0) {
166 | // open swipeout right
167 | this._tweenContent('contentPos', -btnsRightWidth)
168 | this.setState({ contentPos: -btnsRightWidth, openedLeft: false, openedRight: true })
169 | } else if (openLeft && contentPos > 0 && posX > 0) {
170 | // open swipeout left
171 | this._tweenContent('contentPos', btnsLeftWidth)
172 | this.setState({ contentPos: btnsLeftWidth, openedLeft: true, openedRight: false })
173 | }
174 | else {
175 | // close swipeout
176 | this._tweenContent('contentPos', 0)
177 | this.setState({ contentPos: 0, openedLeft: false, openedRight: false })
178 | }
179 | }
180 |
181 | // Allow scroll
182 | if (this.props.scroll) this.props.scroll(true)
183 | }
184 | , _tweenContent: function(state, endValue) {
185 | this.tweenState(state, {
186 | easing: tweenState.easingTypes.easeInOutQuad,
187 | duration: endValue === 0 ? this.state.tweenDuration*1.5 : this.state.tweenDuration,
188 | endValue: endValue
189 | })
190 | }
191 | , _rubberBandEasing: function(value, limit) {
192 | if (value < 0 && value < limit) return limit - Math.pow(limit - value, 0.85);
193 | else if (value > 0 && value > limit) return limit + Math.pow(value - limit, 0.85);
194 | return value;
195 | }
196 |
197 | // close swipeout on button press
198 | , _autoClose: function(btn) {
199 | var onPress = btn.onPress
200 | if (onPress) onPress()
201 | if (this.state.autoClose) this._close()
202 | }
203 | , _close: function() {
204 | this._tweenContent('contentPos', 0)
205 | this.setState({
206 | openedRight: false,
207 | openedLeft: false,
208 | })
209 | }
210 | , render: function() {
211 | var contentWidth = this.state.contentWidth
212 | var posX = this.getTweeningValue('contentPos')
213 |
214 | var styleSwipeout = [styles.swipeout]
215 | if (this.props.backgroundColor) {
216 | styleSwipeout.push([{ backgroundColor: this.props.backgroundColor }])
217 | }
218 |
219 | var limit = -this.state.btnsRightWidth
220 | if (posX > 0) var limit = this.state.btnsLeftWidth
221 |
222 | var styleLeftPos = StyleSheet.create({
223 | left: {
224 | left: 0,
225 | overflow: 'hidden',
226 | width: Math.min(limit*(posX/limit), limit),
227 | }
228 | })
229 | var styleRightPos = StyleSheet.create({
230 | right: {
231 | left: Math.abs(contentWidth + Math.max(limit, posX)),
232 | right: 0,
233 | }
234 | })
235 | var styleContentPos = StyleSheet.create({
236 | content: {
237 | left: this._rubberBandEasing(posX, limit),
238 | }
239 | })
240 |
241 | var styleContent = [styles.swipeoutContent]
242 | styleContent.push(styleContentPos.content)
243 |
244 | var styleRight = [styles.swipeoutBtns]
245 | styleRight.push(styleRightPos.right)
246 |
247 | var styleLeft = [styles.swipeoutBtns]
248 | styleLeft.push(styleLeftPos.left)
249 |
250 | var isRightVisible = posX < 0;
251 | var isLeftVisible = posX > 0;
252 |
253 | return (
254 |
255 |
260 | {this.props.children}
261 |
262 | { this._renderButtons(this.props.right, isRightVisible, styleRight) }
263 | { this._renderButtons(this.props.left, isLeftVisible, styleLeft) }
264 |
265 | )},
266 |
267 | _onLayout: function(event) {
268 | var { width, height } = event.nativeEvent.layout;
269 | this.setState({
270 | contentWidth: width,
271 | contentHeight: height
272 | });
273 | },
274 |
275 | _renderButtons: function(buttons, isVisible, style) {
276 | if (buttons && isVisible) {
277 | return(
278 | { buttons.map(this._renderButton) }
279 | );
280 | } else {
281 | return (
282 |
283 | );
284 | }
285 | },
286 |
287 | _renderButton: function(btn, i) {
288 | return (
289 | this._autoClose(btn)}
296 | text={btn.text}
297 | type={btn.type}
298 | underlayColor={btn.underlayColor}
299 | width={this.state.btnWidth}/>
300 | )
301 | }
302 | })
303 |
304 | module.exports = Swipeout
305 |
--------------------------------------------------------------------------------
/js/components/header.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | */
4 |
5 | 'use strict';
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | StyleSheet,
10 | Text,
11 | View,
12 | Image,
13 | Animated,
14 | Dimensions,
15 | RefreshControl
16 | } from 'react-native';
17 |
18 | import { connect } from 'react-redux';
19 |
20 | import ParallaxScrollView from '../dependencies/parallaxview';
21 |
22 | import { getAllWeather, setWeatherRefreshing } from '../actions';
23 | import type { WeatherObservation, WeatherModel } from '../models/view';
24 |
25 | const renderForecastImage = require('./forecastimage')
26 |
27 | import dateFormat from 'dateformat';
28 | const today = dateFormat(new Date(), 'ddd d mmmm');
29 |
30 | const SCREEN_WIDTH = Dimensions.get('window').width;
31 |
32 | type Props = {
33 | isLoading: bool;
34 | isRefreshing: bool;
35 | weather: Array;
36 | count: number;
37 | totalWidth: number;
38 | distanceToMiddle: number;
39 | children: ?any;
40 | offset: Animated.Value;
41 | current: Animated.Value;
42 | dispatch: any;
43 | };
44 |
45 | type State = {
46 | isRefreshing: bool;
47 | shift: Animated.Value;
48 | };
49 |
50 | const HEADER_HEIGHT = 290;
51 | const TITLE_HEIGHT = 55;
52 |
53 | class Header extends Component {
54 | props: Props;
55 | state: State;
56 |
57 | constructor(props: Props) {
58 | super(props);
59 |
60 | this.state = {
61 | isRefreshing: false,
62 | shift: new Animated.Value(0)
63 | };
64 |
65 | (this: any).onScroll = this.onScroll.bind(this);
66 | (this: any).renderTitle = this.renderTitle.bind(this);
67 | (this: any).renderHeader = this.renderHeader.bind(this);
68 | (this: any).renderRefreshControl = this.renderRefreshControl.bind(this);
69 | (this: any).onRefresh = this.onRefresh.bind(this);
70 | }
71 |
72 | componentWillReceiveProps(next: Props) {
73 | if (this.props.isRefreshing !== next.isRefreshing) {
74 | this.setState({
75 | isRefreshing: next.isRefreshing,
76 | });
77 | }
78 | }
79 |
80 | render() {
81 | const translateY = this.state.shift.interpolate({
82 | inputRange: [-HEADER_HEIGHT, 0, HEADER_HEIGHT / 2, HEADER_HEIGHT],
83 | outputRange: [-30, 0, 25, 30],
84 | extrapolate: 'clamp',
85 | });
86 |
87 | const transform = [{translateY}];
88 |
89 | return (
90 |
101 |
102 | { this.props.children }
103 |
104 |
105 | );
106 | }
107 |
108 | onScroll(e) {
109 | this.state.shift.setValue(e.nativeEvent.contentOffset.y);
110 | }
111 |
112 | renderBackground() {
113 | return (
114 |
115 | );
116 | }
117 |
118 | renderTitle() {
119 | var items = this.props.weather.map((item, index) => {
120 | return this.renderTitleItem(item.observation, index);
121 | });
122 |
123 | return (
124 |
125 | { items }
126 |
127 | );
128 | }
129 |
130 | renderTitleItem(observation: WeatherObservation, index: number) {
131 | var { count, totalWidth, distanceToMiddle } = this.props;
132 | var middle = index * SCREEN_WIDTH;
133 |
134 | var leftOffset = (middle - distanceToMiddle);
135 | var rightOffset = (middle + distanceToMiddle);
136 |
137 | const opacity = this.props.offset.interpolate({
138 | inputRange: [leftOffset, middle, rightOffset],
139 | outputRange: [0, 1, 0],
140 | extrapolate: 'clamp',
141 | });
142 |
143 | const translateX = this.props.offset.interpolate({
144 | inputRange: [leftOffset - 1, leftOffset, middle, rightOffset, rightOffset + 1],
145 | outputRange: [(SCREEN_WIDTH * 2), (distanceToMiddle / 3), 0, -(distanceToMiddle / 3), -(SCREEN_WIDTH * 2)],
146 | extrapolate: 'clamp',
147 | });
148 |
149 | const transforms = { opacity, transform: [{translateX}] };
150 |
151 | return (
152 |
153 |
154 |
155 | { observation.location }
156 |
157 | { today }
158 |
159 |
160 | );
161 | }
162 |
163 | renderHeader() {
164 | var items = this.props.weather.map((item, index) => {
165 | return this.renderHeaderItem(item.observation, index);
166 | });
167 |
168 | return (
169 |
170 | { items }
171 |
172 | );
173 | }
174 |
175 | renderHeaderItem(observation: WeatherObservation, index: number) {
176 | var { count, totalWidth, distanceToMiddle } = this.props;
177 | var middle = index * SCREEN_WIDTH;
178 |
179 | var leftOffset = (middle - distanceToMiddle);
180 | var rightOffset = (middle + distanceToMiddle);
181 |
182 | // global.log({
183 | // leftOffset: leftOffset,
184 | // middle: middle,
185 | // rightOffset: rightOffset,
186 | // count: count,
187 | // });
188 |
189 | const opacity = this.props.offset.interpolate({
190 | inputRange: [leftOffset, middle, rightOffset],
191 | outputRange: [0, 1, 0],
192 | extrapolate: 'clamp',
193 | });
194 |
195 | const translateX = this.props.offset.interpolate({
196 | inputRange: [leftOffset - 1, leftOffset, middle, rightOffset, rightOffset + 1],
197 | outputRange: [(SCREEN_WIDTH * 2), (distanceToMiddle / 3), 0, -(distanceToMiddle / 3), -(SCREEN_WIDTH * 2)],
198 | extrapolate: 'clamp',
199 | });
200 |
201 | const transforms = { opacity, transform: [{translateX}] };
202 |
203 | return (
204 |
205 |
206 |
207 | { observation.location }
208 | { observation.forecast }
209 |
210 |
211 |
212 | { renderForecastImage(observation.icon, 100, 100) }
213 |
214 | { observation.current + '\u00B0'}
215 | Feels like { observation.feelsLike }
216 |
217 |
218 |
219 |
220 |
221 | Today
222 |
223 | { today }
224 |
225 |
226 | { observation.low }
227 |
228 | { observation.high }
229 |
230 |
231 |
232 |
233 |
234 | );
235 | }
236 |
237 | onRefresh() {
238 | this.props.dispatch(setWeatherRefreshing());
239 | this.props.dispatch(getAllWeather());
240 | }
241 |
242 | renderRefreshControl() {
243 | return (
244 |
253 | );
254 | }
255 | }
256 |
257 | const styles = StyleSheet.create({
258 | headerViewAnimated: {
259 | width: SCREEN_WIDTH,
260 | position: 'absolute'
261 | },
262 | titleViewAnimated: {
263 | width: SCREEN_WIDTH,
264 | position: 'absolute'
265 | },
266 | headerView: {
267 | marginRight: 5,
268 | marginLeft: 5
269 | },
270 | location: {
271 | fontSize: 20,
272 | textAlign: 'center',
273 | paddingTop: 35,
274 | color: '#fff'
275 | },
276 | forecast: {
277 | fontSize: 14,
278 | textAlign: 'center',
279 | paddingTop: 3,
280 | color: '#fff'
281 | },
282 | stickyHeaderView: {
283 | paddingTop: 24,
284 | paddingLeft: 12,
285 | flexDirection: 'row'
286 | },
287 | stickyHeaderLocation: {
288 | color: '#fff',
289 | fontWeight: 'bold',
290 | fontSize: 16,
291 | marginRight: 6
292 | },
293 | stickyHeaderToday: {
294 | color: '#fff',
295 | fontSize: 16
296 | },
297 | centerView: {
298 | flexDirection: 'row',
299 | alignItems: 'center',
300 | justifyContent: 'center',
301 | paddingTop: 10
302 | },
303 | centerImageView: {
304 | paddingRight: 20
305 | },
306 | currentTemp: {
307 | color: '#fff',
308 | fontSize: 64,
309 | fontWeight: '200'
310 | },
311 | feelsLike: {
312 | color: '#fff',
313 | fontSize: 12,
314 | fontWeight: '500'
315 | },
316 | bottomView: {
317 | flexDirection: 'row',
318 | justifyContent: 'center',
319 | paddingLeft: 12,
320 | paddingRight: 12,
321 | marginTop: 40
322 | },
323 | bottomViewLeft: {
324 | flex: 1,
325 | flexDirection: 'row'
326 | },
327 | bottomViewToday: {
328 | color: '#fff',
329 | fontWeight: 'bold',
330 | marginRight: 6,
331 | fontSize: 16
332 | },
333 | bottomViewTodayDate: {
334 | color: '#fff',
335 | fontSize: 16
336 | },
337 | bottomViewRight: {
338 | flex: 1,
339 | flexDirection: 'row',
340 | justifyContent: 'flex-end'
341 | },
342 | low: {
343 | color: '#fff',
344 | marginRight: 12,
345 | fontSize: 18,
346 | fontWeight: '300',
347 | width: 22,
348 | textAlign: 'right',
349 | },
350 | high: {
351 | color: '#fff',
352 | fontWeight: '600',
353 | fontSize: 18,
354 | width: 24,
355 | textAlign: 'right',
356 | },
357 | childrenView: {
358 | top: -30
359 | }
360 | });
361 |
362 | function select(store: any, props: Props): Props {
363 | return {
364 | isLoading: store.weather.isLoading,
365 | isRefreshing: store.weather.isRefreshing,
366 | weather: store.weather.data,
367 | count: store.weather.data.length,
368 | totalWidth: SCREEN_WIDTH * store.weather.data.length,
369 | distanceToMiddle: SCREEN_WIDTH / 2,
370 | ...props
371 | };
372 | }
373 |
374 | module.exports = connect(select)(Header);
375 |
--------------------------------------------------------------------------------
/js/dependencies/parallaxview/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | const {
3 | Animated,
4 | Dimensions,
5 | ScrollView,
6 | View
7 | } = require('react-native');
8 |
9 | const styles = require('./styles');
10 |
11 | const { bool, func, number, string } = React.PropTypes;
12 |
13 | const window = Dimensions.get('window');
14 |
15 | const SCROLLVIEW_REF = 'ScrollView';
16 |
17 | const pivotPoint = (a, b) => (a - b);
18 |
19 | const renderEmpty = () => ;
20 |
21 | // Override `toJSON` of interpolated value because of
22 | // an error when serializing style on view inside inspector.
23 | // See: https://github.com/jaysoo/react-native-parallax-scroll-view/issues/23
24 | const interpolate = (value, opts) => {
25 | const x = value.interpolate(opts);
26 | x.toJSON = () => x.__getValue();
27 | return x;
28 | };
29 |
30 | // Properties accepted by `ParallaxScrollView`.
31 | const IPropTypes = {
32 | backgroundColor: string,
33 | backgroundScrollSpeed: number,
34 | fadeOutForeground: bool,
35 | fadeOutBackground: bool,
36 | contentBackgroundColor: string,
37 | onChangeHeaderVisibility: func,
38 | parallaxHeaderHeight: number.isRequired,
39 | renderBackground: func,
40 | renderFixedHeader: func,
41 | renderForeground: func,
42 | renderScrollComponent: func,
43 | renderStickyHeader: func,
44 | stickyHeaderHeight: number
45 | };
46 |
47 | class ParallaxScrollView extends Component {
48 | constructor(props) {
49 | super(props);
50 | if (props.renderStickyHeader && !props.stickyHeaderHeight) {
51 | console.warn('Property `stickyHeaderHeight` must be set if `renderStickyHeader` is used.');
52 | }
53 | if (props.renderParallaxHeader !== renderEmpty && !props.renderForeground) {
54 | console.warn('Property `renderParallaxHeader` is deprecated. Use `renderForeground` instead.');
55 | }
56 | this.state = {
57 | scrollY: new Animated.Value(0),
58 | viewHeight: window.height,
59 | viewWidth: window.width
60 | };
61 | this._footerComponent = { setNativeProps() {} }; // Initial stub
62 | this._footerHeight = 0;
63 | }
64 |
65 | render() {
66 | const {
67 | backgroundColor,
68 | backgroundScrollSpeed,
69 | children,
70 | contentBackgroundColor,
71 | fadeOutForeground,
72 | fadeOutBackground,
73 | parallaxHeaderHeight,
74 | renderBackground,
75 | renderFixedHeader,
76 | renderForeground,
77 | renderParallaxHeader,
78 | renderScrollComponent,
79 | renderStickyHeader,
80 | stickyHeaderHeight,
81 | style,
82 | ...scrollViewProps
83 | } = this.props;
84 |
85 | const background = this._renderBackground({ fadeOutBackground, backgroundScrollSpeed, backgroundColor, parallaxHeaderHeight, stickyHeaderHeight, renderBackground });
86 | const foreground = this._renderForeground({ fadeOutForeground, parallaxHeaderHeight, stickyHeaderHeight, renderForeground: renderForeground || renderParallaxHeader });
87 | const bodyComponent = this._wrapChildren(children, { contentBackgroundColor, stickyHeaderHeight });
88 | const footerSpacer = this._renderFooterSpacer({ contentBackgroundColor });
89 | const maybeStickyHeader = this._maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader });
90 | const scrollElement = renderScrollComponent(scrollViewProps);
91 |
92 | return (
93 | this._maybeUpdateViewDimensions(e)}>
95 | { background }
96 | {
97 | React.cloneElement(scrollElement, {
98 | ref: SCROLLVIEW_REF,
99 | style: [styles.scrollView, scrollElement.props.style],
100 | scrollEventThrottle: 16,
101 | onScroll: this._onScroll.bind(this),
102 | },
103 | foreground,
104 | bodyComponent,
105 | footerSpacer
106 | )
107 | }
108 | { maybeStickyHeader }
109 |
110 | );
111 | }
112 |
113 | /*
114 | * Expose `ScrollView` API so this component is composable with any component that expects a `ScrollView`.
115 | */
116 | getScrollResponder() {
117 | return this.refs[SCROLLVIEW_REF].getScrollResponder();
118 | }
119 | getScrollableNode() {
120 | return this.getScrollResponder().getScrollableNode();
121 | }
122 | getInnerViewNode() {
123 | return this.getScrollResponder().getInnerViewNode();
124 | }
125 | scrollTo(...args) {
126 | this.getScrollResponder().scrollTo(...args);
127 | }
128 | setNativeProps(props) {
129 | this.refs[SCROLLVIEW_REF].setNativeProps(props);
130 | }
131 |
132 | /*
133 | * Private helpers
134 | */
135 |
136 | _onScroll(e) {
137 | const {
138 | parallaxHeaderHeight,
139 | stickyHeaderHeight,
140 | onChangeHeaderVisibility,
141 | onScroll: prevOnScroll = () => {}
142 | } = this.props;
143 |
144 | const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight);
145 |
146 | this._maybeUpdateScrollPosition(e);
147 |
148 | if (e.nativeEvent.contentOffset.y >= p) {
149 | onChangeHeaderVisibility(false);
150 | } else {
151 | onChangeHeaderVisibility(true);
152 | }
153 |
154 | prevOnScroll(e);
155 | }
156 |
157 | // This optimizes the state update of current scrollY since we don't need to
158 | // perform any updates when user has scrolled past the pivot point.
159 | _maybeUpdateScrollPosition(e) {
160 | const { parallaxHeaderHeight, stickyHeaderHeight } = this.props;
161 | const { scrollY } = this.state;
162 | const { nativeEvent: { contentOffset: { y: offsetY } } } = e;
163 | const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight);
164 |
165 | if (offsetY <= p || scrollY._value <= p) {
166 | scrollY.setValue(offsetY);
167 | }
168 | }
169 |
170 | _maybeUpdateViewDimensions(e) {
171 | const { nativeEvent: { layout: { width, height} } } = e;
172 |
173 | if (width !== this.state.viewWidth || height !== this.state.viewHeight) {
174 | this.setState({
175 | viewWidth: width,
176 | viewHeight: height
177 | });
178 | }
179 | }
180 |
181 | _renderBackground({ fadeOutBackground, backgroundScrollSpeed, backgroundColor, parallaxHeaderHeight, stickyHeaderHeight, renderBackground }) {
182 | const { viewWidth, viewHeight, scrollY } = this.state;
183 | const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight);
184 | return (
185 |
212 |
213 | { renderBackground() }
214 |
215 |
216 | );
217 | }
218 |
219 | _renderForeground({ fadeOutForeground, parallaxHeaderHeight, stickyHeaderHeight, renderForeground }) {
220 | const { scrollY } = this.state;
221 | const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight);
222 | return (
223 |
224 |
235 |
236 | { renderForeground() }
237 |
238 |
239 |
240 | );
241 | }
242 |
243 | _wrapChildren(children, { contentBackgroundColor, stickyHeaderHeight }) {
244 | const { viewHeight } = this.state;
245 | return (
246 | {
249 | // Adjust the bottom height so we can scroll the parallax header all the way up.
250 | const { nativeEvent: { layout: { height } } } = e;
251 | const footerHeight = Math.max(0, viewHeight - height - stickyHeaderHeight);
252 | if (this._footerHeight !== footerHeight) {
253 | this._footerComponent.setNativeProps({ style: { height: footerHeight }});
254 | this._footerHeight = footerHeight;
255 | }
256 | }}>
257 | { children }
258 |
259 | );
260 | }
261 |
262 | _renderFooterSpacer({ contentBackgroundColor }) {
263 | return (
264 | this._footerComponent = ref } style={{ backgroundColor: contentBackgroundColor }}/>
265 | );
266 | }
267 |
268 | _maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader }) {
269 | const { viewWidth, scrollY } = this.state;
270 | if (renderStickyHeader || renderFixedHeader) {
271 | const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight);
272 | return (
273 |
274 | {
275 | renderStickyHeader
276 | ? (
277 |
287 | { renderStickyHeader() }
288 |
289 | )
290 | : null
291 | }
292 | { renderFixedHeader && renderFixedHeader() }
293 |
294 | );
295 | } else {
296 | return null;
297 | }
298 | }
299 | }
300 |
301 | ParallaxScrollView.propTypes = IPropTypes;
302 |
303 | ParallaxScrollView.defaultProps = {
304 | backgroundScrollSpeed: 5,
305 | backgroundColor: '#000',
306 | contentBackgroundColor: '#fff',
307 | fadeOutForeground: true,
308 | onChangeHeaderVisibility: () => {},
309 | renderScrollComponent: props => ,
310 | renderBackground: renderEmpty,
311 | renderParallaxHeader: renderEmpty, // Deprecated (will be removed in 0.18.0)
312 | renderForeground: null,
313 | stickyHeaderHeight: 0
314 | };
315 |
316 | module.exports = ParallaxScrollView;
317 |
--------------------------------------------------------------------------------
/android/app/app.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | generateDebugSources
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/js/dependencies/swiper/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * react-native-swiper
3 | * @author leecade
4 | */
5 | import React from 'react'
6 | import ReactNative, {
7 | StyleSheet,
8 | Text,
9 | View,
10 | ScrollView,
11 | Dimensions,
12 | TouchableOpacity,
13 | ViewPagerAndroid,
14 | Platform
15 | } from 'react-native'
16 |
17 | // Using bare setTimeout, setInterval, setImmediate
18 | // and requestAnimationFrame calls is very dangerous
19 | // because if you forget to cancel the request before
20 | // the component is unmounted, you risk the callback
21 | // throwing an exception.
22 | import TimerMixin from 'react-timer-mixin'
23 |
24 | let { width, height } = Dimensions.get('window')
25 |
26 | /**
27 | * Default styles
28 | * @type {StyleSheetPropType}
29 | */
30 | let styles = StyleSheet.create({
31 | container: {
32 | backgroundColor: 'transparent',
33 | position: 'relative',
34 | },
35 |
36 | wrapper: {
37 | backgroundColor: 'transparent',
38 | },
39 |
40 | slide: {
41 | backgroundColor: 'transparent',
42 | },
43 |
44 | pagination_x: {
45 | position: 'absolute',
46 | bottom: 25,
47 | left: 0,
48 | right: 0,
49 | flexDirection: 'row',
50 | flex: 1,
51 | justifyContent: 'center',
52 | alignItems: 'center',
53 | backgroundColor:'transparent',
54 | },
55 |
56 | pagination_y: {
57 | position: 'absolute',
58 | right: 15,
59 | top: 0,
60 | bottom: 0,
61 | flexDirection: 'column',
62 | flex: 1,
63 | justifyContent: 'center',
64 | alignItems: 'center',
65 | backgroundColor:'transparent',
66 | },
67 |
68 | title: {
69 | height: 30,
70 | justifyContent: 'center',
71 | position: 'absolute',
72 | paddingLeft: 10,
73 | bottom: -30,
74 | left: 0,
75 | flexWrap: 'nowrap',
76 | width: 250,
77 | backgroundColor: 'transparent',
78 | },
79 |
80 | buttonWrapper: {
81 | backgroundColor: 'transparent',
82 | flexDirection: 'row',
83 | position: 'absolute',
84 | top: 0,
85 | left: 0,
86 | flex: 1,
87 | paddingHorizontal: 10,
88 | paddingVertical: 10,
89 | justifyContent: 'space-between',
90 | alignItems: 'center'
91 | },
92 |
93 | buttonText: {
94 | fontSize: 50,
95 | color: '#007aff',
96 | fontFamily: 'Arial',
97 | },
98 | })
99 |
100 | // missing `module.exports = exports['default'];` with babel6
101 | // export default React.createClass({
102 | module.exports = React.createClass({
103 |
104 | /**
105 | * Props Validation
106 | * @type {Object}
107 | */
108 | propTypes: {
109 | horizontal : React.PropTypes.bool,
110 | children : React.PropTypes.node.isRequired,
111 | style : View.propTypes.style,
112 | pagingEnabled : React.PropTypes.bool,
113 | showsHorizontalScrollIndicator : React.PropTypes.bool,
114 | showsVerticalScrollIndicator : React.PropTypes.bool,
115 | bounces : React.PropTypes.bool,
116 | scrollsToTop : React.PropTypes.bool,
117 | removeClippedSubviews : React.PropTypes.bool,
118 | automaticallyAdjustContentInsets : React.PropTypes.bool,
119 | showsPagination : React.PropTypes.bool,
120 | showsButtons : React.PropTypes.bool,
121 | loop : React.PropTypes.bool,
122 | autoplay : React.PropTypes.bool,
123 | autoplayTimeout : React.PropTypes.number,
124 | autoplayDirection : React.PropTypes.bool,
125 | index : React.PropTypes.number,
126 | renderPagination : React.PropTypes.func,
127 | onScroll : React.PropTypes.func,
128 | onSelectedIndexChange : React.PropTypes.func,
129 | },
130 |
131 | mixins: [TimerMixin],
132 |
133 | /**
134 | * Default props
135 | * @return {object} props
136 | * @see http://facebook.github.io/react-native/docs/scrollview.html
137 | */
138 | getDefaultProps() {
139 | return {
140 | horizontal : true,
141 | pagingEnabled : true,
142 | showsHorizontalScrollIndicator : false,
143 | showsVerticalScrollIndicator : false,
144 | bounces : false,
145 | scrollsToTop : false,
146 | removeClippedSubviews : true,
147 | automaticallyAdjustContentInsets : false,
148 | showsPagination : true,
149 | showsButtons : false,
150 | loop : true,
151 | autoplay : false,
152 | autoplayTimeout : 2.5,
153 | autoplayDirection : true,
154 | index : 0,
155 | }
156 | },
157 |
158 | /**
159 | * Init states
160 | * @return {object} states
161 | */
162 | getInitialState() {
163 | return this.initState(this.props)
164 | },
165 |
166 | /**
167 | * autoplay timer
168 | * @type {null}
169 | */
170 | autoplayTimer: null,
171 |
172 | componentWillMount() {
173 | this.props = this.injectState(this.props)
174 | },
175 |
176 | componentWillReceiveProps(props) {
177 | this.setState(this.initState(props))
178 | },
179 |
180 | componentDidMount() {
181 | this.autoplay()
182 | },
183 |
184 | initState(props) {
185 | let initState = {
186 | isScrolling: false,
187 | autoplayEnd: false,
188 | }
189 |
190 | initState.total = props.children ? props.children.length || 1 : 0
191 | initState.index = initState.total > 1 ? Math.min(props.index, initState.total - 1) : 0
192 |
193 | // Default: horizontal
194 | initState.dir = props.horizontal == false ? 'y' : 'x'
195 | initState.width = props.width || width
196 | initState.height = props.height || height
197 | initState.offset = {}
198 |
199 | if (initState.total > 1) {
200 | var setup = initState.index
201 | if ( props.loop ) {
202 | setup++
203 | }
204 | initState.offset[initState.dir] = initState.dir == 'y'
205 | ? initState.height * setup
206 | : initState.width * setup
207 | }
208 | return initState
209 | },
210 |
211 | /**
212 | * Automatic rolling
213 | */
214 | autoplay() {
215 | if(!Array.isArray(this.props.children)
216 | || !this.props.autoplay
217 | || this.state.isScrolling
218 | || this.state.autoplayEnd) return
219 |
220 | clearTimeout(this.autoplayTimer)
221 |
222 | this.autoplayTimer = this.setTimeout(() => {
223 | if(!this.props.loop && (this.props.autoplayDirection
224 | ? this.state.index == this.state.total - 1
225 | : this.state.index == 0)) return this.setState({
226 | autoplayEnd: true
227 | })
228 | this.scrollTo(this.props.autoplayDirection ? 1 : -1)
229 | }, this.props.autoplayTimeout * 1000)
230 | },
231 |
232 | /**
233 | * Scroll begin handle
234 | * @param {object} e native event
235 | */
236 | onScrollBegin(e) {
237 | // update scroll state
238 | this.setState({
239 | isScrolling: true
240 | })
241 |
242 | this.setTimeout(() => {
243 | this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e, this.state, this)
244 | })
245 | },
246 |
247 | /**
248 | * Scroll end handle
249 | * @param {object} e native event
250 | */
251 | onScrollEnd(e) {
252 | // update scroll state
253 | this.setState({
254 | isScrolling: false
255 | })
256 |
257 | // making our events coming from android compatible to updateIndex logic
258 | if (!e.nativeEvent.contentOffset) {
259 | if (this.state.dir == 'x') {
260 | e.nativeEvent.contentOffset = {x: e.nativeEvent.position * this.state.width}
261 | } else {
262 | e.nativeEvent.contentOffset = {y: e.nativeEvent.position * this.state.height}
263 | }
264 | }
265 |
266 | this.updateIndex(e.nativeEvent.contentOffset, this.state.dir)
267 |
268 | // Note: `this.setState` is async, so I call the `onMomentumScrollEnd`
269 | // in setTimeout to ensure synchronous update `index`
270 | this.setTimeout(() => {
271 | this.autoplay()
272 |
273 | // if `onMomentumScrollEnd` registered will be called here
274 | this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e, this.state, this)
275 | })
276 | },
277 |
278 | /**
279 | * Update index after scroll
280 | * @param {object} offset content offset
281 | * @param {string} dir 'x' || 'y'
282 | */
283 | updateIndex(offset, dir) {
284 |
285 | let state = this.state
286 | let index = state.index
287 | let diff = offset[dir] - state.offset[dir]
288 | let step = dir == 'x' ? state.width : state.height
289 |
290 | // Do nothing if offset no change.
291 | if(index > 0 && !diff) {
292 | return;
293 | }
294 |
295 | // Note: if touch very very quickly and continuous,
296 | // the variation of `index` more than 1.
297 | index = index + diff / step
298 |
299 | if(this.props.loop) {
300 | if(index <= -1) {
301 | index = state.total - 1
302 | offset[dir] = step * state.total
303 | }
304 | else if(index >= state.total) {
305 | index = 0
306 | offset[dir] = step
307 | }
308 | }
309 |
310 | this.setState({
311 | index: index,
312 | offset: offset,
313 | })
314 |
315 | this.props.onSelectedIndexChange && this.props.onSelectedIndexChange(index, offset);
316 | },
317 |
318 | /**
319 | * Scroll by index
320 | * @param {number} index offset index
321 | */
322 | scrollTo(index) {
323 | if (this.state.isScrolling || this.state.total < 2) return
324 | let state = this.state
325 | let diff = (this.props.loop ? 1 : 0) + index + this.state.index
326 | let x = 0
327 | let y = 0
328 | if(state.dir == 'x') x = diff * state.width
329 | if(state.dir == 'y') y = diff * state.height
330 |
331 | if (Platform.OS === 'android') {
332 | this.refs.scrollView && this.refs.scrollView.setPage(diff)
333 | } else {
334 | this.refs.scrollView && this.refs.scrollView.scrollTo({
335 | y: y,
336 | x: x
337 | })
338 | }
339 |
340 | // update scroll state
341 | this.setState({
342 | isScrolling: true,
343 | autoplayEnd: false,
344 | })
345 |
346 | // trigger onScrollEnd manually in android
347 | if (Platform.OS === 'android') {
348 | this.setTimeout(() => {
349 | this.onScrollEnd({
350 | nativeEvent: {
351 | position: diff,
352 | }
353 | });
354 | }, 50);
355 | }
356 |
357 | },
358 |
359 | /**
360 | * Render pagination
361 | * @return {object} react-dom
362 | */
363 | renderPagination() {
364 |
365 | // By default, dots only show when `total` > 2
366 | if(this.state.total <= 1) return null
367 |
368 | let dots = []
369 | let ActiveDot = this.props.activeDot || ;
379 | let Dot = this.props.dot || ;
389 | for(let i = 0; i < this.state.total; i++) {
390 | dots.push(i === this.state.index
391 | ?
392 | React.cloneElement(ActiveDot, {key: i})
393 | :
394 | React.cloneElement(Dot, {key: i})
395 | )
396 | }
397 |
398 | return (
399 |
400 | {dots}
401 |
402 | )
403 | },
404 |
405 | renderTitle() {
406 | let child = this.props.children[this.state.index]
407 | let title = child && child.props.title
408 | return title
409 | ? (
410 |
411 | {this.props.children[this.state.index].props.title}
412 |
413 | )
414 | : null
415 | },
416 |
417 | renderNextButton() {
418 | let button;
419 |
420 | if (this.props.loop || this.state.index != this.state.total - 1) {
421 | button = this.props.nextButton || ›
422 | }
423 |
424 | return (
425 | button !== null && this.scrollTo.call(this, 1)}>
426 |
427 | {button}
428 |
429 |
430 | )
431 | },
432 |
433 | renderPrevButton() {
434 | let button = null
435 |
436 | if (this.props.loop || this.state.index != 0) {
437 | button = this.props.prevButton || ‹
438 | }
439 |
440 | return (
441 | button !== null && this.scrollTo.call(this, -1)}>
442 |
443 | {button}
444 |
445 |
446 | )
447 | },
448 |
449 | renderButtons() {
450 | return (
451 |
452 | {this.renderPrevButton()}
453 | {this.renderNextButton()}
454 |
455 | )
456 | },
457 | renderScrollView(pages) {
458 | if (Platform.OS === 'ios')
459 | return (
460 |
466 | {pages}
467 |
468 | );
469 | return (
470 |
475 | {pages}
476 |
477 | );
478 | },
479 | /**
480 | * Inject state to ScrollResponder
481 | * @param {object} props origin props
482 | * @return {object} props injected props
483 | */
484 | injectState(props) {
485 | /* const scrollResponders = [
486 | 'onMomentumScrollBegin',
487 | 'onTouchStartCapture',
488 | 'onTouchStart',
489 | 'onTouchEnd',
490 | 'onResponderRelease',
491 | ]*/
492 |
493 | for(let prop in props) {
494 | // if(~scrollResponders.indexOf(prop)
495 | if(typeof props[prop] === 'function'
496 | && prop !== 'onMomentumScrollEnd'
497 | && prop !== 'renderPagination'
498 | && prop !== 'onScrollBeginDrag'
499 | && prop !== 'onScroll'
500 | ) {
501 | let originResponder = props[prop]
502 | props[prop] = (e) => originResponder(e, this.state, this)
503 | }
504 | }
505 |
506 | return props
507 | },
508 |
509 | /**
510 | * Default render
511 | * @return {object} react-dom
512 | */
513 | render() {
514 | let state = this.state
515 | let props = this.props
516 | let children = props.children
517 | let index = state.index
518 | let total = state.total
519 | let loop = props.loop
520 | let dir = state.dir
521 | let key = 0
522 |
523 | let pages = []
524 | let pageStyle = [{width: state.width, height: state.height}, styles.slide]
525 |
526 | // For make infinite at least total > 1
527 | if(total > 1) {
528 |
529 | // Re-design a loop model for avoid img flickering
530 | pages = Object.keys(children)
531 | if(loop) {
532 | pages.unshift(total - 1)
533 | pages.push(0)
534 | }
535 |
536 | pages = pages.map((page, i) =>
537 | {children[page]}
538 | )
539 | }
540 | else pages = {children}
541 |
542 | return (
543 |
547 | {this.renderScrollView(pages)}
548 | {props.showsPagination && (props.renderPagination
549 | ? this.props.renderPagination(state.index, state.total, this)
550 | : this.renderPagination())}
551 | {this.renderTitle()}
552 | {this.props.showsButtons && this.renderButtons()}
553 |
554 | )
555 | }
556 | })
557 |
--------------------------------------------------------------------------------