├── .babelrc
├── .gitignore
├── README.md
├── api
└── registerForPushNotificationsAsync.js
├── assets
├── .DS_Store
├── fonts
│ └── SpaceMono-Regular.ttf
└── images
│ ├── 1.jpg
│ ├── 10.jpeg
│ ├── 11.jpg
│ ├── 12.jpeg
│ ├── 13.jpeg
│ ├── 14.jpeg
│ ├── 15.jpg
│ ├── 16.jpg
│ ├── 17.jpeg
│ ├── 18.jpg
│ ├── 2.jpeg
│ ├── 3.jpg
│ ├── 4.jpg
│ ├── 5.jpeg
│ ├── 6.jpg
│ ├── 7.jpg
│ ├── 8.jpeg
│ ├── 9.jpeg
│ ├── exponent-icon@2x.png
│ ├── exponent-icon@3x.png
│ └── exponent-wordmark.png
├── constants
├── Alerts.js
├── Colors.js
└── Layout.js
├── demo.gif
├── exp.json
├── main.js
├── package.json
├── screens
├── Browse
│ └── index.js
├── Landing
│ └── index.js
├── common
│ ├── coverflow-item.js
│ ├── coverflow.js
│ ├── dimensions.js
│ ├── footer.js
│ ├── header.js
│ ├── imgs.js
│ ├── playlist-item.js
│ ├── playlist.js
│ └── tab-bar-navigation.js
└── main.js
└── utilities
├── __tests__
└── cacheAssetsAsync-test.js
└── cacheAssetsAsync.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["babel-preset-exponent"],
3 | "env": {
4 | "development": {
5 | "plugins": ["transform-react-jsx-source"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .exponent/**/*
3 | .idea
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-spotify-ui
2 | Pure javascript prototype of iOS Facebook UI for React Native framework. This demo only includes the landing page.
3 | I will add more views in the future.
4 |
5 | ## Sponsored by Spurwing
6 |
7 | 
8 |
9 | This repo is sponsored by Spurwing, where their API Makes Adding Scheduling Quick, Reliable and Scalable.
10 |
11 | - Scheduling API
12 | - Enterprise Scheduling API
13 | - Scheduling API for Business
14 | - Scheduling and Calendar Management API
15 | - Booking API
16 |
17 | Check them out [here](https://github.com/Spurwing/Appointment-Scheduling-API)!
18 |
19 | ## Inspiration
20 | I was always amazed by the amazing Spotify UI, and I decided to challenge myself. THe pulling drawer from the bottom was the hardest challenge--
21 | there are so many bugs to combinate pan-responder with other components. Buttons do not respond with PanResponder. If anyone knows a fix, please message me!
22 |
23 | [FIX] Now buttons are clickable, thanks to Dan Horrigan's recommendation
24 | - Regarding the buttons in the PanResponder not working: I believe this is because you have `onStartShouldSetPanResponderCapture` returning `true`. I believe this is causing it to capture all touches on the View. Instead you should just have `onStartShouldSetPanResponder` return `true`, which will respect PanResponder's touch bubbling algorithm. The PR is pretty good at handling these conflicts in a predictable way most of the time.
25 |
26 | Another option: Make the "Header" of that slide-up View the "handle", so that the user has to start the gesture on that View. This is the way the Spotify app works anyways. You can't swipe down on any part of the scene except the Header.
27 |
28 | [FIX] Fixed screen flickering issue with gesture.y0.
29 |
30 |
31 | The design aspect of this demo belong to Spotify.
32 |
33 |
34 | ## Demo
35 |
36 | 
37 |
38 | ## Try it out
39 |
40 | Try it with Exponent: https://getexponent.com/@sungwoopark95/react-native-spotify-ui
41 |
42 | ## Run it locally
43 |
44 | To install, there are two steps:
45 |
46 | 1. Install Exponent XDE [following this
47 | guide](https://docs.getexponent.com/versions/latest/introduction/installation.html).
48 | Also install the Exponent app on your phone if you want to test it on
49 | your device, otherwise you don't need to do anything for the simulator.
50 | 2. Clone this repo and run `npm install`
51 | ```bash
52 | git clone https://github.com/ggomaeng/react-native-spotify-ui.git spotify
53 |
54 | cd spotify
55 | npm install
56 | ```
57 | 3. Open the project with Exponent XDE and run it.
58 |
59 | The MIT License (MIT)
60 | =====================
61 |
62 | Copyright © 2016 Sung Woo Park
63 |
64 | Permission is hereby granted, free of charge, to any person
65 | obtaining a copy of this software and associated documentation
66 | files (the “Software”), to deal in the Software without
67 | restriction, including without limitation the rights to use,
68 | copy, modify, merge, publish, distribute, sublicense, and/or sell
69 | copies of the Software, and to permit persons to whom the
70 | Software is furnished to do so, subject to the following
71 | conditions:
72 |
73 | The above copyright notice and this permission notice shall be
74 | included in all copies or substantial portions of the Software.
75 |
76 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
77 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
78 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
79 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
80 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
81 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
82 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
83 | OTHER DEALINGS IN THE SOFTWARE.
84 |
--------------------------------------------------------------------------------
/api/registerForPushNotificationsAsync.js:
--------------------------------------------------------------------------------
1 | import {
2 | Platform,
3 | } from 'react-native';
4 | import {
5 | Permissions,
6 | Notifications,
7 | } from 'exponent';
8 |
9 | // Example server, implemented in Rails: https://git.io/vKHKv
10 | const PUSH_ENDPOINT = 'https://exponent-push-server.herokuapp.com/tokens';
11 |
12 | export default async function registerForPushNotificationsAsync() {
13 | // Android remote notification permissions are granted during the app
14 | // install, so this will only ask on iOS
15 | let { status } = await Permissions.askAsync(Permissions.REMOTE_NOTIFICATIONS);
16 |
17 | // Stop here if the user did not grant permissions
18 | if (status !== 'granted') {
19 | return;
20 | }
21 |
22 | // Get the token that uniquely identifies this device
23 | let token = await Notifications.getExponentPushTokenAsync();
24 |
25 | // POST the token to our backend so we can use it to send pushes from there
26 | return fetch(PUSH_ENDPOINT, {
27 | method: 'POST',
28 | headers: {
29 | 'Accept': 'application/json',
30 | 'Content-Type': 'application/json',
31 | },
32 | body: JSON.stringify({
33 | token: {
34 | value: token,
35 | },
36 | }),
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/.DS_Store
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/assets/images/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/1.jpg
--------------------------------------------------------------------------------
/assets/images/10.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/10.jpeg
--------------------------------------------------------------------------------
/assets/images/11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/11.jpg
--------------------------------------------------------------------------------
/assets/images/12.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/12.jpeg
--------------------------------------------------------------------------------
/assets/images/13.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/13.jpeg
--------------------------------------------------------------------------------
/assets/images/14.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/14.jpeg
--------------------------------------------------------------------------------
/assets/images/15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/15.jpg
--------------------------------------------------------------------------------
/assets/images/16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/16.jpg
--------------------------------------------------------------------------------
/assets/images/17.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/17.jpeg
--------------------------------------------------------------------------------
/assets/images/18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/18.jpg
--------------------------------------------------------------------------------
/assets/images/2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/2.jpeg
--------------------------------------------------------------------------------
/assets/images/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/3.jpg
--------------------------------------------------------------------------------
/assets/images/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/4.jpg
--------------------------------------------------------------------------------
/assets/images/5.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/5.jpeg
--------------------------------------------------------------------------------
/assets/images/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/6.jpg
--------------------------------------------------------------------------------
/assets/images/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/7.jpg
--------------------------------------------------------------------------------
/assets/images/8.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/8.jpeg
--------------------------------------------------------------------------------
/assets/images/9.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/9.jpeg
--------------------------------------------------------------------------------
/assets/images/exponent-icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/exponent-icon@2x.png
--------------------------------------------------------------------------------
/assets/images/exponent-icon@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/exponent-icon@3x.png
--------------------------------------------------------------------------------
/assets/images/exponent-wordmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/assets/images/exponent-wordmark.png
--------------------------------------------------------------------------------
/constants/Alerts.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import Colors from './Colors';
3 |
4 | export default {
5 | error: StyleSheet.create({
6 | container: {
7 | backgroundColor: Colors.errorBackground,
8 | },
9 | text: {
10 | color: Colors.errorText,
11 | },
12 | }),
13 |
14 | warning: StyleSheet.create({
15 | container: {
16 | backgroundColor: Colors.warningBackground,
17 | },
18 | text: {
19 | color: Colors.warningText,
20 | },
21 | }),
22 |
23 | notice: StyleSheet.create({
24 | container: {
25 | backgroundColor: Colors.noticeBackground,
26 | },
27 | text: {
28 | color: Colors.noticeText,
29 | },
30 | }),
31 | }
32 |
--------------------------------------------------------------------------------
/constants/Colors.js:
--------------------------------------------------------------------------------
1 | const tintColor = '#2f95dc';
2 |
3 | export default {
4 | tintColor,
5 | tabIconDefault: '#888',
6 | tabIconSelected: tintColor,
7 | tabBar: '#fefefe',
8 | errorBackground: 'red',
9 | errorText: '#fff',
10 | warningBackground: '#EAEB5E',
11 | warningText: '#666804',
12 | noticeBackground: tintColor,
13 | noticeText: '#fff',
14 | };
15 |
--------------------------------------------------------------------------------
/constants/Layout.js:
--------------------------------------------------------------------------------
1 | import {
2 | Dimensions,
3 | } from 'react-native';
4 |
5 | export default {
6 | window: {
7 | width: Dimensions.get('window').width,
8 | height: Dimensions.get('window').height,
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/1f638f10b9e0c5f0a59bf2901565a0c68a5fdc70/demo.gif
--------------------------------------------------------------------------------
/exp.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-spotify-ui",
3 | "description": "No description",
4 | "slug": "react-native-spotify-ui",
5 | "sdkVersion": "12.0.0",
6 | "version": "1.0.0",
7 | "orientation": "portrait",
8 | "primaryColor": "#cccccc",
9 | "iconUrl": "https://s3.amazonaws.com/exp-brand-assets/ExponentEmptyManifest_192.png",
10 | "notification": {
11 | "iconUrl": "https://s3.amazonaws.com/exp-us-standard/placeholder-push-icon-blue-circle.png",
12 | "color": "#000000"
13 | },
14 | "loading": {
15 | "iconUrl": "https://s3.amazonaws.com/exp-brand-assets/ExponentEmptyManifest_192.png",
16 | "hideExponentText": false
17 | },
18 | "packagerOpts": {
19 | "assetExts": ["ttf"]
20 | },
21 | "ios": {
22 | "supportsTablet": true
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | import Exponent from 'exponent';
2 | import React from 'react';
3 | import {
4 | AppRegistry,
5 | Platform,
6 | StatusBar,
7 | StyleSheet,
8 | View,
9 | } from 'react-native';
10 |
11 | import {
12 | FontAwesome,
13 | } from '@exponent/vector-icons';
14 |
15 | import Main from './screens/main';
16 |
17 | import cacheAssetsAsync from './utilities/cacheAssetsAsync';
18 |
19 | class AppContainer extends React.Component {
20 | state = {
21 | appIsReady: false,
22 | }
23 |
24 | componentWillMount() {
25 | this._loadAssetsAsync();
26 | }
27 |
28 | async _loadAssetsAsync() {
29 | try {
30 | await cacheAssetsAsync({
31 | images: [
32 | require('./assets/images/exponent-wordmark.png'),
33 | ],
34 | fonts: [
35 | FontAwesome.font,
36 | {'space-mono': require('./assets/fonts/SpaceMono-Regular.ttf')},
37 | ],
38 | });
39 | } catch(e) {
40 | console.warn(
41 | 'There was an error caching assets (see: main.js), perhaps due to a ' +
42 | 'network timeout, so we skipped caching. Reload the app to try again.'
43 | );
44 | console.log(e.message);
45 | } finally {
46 | this.setState({appIsReady: true});
47 | }
48 | }
49 |
50 | render() {
51 | if(!this.state.appIsReady) {
52 | return
53 | }
54 | return (
55 |
56 |
57 |
58 | )
59 |
60 | }
61 | }
62 |
63 | const styles = StyleSheet.create({
64 | container: {
65 | flex: 1,
66 | backgroundColor: '#fff',
67 | },
68 | statusBarUnderlay: {
69 | height: 24,
70 | backgroundColor: 'rgba(0,0,0,0.2)',
71 | },
72 | });
73 |
74 | Exponent.registerRootComponent(AppContainer);
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-spotify-ui",
3 | "version": "0.0.0",
4 | "description": "Hello Exponent!",
5 | "author": null,
6 | "main": "main.js",
7 | "scripts": {
8 | "test": "jest"
9 | },
10 | "jest": {
11 | "preset": "jest-exponent"
12 | },
13 | "dependencies": {
14 | "@exponent/ex-navigation": "~2.3.0",
15 | "@exponent/samples": "~1.0.2",
16 | "@exponent/vector-icons": "~2.0.3",
17 | "exponent": "~12.0.3",
18 | "react": "~15.3.2",
19 | "react-native": "git+https://github.com/exponentjs/react-native#sdk-12.0.0"
20 | },
21 | "devDependencies": {
22 | "jest-exponent": "~0.1.4"
23 | }
24 | }
--------------------------------------------------------------------------------
/screens/Browse/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/23/16.
3 | */
4 | import React from 'react';
5 | import {
6 | View
7 | } from 'react-native';
8 |
9 | export default () => {
10 | return (
11 |
12 |
13 |
14 | )
15 | }
--------------------------------------------------------------------------------
/screens/Landing/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/22/16.
3 | */
4 | import React, {Component} from 'react';
5 | import {
6 | View,
7 | ScrollView,
8 | Text,
9 | StyleSheet
10 | } from 'react-native';
11 |
12 | import Header from '../common/header';
13 |
14 | import PlayList from '../common/playlist';
15 |
16 | import img from '../common/imgs';
17 |
18 | import {TOGETHER} from '../common/footer';
19 |
20 | export default class Landing extends Component {
21 |
22 | state = {
23 | playlists: this.generatePlaylists(img, 5)
24 | };
25 |
26 | title = ['Just For You', 'Recently Played', 'Inspired by your Recent Listening', 'New Music Friday!'];
27 |
28 |
29 |
30 | generatePlaylists(array, size) {
31 | let results = [];
32 | while (array.length) {
33 | results.push(array.splice(0, size));
34 | }
35 | console.log(results);
36 | return results;
37 | }
38 |
39 | renderPlaylists() {
40 | return this.state.playlists.map((playlist, i) => {
41 | if(i == 1) return
42 | return (
43 |
44 | )
45 | })
46 |
47 | }
48 |
49 | render() {
50 | return (
51 |
52 |
54 | {this.renderPlaylists()}
55 |
56 | {/*for the gap*/}
57 |
58 |
59 |
60 |
61 | )
62 | }
63 | }
64 |
65 | const styles = StyleSheet.create({
66 | container: {
67 | flex: 1,
68 | backgroundColor: 'black'
69 | },
70 | sv: {
71 | paddingTop: 72,
72 | }
73 | });
--------------------------------------------------------------------------------
/screens/common/coverflow-item.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/23/16.
3 | */
4 | import React from 'react';
5 | import {
6 | View,
7 | Text,
8 | Image,
9 | StyleSheet
10 | } from 'react-native';
11 |
12 | export default (props) => {
13 | return (
14 |
15 |
16 |
17 | )
18 | }
--------------------------------------------------------------------------------
/screens/common/coverflow.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/23/16.
3 | */
4 | import React, {Component} from 'react';
5 | import {
6 | View,
7 | Text,
8 | Image,
9 | TouchableOpacity,
10 | ScrollView,
11 | StyleSheet,
12 |
13 | } from 'react-native';
14 |
15 | import {Ionicons} from '@exponent/vector-icons';
16 |
17 |
18 | import D from './dimensions';
19 |
20 | import CoverFlowItem from './coverflow-item';
21 |
22 | export default class CoverFlow extends Component {
23 | state = {
24 | play: true,
25 | };
26 |
27 | renderHeader() {
28 | return (
29 |
30 | this.props.scrollDown()}>
31 |
32 |
33 | NOW PLAYING
34 |
35 |
36 | )
37 | }
38 |
39 | renderCoverflow() {
40 | const width = D.width * 3.2/5,
41 | height = D.width * 3.2/5;
42 | return (
43 |
44 |
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | renderInfo() {
52 | return (
53 |
54 |
55 |
56 |
57 | Awesome Title
58 | Artist ggomaeng
59 |
60 |
61 |
62 |
63 |
64 |
65 | )
66 | }
67 |
68 | renderButtons() {
69 | const {play} = this.state;
70 | return (
71 |
72 |
73 |
74 | this.setState({play: !play})}
76 | style={[styles.playContainer, play ? {paddingLeft: 8} : {}]}>
77 |
78 |
79 |
80 |
81 |
82 |
83 | )
84 | }
85 |
86 | renderName() {
87 | return (
88 |
89 |
90 | SUNG'S MACBOOK PRO
91 |
92 |
93 | )
94 | }
95 |
96 | render() {
97 | return (
98 |
99 |
100 | {this.renderHeader()}
101 | {this.renderCoverflow()}
102 | {this.renderInfo()}
103 | {this.renderButtons()}
104 | {this.renderName()}
105 |
106 | )
107 | }
108 | }
109 |
110 | const styles = StyleSheet.create({
111 | container: {
112 | flex: 1,
113 | paddingTop: 20
114 | },
115 |
116 | header: {
117 | padding: 16,
118 | flexDirection: 'row',
119 | justifyContent: 'space-between',
120 | alignItems: 'flex-end',
121 | marginBottom: 16,
122 | },
123 |
124 | playing: {
125 | color: 'white',
126 | fontWeight: '300',
127 | fontSize: 12,
128 | marginBottom: 12
129 | },
130 |
131 | infoContainer: {
132 |
133 |
134 | },
135 |
136 | titleContainer: {
137 | flexDirection: 'row',
138 | justifyContent: 'space-between',
139 | alignItems: 'center',
140 | padding: 32
141 | },
142 |
143 | title: {
144 | color: 'white',
145 | fontSize: 20,
146 | fontWeight: '700'
147 | },
148 |
149 | artist: {
150 | color: 'white',
151 | fontSize: 14,
152 | fontWeight: '400'
153 | },
154 |
155 |
156 | buttonContainer: {
157 | flexDirection: 'row',
158 | justifyContent: 'space-around',
159 | alignItems: 'center',
160 | padding: 16
161 | },
162 |
163 | playContainer: {
164 | width: 80,
165 | height: 80,
166 | borderRadius: 40,
167 | borderColor: 'white',
168 | borderWidth: 1,
169 | justifyContent: 'center',
170 | alignItems: 'center'
171 | },
172 |
173 | play: {
174 | color: 'white',
175 | backgroundColor:'transparent',
176 | margin: 16,
177 | fontSize: 48,
178 | fontWeight: '800'
179 | },
180 |
181 | progress: {
182 | height: 2,
183 | marginLeft: 16,
184 | marginRight: 16,
185 | marginBottom: 16,
186 | backgroundColor: '#3c3d41'
187 | },
188 |
189 | text: {
190 | color: '#429962',
191 | fontSize: 10,
192 | marginBottom: 10,
193 | alignSelf: 'center',
194 | fontWeight: '300'
195 | }
196 |
197 |
198 |
199 |
200 |
201 | });
--------------------------------------------------------------------------------
/screens/common/dimensions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/22/16.
3 | */
4 | import {Dimensions} from 'react-native';
5 | const window = Dimensions.get('window');
6 | export default {width: window.width, height: window.height};
--------------------------------------------------------------------------------
/screens/common/footer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/23/16.
3 | */
4 | import React, {Component} from 'react';
5 | import {
6 | Animated,
7 | PanResponder,
8 | View,
9 | Text,
10 | TouchableOpacity,
11 | StyleSheet,
12 | StatusBar
13 | } from 'react-native';
14 |
15 | import D from './dimensions';
16 |
17 | import CoverFlow from './coverflow';
18 | import {Ionicons} from '@exponent/vector-icons';
19 |
20 | export const FOOTER_HEIGHT = 48;
21 | export const TABBAR_HEIGHT = 56;
22 | export const TOGETHER = FOOTER_HEIGHT + TABBAR_HEIGHT;
23 |
24 | export default class Footer extends Component {
25 | state = {
26 | pan: new Animated.ValueXY(),
27 | opacity: new Animated.Value(1)
28 |
29 | };
30 |
31 | moving = false;
32 | open = false;
33 | hiding = false;
34 | offY = 0;
35 |
36 |
37 | componentDidMount() {
38 | setTimeout(() => this.measureView(), 0);
39 | }
40 |
41 | measureView() {
42 | this.refs.view.measure((a, b, w, h, px, py) => {
43 | this.offY = py;
44 | })
45 | }
46 |
47 | hideTabBarNavigation(dy) {
48 | let value = (this.offY + dy) % (D.height - TOGETHER);
49 | if(value < 0 ) {
50 | value = 0;
51 | }
52 |
53 |
54 | this.props.hideTabBarNavigation(value);
55 | // console.log(value);
56 | }
57 |
58 | componentWillMount() {
59 | let panMover = Animated.event([null,{
60 | dy : this.state.pan.y,
61 | }]);
62 | this._panResponder = PanResponder.create({
63 | onStartShouldSetPanResponder: () => true,
64 | onMoveShouldSetPanResponder: (e, g) => !(g.dx === 0 || g.dy === 0),
65 | onPanResponderTerminationRequest: () => false,
66 | onStartShouldSetPanResponderCapture: () => false,
67 | onMoveShouldSetPanResponderCapture: () => false,
68 |
69 | onPanResponderGrant: (e, gestureState) => {
70 | },
71 |
72 | onPanResponderMove: (e, g) => {
73 |
74 |
75 |
76 | if(this.moving || (!this.open && g.dy > 0) || (this.open && g.dy < 0)) {
77 | // console.log('shouldnt move!!');
78 | return
79 | }
80 | if((!this.open && g.dy < -5) || (this.open && g.dy > 5)) {
81 | this.hideTabBarNavigation(g.dy);
82 | }
83 |
84 | if(!this.open && g.dy < 0) {
85 | const value = g.dy/70 + 1;
86 | if (0 < value && value < 1) {
87 | this.state.opacity.setValue(value);
88 | }
89 |
90 | }
91 |
92 | if(this.open && g.dy > 0) {
93 | const value = g.dy / 250 - 1;
94 | if (0 < value && value < 1) {
95 | this.state.opacity.setValue(value);
96 | }
97 | }
98 |
99 | return panMover(e,g);
100 | },
101 | onPanResponderRelease: (e, g) => {
102 | if(this.moving || (!this.open && g.dy > 0) || (this.open && g.dy < 0)) {
103 | // console.log('shouldnt release');
104 | return
105 | } else {
106 | const offsetY = g.dy;
107 | // console.log(offsetY);
108 |
109 | if(!this.open) {
110 | /*s
111 | If you are swiping up quickly and your finger goes off the screen, the View doesn't always open fully (it stops a few px from the top).
112 | This sort of thing happens because the event system couldn't keep up with the fast swipe, and the last event it gets is from a few milliseconds before it hit the top.
113 | You can fix this by always fully opening the View when its `y` is within some distance from the top.
114 | I think you can just add `if (g.y0 <= 100) this.scrollUp();` in your `onPanResponderRelease`
115 | */
116 | if(g.y0 >= 100) this.openPlaying(offsetY);
117 | } else {
118 | this.closePlaying(offsetY);
119 | }
120 | }
121 |
122 | },
123 |
124 | });
125 | }
126 |
127 | openPlaying(offsetY) {
128 | if (offsetY < -100) {
129 | console.log('open');
130 | this.moving = true;
131 | this.props.hide();
132 | StatusBar.setHidden(true, true);
133 | this.state.opacity.setValue(0);
134 | Animated.timing(
135 | this.state.pan.y,
136 | {
137 | toValue: -D.height + TOGETHER,
138 | duration: 200,
139 | }
140 | ).start(() => {
141 | console.log('opened');
142 | //hide tab bar
143 |
144 | setTimeout(() => {
145 | this.open = true;
146 | this.moving = false;
147 | }, 200);
148 | this.state.pan.setOffset({y: -D.height + TOGETHER});
149 | this.state.pan.setValue({y: 0});
150 | });
151 | } else {
152 | this.moving = true;
153 | this.reset();
154 | console.log('back to original state 1!', this.state.pan.y);
155 | this.props.show();
156 | Animated.timing(
157 | this.state.pan.y,
158 | {toValue: 0}
159 | ).start(() => {
160 | setTimeout(() => this.moving = false, 200);
161 | this.state.pan.setOffset({y: 0});
162 | });
163 | }
164 | }
165 |
166 | closePlaying(offsetY) {
167 | if(offsetY > 100) {
168 | console.log('closing');
169 | this.reset();
170 | this.moving = true;
171 | this.props.show();
172 | StatusBar.setHidden(false, true);
173 | Animated.timing(
174 | this.state.pan.y,
175 | {toValue: D.height - TOGETHER, duration: 200}
176 | ).start(() => {
177 | console.log('closed');
178 | setTimeout(() => {
179 | this.open = false;
180 | this.moving = false;
181 | }, 200);
182 | this.state.pan.setOffset({y: 0});
183 | this.state.pan.setValue({y: 0});
184 | });
185 | } else {
186 | this.moving = true;
187 | console.log('back to original state 2!');
188 | this.props.hide();
189 | Animated.timing(
190 | this.state.pan.y,
191 | {toValue: 0}
192 | ).start(() => {
193 | setTimeout(() => this.moving = false, 200);
194 | this.state.pan.setOffset({y: -D.height + TOGETHER});
195 | });
196 | }
197 | }
198 |
199 |
200 | scrollUp() {
201 | Animated.spring(
202 | this.state.opacity,
203 | {toValue: 0}
204 | ).start();
205 |
206 | this.openPlaying(-101);
207 | }
208 |
209 | scrollDown() {
210 | Animated.spring(
211 | this.state.opacity,
212 | {toValue: 1}
213 | ).start();
214 | this.closePlaying(101);
215 | }
216 |
217 | reset() {
218 | Animated.spring(
219 | this.state.opacity,
220 | {toValue: 1}
221 | ).start();
222 | }
223 |
224 |
225 | getStyle() {
226 | return {transform: [{translateY: this.state.pan.y}]};
227 | }
228 |
229 | renderDefault() {
230 | const {opacity} = this.state;
231 | return (
232 |
236 |
238 | this.scrollUp()} >
239 |
240 |
241 |
242 | Awesome Title · Artist ggomaeng
243 |
244 | SUNG'S MACBOOK PRO
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 | )
253 | }
254 |
255 | render() {
256 | return (
257 |
259 |
260 |
261 |
264 |
265 | this.scrollDown()}/>
266 | {this.renderDefault()}
267 |
268 |
269 |
270 | )
271 | }
272 | }
273 |
274 | const styles = StyleSheet.create({
275 | container: {
276 | position: 'absolute',
277 | width: D.width,
278 | top: D.height-TOGETHER
279 | },
280 | playing: {
281 | backgroundColor: 'rgba(0,0,0,.95)',
282 | height: D.height,
283 | paddingBottom: FOOTER_HEIGHT,
284 | },
285 | firstView: {
286 |
287 | position: 'absolute',
288 | top: 0,
289 | height: FOOTER_HEIGHT + 10,
290 | width: D.width,
291 | backgroundColor: '#222327',
292 | borderTopColor: '#3c3d41',
293 | borderTopWidth: 4,
294 | },
295 | pause: {
296 | width: 24,
297 | height: 24,
298 | borderRadius: 12,
299 | justifyContent: 'center',
300 | alignItems: 'center',
301 | borderWidth: 1,
302 | borderColor: 'white'
303 | },
304 |
305 | title: {
306 | fontSize: 12,
307 | color: 'white',
308 | fontWeight: '600'
309 | },
310 |
311 | author: {
312 | fontWeight: '600',
313 | color: '#aeafb3',
314 | fontSize: 12
315 | },
316 |
317 | music: {
318 | fontWeight: '300',
319 | color: '#40bf7c',
320 | fontSize: 12
321 | }
322 |
323 | })
--------------------------------------------------------------------------------
/screens/common/header.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/22/16.
3 | */
4 | import React from 'react';
5 | import {
6 | View,
7 | Text,
8 | StyleSheet
9 | } from 'react-native';
10 |
11 | import D from './dimensions';
12 |
13 | export default (props) => {
14 | return (
15 |
16 | {props.name}
17 |
18 | )
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | position:'absolute',
24 | top: 0,
25 | backgroundColor: 'rgba(27,27,27,.9)',
26 | width: D.width,
27 | height: 72,
28 | justifyContent: 'center',
29 | alignItems: 'center',
30 | paddingTop: 20
31 | },
32 |
33 | text: {
34 | fontWeight: '500',
35 | color: 'white'
36 | }
37 | });
--------------------------------------------------------------------------------
/screens/common/imgs.js:
--------------------------------------------------------------------------------
1 | // /**
2 | // * Created by ggoma on 12/23/16.
3 | // */
4 |
5 |
6 | export default [
7 | {
8 | source: require('../../assets/images/1.jpg'),
9 | name: 'Royalty - Drake',
10 | followers: 12392
11 | },
12 | {
13 | source: require('../../assets/images/2.jpeg'),
14 | name: 'Adele Playlist',
15 | followers: 1837234
16 | },
17 | {
18 | source: require('../../assets/images/3.jpg'),
19 | name: 'Micahel Jackson',
20 | followers: 12392231
21 | },
22 | {
23 | source: require('../../assets/images/5.jpeg'),
24 | name: 'Scary Girl',
25 | followers: 22392
26 | },
27 | {
28 | source: require('../../assets/images/6.jpg'),
29 | name: 'Pink Floyd',
30 | followers: 392331
31 | },
32 | {
33 | source: require('../../assets/images/7.jpg'),
34 | name: 'KISS',
35 | followers: 12392
36 | },
37 | {
38 | source: require('../../assets/images/9.jpeg'),
39 | name: 'The Lumineers',
40 | followers: 122392
41 | },
42 | {
43 | source: require('../../assets/images/10.jpeg'),
44 | name: 'Loud',
45 | followers: 112392
46 | },
47 | {
48 | source: require('../../assets/images/11.jpg'),
49 | name: 'Bionic',
50 | followers: 523292
51 | },
52 | {
53 | source: require('../../assets/images/8.jpeg'),
54 | name: 'John Lenon',
55 | followers: 123592
56 | },
57 | {
58 | source: require('../../assets/images/12.jpeg'),
59 | name: 'Ariana Grande',
60 | followers: 1239121
61 | },
62 | {
63 | source: require('../../assets/images/13.jpeg'),
64 | name: 'King Michael',
65 | followers: 12392991
66 | },
67 | {
68 | source: require('../../assets/images/14.jpeg'),
69 | name: 'Eminem',
70 | followers: 1227392
71 | },
72 | {
73 | source: require('../../assets/images/15.jpg'),
74 | name: 'Ocastus',
75 | followers: 723192
76 | },
77 | {
78 | source: require('../../assets/images/16.jpg'),
79 | name: 'NAMELESS',
80 | followers: 1239112
81 | },
82 | {
83 | source: require('../../assets/images/17.jpeg'),
84 | name: 'Sam Smith',
85 | followers: 2312392
86 | },
87 | {
88 | source: require('../../assets/images/18.jpg'),
89 | name: 'Eminem Again',
90 | followers: 122392
91 | }
92 | ]
--------------------------------------------------------------------------------
/screens/common/playlist-item.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/22/16.
3 | */
4 | import React from 'react';
5 | import {
6 | View,
7 | Text,
8 | Image,
9 | StyleSheet
10 | } from 'react-native';
11 |
12 | import D from './dimensions';
13 |
14 | function numberWithCommas(x) {
15 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
16 | }
17 |
18 | export default (props) => {
19 | return (
20 |
21 |
22 |
23 | {props.title}
24 | {numberWithCommas(props.followers)} FOLLOWERS
25 |
26 |
27 | )
28 | }
29 |
30 | const styles = StyleSheet.create({
31 | container: {
32 | margin: 8,
33 | marginLeft: 12
34 | },
35 |
36 | album: {
37 | width: D.width * 4.2/10,
38 | height: D.width * 4.2/10,
39 | backgroundColor: 'white'
40 | },
41 |
42 | img: {
43 | flex: 1,
44 | height: null,
45 | width: null
46 | },
47 |
48 | text: {
49 | fontSize: 10,
50 | color: 'white',
51 | fontWeight: '600',
52 | alignSelf: 'center',
53 | marginTop: 8,
54 | },
55 |
56 | followers: {
57 | fontSize: 8,
58 | color: 'gray',
59 | alignSelf: 'center',
60 | fontWeight: '600',
61 | marginTop: 4
62 | }
63 |
64 | });
--------------------------------------------------------------------------------
/screens/common/playlist.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/22/16.
3 | */
4 | import React, {Component} from 'react';
5 | import {
6 | View,
7 | Text,
8 | ScrollView,
9 | StyleSheet
10 | } from 'react-native';
11 |
12 | import PlaylistItem from './playlist-item';
13 |
14 | export default class PlayList extends Component {
15 |
16 |
17 | renderItems() {
18 | const {circle} = this.props;
19 | return this.props.items.map((a, i ) => {
20 | return (
21 |
22 | )
23 | })
24 |
25 | }
26 | render() {
27 | const {title} = this.props;
28 |
29 | return (
30 |
31 |
32 | {title}
33 |
34 | {this.renderItems()}
35 |
36 |
37 |
38 | )
39 | }
40 | }
41 |
42 | const styles = StyleSheet.create({
43 | container: {
44 | flexDirection: 'row',
45 | paddingTop: 24,
46 | paddingBottom: 56
47 | },
48 |
49 | title: {
50 | alignSelf: 'center',
51 | color: 'white',
52 | fontSize: 16,
53 | fontWeight: '700',
54 | marginBottom: 8,
55 | }
56 | });
--------------------------------------------------------------------------------
/screens/common/tab-bar-navigation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/23/16.
3 | */
4 | import React, {Component} from 'react';
5 | import {
6 | Animated,
7 | View,
8 | Text,
9 | TouchableWithoutFeedback,
10 | StyleSheet
11 | } from 'react-native';
12 |
13 | import D from './dimensions';
14 | import {Ionicons} from '@exponent/vector-icons';
15 |
16 | export default class tabBarNavigation extends Component {
17 | state = {
18 | selected: 0,
19 | translateY: new Animated.Value(0)
20 | };
21 |
22 | details = ['Home', 'Browse', 'Search', 'Radio', 'Library'];
23 | icons = ['ios-home', 'ios-albums', 'ios-search', 'ios-radio-outline', 'ios-book-outline'];
24 | renderIcons() {
25 | return this.details.map((item, i) => {
26 | const color = this.state.selected == i ? '#41b177' : '#bdbec2';
27 | return (
28 | this.setState({selected: i})} key={i}>
29 |
30 |
31 | {item}
32 |
33 |
34 | )
35 | })
36 | }
37 |
38 | setHeight(h) {
39 | // console.log('setting height:', h);
40 | this.state.translateY.setValue(Math.abs((h/10) - 56));
41 |
42 | }
43 |
44 | hide() {
45 | Animated.timing(
46 | this.state.translateY,
47 | {toValue: 56}
48 | ).start();
49 |
50 | }
51 |
52 | show() {
53 | Animated.timing(
54 | this.state.translateY,
55 | {toValue: 0}
56 | ).start();
57 |
58 | }
59 |
60 |
61 | render() {
62 | const {translateY} = this.state;
63 | return (
64 |
65 | {this.renderIcons()}
66 |
67 | )
68 | }
69 | }
70 |
71 | const styles = StyleSheet.create({
72 | container: {
73 | position:'absolute',
74 | borderTopColor: '#211f20',
75 | borderTopWidth: 3,
76 | flexDirection: 'row',
77 | alignItems: 'center',
78 | justifyContent: 'space-around',
79 | width: D.width,
80 | bottom: 0,
81 | height: 56,
82 | backgroundColor: '#222327'
83 | },
84 |
85 | tab_item: {
86 | alignItems: 'center',
87 | },
88 |
89 | text: {
90 | fontSize: 10,
91 | }
92 | })
--------------------------------------------------------------------------------
/screens/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by ggoma on 12/23/16.
3 | */
4 | import React, { Component } from 'react';
5 | import {
6 | View,
7 | StyleSheet,
8 | Navigator
9 | } from 'react-native';
10 |
11 | import Landing from './Landing';
12 | import Browse from './Browse';
13 |
14 |
15 | import Footer from './common/footer';
16 | import TabBarNavigation from './common/tab-bar-navigation';
17 |
18 | var ROUTES = {
19 | landing: Landing,
20 | browser: Browse
21 | };
22 |
23 | export default class Main extends Component {
24 | renderScene(route, navigator) {
25 | var Component = ROUTES[route.name];
26 |
27 | return
28 | }
29 |
30 | configureScene(route) {
31 | if (route.sceneConfig) {
32 | return route.sceneConfig;
33 | }
34 | return {
35 | ...CustomNavigatorSceneConfigs.FloatFromRight,
36 | gestures: {}
37 | };
38 | }
39 |
40 | render() {
41 | return (
42 |
43 | ({ ... Navigator.SceneConfigs.VerticalDownSwipeJump, gestures: false })}
47 | style={styles.container}
48 | />
49 |
55 | )
56 | }
57 | }
58 |
59 | var styles = StyleSheet.create({
60 | container: {
61 | flex: 1
62 | }
63 | });
--------------------------------------------------------------------------------
/utilities/__tests__/cacheAssetsAsync-test.js:
--------------------------------------------------------------------------------
1 | describe('example test', () => {
2 | it('works', () => {
3 | expect(1).toBe(1);
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/utilities/cacheAssetsAsync.js:
--------------------------------------------------------------------------------
1 | import {
2 | Image,
3 | } from 'react-native';
4 | import {
5 | Asset,
6 | Font,
7 | } from 'exponent';
8 |
9 | export default function cacheAssetsAsync({images = [], fonts = []}) {
10 | return Promise.all([
11 | ...cacheImages(images),
12 | ...cacheFonts(fonts),
13 | ]);
14 | }
15 |
16 | function cacheImages(images) {
17 | return images.map(image => {
18 | if (typeof image === 'string') {
19 | return Image.prefetch(image);
20 | } else {
21 | return Asset.fromModule(image).downloadAsync();
22 | }
23 | });
24 | }
25 |
26 | function cacheFonts(fonts) {
27 | return fonts.map(font => Font.loadAsync(font));
28 | }
29 |
--------------------------------------------------------------------------------