Repository: SKempin/Lyrics-King-React-Native
Branch: master
Commit: 15918ffb1585
Files: 21
Total size: 41.5 KB
Directory structure:
gitextract_37vbbbyq/
├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .travis.yml
├── App.js
├── App.test.js
├── LICENSE
├── README.md
├── _design_assets/
│ └── adobeXD/
│ └── Lyrics King App.xd
├── app.json
├── components/
│ ├── Credits.js
│ ├── SocialButton.js
│ └── Suggestions.js
├── config/
│ ├── colours.js
│ └── router.js
├── lib/
│ └── constants.js
├── package.json
├── screens/
│ ├── AboutScreen.js
│ ├── DetailsScreen.js
│ └── SearchScreen.js
└── utils/
└── shareHelper.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
================================================
FILE: .eslintrc.json
================================================
{
"extends": "airbnb",
"env": {
"browser": true,
"es6": true,
"jest/globals": true
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".android.js", ".ios.js"]
}
}
},
"plugins": ["jest"],
"parser": "babel-eslint",
"rules": {
"no-use-before-define": 0,
"import/prefer-default-export": 0,
"react/jsx-filename-extension": [
"error",
{
"extensions": [".js", ".jsx"]
}
],
"react/prefer-stateless-function": 0,
"react/jsx-indent-props": ["error", 2],
"react/jsx-indent": ["error", 2],
"consistent-return": 0,
"max-len": 0,
"react/forbid-prop-types": 0,
"no-nested-ternary": 0,
"no-console": 0,
"no-mixed-operators": 0,
"react/no-array-index-key": 0,
"camelcase": 0,
"no-underscore-dangle": 0,
"react/sort-comp": 0,
"no-return-assign": 0,
"comma-dangle": ["error", "never"]
}
}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
*/*.DS_Store
*.DS_Store
# expo
.expo/
# dependencies
/node_modules
# misc
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
.vscode
================================================
FILE: .travis.yml
================================================
#
# Travis CI config
# https://docs.travis-ci.com/user/customizing-the-build/
#
env:
- COVERALLS_ENV=production
branches:
only:
- master
language: node_js
node_js:
- "11.2.0"
cache:
directories:
- "node_modules"
script:
- npm run lint
================================================
FILE: App.js
================================================
import React from 'react';
import Sentry from 'sentry-expo';
import { createAppContainer } from 'react-navigation';
import { RootStack } from './config/router';
// Remove this once Sentry is correctly setup.
Sentry.enableInExpoDevelopment = false;
Sentry.config(
'https://705f92b0edb44599b814955f3219c1cd@sentry.io/1367138',
).install();
// App Containers
const AppContainer = createAppContainer(RootStack);
export default class App extends React.Component {
render() {
return <AppContainer />;
}
}
================================================
FILE: App.test.js
================================================
import React from 'react';
import renderer from 'react-test-renderer';
import App from './App';
it('renders without crashing', () => {
const rendered = renderer.create(<App />).toJSON();
expect(rendered).toBeTruthy();
});
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Stephen Kempin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Lyrics King <img src="_github/lk-logo.gif" width="80">



[](https://travis-ci.org/SKempin/Lyrics-King-React-Native)
[](https://github.com/expo/awesome-expo)
A [React Native](https://facebook.github.io/react-native/) native app utilising [Expo](https://expo.io/), [React Navigation](https://reactnavigation.org) and fetching data from multiple API's ([Deezer](https://developers.deezer.com/) and [Lyrics.OVH](https://www.lyrics.ovh)). UI built with [Adobe XD](https://www.adobe.com/uk/products/xd.html).
<br><br>
Built as a personal training project for [React Native](https://facebook.github.io/react-native/). Designed in [Adobe XD](https://www.adobe.com/uk/products/xd.html). Design and development by [Stephen Kempin](https://www.stephenkempin.co.uk). This project was bootstrapped with [Create React Native App](https://github.com/react-community/create-react-native-app).
### [Expo Demo Link](https://expo.io/@skempin/lyrics-king)
<img src="https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/header-overview.jpg" width="900" alt="Lyrics King - React Native Expo app">
## Contents
- [App Preview](#app-preview)
- [Video Preview](#video-preview)
- [Search Screen](#search-screen)
- [Details Screen](#details-screen)
- [About Screen](#about-screen)
- [Navigation (Drawer)](#navigation-drawer)
- [Expo Project Page](#expo-project-page)
- [Adobe XD files](#adobe-xd-files)
- [App Features](#app-features)
- [Screens](#screens)
- [Components](#components)
- [Config](#config)
- [Lib](#lib)
- [Utils](#utils)
- [Getting Started](#getting-started)
- [What's Included](#whats-included)
- [API's Used](#apis-used)
- [Contributing](#contributing)
- [Author](#author)
- [Google Play Store](#google-play-store)
- [Donate](#donate)
- [License](#license)
## App Preview
### Video Preview
<a href="https://expo.io/@skempin/lyrics-king">
<img src="https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/screenshots/video.gif" width="350" >
</a>
### Search Screen
<img src="https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/screenshots/search.jpg" width="270" alt="Lyrics King - Search screen" hspace="5"><img src="https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/screenshots/suggestions.jpg" width="270" alt="Lyrics King - Suggestions on search screen">
### Details Screen
<img src="https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/screenshots/details-ariana.jpg" width="270" hspace="5" alt="Lyrics King - Details screen, Ariana Grande"><img src="https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/screenshots/details-above.jpg" width="270" hspace="5" alt="Lyrics King - Details screen, Above and Beyond"><img src="https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/screenshots/details-dua.jpg" width="270" alt="Lyrics King - Details screen, Dua Lipa">
### About Screen
<img src="https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/screenshots/about.jpg" width="270" alt="Lyrics King - About screen">
### Navigation (Drawer)
<img src="https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/screenshots/navigation.jpg" width="270" alt="Lyrics King - Navigation drawer">
## [Expo Project Page](https://expo.io/@skempin/lyrics-king)
This project has been built using [Expo](https://expo.io/). Please install `npm install expo-cli --global` to run this project locally.
Scan the below QR code to open the project on Android:

<br>
## Adobe XD files
Design files for the UI can be found in `_design_assets/adobeXD` in the project root. UI design implemented with [flexbox](https://docs.expo.io/versions/latest/react-native/flexbox).
## App Features
### Screens
`src/screens/`
- `SearchScreen.js` - Search the [Deezer API](https://developers.deezer.com/) by song title (_class component_)
- `DetailsScreen.js` - Selected song details (including [Lyrics.ovh](https://www.lyrics.ovh/) API call) (_class component_)
- `AboutScreen.js` - About details (_functional component_)
### Components
`src/components/`
- `Credits.js` - Development credentials template (_functional component_)
- `SocialButton.js` - Button template for sharing links/ the app (_functional component_)
- `Suggestions.js` - Song suggestions (_functional component_)
### Config
`src/config/`
- `router.js` - App navigation routing (including drawer nav render method)
- `colours.js` - Colour constants
### Lib
`src/lib/`
- `constants.js` - Expo manifest [constants](https://docs.expo.io/versions/latest/sdk/constants#__next) and functions
### Utils
`src/utils/`
- `shareHelper.js` - Native device [share method](https://docs.expo.io/versions/latest/react-native/share)
## Getting Started
1. Install the latest Node
2. Install [Expo](https://expo.io/) - `npm install expo-cli --global`
3. `cd` into this project directory
4. `npm install` or `yarn install`
5. Run `expo start`
## What's Included
| Name | Description |
| :----------------------------------------------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Expo (incl. React Native)](https://expo.io/) | Expo is a free and open source toolchain built around React Native to help you build native iOS and Android projects using JavaScript and React. |
| [React Navigation](https://reactnavigation.org/) | Routing and navigation for your React Native apps. |
| [Format Duration](https://github.com/hypermodules/format-duration) | Convert a number in milliseconds to a standard duration string. |
| [RN-Placeholder](https://github.com/mfrachet/rn-placeholder) | Display some placeholder stuff before rendering your text or media content in React Native. |
## API's Used
- [Deezer](https://developers.deezer.com/)
- [Lyrics.OVH](https://api.lyrics.ovh)
## Contributing
Due to time constraints there are several features that I haven’t been able to develop yet. If you would like to develop your React Native skills and contribute any of the features below this would be hugely beneficial! :tada:
- [x] [Debouncing or throttling](https://www.peterbe.com/plog/how-to-throttle-and-debounce-an-autocomplete-input-in-react) on search functionality.
- [x] [PropTypes](https://reactjs.org/docs/typechecking-with-proptypes.html) on components.
- [ ] Adding clear search button functionality on Android. This functionality [already exists on iOS](https://facebook.github.io/react-native/docs/textinput#clearbuttonmode).
- [ ] [Animations](https://docs.expo.io/versions/latest/react-native/animations) would be a nice touch! Fading in the details screen background image would be priority.
- [ ] Any general performance improvements.
Other contributions and suggestions are always very welcome! [Contact me](https://www.stephenkempin.co.uk) if you wish to discuss anything.
## Author
[Stephen Kempin](https://www.stephenkempin.co.uk)
[Lyrics King Project Github](https://github.com/SKempin/Lyrics-King-React-Native)
## Google Play Store
View my commercial apps on the [SK-UK Google Play Store](https://play.google.com/store/apps/developer?id=SK+-+UK)
<a href='https://play.google.com/store/apps/developer?id=SK+-+UK&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='SK-UK Google Play Store' src='https://github.com/SKempin/Lyrics-King-React-Native/blob/master/_github/google-play.jpg' width='180px'></a>
## Donate
If you like this project and wish to say to say thanks - I'm always open to a coffee! :coffee:
<a href="https://www.buymeacoffee.com/oru9CZh" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/black_img.png" alt="Buy Me A Coffee" width='180px' ></a>
## License
[MIT](https://github.com/SKempin/Lyrics-King-React-Native/blob/master/LICENSE)
You are welcome to use this however you wish within the MIT license, but please retain [my credentials](https://www.stephenkempin.co.uk/) and links back to [this repo](https://github.com/SKempin/Lyrics-King-React-Native).
================================================
FILE: app.json
================================================
{
"expo": {
"name": "Lyrics King",
"description": "Lyrics King is a lyrics search app, fetching data from Deezer an Lyrics.ovh. Designed with Adobe XD and built with Expo, by Stephen Kempin. https://github.com/SKempin/Lyrics-King-React-Native",
"icon": "./assets/images/icon.png",
"slug": "lyrics-king",
"sdkVersion": "32.0.0",
"platforms": ["ios", "android"],
"githubUrl":"https://github.com/SKempin/Lyrics-King-React-Native",
"orientation": "portrait",
"privacy": "public",
"primaryColor": "#07CCBA",
"splash": {
"backgroundColor": "#191919 ",
"image": "./assets/images/splash.png",
"resizeMode": "cover"
},
"extra": {
"appName": "Lyrics King",
"developerName": "SK-UK",
"googleAnalytics": "UA-131961084",
"social": {
"expoApp": "https://expo.io/@skempin/lyrics-king",
"github": "https://github.com/SKempin/Lyrics-King-React-Native",
"googlePlayStore": "https://play.google.com/store/apps/developer?id=SK+-+UK",
"iTunesStore": "https://www.stephenkempin.co.uk",
"portfolio": "https://www.stephenkempin.co.uk"
}
}
}
}
================================================
FILE: components/Credits.js
================================================
import React from 'react';
import {
Text,
TouchableOpacity,
StyleSheet,
Linking,
Image
} from 'react-native';
import Proptypes from 'prop-types';
import * as Expo from 'expo';
import { Analytics, Event } from 'expo-analytics';
import SK from '../assets/images/SK.png';
// Config
import colours from '../config/colours';
// Constants
const { portfolio } = Expo.Constants.manifest.extra.social;
const ID = Expo.Constants.manifest.extra.googleAnalytics;
// GA tracking
const analytics = new Analytics(ID);
const Credits = props => (
<TouchableOpacity
style={styles.creditsContainer}
onPress={() => Linking.openURL(portfolio).then(() => {
Expo.Amplitude.logEvent(`BUTTON: Credits - ${props.screen} Screen`);
analytics.event(
new Event('Button', 'Tap', `Credits - ${props.screen} Screen`)
);
})
}
>
<Image source={SK} style={styles.creditsImage} />
<Text style={styles.creditsText}>
Design and development
{'\n'}
{' '}
by Stephen Kempin
</Text>
</TouchableOpacity>
);
Credits.propTypes = {
screen: Proptypes.string.isRequired
};
export default Credits;
// Styles
const styles = StyleSheet.create({
creditsContainer: {
flexDirection: 'row',
alignItems: 'center'
},
creditsText: {
fontSize: 12,
color: colours.secondaryGrey,
textAlign: 'left',
paddingLeft: 20
},
creditsImage: {
width: 30,
height: 30,
opacity: 0.2,
alignSelf: 'flex-start'
}
});
================================================
FILE: components/SocialButton.js
================================================
import React from 'react';
import {
Text,
TouchableOpacity,
StyleSheet,
Linking,
Image
} from 'react-native';
import * as Expo from 'expo';
import PropTypes from 'prop-types';
/* eslint-disable import/no-extraneous-dependencies */
import { Entypo, EvilIcons } from '@expo/vector-icons';
/* eslint-enable import/no-extraneous-dependencies */
import { Analytics, Event } from 'expo-analytics';
import SK from '../assets/images/SK.png';
// Config
import colours from '../config/colours';
// Helper functions
import handleShare from '../utils/shareHelper';
// Constants
const { ...extra } = Expo.Constants.manifest.extra;
const ID = Expo.Constants.manifest.extra.googleAnalytics;
// GA tracking
const analytics = new Analytics(ID);
const SocialButton = ({
label, url, screen, icon
}) => (
<TouchableOpacity
style={styles.button}
onPress={() => (label !== 'Share'
? Linking.openURL(url).then(() => {
Expo.Amplitude.logEvent(`BUTTON: ${label}`);
analytics.event(new Event('Button', 'Tap', `${label}`));
})
: handleShare(
`Check out ${extra.appName} on Expo today!`,
`${extra.social.expoApp}`,
`${extra.appName}`,
`${screen}`
))
}
>
{icon ? (
<Entypo name={icon} size={20} style={styles.icon_left} />
) : (
<Image source={SK} style={styles.icon_sk} />
)}
<Text style={styles.label}>
{label}
</Text>
<EvilIcons name="chevron-right" size={34} style={styles.icon_right} />
</TouchableOpacity>
);
SocialButton.propTypes = {
icon: PropTypes.string,
label: PropTypes.string.isRequired,
url: PropTypes.string,
screen: PropTypes.string.isRequired
};
SocialButton.defaultProps = {
icon: '',
url: ''
};
export default SocialButton;
// Styles
const styles = StyleSheet.create({
button: {
padding: 10,
backgroundColor: colours.tertiaryBlack,
flex: 1,
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'center',
justifyContent: 'flex-start',
marginBottom: 10
},
icon_left: {
marginRight: 15,
color: colours.primaryGrey
},
icon_sk: {
marginRight: 15,
opacity: 0.5,
width: 22,
height: 22
},
label: {
flex: 1,
color: colours.primaryGrey
},
icon_right: {
color: colours.primaryGrey,
justifyContent: 'flex-end',
opacity: 0.4
}
});
================================================
FILE: components/Suggestions.js
================================================
import React from 'react';
import {
Image,
FlatList,
Text,
TouchableOpacity,
Keyboard,
StyleSheet,
View
} from 'react-native';
import PropTypes from 'prop-types';
/* eslint-disable import/no-extraneous-dependencies */
import { EvilIcons } from '@expo/vector-icons';
/* eslint-enable import/no-extraneous-dependencies */
// Config
import colours from '../config/colours';
const Suggestions = ({ results, navigation }) => (
<FlatList
onScrollBeginDrag={Keyboard.dismiss}
data={results}
styles={{ alignSelf: 'stretch' }}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.suggestionItem}
onPress={() => navigation.navigate('Details', { ...item })}
>
<Image
style={styles.image}
source={{ uri: item.artist.picture_medium }}
/>
<View numberOfLines={1} style={styles.detailsContainer}>
<Text numberOfLines={1} style={styles.songTitle}>
{item.title_short}
</Text>
<Text numberOfLines={1} style={styles.artistDetails}>
{item.artist.name}
</Text>
<Text numberOfLines={1} style={styles.artistDetails}>
{item.album.title}
</Text>
</View>
<EvilIcons name="chevron-right" size={54} color="#333" />
</TouchableOpacity>
)}
keyExtractor={(item, index) => index.toString()}
/>
);
Suggestions.propTypes = {
results: PropTypes.array.isRequired,
navigation: PropTypes.object.isRequired
};
export default Suggestions;
// Styles
const styles = StyleSheet.create({
suggestionItem: {
flex: 1,
flexDirection: 'row',
flexWrap: 'nowrap',
backgroundColor: colours.tertiaryBlack,
elevation: 1,
justifyContent: 'flex-start',
alignItems: 'center',
paddingTop: 12,
paddingBottom: 12,
paddingLeft: 18,
paddingRight: 12,
marginLeft: 14,
marginRight: 14,
marginTop: 0,
marginBottom: 10
},
image: {
width: 66,
height: 66,
borderRadius: 66 / 2,
alignSelf: 'center',
borderColor: colours.primaryWhite,
borderWidth: 2,
marginRight: 17,
flex: 0
},
detailsContainer: {
width: 145,
marginRight: 20
},
songTitle: {
color: 'white',
paddingBottom: 2
},
artistDetails: {
color: colours.primaryGrey,
paddingBottom: 2
}
});
================================================
FILE: config/colours.js
================================================
// Colours
const colours = {
primaryBlack: '#101010',
secondaryBlack: '#1D1D1D',
tertiaryBlack: '#141414',
highlightBlack: '#0B0B0B',
primaryWhite: '#fff',
primaryTeal: '#07CCBA',
primaryGrey: '#AAAAAA',
secondaryGrey: '#3E3E3E'
};
export default colours;
================================================
FILE: config/router.js
================================================
import React from 'react';
import {
createDrawerNavigator,
createStackNavigator,
DrawerItems
} from 'react-navigation';
import * as Expo from 'expo';
/* eslint-disable import/no-extraneous-dependencies */
import { Ionicons } from '@expo/vector-icons';
/* eslint-enable import/no-extraneous-dependencies */
import {
StyleSheet,
Image,
ScrollView,
SafeAreaView,
View
} from 'react-native';
import Logo from '../assets/images/lk-logo.png';
// Components
import SocialButton from '../components/SocialButton';
// Helper functions
import handleShare from '../utils/shareHelper';
// Config
import colours from './colours';
// Constants
import { socialLinks } from '../lib/constants';
// Screens
import SearchScreen from '../screens/SearchScreen';
import DetailsScreen from '../screens/DetailsScreen';
import AboutScreen from '../screens/AboutScreen';
const { ...extra } = Expo.Constants.manifest.extra;
// Main stack
export const MainStack = createStackNavigator({
Search: {
screen: SearchScreen,
navigationOptions: ({ navigation }) => ({
title: null,
headerTransparent: true,
headerLeft: (
<Ionicons
name="md-menu"
size={26}
style={{ marginLeft: 10, padding: 10 }}
color={colours.secondaryGrey}
onPress={() => navigation.openDrawer()}
/>
)
})
},
Details: {
screen: DetailsScreen,
navigationOptions: ({ navigation }) => ({
headerStyle: {
borderBottomWidth: 0,
backgroundColor: 'rgba(0,0,0,0.2)',
elevation: 0
},
headerTransparent: true,
headerTintColor: 'rgba(255,255,255,0.7)',
headerRight: (
<Ionicons
name="md-share"
title="Share"
size={24}
style={{ marginRight: 10, padding: 10 }}
color="rgba(255,255,255,0.7)"
onPress={() => handleShare(
`Check out lyrics for ${navigation.state.params.title} by ${
navigation.state.params.artist.name
} on ${extra.appName}!`,
`${extra.social.expoApp}`,
`${extra.appName}`,
'Details'
)
}
/>
)
})
}
});
// About stack
export const AboutStack = createStackNavigator({
About: {
screen: AboutScreen,
navigationOptions: ({ navigation }) => ({
title: null,
headerTransparent: true,
headerLeft: (
<Ionicons
name="md-menu"
size={26}
style={{ marginLeft: 10, padding: 10 }}
color={colours.secondaryGrey}
onPress={() => navigation.openDrawer()}
/>
)
})
}
});
// =====================================================
// Side drawer
export const RootStack = createDrawerNavigator(
{ Search: { screen: MainStack }, About: { screen: AboutStack } },
{
contentComponent: props => (
<ScrollView style={{ backgroundColor: colours.primaryBlack }}>
<SafeAreaView
style={styles.container}
forceInset={{ top: 'always', horizontal: 'never' }}
>
<Image style={styles.logo} source={Logo} />
<DrawerItems
{...props}
activeTintColor={colours.primaryWhite}
activeBackgroundColor={colours.highlightBlack}
inactiveTintColor={colours.secondaryGrey}
itemStyle={styles.itemStyle}
/>
<View style={styles.socialLinksContainer}>
{socialLinks.map((socialLink, index) => (
<SocialButton
key={socialLink.label + index}
icon={socialLink.icon}
label={socialLink.label}
url={socialLink.url}
screen="About"
/>
))}
</View>
</SafeAreaView>
</ScrollView>
)
}
);
// Styles
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 50
},
logo: {
width: 150,
height: 150,
alignSelf: 'center',
marginBottom: 30
},
itemStyle: {
borderLeftWidth: 3,
borderLeftColor: colours.primaryTeal
},
socialLinksContainer: {
marginLeft: 20,
marginRight: 20,
marginTop: 45,
fontSize: 4,
paddingTop: 30,
borderTopColor: colours.secondaryGrey,
borderTopWidth: 1
}
});
================================================
FILE: lib/constants.js
================================================
import * as Expo from 'expo';
const { ...social } = Expo.Constants.manifest.extra.social;
// Expo constants
export const { expoVersion } = Expo.Constants;
// export const iOSBuild = Constants.platform.ios.buildNumber;
export const { manifest } = Expo.Constants;
// functions
export const getCurrentYear = new Date().getFullYear();
// Social Links
export const socialLinks = [
{
icon: 'google-play',
label: 'SK-UK Play Store',
url: social.googlePlayStore
},
{ icon: 'app-store', label: 'SK-UK iTunes Store', url: social.iTunesStore },
{ icon: 'github', label: 'Github Project', url: social.github },
{ icon: '', label: 'SK Portfolio', url: social.portfolio }
];
================================================
FILE: package.json
================================================
{
"name": "lyrics-king",
"version": "1.0.3",
"private": true,
"author": "Stephen Kempin (https://stephenkempin.co.uk/)",
"license": "MIT",
"keywords": [
"expo",
"react-native",
"react",
"es6",
"android",
"ios"
],
"devDependencies": {
"babel-eslint": "^10.0.1",
"eslint": "^5.3.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jest": "^22.2.2",
"eslint-plugin-jsx-a11y": "^6.2.0",
"eslint-plugin-react": "^7.12.4",
"jest-expo": "^32.0.0",
"react-test-renderer": "16.3.1"
},
"main": "./node_modules/expo/AppEntry.js",
"scripts": {
"start": "react-native-scripts start",
"eject": "react-native-scripts eject",
"android": "react-native-scripts android",
"ios": "react-native-scripts ios",
"lint": "./node_modules/.bin/eslint .",
"test": "jest"
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"expo": "^32.0.0",
"expo-analytics": "^1.0.7",
"format-duration": "^1.3.1",
"prop-types": "^15.6.2",
"react": "16.14.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz",
"react-navigation": "^3.0.9",
"rn-placeholder": "^1.3.0",
"sentry-expo": "^1.11.0",
"throttle-debounce": "^2.1.0"
}
}
================================================
FILE: screens/AboutScreen.js
================================================
import React from 'react';
import {
Text,
View,
StyleSheet,
ScrollView,
Image,
SafeAreaView,
StatusBar
} from 'react-native';
import * as Expo from 'expo';
// Config
import { Analytics, ScreenHit } from 'expo-analytics';
import colours from '../config/colours';
// Components
import SocialButton from '../components/SocialButton';
import { getCurrentYear, socialLinks, manifest } from '../lib/constants';
import SK_LOGO from '../assets/images/lk-logo.png';
// Constants
const { ...meta } = Expo.Constants.manifest;
// GA tracking
const ID = Expo.Constants.manifest.extra.googleAnalytics;
const analytics = new Analytics(ID);
export default class AboutScreen extends React.Component {
componentDidMount() {
Expo.Amplitude.logEvent('SCREEN: About');
analytics.hit(new ScreenHit('SCREEN: About'));
}
render() {
return (
<SafeAreaView style={styles.safeView}>
<StatusBar barStyle="light-content" />
<ScrollView style={styles.container}>
<View style={{ flex: 1 }}>
<Text style={styles.headingText}>
Social
</Text>
{socialLinks.map((socialLink, index) => (
<SocialButton
key={socialLink.label + index}
icon={socialLink.icon}
label={socialLink.label}
url={socialLink.url}
screen="About Screen"
/>
))}
<Text style={styles.headingText}>
Share
</Text>
<SocialButton icon="share" label="Share" screen="About" />
<Text style={styles.headingText}>
Donate
</Text>
<SocialButton
icon="heart"
label="Buy me a coffee to say thanks!"
url="https://www.buymeacoffee.com/oru9CZh"
screen="About Screen"
/>
<Text style={styles.headingText}>
App Info
</Text>
<Image style={styles.logo} source={SK_LOGO} />
<View style={styles.detailsContainer}>
<Text style={styles.detailsContainerText}>
{`${meta.extra.appName}: ${manifest.version}`}
</Text>
<Text style={styles.detailsContainerText}>
©
{' '}
{getCurrentYear}
{' '}
{meta.extra.developerName}
. Design &
development by
{' '}
{meta.extra.developerName}
.
</Text>
<Text style={styles.detailsContainerText}>
Expo SDK:
{' '}
{meta.sdkVersion}
</Text>
<Text style={styles.detailsContainerText}>
Released under MIT licence.
</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}
}
// Styles
const styles = StyleSheet.create({
safeView: {
flex: 1,
backgroundColor: colours.primaryBlack
},
container: {
flex: 1,
backgroundColor: colours.primaryBlacks,
flexDirection: 'column',
paddingLeft: 30,
paddingRight: 30,
paddingTop: 40
},
headingText: {
color: colours.primaryTeal,
fontSize: 20,
marginBottom: 20,
marginTop: 30,
fontWeight: '300'
},
detailsContainer: {
paddingBottom: 20
},
detailsContainerText: {
color: colours.primaryGrey,
fontSize: 12,
paddingBottom: 6
},
heading: {
paddingTop: 50,
paddingBottom: 15,
backgroundColor: colours.primaryBlack
},
logo: {
width: 76,
height: 76,
marginBottom: 20
}
});
================================================
FILE: screens/DetailsScreen.js
================================================
import React from 'react';
import {
StyleSheet,
Text,
Image,
View,
ScrollView,
ImageBackground
} from 'react-native';
import * as Expo from 'expo';
import PropTypes from 'prop-types';
import format from 'format-duration';
import Placeholder from 'rn-placeholder';
import { Analytics, ScreenHit } from 'expo-analytics';
// Config
import colours from '../config/colours';
// Components
import Credits from '../components/Credits';
// GA tracking
const ID = Expo.Constants.manifest.extra.googleAnalytics;
const analytics = new Analytics(ID);
export default class DetailsScreen extends React.Component {
static get propTypes() {
return {
navigation: PropTypes.object.isRequired
};
}
constructor(props) {
super(props);
this.state = { lyrics: null, /* err: null, */ isReady: null };
}
async componentDidMount() {
const {
navigation: {
state: {
params: { title, artist }
}
}
} = this.props;
Expo.Amplitude.logEvent(`SCREEN - Details: ${title} by ${artist.name}`);
analytics.hit(
new ScreenHit(`SCREEN - Details: ${title} by ${artist.name}`)
);
const lyricsQuery = `${artist.name}/${title}`;
this.getLyrics(lyricsQuery);
}
// fix issue #5 - setting state on unmounted component
componentWillUnmount() {
this.isCancelled = true;
}
getLyrics = async (lyricsQuery) => {
try {
const res = await fetch(`https://api.lyrics.ovh/v1/${lyricsQuery}`);
const response = await res.json();
if (!this.isCancelled) this.setState({ lyrics: response.lyrics, isReady: true });
} catch (e) {
console.log(e); // this.setState({ err: e.message });
}
};
displayLyrics() {
const { lyrics, isReady } = this.state;
if (!lyrics) {
return (
<Text style={{ color: colours.primaryWhite }}>
Sorry, no lyrics can be found for this song.
</Text>
);
}
return (
<Placeholder.Paragraph
lineNumber={4}
textSize={12}
lineSpacing={7}
color="#242424"
width="60%"
lastLineWidth="80%"
firstLineWidth="30%"
onReady={isReady}
>
<Text style={styles.lyrics}>
{lyrics}
</Text>
</Placeholder.Paragraph>
);
}
render() {
const {
navigation: {
state: {
params: {
title, artist, album, duration
}
}
}
} = this.props;
return (
<ScrollView style={styles.container}>
<View style={{ flex: 1 }}>
<ImageBackground
source={{ uri: artist.picture_xl }}
style={styles.backgroundImage}
>
<Expo.LinearGradient
colors={['transparent', colours.primaryBlack]}
locations={[0.4, 1.2]}
style={styles.gradient}
/>
<View
style={{
flexDirection: 'column',
alignSelf: 'flex-end',
paddingBottom: 40,
paddingLeft: 19
}}
>
<Text style={styles.artistHeading}>
{artist.name}
</Text>
<Text style={styles.songHeading}>
{title}
</Text>
</View>
</ImageBackground>
</View>
<View style={{ flex: 1, paddingLeft: 19, paddingRight: 19 }}>
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-start',
marginBottom: 30
}}
>
<Image
style={styles.albumImage}
source={{ uri: album.cover_medium }}
/>
<View
style={{
flexDirection: 'column',
flex: 1,
alignSelf: 'center',
paddingRight: 10
}}
>
<Text style={styles.detailsHeading}>
Album
</Text>
<Text style={styles.details}>
{album.title}
</Text>
<Text style={styles.detailsHeading}>
Duration
</Text>
<Text style={styles.details}>
{format(duration * 1000)}
</Text>
</View>
</View>
{this.displayLyrics()}
</View>
<View style={styles.creditsContainer}>
<Credits screen="Details" />
</View>
</ScrollView>
);
}
}
// Styles
const styles = StyleSheet.create({
container: {
backgroundColor: colours.primaryBlack,
flex: 1
},
backgroundImage: { flex: 1, minHeight: 360, flexDirection: 'row' },
gradient: {
backgroundColor: 'transparent',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0
},
artistHeading: {
color: colours.primaryWhite,
fontSize: 35,
lineHeight: 35,
fontWeight: '300',
paddingBottom: 7,
shadowOpacity: 0.6,
shadowRadius: 3,
shadowOffset: {
height: 0,
width: 0
}
},
songHeading: {
color: colours.primaryWhite,
fontSize: 45,
lineHeight: 45,
fontWeight: 'bold',
paddingBottom: 0,
shadowOpacity: 0.6,
shadowRadius: 3,
shadowOffset: {
height: 0,
width: 0
}
},
albumImage: {
width: 130,
height: 130,
borderRadius: 130 / 2,
borderWidth: 3,
borderColor: colours.primaryWhite,
marginRight: 25
},
detailsHeading: { color: colours.primaryGrey, marginBottom: 3 },
details: {
color: colours.primaryWhite,
fontWeight: 'bold',
marginBottom: 15,
fontSize: 16
},
lyrics: {
color: colours.primaryWhite,
lineHeight: 22,
paddingBottom: 20
},
creditsContainer: {
flex: 1,
alignSelf: 'center',
paddingTop: 40,
paddingBottom: 30
}
});
================================================
FILE: screens/SearchScreen.js
================================================
import React from 'react';
import {
StyleSheet,
TextInput,
View,
Image,
Keyboard,
SafeAreaView,
StatusBar,
TouchableWithoutFeedback
} from 'react-native';
import * as Expo from 'expo';
import PropTypes from 'prop-types';
/* eslint-disable import/no-extraneous-dependencies */
import { EvilIcons } from '@expo/vector-icons';
/* eslint-enable import/no-extraneous-dependencies */
import { Analytics, ScreenHit } from 'expo-analytics';
// search throlle and debounce
import { throttle, debounce } from 'throttle-debounce';
import LK_LOGO from '../assets/images/lk-logo.png';
import SK from '../assets/images/SK.png';
// Config
import colours from '../config/colours';
// Components
import Suggestions from '../components/Suggestions';
import Credits from '../components/Credits';
// Cache images
function cacheImages(images) {
return images.map((image) => {
if (typeof image === 'string') {
return Image.prefetch(image);
}
return Expo.Asset.fromModule(image).downloadAsync();
});
}
// GA tracking
const ID = Expo.Constants.manifest.extra.googleAnalytics;
const analytics = new Analytics(ID);
export default class SearchScreen extends React.Component {
static get propTypes() {
return {
navigation: PropTypes.object.isRequired
};
}
constructor(props) {
super(props);
this.state = { results: [], text: null, showLogo: true };
this.throttleSearch = throttle(400, this.getInfo);
this.debounceSearch = debounce(700, this.getInfo);
this.cache = {}; // caching autocomplete results
}
componentDidMount() {
Expo.Amplitude.initialize('6460727d017e832e2083e13916c7c9e5');
Expo.Amplitude.logEvent('SCREEN: Search');
analytics.hit(new ScreenHit('SCREEN: Search'));
}
componentDidUpdate(prevProps, prevState) {
const { text } = this.state;
if (text !== prevState.text) {
if (text.length >= 1) {
if (text.length < 5 || text.endsWith(' ')) this.throttleSearch(text);
else this.debounceSearch(text);
} else {
this.submitAndClear();
}
}
}
// Load logos
_loadAssetsAsync = async () => {
const imageAssets = cacheImages([LK_LOGO, SK]);
await Promise.all([...imageAssets]);
};
getInfo = () => {
const { text } = this.state;
const url = `https://api.deezer.com/search?q=track:"${text}"&limit=20&order=RANKING?strict=on`;
const cached = this.cache[url];
if (cached) {
this.setState({ results: cached, showLogo: false });
return;
}
fetch(url)
.then(response => response.json())
.then((data) => {
this.cache[url] = data.data;
this.setState({ results: data.data, showLogo: false });
});
};
submitAndClear = () => {
this.setState({ text: '', showLogo: true });
Keyboard.dismiss();
};
render() {
const {
isReady, showLogo, text, results
} = this.state;
const { navigation } = this.props;
if (!isReady) {
return (
<Expo.AppLoading
startAsync={this._loadAssetsAsync}
onFinish={() => this.setState({ isReady: true })}
onError={console.warn}
/>
);
}
return (
<SafeAreaView style={styles.safeView}>
<StatusBar barStyle="light-content" />
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={styles.container}>
{showLogo && <Image style={styles.logo} source={LK_LOGO} />}
<View style={{ flex: 1, alignItems: 'center' }}>
<View style={styles.searchContainer}>
<EvilIcons name="search" size={30} color="#07CCBA" />
<TextInput
style={styles.TextInput}
onChangeText={changedText => this.setState({ text: changedText })
}
value={text}
placeholder="Search song"
placeholderTextColor="#fff"
clearButtonMode="always"
/>
</View>
{results.length > 0 && text.length > 0 && (
<Suggestions
style={styles.Suggestions}
results={results}
navigation={navigation}
/>
)}
</View>
{showLogo && <Credits screen="Search" />}
</View>
</TouchableWithoutFeedback>
</SafeAreaView>
);
}
}
// Styles
const styles = StyleSheet.create({
safeView: {
flex: 1,
backgroundColor: colours.primaryBlack
},
container: {
flex: 1,
backgroundColor: colours.primaryBlack,
alignItems: 'center',
justifyContent: 'center',
paddingTop: 40,
paddingBottom: 30
},
logo: {
width: 160,
height: 160,
marginBottom: 60,
marginTop: 40
},
searchContainer: {
flexDirection: 'row',
flexWrap: 'nowrap',
width: 280,
paddingTop: 18,
paddingBottom: 18,
paddingRight: 20,
paddingLeft: 10,
marginBottom: 20,
backgroundColor: colours.highlightBlack,
alignItems: 'center',
justifyContent: 'center'
},
TextInput: {
flex: 1,
fontSize: 16,
textAlign: 'center',
alignItems: 'center',
flexWrap: 'nowrap',
color: colours.primaryWhite
},
Suggestions: {
flex: 1,
alignItems: 'center',
color: colours.primaryWhite
},
creditsContainer: {
flexDirection: 'row',
width: 170
},
creditsText: {
fontSize: 12,
color: colours.secondaryGrey,
textAlign: 'left',
paddingLeft: 20
},
creditsImage: {
width: 30,
height: 30,
opacity: 0.2,
alignSelf: 'flex-start'
}
});
================================================
FILE: utils/shareHelper.js
================================================
import { Share } from 'react-native';
import * as Expo from 'expo';
import { Analytics, Event } from 'expo-analytics';
// GA tracking
const ID = Expo.Constants.manifest.extra.googleAnalytics;
const analytics = new Analytics(ID);
const handleShare = (message, url, title, screen) => {
Share.share(
{
message,
url,
title
},
{
// Android only:
dialogTitle: `Share ${title}`, // iOS only:
excludedActivityTypes: ['com.apple.UIKit.activity.PostToTwitter']
}
).then(() => {
Expo.Amplitude.logEvent(`BUTTON: Share - ${screen} Screen`);
analytics.event(new Event('Button', 'Tap', `Share - ${screen} Screen`));
});
};
export default handleShare;
gitextract_37vbbbyq/
├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .travis.yml
├── App.js
├── App.test.js
├── LICENSE
├── README.md
├── _design_assets/
│ └── adobeXD/
│ └── Lyrics King App.xd
├── app.json
├── components/
│ ├── Credits.js
│ ├── SocialButton.js
│ └── Suggestions.js
├── config/
│ ├── colours.js
│ └── router.js
├── lib/
│ └── constants.js
├── package.json
├── screens/
│ ├── AboutScreen.js
│ ├── DetailsScreen.js
│ └── SearchScreen.js
└── utils/
└── shareHelper.js
SYMBOL INDEX (19 symbols across 4 files)
FILE: App.js
class App (line 16) | class App extends React.Component {
method render (line 17) | render() {
FILE: screens/AboutScreen.js
class AboutScreen (line 27) | class AboutScreen extends React.Component {
method componentDidMount (line 28) | componentDidMount() {
method render (line 33) | render() {
FILE: screens/DetailsScreen.js
class DetailsScreen (line 24) | class DetailsScreen extends React.Component {
method propTypes (line 25) | static get propTypes() {
method constructor (line 31) | constructor(props) {
method componentDidMount (line 36) | async componentDidMount() {
method componentWillUnmount (line 54) | componentWillUnmount() {
method displayLyrics (line 68) | displayLyrics() {
method render (line 95) | render() {
FILE: screens/SearchScreen.js
function cacheImages (line 31) | function cacheImages(images) {
class SearchScreen (line 44) | class SearchScreen extends React.Component {
method propTypes (line 45) | static get propTypes() {
method constructor (line 51) | constructor(props) {
method componentDidMount (line 59) | componentDidMount() {
method componentDidUpdate (line 65) | componentDidUpdate(prevProps, prevState) {
method render (line 106) | render() {
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (45K chars).
[
{
"path": ".babelrc",
"chars": 174,
"preview": "{\n \"presets\": [\"module:metro-react-native-babel-preset\"],\n \"plugins\": [\n [\n \"@babel/plugin-proposal-decorators"
},
{
"path": ".eslintrc.json",
"chars": 950,
"preview": "{\n \"extends\": \"airbnb\",\n \"env\": {\n \"browser\": true,\n \"es6\": true,\n \"jest/globals\": true\n },\n \"settings\": {\n"
},
{
"path": ".gitignore",
"chars": 312,
"preview": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n\n*/*.DS_Store\n*.DS_Store\n\n# expo\n.expo/\n\n# d"
},
{
"path": ".travis.yml",
"chars": 259,
"preview": "#\n# Travis CI config\n# https://docs.travis-ci.com/user/customizing-the-build/\n#\n\nenv:\n - COVERALLS_ENV=production\n\nbran"
},
{
"path": "App.js",
"chars": 513,
"preview": "import React from 'react';\nimport Sentry from 'sentry-expo';\nimport { createAppContainer } from 'react-navigation';\nimpo"
},
{
"path": "App.test.js",
"chars": 227,
"preview": "import React from 'react';\nimport renderer from 'react-test-renderer';\nimport App from './App';\n\nit('renders without cra"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2019 Stephen Kempin\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.md",
"chars": 9109,
"preview": "# Lyrics King <img src=\"_github/lk-logo.gif\" width=\"80\">\n\n
About this extraction
This page contains the full source code of the SKempin/Lyrics-King-React-Native GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (41.5 KB), approximately 11.0k tokens, and a symbol index with 19 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.