├── .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 | ![img](https://github.com/ggomaeng/public/blob/main/spurwing-cover.png?raw=true) 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 | ![demo](https://raw.githubusercontent.com/ggomaeng/react-native-spotify-ui/master/demo.gif) 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 |