added React Native code
This commit is contained in:
parent
03d218680e
commit
1bcb1ff228
13744 changed files with 2596354 additions and 6 deletions
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules/
|
2
app/bundle.sh
Executable file
2
app/bundle.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
react-native bundle --dev false --entry-file src/index.js --assets-dest ../lbry-ios --bundle-output ../lbry-ios/index.ios.jsbundle --platform ios
|
31
app/package.json
Normal file
31
app/package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "LBRYApp",
|
||||
"version": "0.0.1",
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start"
|
||||
},
|
||||
"dependencies": {
|
||||
"lbry-redux": "lbryio/lbry-redux",
|
||||
"moment": "^2.22.1",
|
||||
"react": "16.2.0",
|
||||
"react-native": "0.55.3",
|
||||
"react-native-image-zoom-viewer": "^2.2.5",
|
||||
"react-native-vector-icons": "^4.5.0",
|
||||
"react-native-video": "3.1.0",
|
||||
"react-navigation": "^1.5.12",
|
||||
"react-navigation-redux-helpers": "^1.0.1",
|
||||
"react-redux": "^5.0.3",
|
||||
"redux": "^3.6.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-persist": "^4.8.0",
|
||||
"redux-persist-transform-compress": "^4.2.0",
|
||||
"redux-persist-transform-filter": "0.0.10",
|
||||
"redux-thunk": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-stage-2": "^6.18.0",
|
||||
"flow-babel-webpack-plugin": "^1.1.1"
|
||||
}
|
||||
}
|
BIN
app/src/assets/stripe@2x.png
Normal file
BIN
app/src/assets/stripe@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
231
app/src/component/AppNavigator.js
Normal file
231
app/src/component/AppNavigator.js
Normal file
|
@ -0,0 +1,231 @@
|
|||
import React from 'react';
|
||||
import AboutPage from '../page/about';
|
||||
import DiscoverPage from '../page/discover';
|
||||
import FilePage from '../page/file';
|
||||
import FirstRunScreen from '../page/firstRun';
|
||||
import SearchPage from '../page/search';
|
||||
import TrendingPage from '../page/trending';
|
||||
import SettingsPage from '../page/settings';
|
||||
import SplashScreen from '../page/splash';
|
||||
import TransactionHistoryPage from '../page/transactionHistory';
|
||||
import WalletPage from '../page/wallet';
|
||||
import SearchInput from '../component/searchInput';
|
||||
import {
|
||||
addNavigationHelpers,
|
||||
DrawerNavigator,
|
||||
StackNavigator,
|
||||
NavigationActions
|
||||
} from 'react-navigation';
|
||||
import { connect } from 'react-redux';
|
||||
import { addListener } from '../utils/redux';
|
||||
import {
|
||||
AppState,
|
||||
AsyncStorage,
|
||||
BackHandler,
|
||||
Linking,
|
||||
NativeModules,
|
||||
TextInput,
|
||||
ToastAndroid
|
||||
} from 'react-native';
|
||||
import { SETTINGS, doHideNotification, selectNotification } from 'lbry-redux';
|
||||
import { makeSelectClientSetting } from '../redux/selectors/settings';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import discoverStyle from '../styles/discover';
|
||||
import searchStyle from '../styles/search';
|
||||
import SearchRightHeaderIcon from "../component/searchRightHeaderIcon";
|
||||
|
||||
const discoverStack = StackNavigator({
|
||||
Discover: {
|
||||
screen: DiscoverPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Discover',
|
||||
headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
|
||||
})
|
||||
},
|
||||
File: {
|
||||
screen: FilePage,
|
||||
navigationOptions: {
|
||||
header: null,
|
||||
drawerLockMode: 'locked-closed'
|
||||
}
|
||||
},
|
||||
Search: {
|
||||
screen: SearchPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
drawerLockMode: 'locked-closed'
|
||||
})
|
||||
}
|
||||
}, {
|
||||
headerMode: 'screen',
|
||||
});
|
||||
|
||||
const trendingStack = StackNavigator({
|
||||
Trending: {
|
||||
screen: TrendingPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Trending',
|
||||
headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const walletStack = StackNavigator({
|
||||
Wallet: {
|
||||
screen: WalletPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Wallet',
|
||||
headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
|
||||
})
|
||||
},
|
||||
TransactionHistory: {
|
||||
screen: TransactionHistoryPage,
|
||||
navigationOptions: {
|
||||
title: 'Transaction History',
|
||||
drawerLockMode: 'locked-closed'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
headerMode: 'screen'
|
||||
});
|
||||
|
||||
const drawer = DrawerNavigator({
|
||||
DiscoverStack: { screen: discoverStack },
|
||||
TrendingStack: { screen: trendingStack },
|
||||
WalletStack: { screen: walletStack },
|
||||
Settings: { screen: SettingsPage, navigationOptions: { drawerLockMode: 'locked-closed' } },
|
||||
About: { screen: AboutPage, navigationOptions: { drawerLockMode: 'locked-closed' } }
|
||||
}, {
|
||||
drawerWidth: 300,
|
||||
headerMode: 'none'
|
||||
});
|
||||
|
||||
export const AppNavigator = new StackNavigator({
|
||||
FirstRun: {
|
||||
screen: FirstRunScreen,
|
||||
navigationOptions: {
|
||||
drawerLockMode: 'locked-closed'
|
||||
}
|
||||
},
|
||||
Splash: {
|
||||
screen: SplashScreen,
|
||||
navigationOptions: {
|
||||
drawerLockMode: 'locked-closed'
|
||||
}
|
||||
},
|
||||
Main: {
|
||||
screen: drawer
|
||||
}
|
||||
}, {
|
||||
headerMode: 'none'
|
||||
});
|
||||
|
||||
class AppWithNavigationState extends React.Component {
|
||||
static supportedDisplayTypes = ['toast'];
|
||||
|
||||
componentWillMount() {
|
||||
AppState.addEventListener('change', this._handleAppStateChange);
|
||||
BackHandler.addEventListener('hardwareBackPress', function() {
|
||||
const { dispatch, nav } = this.props;
|
||||
// There should be a better way to check this
|
||||
if (nav.routes.length > 0) {
|
||||
const subRoutes = nav.routes[0].routes[0].routes;
|
||||
const lastRoute = subRoutes[subRoutes.length - 1];
|
||||
if (nav.routes[0].routes[0].index > 0 &&
|
||||
['About', 'Settings'].indexOf(lastRoute.key) > -1) {
|
||||
dispatch(NavigationActions.back());
|
||||
return true;
|
||||
}
|
||||
if (nav.routes[0].routeName === 'Main') {
|
||||
if (nav.routes[0].routes[0].routes[0].index > 0) {
|
||||
dispatch(NavigationActions.back());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Linking.addEventListener('url', this._handleUrl);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this._handleAppStateChange);
|
||||
BackHandler.removeEventListener('hardwareBackPress');
|
||||
Linking.removeEventListener('url', this._handleUrl);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
const { notification } = nextProps;
|
||||
if (notification) {
|
||||
const { displayType, message } = notification;
|
||||
let currentDisplayType;
|
||||
if (displayType.length) {
|
||||
for (let i = 0; i < displayType.length; i++) {
|
||||
const type = displayType[i];
|
||||
if (AppWithNavigationState.supportedDisplayTypes.indexOf(type) > -1) {
|
||||
currentDisplayType = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (AppWithNavigationState.supportedDisplayTypes.indexOf(displayType) > -1) {
|
||||
currentDisplayType = displayType;
|
||||
}
|
||||
|
||||
if ('toast' === currentDisplayType) {
|
||||
ToastAndroid.show(message, ToastAndroid.SHORT);
|
||||
}
|
||||
|
||||
dispatch(doHideNotification());
|
||||
}
|
||||
}
|
||||
|
||||
_handleAppStateChange = (nextAppState) => {
|
||||
// Check if the app was suspended
|
||||
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
|
||||
AsyncStorage.getItem('firstLaunchTime').then(start => {
|
||||
if (start !== null && !isNaN(parseInt(start, 10))) {
|
||||
// App suspended during first launch?
|
||||
// If so, this needs to be included as a property when tracking
|
||||
AsyncStorage.setItem('firstLaunchSuspended', 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_handleUrl = (evt) => {
|
||||
const { dispatch } = this.props;
|
||||
if (evt.url) {
|
||||
const navigateAction = NavigationActions.navigate({
|
||||
routeName: 'File',
|
||||
key: evt.url,
|
||||
params: { uri: evt.url }
|
||||
});
|
||||
dispatch(navigateAction);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, nav } = this.props;
|
||||
return (
|
||||
<AppNavigator
|
||||
navigation={addNavigationHelpers({
|
||||
dispatch,
|
||||
state: nav,
|
||||
addListener,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
nav: state.nav,
|
||||
notification: selectNotification(state),
|
||||
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
|
||||
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(AppWithNavigationState);
|
7
app/src/component/address/index.js
Normal file
7
app/src/component/address/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doNotify } from 'lbry-redux';
|
||||
import Address from './view';
|
||||
|
||||
export default connect(null, {
|
||||
doNotify,
|
||||
})(Address);
|
29
app/src/component/address/view.js
Normal file
29
app/src/component/address/view.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Clipboard, Text, View } from 'react-native';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
address: string,
|
||||
doNotify: ({ message: string, displayType: Array<string> }) => void,
|
||||
};
|
||||
|
||||
export default class Address extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { address, doNotify, style } = this.props;
|
||||
|
||||
return (
|
||||
<View style={[walletStyle.row, style]}>
|
||||
<Text selectable={true} numberOfLines={1} style={walletStyle.address}>{address || ''}</Text>
|
||||
<Button icon={'clipboard'} style={walletStyle.button} onPress={() => {
|
||||
Clipboard.setString(address);
|
||||
doNotify({
|
||||
message: 'Address copied',
|
||||
displayType: ['toast'],
|
||||
});
|
||||
}} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
4
app/src/component/button/index.js
Normal file
4
app/src/component/button/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Button from './view';
|
||||
|
||||
export default connect(null, null)(Button);
|
41
app/src/component/button/view.js
Normal file
41
app/src/component/button/view.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import buttonStyle from '../../styles/button';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
export default class Button extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
disabled,
|
||||
style,
|
||||
text,
|
||||
icon,
|
||||
onPress
|
||||
} = this.props;
|
||||
|
||||
let styles = [buttonStyle.button, buttonStyle.row];
|
||||
if (style) {
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
styles.push(buttonStyle.disabled);
|
||||
}
|
||||
|
||||
const textStyles = [buttonStyle.text];
|
||||
if (icon && icon.trim().length > 0) {
|
||||
textStyles.push(buttonStyle.textWithIcon);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity disabled={disabled} style={styles} onPress={onPress}>
|
||||
{icon && <Icon name={icon} size={18} color='#ffffff' class={buttonStyle.icon} /> }
|
||||
{text && (text.trim().length > 0) && <Text style={textStyles}>{text}</Text>}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
};
|
25
app/src/component/fileDownloadButton/index.js
Normal file
25
app/src/component/fileDownloadButton/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchCostInfoForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectDownloadingForUri,
|
||||
makeSelectLoadingForUri,
|
||||
makeSelectCostInfoForUri
|
||||
} from 'lbry-redux';
|
||||
import { doPurchaseUri, doStartDownload } from '../../redux/actions/file';
|
||||
import FileDownloadButton from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
downloading: makeSelectDownloadingForUri(props.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
loading: makeSelectLoadingForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
purchaseUri: uri => dispatch(doPurchaseUri(uri)),
|
||||
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileDownloadButton);
|
91
app/src/component/fileDownloadButton/view.js
Normal file
91
app/src/component/fileDownloadButton/view.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||
import fileDownloadButtonStyle from '../../styles/fileDownloadButton';
|
||||
|
||||
class FileDownloadButton extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { costInfo, fetchCostInfo, uri } = this.props;
|
||||
if (costInfo === undefined) {
|
||||
fetchCostInfo(uri);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
//this.checkAvailability(nextProps.uri);
|
||||
this.restartDownload(nextProps);
|
||||
}
|
||||
|
||||
restartDownload(props) {
|
||||
const { downloading, fileInfo, uri, restartDownload } = props;
|
||||
|
||||
if (
|
||||
!downloading &&
|
||||
fileInfo &&
|
||||
!fileInfo.completed &&
|
||||
fileInfo.written_bytes !== false &&
|
||||
fileInfo.written_bytes < fileInfo.total_bytes
|
||||
) {
|
||||
restartDownload(uri, fileInfo.outpoint);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
fileInfo,
|
||||
downloading,
|
||||
uri,
|
||||
purchaseUri,
|
||||
costInfo,
|
||||
isPlayable,
|
||||
onPlay,
|
||||
loading,
|
||||
doPause,
|
||||
style,
|
||||
openFile
|
||||
} = this.props;
|
||||
|
||||
if (loading || downloading) {
|
||||
const progress =
|
||||
fileInfo && fileInfo.written_bytes ? fileInfo.written_bytes / fileInfo.total_bytes * 100 : 0,
|
||||
label = fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...';
|
||||
|
||||
return (
|
||||
<View style={[style, fileDownloadButtonStyle.container]}>
|
||||
<View style={{ width: `${progress}%`, backgroundColor: '#ff0000', position: 'absolute', left: 0, top: 0 }}></View>
|
||||
<Text style={fileDownloadButtonStyle.text}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (fileInfo === null && !downloading) {
|
||||
if (!costInfo) {
|
||||
return (
|
||||
<View style={[style, fileDownloadButtonStyle.container]}>
|
||||
<Text style={fileDownloadButtonStyle.text}>Fetching cost info...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity style={[style, fileDownloadButtonStyle.container]} onPress={() => {
|
||||
if (NativeModules.Mixpanel) {
|
||||
NativeModules.Mixpanel.track('Purchase Uri', { Uri: uri });
|
||||
}
|
||||
purchaseUri(uri);
|
||||
if (isPlayable && onPlay) {
|
||||
this.props.onPlay();
|
||||
}
|
||||
}}>
|
||||
<Text style={fileDownloadButtonStyle.text}>{isPlayable ? 'Play' : 'Download'}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else if (fileInfo && fileInfo.download_path) {
|
||||
return (
|
||||
<TouchableOpacity style={[style, fileDownloadButtonStyle.container]} onPress={openFile}>
|
||||
<Text style={fileDownloadButtonStyle.text}>Open</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileDownloadButton;
|
26
app/src/component/fileItem/index.js
Normal file
26
app/src/component/fileItem/index.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doResolveUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectIsUriResolving,
|
||||
selectRewardContentClaimIds
|
||||
} from 'lbry-redux';
|
||||
import { selectShowNsfw } from '../../redux/selectors/settings';
|
||||
import FileItem from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
obscureNsfw: !selectShowNsfw(state)
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileItem);
|
78
app/src/component/fileItem/view.js
Normal file
78
app/src/component/fileItem/view.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import React from 'react';
|
||||
import { normalizeURI } from 'lbry-redux';
|
||||
import { NavigationActions } from 'react-navigation';
|
||||
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||
import FileItemMedia from '../fileItemMedia';
|
||||
import FilePrice from '../filePrice';
|
||||
import Link from '../link';
|
||||
import NsfwOverlay from '../nsfwOverlay';
|
||||
import discoverStyle from '../../styles/discover';
|
||||
|
||||
class FileItem extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.resolve(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.resolve(nextProps);
|
||||
}
|
||||
|
||||
resolve(props) {
|
||||
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||
|
||||
if (!isResolvingUri && claim === undefined && uri) {
|
||||
resolveUri(uri);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
fileInfo,
|
||||
metadata,
|
||||
isResolvingUri,
|
||||
rewardedContentClaimIds,
|
||||
style,
|
||||
navigation
|
||||
} = this.props;
|
||||
|
||||
const uri = normalizeURI(this.props.uri);
|
||||
const title = metadata && metadata.title ? metadata.title : uri;
|
||||
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||
const channelName = claim ? claim.channel_name : null;
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<TouchableOpacity style={discoverStyle.container} onPress={() => {
|
||||
if (NativeModules.Mixpanel) {
|
||||
NativeModules.Mixpanel.track('Discover Tap', { Uri: uri });
|
||||
}
|
||||
navigation.navigate({ routeName: 'File', key: uri, params: { uri } });
|
||||
}
|
||||
}>
|
||||
<FileItemMedia title={title}
|
||||
thumbnail={thumbnail}
|
||||
blurRadius={obscureNsfw ? 15 : 0}
|
||||
resizeMode="cover"
|
||||
isResolvingUri={isResolvingUri} />
|
||||
<FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />
|
||||
<Text style={discoverStyle.fileItemName}>{title}</Text>
|
||||
{channelName &&
|
||||
<Link style={discoverStyle.channelName} text={channelName} onPress={() => {
|
||||
const channelUri = normalizeURI(channelName);
|
||||
navigation.navigate({ routeName: 'File', key: channelUri, params: { uri: channelUri }});
|
||||
}} />}
|
||||
</TouchableOpacity>
|
||||
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate('Settings')} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileItem;
|
7
app/src/component/fileItemMedia/index.js
Normal file
7
app/src/component/fileItemMedia/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FileItemMedia from './view';
|
||||
|
||||
const select = state => ({});
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(select, perform)(FileItemMedia);
|
66
app/src/component/fileItemMedia/view.js
Normal file
66
app/src/component/fileItemMedia/view.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
import React from 'react';
|
||||
import { ActivityIndicator, Image, Text, View } from 'react-native';
|
||||
import Colors from '../../styles/colors';
|
||||
import fileItemMediaStyle from '../../styles/fileItemMedia';
|
||||
|
||||
class FileItemMedia extends React.PureComponent {
|
||||
static AUTO_THUMB_STYLES = [
|
||||
fileItemMediaStyle.autothumbPurple,
|
||||
fileItemMediaStyle.autothumbRed,
|
||||
fileItemMediaStyle.autothumbPink,
|
||||
fileItemMediaStyle.autothumbIndigo,
|
||||
fileItemMediaStyle.autothumbBlue,
|
||||
fileItemMediaStyle.autothumbLightBlue,
|
||||
fileItemMediaStyle.autothumbCyan,
|
||||
fileItemMediaStyle.autothumbTeal,
|
||||
fileItemMediaStyle.autothumbGreen,
|
||||
fileItemMediaStyle.autothumbYellow,
|
||||
fileItemMediaStyle.autothumbOrange,
|
||||
];
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
autoThumbStyle:
|
||||
FileItemMedia.AUTO_THUMB_STYLES[
|
||||
Math.floor(Math.random() * FileItemMedia.AUTO_THUMB_STYLES.length)
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let style = this.props.style;
|
||||
const { blurRadius, isResolvingUri, thumbnail, title, resizeMode } = this.props;
|
||||
const atStyle = this.state.autoThumbStyle;
|
||||
|
||||
if (thumbnail && ((typeof thumbnail) === 'string')) {
|
||||
if (style == null) {
|
||||
style = fileItemMediaStyle.thumbnail;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image source={{uri: thumbnail}}
|
||||
blurRadius={blurRadius}
|
||||
resizeMode={resizeMode ? resizeMode : "cover"}
|
||||
style={style} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[style ? style : fileItemMediaStyle.autothumb, atStyle]}>
|
||||
{isResolvingUri && (
|
||||
<View style={fileItemMediaStyle.resolving}>
|
||||
<ActivityIndicator color={Colors.White} size={"large"} />
|
||||
<Text style={fileItemMediaStyle.text}>Resolving...</Text>
|
||||
</View>
|
||||
)}
|
||||
{!isResolvingUri && <Text style={fileItemMediaStyle.autothumbText}>{title &&
|
||||
title
|
||||
.replace(/\s+/g, '')
|
||||
.substring(0, Math.min(title.replace(' ', '').length, 5))
|
||||
.toUpperCase()}</Text>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileItemMedia;
|
11
app/src/component/fileList/index.js
Normal file
11
app/src/component/fileList/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FileList from './view';
|
||||
import { selectClaimsById } from 'lbry-redux';
|
||||
|
||||
const select = state => ({
|
||||
claimsById: selectClaimsById(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(select, perform)(FileList);
|
184
app/src/component/fileList/view.js
Normal file
184
app/src/component/fileList/view.js
Normal file
|
@ -0,0 +1,184 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { buildURI } from 'lbry-redux';
|
||||
import { FlatList } from 'react-native';
|
||||
import FileItem from '../fileItem';
|
||||
import discoverStyle from '../../styles/discover';
|
||||
|
||||
// In the future, all Flow types need to be specified in a common source (lbry-redux, perhaps?)
|
||||
type FileInfo = {
|
||||
name: string,
|
||||
channelName: ?string,
|
||||
pending?: boolean,
|
||||
channel_claim_id: string,
|
||||
value?: {
|
||||
publisherSignature: {
|
||||
certificateId: string,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
publisherSignature: {
|
||||
certificateId: string,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
hideFilter: boolean,
|
||||
sortByHeight?: boolean,
|
||||
claimsById: Array<{}>,
|
||||
fileInfos: Array<FileInfo>,
|
||||
checkPending?: boolean,
|
||||
};
|
||||
|
||||
type State = {
|
||||
sortBy: string,
|
||||
};
|
||||
|
||||
class FileList extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
hideFilter: false,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
sortBy: 'dateNew',
|
||||
};
|
||||
|
||||
(this: any).handleSortChanged = this.handleSortChanged.bind(this);
|
||||
|
||||
this.sortFunctions = {
|
||||
dateNew: fileInfos =>
|
||||
this.props.sortByHeight
|
||||
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
if (fileInfo1.pending) {
|
||||
return -1;
|
||||
}
|
||||
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
||||
? this.props.claimsById[fileInfo1.claim_id].height
|
||||
: 0;
|
||||
const height2 = this.props.claimsById[fileInfo2.claim_id]
|
||||
? this.props.claimsById[fileInfo2.claim_id].height
|
||||
: 0;
|
||||
if (height1 > height2) {
|
||||
return -1;
|
||||
} else if (height1 < height2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
: [...fileInfos].reverse(),
|
||||
dateOld: fileInfos =>
|
||||
this.props.sortByHeight
|
||||
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
||||
? this.props.claimsById[fileInfo1.claim_id].height
|
||||
: 999999;
|
||||
const height2 = this.props.claimsById[fileInfo2.claim_id]
|
||||
? this.props.claimsById[fileInfo2.claim_id].height
|
||||
: 999999;
|
||||
if (height1 < height2) {
|
||||
return -1;
|
||||
} else if (height1 > height2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
: fileInfos,
|
||||
title: fileInfos =>
|
||||
fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
const getFileTitle = fileInfo => {
|
||||
const { value, metadata, name, claim_name: claimName } = fileInfo;
|
||||
if (metadata) {
|
||||
// downloaded claim
|
||||
return metadata.title || claimName;
|
||||
} else if (value) {
|
||||
// published claim
|
||||
const { title } = value.stream.metadata;
|
||||
return title || name;
|
||||
}
|
||||
// Invalid claim
|
||||
return '';
|
||||
};
|
||||
const title1 = getFileTitle(fileInfo1).toLowerCase();
|
||||
const title2 = getFileTitle(fileInfo2).toLowerCase();
|
||||
if (title1 < title2) {
|
||||
return -1;
|
||||
} else if (title1 > title2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
filename: fileInfos =>
|
||||
fileInfos.slice().sort(({ file_name: fileName1 }, { file_name: fileName2 }) => {
|
||||
const fileName1Lower = fileName1.toLowerCase();
|
||||
const fileName2Lower = fileName2.toLowerCase();
|
||||
if (fileName1Lower < fileName2Lower) {
|
||||
return -1;
|
||||
} else if (fileName2Lower > fileName1Lower) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
getChannelSignature = (fileInfo: FileInfo) => {
|
||||
if (fileInfo.pending) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (fileInfo.value) {
|
||||
return fileInfo.value.publisherSignature.certificateId;
|
||||
}
|
||||
return fileInfo.channel_claim_id;
|
||||
};
|
||||
|
||||
handleSortChanged(event: SyntheticInputEvent<*>) {
|
||||
this.setState({
|
||||
sortBy: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
sortFunctions: {};
|
||||
|
||||
render() {
|
||||
const { fileInfos, hideFilter, checkPending, navigation, style } = this.props;
|
||||
const { sortBy } = this.state;
|
||||
const items = [];
|
||||
|
||||
if (!fileInfos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = fileInfo;
|
||||
const uriParams = {};
|
||||
|
||||
// This is unfortunate
|
||||
// https://github.com/lbryio/lbry/issues/1159
|
||||
const name = claimName || claimNameDownloaded;
|
||||
uriParams.contentName = name;
|
||||
uriParams.claimId = claimId;
|
||||
const uri = buildURI(uriParams);
|
||||
|
||||
items.push(uri);
|
||||
});
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
style={style}
|
||||
data={items}
|
||||
keyExtractor={(item, index) => item}
|
||||
renderItem={({item}) => (
|
||||
<FileItem style={discoverStyle.fileItem}
|
||||
uri={item}
|
||||
navigation={navigation} />
|
||||
)} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileList;
|
20
app/src/component/filePrice/index.js
Normal file
20
app/src/component/filePrice/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchCostInfoForUri,
|
||||
makeSelectCostInfoForUri,
|
||||
makeSelectFetchingCostInfoForUri,
|
||||
makeSelectClaimForUri
|
||||
} from 'lbry-redux';
|
||||
import FilePrice from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
fetching: makeSelectFetchingCostInfoForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FilePrice);
|
120
app/src/component/filePrice/view.js
Normal file
120
app/src/component/filePrice/view.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text, View } from 'react-native';
|
||||
import { formatCredits, formatFullPrice } from 'lbry-redux';
|
||||
|
||||
class CreditAmount extends React.PureComponent {
|
||||
static propTypes = {
|
||||
amount: PropTypes.number.isRequired,
|
||||
precision: PropTypes.number,
|
||||
isEstimate: PropTypes.bool,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
showFree: PropTypes.bool,
|
||||
showFullPrice: PropTypes.bool,
|
||||
showPlus: PropTypes.bool,
|
||||
look: PropTypes.oneOf(['indicator', 'plain', 'fee']),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
precision: 2,
|
||||
label: true,
|
||||
showFree: false,
|
||||
look: 'indicator',
|
||||
showFullPrice: false,
|
||||
showPlus: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const minimumRenderableAmount = Math.pow(10, -1 * this.props.precision);
|
||||
const { amount, precision, showFullPrice, style } = this.props;
|
||||
|
||||
let formattedAmount;
|
||||
const fullPrice = formatFullPrice(amount, 2);
|
||||
|
||||
if (showFullPrice) {
|
||||
formattedAmount = fullPrice;
|
||||
} else {
|
||||
formattedAmount =
|
||||
amount > 0 && amount < minimumRenderableAmount
|
||||
? `<${minimumRenderableAmount}`
|
||||
: formatCredits(amount, precision);
|
||||
}
|
||||
|
||||
let amountText;
|
||||
if (this.props.showFree && parseFloat(this.props.amount) === 0) {
|
||||
amountText = 'FREE';
|
||||
} else {
|
||||
if (this.props.label) {
|
||||
const label =
|
||||
typeof this.props.label === 'string'
|
||||
? this.props.label
|
||||
: parseFloat(amount) == 1 ? 'credit' : 'credits';
|
||||
|
||||
amountText = `${formattedAmount} ${label}`;
|
||||
} else {
|
||||
amountText = formattedAmount;
|
||||
}
|
||||
if (this.props.showPlus && amount > 0) {
|
||||
amountText = `+${amountText}`;
|
||||
}
|
||||
}
|
||||
|
||||
/*{this.props.isEstimate ? (
|
||||
<span
|
||||
className="credit-amount__estimate"
|
||||
title={__('This is an estimate and does not include data fees')}
|
||||
>
|
||||
*
|
||||
</span>
|
||||
) : null}*/
|
||||
return (
|
||||
<Text style={style}>{amountText}</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FilePrice extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
this.fetchCost(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.fetchCost(nextProps);
|
||||
}
|
||||
|
||||
fetchCost(props) {
|
||||
const { costInfo, fetchCostInfo, uri, fetching, claim } = props;
|
||||
|
||||
if (costInfo === undefined && !fetching && claim) {
|
||||
fetchCostInfo(uri);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { costInfo, look = 'indicator', showFullPrice = false, style, textStyle } = this.props;
|
||||
|
||||
const isEstimate = costInfo ? !costInfo.includesData : null;
|
||||
|
||||
if (!costInfo) {
|
||||
return (
|
||||
<View style={style}>
|
||||
<Text style={textStyle}>???</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<CreditAmount
|
||||
style={textStyle}
|
||||
label={false}
|
||||
amount={costInfo.cost}
|
||||
isEstimate={isEstimate}
|
||||
showFree
|
||||
showFullPrice={showFullPrice}>???</CreditAmount>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FilePrice;
|
9
app/src/component/link/index.js
Normal file
9
app/src/component/link/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doNotify } from 'lbry-redux';
|
||||
import Link from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
notify: (data) => dispatch(doNotify(data))
|
||||
});
|
||||
|
||||
export default connect(null, perform)(Link);
|
62
app/src/component/link/view.js
Normal file
62
app/src/component/link/view.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React from 'react';
|
||||
import { Linking, Text, TouchableOpacity } from 'react-native';
|
||||
|
||||
export default class Link extends React.PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
tappedStyle: false,
|
||||
}
|
||||
this.addTappedStyle = this.addTappedStyle.bind(this)
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
const { error, href, navigation, notify } = this.props;
|
||||
|
||||
if (navigation && href.startsWith('#')) {
|
||||
navigation.navigate(href.substring(1));
|
||||
} else {
|
||||
if (this.props.effectOnTap) this.addTappedStyle();
|
||||
Linking.openURL(href)
|
||||
.then(() => setTimeout(() => { this.setState({ tappedStyle: false }); }, 2000))
|
||||
.catch(err => {
|
||||
notify({ message: error, displayType: ['toast']})
|
||||
this.setState({tappedStyle: false})
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
addTappedStyle() {
|
||||
this.setState({ tappedStyle: true });
|
||||
setTimeout(() => { this.setState({ tappedStyle: false }); }, 2000);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
onPress,
|
||||
style,
|
||||
text
|
||||
} = this.props;
|
||||
|
||||
let styles = [];
|
||||
if (style) {
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.effectOnTap && this.state.tappedStyle) {
|
||||
styles.push(this.props.effectOnTap);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text style={styles} onPress={onPress ? onPress : this.handlePress}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
};
|
11
app/src/component/mediaPlayer/index.js
Normal file
11
app/src/component/mediaPlayer/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { SETTINGS } from 'lbry-redux';
|
||||
import { makeSelectClientSetting } from '../../redux/selectors/settings';
|
||||
import MediaPlayer from './view';
|
||||
|
||||
const select = state => ({
|
||||
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
|
||||
});
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(select, perform)(MediaPlayer);
|
323
app/src/component/mediaPlayer/view.js
Normal file
323
app/src/component/mediaPlayer/view.js
Normal file
|
@ -0,0 +1,323 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
NativeModules,
|
||||
PanResponder,
|
||||
Text,
|
||||
View,
|
||||
ScrollView,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
import Video from 'react-native-video';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
import FileItemMedia from '../fileItemMedia';
|
||||
import mediaPlayerStyle from '../../styles/mediaPlayer';
|
||||
|
||||
class MediaPlayer extends React.PureComponent {
|
||||
static ControlsTimeout = 3000;
|
||||
|
||||
seekResponder = null;
|
||||
|
||||
seekerWidth = 0;
|
||||
|
||||
trackingOffset = 0;
|
||||
|
||||
tracking = null;
|
||||
|
||||
video = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
rate: 1,
|
||||
volume: 1,
|
||||
muted: false,
|
||||
resizeMode: 'stretch',
|
||||
duration: 0.0,
|
||||
currentTime: 0.0,
|
||||
paused: !props.autoPlay,
|
||||
fullscreenMode: false,
|
||||
areControlsVisible: true,
|
||||
controlsTimeout: -1,
|
||||
seekerOffset: 0,
|
||||
seekerPosition: 0,
|
||||
firstPlay: true,
|
||||
seekTimeout: -1
|
||||
};
|
||||
}
|
||||
|
||||
formatTime(time) {
|
||||
let str = '';
|
||||
let minutes = 0, hours = 0, seconds = parseInt(time, 10);
|
||||
if (seconds > 60) {
|
||||
minutes = parseInt(seconds / 60, 10);
|
||||
seconds = seconds % 60;
|
||||
|
||||
if (minutes > 60) {
|
||||
hours = parseInt(minutes / 60, 10);
|
||||
minutes = minutes % 60;
|
||||
}
|
||||
|
||||
str = (hours > 0 ? this.pad(hours) + ':' : '') + this.pad(minutes) + ':' + this.pad(seconds);
|
||||
} else {
|
||||
str = '00:' + this.pad(seconds);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
pad(value) {
|
||||
if (value < 10) {
|
||||
return '0' + String(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
onLoad = (data) => {
|
||||
this.setState({
|
||||
duration: data.duration
|
||||
});
|
||||
if (this.props.onMediaLoaded) {
|
||||
this.props.onMediaLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
onProgress = (data) => {
|
||||
this.setState({ currentTime: data.currentTime });
|
||||
|
||||
if (!this.state.seeking) {
|
||||
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||
}
|
||||
|
||||
if (this.state.firstPlay) {
|
||||
if (NativeModules.Mixpanel) {
|
||||
const { uri } = this.props;
|
||||
NativeModules.Mixpanel.track('Play', { Uri: uri });
|
||||
}
|
||||
this.setState({ firstPlay: false });
|
||||
this.hidePlayerControls();
|
||||
}
|
||||
}
|
||||
|
||||
clearControlsTimeout = () => {
|
||||
if (this.state.controlsTimeout > -1) {
|
||||
clearTimeout(this.state.controlsTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
showPlayerControls = () => {
|
||||
this.clearControlsTimeout();
|
||||
if (!this.state.areControlsVisible) {
|
||||
this.setState({ areControlsVisible: true });
|
||||
}
|
||||
this.hidePlayerControls();
|
||||
}
|
||||
|
||||
hidePlayerControls() {
|
||||
const player = this;
|
||||
let timeout = setTimeout(() => {
|
||||
player.setState({ areControlsVisible: false });
|
||||
}, MediaPlayer.ControlsTimeout);
|
||||
player.setState({ controlsTimeout: timeout });
|
||||
}
|
||||
|
||||
togglePlay = () => {
|
||||
this.showPlayerControls();
|
||||
this.setState({ paused: !this.state.paused });
|
||||
}
|
||||
|
||||
toggleFullscreenMode = () => {
|
||||
this.showPlayerControls();
|
||||
const { onFullscreenToggled } = this.props;
|
||||
this.setState({ fullscreenMode: !this.state.fullscreenMode }, () => {
|
||||
this.setState({ resizeMode: this.state.fullscreenMode ? 'contain' : 'stretch' });
|
||||
if (onFullscreenToggled) {
|
||||
onFullscreenToggled(this.state.fullscreenMode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onEnd = () => {
|
||||
this.setState({ paused: true });
|
||||
this.video.seek(0);
|
||||
}
|
||||
|
||||
setSeekerPosition(position = 0) {
|
||||
position = this.checkSeekerPosition(position);
|
||||
this.setState({ seekerPosition: position });
|
||||
if (!this.state.seeking) {
|
||||
this.setState({ seekerOffset: position });
|
||||
}
|
||||
}
|
||||
|
||||
checkSeekerPosition(val = 0) {
|
||||
if (val < 0) {
|
||||
val = 0;
|
||||
} else if (val >= this.seekerWidth) {
|
||||
return this.seekerWidth;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
seekTo(time = 0) {
|
||||
if (time > this.state.duration) {
|
||||
return;
|
||||
}
|
||||
this.video.seek(time);
|
||||
this.setState({ currentTime: time });
|
||||
}
|
||||
|
||||
initSeeker() {
|
||||
this.seekResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: (evt, gestureState) => true,
|
||||
onMoveShouldSetPanResponder: (evt, gestureState) => true,
|
||||
|
||||
onPanResponderGrant: (evt, gestureState) => {
|
||||
this.clearControlsTimeout();
|
||||
if (this.state.seekTimeout > 0) {
|
||||
clearTimeout(this.state.seekTimeout);
|
||||
}
|
||||
this.setState({ seeking: true });
|
||||
},
|
||||
|
||||
onPanResponderMove: (evt, gestureState) => {
|
||||
const position = this.state.seekerOffset + gestureState.dx;
|
||||
this.setSeekerPosition(position);
|
||||
},
|
||||
|
||||
onPanResponderRelease: (evt, gestureState) => {
|
||||
const time = this.getCurrentTimeForSeekerPosition();
|
||||
if (time >= this.state.duration) {
|
||||
this.setState({ paused: true });
|
||||
this.onEnd();
|
||||
} else {
|
||||
this.seekTo(time);
|
||||
this.setState({ seekTimeout: setTimeout(() => { this.setState({ seeking: false }); }, 100) });
|
||||
}
|
||||
this.hidePlayerControls();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getTrackingOffset() {
|
||||
return this.state.fullscreenMode ? this.trackingOffset : 0;
|
||||
}
|
||||
|
||||
getCurrentTimeForSeekerPosition() {
|
||||
return this.state.duration * (this.state.seekerPosition / this.seekerWidth);
|
||||
}
|
||||
|
||||
calculateSeekerPosition() {
|
||||
return this.seekerWidth * this.getCurrentTimePercentage();
|
||||
}
|
||||
|
||||
getCurrentTimePercentage() {
|
||||
if (this.state.currentTime > 0) {
|
||||
return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.initSeeker();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clearControlsTimeout();
|
||||
this.setState({ paused: true, fullscreenMode: false });
|
||||
const { onFullscreenToggled } = this.props;
|
||||
if (onFullscreenToggled) {
|
||||
onFullscreenToggled(false);
|
||||
}
|
||||
}
|
||||
|
||||
renderPlayerControls() {
|
||||
if (this.state.areControlsVisible) {
|
||||
return (
|
||||
<View style={mediaPlayerStyle.playerControlsContainer}>
|
||||
<TouchableOpacity style={mediaPlayerStyle.playPauseButton}
|
||||
onPress={this.togglePlay}>
|
||||
{this.state.paused && <Icon name="play" size={32} color="#ffffff" />}
|
||||
{!this.state.paused && <Icon name="pause" size={32} color="#ffffff" />}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}>
|
||||
{this.state.fullscreenMode && <Icon name="compress" size={16} color="#ffffff" />}
|
||||
{!this.state.fullscreenMode && <Icon name="expand" size={16} color="#ffffff" />}
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={mediaPlayerStyle.elapsedDuration}>{this.formatTime(this.state.currentTime)}</Text>
|
||||
<Text style={mediaPlayerStyle.totalDuration}>{this.formatTime(this.state.duration)}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { backgroundPlayEnabled, fileInfo, thumbnail, onLayout, style } = this.props;
|
||||
const completedWidth = this.getCurrentTimePercentage() * this.seekerWidth;
|
||||
const remainingWidth = this.seekerWidth - completedWidth;
|
||||
let styles = [this.state.fullscreenMode ? mediaPlayerStyle.fullscreenContainer : mediaPlayerStyle.container];
|
||||
if (style) {
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
|
||||
const trackingStyle = [mediaPlayerStyle.trackingControls, this.state.fullscreenMode ?
|
||||
mediaPlayerStyle.fullscreenTrackingControls : mediaPlayerStyle.containedTrackingControls];
|
||||
|
||||
return (
|
||||
<View style={styles} onLayout={onLayout}>
|
||||
<Video source={{ uri: 'file:///' + fileInfo.download_path }}
|
||||
ref={(ref: Video) => { this.video = ref }}
|
||||
resizeMode={this.state.resizeMode}
|
||||
playInBackground={backgroundPlayEnabled}
|
||||
style={mediaPlayerStyle.player}
|
||||
rate={this.state.rate}
|
||||
volume={this.state.volume}
|
||||
paused={this.state.paused}
|
||||
onLoad={this.onLoad}
|
||||
onProgress={this.onProgress}
|
||||
onEnd={this.onEnd}
|
||||
/>
|
||||
|
||||
<TouchableOpacity style={mediaPlayerStyle.playerControls} onPress={this.showPlayerControls}>
|
||||
{this.renderPlayerControls()}
|
||||
</TouchableOpacity>
|
||||
|
||||
{(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) &&
|
||||
<View style={trackingStyle} onLayout={(evt) => {
|
||||
this.trackingOffset = evt.nativeEvent.layout.x;
|
||||
this.seekerWidth = evt.nativeEvent.layout.width;
|
||||
}}>
|
||||
<View style={mediaPlayerStyle.progress}>
|
||||
<View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} />
|
||||
<View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} />
|
||||
</View>
|
||||
</View>}
|
||||
|
||||
{this.state.areControlsVisible &&
|
||||
<View style={{ left: this.getTrackingOffset(), width: this.seekerWidth }}>
|
||||
<View style={[mediaPlayerStyle.seekerHandle,
|
||||
(this.state.fullscreenMode ? mediaPlayerStyle.seekerHandleFs : mediaPlayerStyle.seekerHandleContained),
|
||||
{ left: this.state.seekerPosition }]} { ...this.seekResponder.panHandlers }>
|
||||
<View style={this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle} />
|
||||
</View>
|
||||
</View>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MediaPlayer;
|
6
app/src/component/nsfwOverlay/index.js
Normal file
6
app/src/component/nsfwOverlay/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import NsfwOverlay from './view';
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(null, perform)(NsfwOverlay);
|
15
app/src/component/nsfwOverlay/view.js
Normal file
15
app/src/component/nsfwOverlay/view.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import discoverStyle from '../../styles/discover';
|
||||
|
||||
class NsfwOverlay extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<TouchableOpacity style={discoverStyle.overlay} activeOpacity={0.95} onPress={this.props.onPress}>
|
||||
<Text style={discoverStyle.overlayText}>This content is Not Safe For Work. To view adult content, please change your Settings.</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NsfwOverlay;
|
6
app/src/component/pageHeader/index.js
Normal file
6
app/src/component/pageHeader/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PageHeader from './view';
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(null, perform)(PageHeader);
|
47
app/src/component/pageHeader/view.js
Normal file
47
app/src/component/pageHeader/view.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Based on https://github.com/react-navigation/react-navigation/blob/master/src/views/Header/Header.js
|
||||
import React from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import pageHeaderStyle from '../../styles/pageHeader';
|
||||
|
||||
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
|
||||
const AnimatedText = Animated.Text;
|
||||
|
||||
class PageHeader extends React.PureComponent {
|
||||
render() {
|
||||
const { title, onBackPressed } = this.props;
|
||||
const containerStyles = [
|
||||
pageHeaderStyle.container,
|
||||
{ height: APPBAR_HEIGHT }
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={containerStyles}>
|
||||
<View style={pageHeaderStyle.flexOne}>
|
||||
<View style={pageHeaderStyle.header}>
|
||||
<View style={pageHeaderStyle.title}>
|
||||
<AnimatedText
|
||||
numberOfLines={1}
|
||||
style={pageHeaderStyle.titleText}
|
||||
accessibilityTraits="header">
|
||||
{title}
|
||||
</AnimatedText>
|
||||
</View>
|
||||
<TouchableOpacity style={pageHeaderStyle.left}>
|
||||
<Feather name="arrow-left" size={24} onPress={onBackPressed} style={pageHeaderStyle.backIcon} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PageHeader;
|
16
app/src/component/searchInput/index.js
Normal file
16
app/src/component/searchInput/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { NativeModules } from 'react-native';
|
||||
import { doSearch, doUpdateSearchQuery } from 'lbry-redux';
|
||||
import SearchInput from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
search: search => {
|
||||
if (NativeModules.Mixpanel) {
|
||||
NativeModules.Mixpanel.track('Search', { Query: search });
|
||||
}
|
||||
return dispatch(doSearch(search));
|
||||
},
|
||||
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query, false))
|
||||
});
|
||||
|
||||
export default connect(null, perform)(SearchInput);
|
40
app/src/component/searchInput/view.js
Normal file
40
app/src/component/searchInput/view.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import { TextInput } from 'react-native';
|
||||
|
||||
class SearchInput extends React.PureComponent {
|
||||
static INPUT_TIMEOUT = 500;
|
||||
|
||||
state = {
|
||||
changeTextTimeout: -1
|
||||
};
|
||||
|
||||
handleChangeText = text => {
|
||||
clearTimeout(this.state.changeTextTimeout);
|
||||
if (!text || text.trim().length < 2) {
|
||||
// only perform a search if 2 or more characters have been input
|
||||
return;
|
||||
}
|
||||
const { search, updateSearchQuery } = this.props;
|
||||
updateSearchQuery(text);
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
search(text);
|
||||
}, SearchInput.INPUT_TIMEOUT);
|
||||
this.setState({ changeTextTimeout: timeout });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { style, value } = this.props;
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
style={style}
|
||||
placeholder="Search"
|
||||
underlineColorAndroid="transparent"
|
||||
value={value}
|
||||
onChangeText={text => this.handleChangeText(text)} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchInput;
|
24
app/src/component/searchResultItem/index.js
Normal file
24
app/src/component/searchResultItem/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doResolveUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectIsUriResolving,
|
||||
} from 'lbry-redux';
|
||||
import { selectShowNsfw } from '../../redux/selectors/settings';
|
||||
import SearchResultItem from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
obscureNsfw: !selectShowNsfw(state)
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SearchResultItem);
|
60
app/src/component/searchResultItem/view.js
Normal file
60
app/src/component/searchResultItem/view.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import { normalizeURI, parseURI } from 'lbry-redux';
|
||||
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
|
||||
import Colors from '../../styles/colors';
|
||||
import FileItemMedia from '../fileItemMedia';
|
||||
import NsfwOverlay from '../../component/nsfwOverlay';
|
||||
import searchStyle from '../../styles/search';
|
||||
|
||||
class SearchResultItem extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
metadata,
|
||||
isResolvingUri,
|
||||
showUri,
|
||||
isDownloaded,
|
||||
style,
|
||||
onPress,
|
||||
navigation
|
||||
} = this.props;
|
||||
|
||||
const uri = normalizeURI(this.props.uri);
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const title = metadata && metadata.title ? metadata.title : parseURI(uri).contentName;
|
||||
|
||||
let name;
|
||||
let channel;
|
||||
if (claim) {
|
||||
name = claim.name;
|
||||
channel = claim.channel_name;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<TouchableOpacity style={style} onPress={onPress}>
|
||||
<View style={searchStyle.thumbnailContainer}>
|
||||
<FileItemMedia style={searchStyle.thumbnail}
|
||||
blurRadius={obscureNsfw ? 15 : 0}
|
||||
title={title}
|
||||
thumbnail={metadata ? metadata.thumbnail : null} />
|
||||
</View>
|
||||
<View style={searchStyle.detailsContainer}>
|
||||
{isResolvingUri && (
|
||||
<View>
|
||||
<Text style={searchStyle.uri}>{uri}</Text>
|
||||
<View style={searchStyle.row}>
|
||||
<ActivityIndicator size={"small"} color={Colors.LbryGreen} />
|
||||
</View>
|
||||
</View>)}
|
||||
{!isResolvingUri && <Text style={searchStyle.title}>{title || name}</Text>}
|
||||
{!isResolvingUri && channel && <Text style={searchStyle.publisher}>{channel}</Text>}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate('Settings')} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchResultItem;
|
10
app/src/component/searchRightHeaderIcon/index.js
Normal file
10
app/src/component/searchRightHeaderIcon/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import SearchRightHeaderIcon from './view';
|
||||
import { ACTIONS } from 'lbry-redux';
|
||||
const perform = dispatch => ({
|
||||
clearQuery: () => dispatch({
|
||||
type: ACTIONS.HISTORY_NAVIGATE
|
||||
})
|
||||
});
|
||||
|
||||
export default connect(null, perform)(SearchRightHeaderIcon);
|
20
app/src/component/searchRightHeaderIcon/view.js
Normal file
20
app/src/component/searchRightHeaderIcon/view.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
import { NavigationActions } from 'react-navigation';
|
||||
import Feather from "react-native-vector-icons/Feather";
|
||||
|
||||
class SearchRightHeaderIcon extends React.PureComponent {
|
||||
|
||||
clearAndGoBack() {
|
||||
const { navigation } = this.props;
|
||||
this.props.clearQuery();
|
||||
navigation.dispatch(NavigationActions.back())
|
||||
}
|
||||
|
||||
render() {
|
||||
const { style } = this.props;
|
||||
return <Feather name="x" size={24} style={style} onPress={() => this.clearAndGoBack()} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchRightHeaderIcon;
|
11
app/src/component/transactionList/index.js
Normal file
11
app/src/component/transactionList/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
//import { selectClaimedRewardsByTransactionId } from 'redux/selectors/rewards';
|
||||
import { selectAllMyClaimsByOutpoint } from 'lbry-redux';
|
||||
import TransactionList from './view';
|
||||
|
||||
const select = state => ({
|
||||
//rewards: selectClaimedRewardsByTransactionId(state),
|
||||
myClaims: selectAllMyClaimsByOutpoint(state),
|
||||
});
|
||||
|
||||
export default connect(select, null)(TransactionList);
|
|
@ -0,0 +1,59 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View, Linking } from 'react-native';
|
||||
import { buildURI, formatCredits } from 'lbry-redux';
|
||||
import Link from '../../link';
|
||||
import moment from 'moment';
|
||||
import transactionListStyle from '../../../styles/transactionList';
|
||||
|
||||
class TransactionListItem extends React.PureComponent {
|
||||
capitalize(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { transaction, navigation } = this.props;
|
||||
const { amount, claim_id: claimId, claim_name: name, date, fee, txid, type } = transaction;
|
||||
|
||||
return (
|
||||
<View style={transactionListStyle.listItem}>
|
||||
<View style={[transactionListStyle.row, transactionListStyle.topRow]}>
|
||||
<View style={transactionListStyle.col}>
|
||||
<Text style={transactionListStyle.text}>{this.capitalize(type)}</Text>
|
||||
{name && claimId && (
|
||||
<Link
|
||||
style={transactionListStyle.link}
|
||||
onPress={() => navigation && navigation.navigate({
|
||||
routeName: 'File',
|
||||
key: evt.Url,
|
||||
params: { uri: buildURI({ claimName: name, claimId }) }})
|
||||
}
|
||||
text={name} />
|
||||
)}
|
||||
</View>
|
||||
<View style={transactionListStyle.col}>
|
||||
<Text style={[transactionListStyle.amount, transactionListStyle.text]}>{formatCredits(amount, 8)}</Text>
|
||||
{ fee !== 0 && (<Text style={[transactionListStyle.amount, transactionListStyle.text]}>fee {formatCredits(fee, 8)}</Text>) }
|
||||
</View>
|
||||
</View>
|
||||
<View style={transactionListStyle.row}>
|
||||
<View style={transactionListStyle.col}>
|
||||
<Link style={transactionListStyle.smallLink}
|
||||
text={txid.substring(0, 8)}
|
||||
href={`https://explorer.lbry.io/tx/${txid}`}
|
||||
error={'The transaction URL could not be opened'} />
|
||||
</View>
|
||||
<View style={transactionListStyle.col}>
|
||||
{date ? (
|
||||
<Text style={transactionListStyle.smallText}>{moment(date).format('MMM D')}</Text>
|
||||
) : (
|
||||
<Text style={transactionListStyle.smallText}>Pending</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionListItem;
|
70
app/src/component/transactionList/view.js
Normal file
70
app/src/component/transactionList/view.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import TransactionListItem from './internal/transaction-list-item';
|
||||
import transactionListStyle from '../../styles/transactionList';
|
||||
|
||||
export type Transaction = {
|
||||
amount: number,
|
||||
claim_id: string,
|
||||
claim_name: string,
|
||||
fee: number,
|
||||
nout: number,
|
||||
txid: string,
|
||||
type: string,
|
||||
date: Date,
|
||||
};
|
||||
|
||||
class TransactionList extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filter: 'all',
|
||||
};
|
||||
|
||||
(this: any).handleFilterChanged = this.handleFilterChanged.bind(this);
|
||||
(this: any).filterTransaction = this.filterTransaction.bind(this);
|
||||
}
|
||||
|
||||
handleFilterChanged(event: React.SyntheticInputEvent<*>) {
|
||||
this.setState({
|
||||
filter: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
filterTransaction(transaction: Transaction) {
|
||||
const { filter } = this.state;
|
||||
|
||||
return filter === 'all' || filter === transaction.type;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { emptyMessage, rewards, transactions, navigation } = this.props;
|
||||
const { filter } = this.state;
|
||||
const transactionList = transactions.filter(this.filterTransaction);
|
||||
|
||||
return (
|
||||
<View>
|
||||
{!transactionList.length && (
|
||||
<Text style={transactionListStyle.noTransactions}>{emptyMessage || 'No transactions to list.'}</Text>
|
||||
)}
|
||||
|
||||
{!!transactionList.length && (
|
||||
<View>
|
||||
{transactionList.map(t => (
|
||||
<TransactionListItem
|
||||
key={`${t.txid}:${t.nout}`}
|
||||
transaction={t}
|
||||
navigation={navigation}
|
||||
reward={rewards && rewards[t.txid]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionList;
|
20
app/src/component/transactionListRecent/index.js
Normal file
20
app/src/component/transactionListRecent/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchTransactions,
|
||||
selectRecentTransactions,
|
||||
selectHasTransactions,
|
||||
selectIsFetchingTransactions,
|
||||
} from 'lbry-redux';
|
||||
import TransactionListRecent from './view';
|
||||
|
||||
const select = state => ({
|
||||
fetchingTransactions: selectIsFetchingTransactions(state),
|
||||
transactions: selectRecentTransactions(state),
|
||||
hasTransactions: selectHasTransactions(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchTransactions: () => dispatch(doFetchTransactions()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(TransactionListRecent);
|
50
app/src/component/transactionListRecent/view.js
Normal file
50
app/src/component/transactionListRecent/view.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
//import BusyIndicator from 'component/common/busy-indicator';
|
||||
import { Text, View } from 'react-native';
|
||||
import Button from '../button';
|
||||
import Link from '../link';
|
||||
import TransactionList from '../transactionList';
|
||||
import type { Transaction } from '../transactionList/view';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
fetchTransactions: () => void,
|
||||
fetchingTransactions: boolean,
|
||||
hasTransactions: boolean,
|
||||
transactions: Array<Transaction>,
|
||||
};
|
||||
|
||||
class TransactionListRecent extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchTransactions();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fetchingTransactions, hasTransactions, transactions, navigation } = this.props;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.transactionsCard}>
|
||||
<View style={[walletStyle.row, walletStyle.transactionsHeader]}>
|
||||
<Text style={walletStyle.transactionsTitle}>Recent Transactions</Text>
|
||||
<Link style={walletStyle.link}
|
||||
navigation={navigation}
|
||||
text={'View All'}
|
||||
href={'#TransactionHistory'} />
|
||||
</View>
|
||||
{fetchingTransactions && (
|
||||
<Text style={walletStyle.infoText}>Fetching transactions...</Text>
|
||||
)}
|
||||
{!fetchingTransactions && (
|
||||
<TransactionList
|
||||
navigation={navigation}
|
||||
transactions={transactions}
|
||||
emptyMessage={"Looks like you don't have any recent transactions."}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionListRecent;
|
17
app/src/component/uriBar/index.js
Normal file
17
app/src/component/uriBar/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doUpdateSearchQuery, selectSearchState as selectSearch } from 'lbry-redux';
|
||||
import UriBar from './view';
|
||||
|
||||
const select = state => {
|
||||
const { ...searchState } = selectSearch(state);
|
||||
|
||||
return {
|
||||
...searchState
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(UriBar);
|
38
app/src/component/uriBar/internal/uri-bar-item.js
Normal file
38
app/src/component/uriBar/internal/uri-bar-item.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { SEARCH_TYPES, normalizeURI } from 'lbry-redux';
|
||||
import { Text, TouchableOpacity, View } from 'react-native';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import uriBarStyle from '../../../styles/uriBar';
|
||||
|
||||
class UriBarItem extends React.PureComponent {
|
||||
render() {
|
||||
const { item, onPress } = this.props;
|
||||
const { shorthand, type, value } = item;
|
||||
|
||||
let icon;
|
||||
switch (type) {
|
||||
case SEARCH_TYPES.CHANNEL:
|
||||
icon = <Feather name="at-sign" size={18} />
|
||||
break;
|
||||
|
||||
case SEARCH_TYPES.SEARCH:
|
||||
icon = <Feather name="search" size={18} />
|
||||
break;
|
||||
|
||||
case SEARCH_TYPES.FILE:
|
||||
default:
|
||||
icon = <Feather name="file" size={18} />
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={uriBarStyle.item} onPress={onPress}>
|
||||
{icon}
|
||||
<Text style={uriBarStyle.itemText} numberOfLines={1}>{shorthand || value} - {type === 'search' ? 'Search' : value}</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default UriBarItem;
|
123
app/src/component/uriBar/view.js
Normal file
123
app/src/component/uriBar/view.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { SEARCH_TYPES, isNameValid, isURIValid, normalizeURI } from 'lbry-redux';
|
||||
import { FlatList, Keyboard, TextInput, View } from 'react-native';
|
||||
import UriBarItem from './internal/uri-bar-item';
|
||||
import uriBarStyle from '../../styles/uriBar';
|
||||
|
||||
class UriBar extends React.PureComponent {
|
||||
static INPUT_TIMEOUT = 500;
|
||||
|
||||
textInput = null;
|
||||
|
||||
keyboardDidHideListener = null;
|
||||
|
||||
componentDidMount () {
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.keyboardDidHideListener) {
|
||||
this.keyboardDidHideListener.remove();
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
changeTextTimeout: null,
|
||||
currentValue: null,
|
||||
inputText: null,
|
||||
focused: false
|
||||
};
|
||||
}
|
||||
|
||||
handleChangeText = text => {
|
||||
const newValue = text ? text : '';
|
||||
clearTimeout(this.state.changeTextTimeout);
|
||||
const { updateSearchQuery } = this.props;
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
updateSearchQuery(text);
|
||||
}, UriBar.INPUT_TIMEOUT);
|
||||
this.setState({ inputText: newValue, currentValue: newValue, changeTextTimeout: timeout });
|
||||
}
|
||||
|
||||
handleItemPress = (item) => {
|
||||
const { navigation, updateSearchQuery } = this.props;
|
||||
const { type, value } = item;
|
||||
|
||||
Keyboard.dismiss();
|
||||
|
||||
if (SEARCH_TYPES.SEARCH === type) {
|
||||
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: value }});
|
||||
} else {
|
||||
const uri = normalizeURI(value);
|
||||
navigation.navigate({ routeName: 'File', key: uri, params: { uri }});
|
||||
}
|
||||
}
|
||||
|
||||
_keyboardDidHide = () => {
|
||||
if (this.textInput) {
|
||||
this.textInput.blur();
|
||||
}
|
||||
this.setState({ focused: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navigation, suggestions, updateSearchQuery, value } = this.props;
|
||||
if (this.state.currentValue === null) {
|
||||
this.setState({ currentValue: value });
|
||||
}
|
||||
|
||||
let style = [uriBarStyle.overlay];
|
||||
if (this.state.focused) {
|
||||
style.push(uriBarStyle.inFocus);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
{this.state.focused && (
|
||||
<View style={uriBarStyle.suggestions}>
|
||||
<FlatList style={uriBarStyle.suggestionList}
|
||||
data={suggestions}
|
||||
keyboardShouldPersistTaps={'handled'}
|
||||
keyExtractor={(item, value) => item.value}
|
||||
renderItem={({item}) => <UriBarItem item={item}
|
||||
navigation={navigation}
|
||||
onPress={() => this.handleItemPress(item)} />} />
|
||||
</View>)}
|
||||
<View style={uriBarStyle.uriContainer}>
|
||||
<TextInput ref={(ref) => { this.textInput = ref }}
|
||||
style={uriBarStyle.uriText}
|
||||
selectTextOnFocus={true}
|
||||
placeholder={'Search for videos, music, games and more'}
|
||||
underlineColorAndroid={'transparent'}
|
||||
numberOfLines={1}
|
||||
clearButtonMode={'while-editing'}
|
||||
value={this.state.currentValue}
|
||||
returnKeyType={'go'}
|
||||
inlineImageLeft={'baseline_search_black_24'}
|
||||
inlineImagePadding={16}
|
||||
onFocus={() => this.setState({ focused: true })}
|
||||
onBlur={() => this.setState({ focused: false })}
|
||||
onChangeText={this.handleChangeText}
|
||||
onSubmitEditing={() => {
|
||||
if (this.state.inputText) {
|
||||
let inputText = this.state.inputText;
|
||||
if (isNameValid(inputText) || isURIValid(inputText)) {
|
||||
const uri = normalizeURI(inputText);
|
||||
navigation.navigate({ routeName: 'File', key: uri, params: { uri }});
|
||||
} else {
|
||||
// Open the search page with the query populated
|
||||
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: inputText }});
|
||||
}
|
||||
}
|
||||
}}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UriBar;
|
20
app/src/component/walletAddress/index.js
Normal file
20
app/src/component/walletAddress/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doCheckAddressIsMine,
|
||||
doGetNewAddress,
|
||||
selectReceiveAddress,
|
||||
selectGettingNewAddress,
|
||||
} from 'lbry-redux';
|
||||
import WalletAddress from './view';
|
||||
|
||||
const select = state => ({
|
||||
receiveAddress: selectReceiveAddress(state),
|
||||
gettingNewAddress: selectGettingNewAddress(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
checkAddressIsMine: address => dispatch(doCheckAddressIsMine(address)),
|
||||
getNewAddress: () => dispatch(doGetNewAddress()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(WalletAddress);
|
47
app/src/component/walletAddress/view.js
Normal file
47
app/src/component/walletAddress/view.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import Address from '../address';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
checkAddressIsMine: string => void,
|
||||
receiveAddress: string,
|
||||
getNewAddress: () => void,
|
||||
gettingNewAddress: boolean,
|
||||
};
|
||||
|
||||
class WalletAddress extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
const { checkAddressIsMine, receiveAddress, getNewAddress } = this.props;
|
||||
if (!receiveAddress) {
|
||||
getNewAddress();
|
||||
} else {
|
||||
checkAddressIsMine(receiveAddress);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { receiveAddress, getNewAddress, gettingNewAddress } = this.props;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.card}>
|
||||
<Text style={walletStyle.title}>Receive Credits</Text>
|
||||
<Text style={[walletStyle.text, walletStyle.bottomMarginMedium]}>Use this wallet address to receive credits sent by another user (or yourself).</Text>
|
||||
<Address address={receiveAddress} style={walletStyle.bottomMarginSmall} />
|
||||
<Button style={[walletStyle.button, walletStyle.bottomMarginLarge]}
|
||||
icon={'refresh'}
|
||||
text={'Get New Address'}
|
||||
onPress={getNewAddress}
|
||||
disabled={gettingNewAddress}
|
||||
/>
|
||||
<Text style={walletStyle.smallText}>
|
||||
You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletAddress;
|
9
app/src/component/walletBalance/index.js
Normal file
9
app/src/component/walletBalance/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectBalance } from 'lbry-redux';
|
||||
import WalletBalance from './view';
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
});
|
||||
|
||||
export default connect(select, null)(WalletBalance);
|
29
app/src/component/walletBalance/view.js
Normal file
29
app/src/component/walletBalance/view.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Image, Text, View } from 'react-native';
|
||||
import { formatCredits } from 'lbry-redux'
|
||||
import Address from '../address';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
};
|
||||
|
||||
class WalletBalance extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { balance } = this.props;
|
||||
return (
|
||||
<View style={walletStyle.balanceCard}>
|
||||
<Image style={walletStyle.balanceBackground} resizeMode={'cover'} source={require('../../assets/stripe.png')} />
|
||||
<Text style={walletStyle.balanceTitle}>Balance</Text>
|
||||
<Text style={walletStyle.balanceCaption}>You currently have</Text>
|
||||
<Text style={walletStyle.balance}>
|
||||
{(balance || balance === 0) && (formatCredits(balance, 2) + ' LBC')}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletBalance;
|
22
app/src/component/walletSend/index.js
Normal file
22
app/src/component/walletSend/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doNotify,
|
||||
doSendDraftTransaction,
|
||||
selectDraftTransaction,
|
||||
selectDraftTransactionError,
|
||||
selectBalance
|
||||
} from 'lbry-redux';
|
||||
import WalletSend from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
sendToAddress: (address, amount) => dispatch(doSendDraftTransaction(address, amount)),
|
||||
notify: (data) => dispatch(doNotify(data))
|
||||
});
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
draftTransaction: selectDraftTransaction(state),
|
||||
transactionError: selectDraftTransactionError(state),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(WalletSend);
|
119
app/src/component/walletSend/view.js
Normal file
119
app/src/component/walletSend/view.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { regexAddress } from 'lbry-redux';
|
||||
import { TextInput, Text, View } from 'react-native';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type DraftTransaction = {
|
||||
address: string,
|
||||
amount: ?number, // So we can use a placeholder in the input
|
||||
};
|
||||
|
||||
type Props = {
|
||||
sendToAddress: (string, number) => void,
|
||||
balance: number,
|
||||
};
|
||||
|
||||
class WalletSend extends React.PureComponent<Props> {
|
||||
amountInput = null;
|
||||
|
||||
state = {
|
||||
amount: null,
|
||||
address: null,
|
||||
addressChanged: false,
|
||||
addressValid: false
|
||||
};
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
const { draftTransaction, transactionError } = nextProps;
|
||||
if (transactionError && transactionError.trim().length > 0) {
|
||||
this.setState({ address: draftTransaction.address, amount: draftTransaction.amount });
|
||||
}
|
||||
}
|
||||
|
||||
handleSend = () => {
|
||||
const { balance, sendToAddress, notify } = this.props;
|
||||
const { address, amount } = this.state;
|
||||
if (address && !regexAddress.test(address)) {
|
||||
notify({
|
||||
message: 'The recipient address is not a valid LBRY address.',
|
||||
displayType: ['toast']
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > balance) {
|
||||
notify({
|
||||
message: 'Insufficient credits',
|
||||
displayType: ['toast']
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount && address) {
|
||||
sendToAddress(address, parseFloat(amount));
|
||||
this.setState({ address: null, amount: null });
|
||||
}
|
||||
}
|
||||
|
||||
handleAddressInputBlur = () => {
|
||||
if (this.state.addressChanged && !this.state.addressValid) {
|
||||
const { notify } = this.props;
|
||||
notify({
|
||||
message: 'The recipient address is not a valid LBRY address.',
|
||||
displayType: ['toast']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleAddressInputSubmit = () => {
|
||||
if (this.amountInput) {
|
||||
this.amountInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { balance } = this.props;
|
||||
const canSend = this.state.address &&
|
||||
this.state.amount > 0 &&
|
||||
this.state.address.trim().length > 0;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.card}>
|
||||
<Text style={walletStyle.title}>Send Credits</Text>
|
||||
<Text style={walletStyle.text}>Recipient address</Text>
|
||||
<View style={[walletStyle.row, walletStyle.bottomMarginMedium]}>
|
||||
<TextInput onChangeText={value => this.setState({
|
||||
address: value,
|
||||
addressChanged: true,
|
||||
addressValid: (value.trim().length == 0 || regexAddress.test(value))
|
||||
})}
|
||||
onBlur={this.handleAddressInputBlur}
|
||||
onSubmitEditing={this.handleAddressInputSubmit}
|
||||
placeholder={'bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs'}
|
||||
value={this.state.address}
|
||||
returnKeyType={'next'}
|
||||
style={[walletStyle.input, walletStyle.addressInput, walletStyle.bottomMarginMedium]} />
|
||||
</View>
|
||||
<Text style={walletStyle.text}>Amount</Text>
|
||||
<View style={walletStyle.row}>
|
||||
<View style={walletStyle.amountRow}>
|
||||
<TextInput ref={ref => this.amountInput = ref}
|
||||
onChangeText={value => this.setState({amount: value})}
|
||||
keyboardType={'numeric'}
|
||||
value={this.state.amount}
|
||||
style={[walletStyle.input, walletStyle.amountInput]} />
|
||||
<Text style={[walletStyle.text, walletStyle.currency]}>LBC</Text>
|
||||
</View>
|
||||
<Button text={'Send'}
|
||||
style={[walletStyle.button, walletStyle.sendButton]}
|
||||
disabled={!canSend}
|
||||
onPress={this.handleSend} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletSend;
|
5
app/src/constants.js
Normal file
5
app/src/constants.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const Constants = {
|
||||
SETTING_ALPHA_UNDERSTANDS_RISKS: "ALPHA_UNDERSTANDS_RISKS"
|
||||
};
|
||||
|
||||
export default Constants;
|
130
app/src/index.js
Normal file
130
app/src/index.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
import React from 'react';
|
||||
import { Provider, connect } from 'react-redux';
|
||||
import DiscoverPage from './page/discover';
|
||||
import {
|
||||
AppRegistry,
|
||||
AppState,
|
||||
AsyncStorage,
|
||||
Text,
|
||||
View,
|
||||
NativeModules
|
||||
} from 'react-native';
|
||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||
import {
|
||||
StackNavigator, addNavigationHelpers
|
||||
} from 'react-navigation';
|
||||
import { AppNavigator } from './component/AppNavigator';
|
||||
import AppWithNavigationState from './component/AppNavigator';
|
||||
import { persistStore, autoRehydrate } from 'redux-persist';
|
||||
import createCompressor from 'redux-persist-transform-compress';
|
||||
import createFilter from 'redux-persist-transform-filter';
|
||||
import thunk from 'redux-thunk';
|
||||
import {
|
||||
Lbry,
|
||||
claimsReducer,
|
||||
costInfoReducer,
|
||||
fileInfoReducer,
|
||||
notificationsReducer,
|
||||
searchReducer,
|
||||
walletReducer
|
||||
} from 'lbry-redux';
|
||||
import settingsReducer from './redux/reducers/settings';
|
||||
import moment from 'moment';
|
||||
import { reactNavigationMiddleware } from './utils/redux';
|
||||
|
||||
function isFunction(object) {
|
||||
return typeof object === 'function';
|
||||
}
|
||||
|
||||
function isNotFunction(object) {
|
||||
return !isFunction(object);
|
||||
}
|
||||
|
||||
function createBulkThunkMiddleware() {
|
||||
return ({ dispatch, getState }) => next => action => {
|
||||
if (action.type === 'BATCH_ACTIONS') {
|
||||
action.actions.filter(isFunction).map(actionFn => actionFn(dispatch, getState));
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
}
|
||||
|
||||
function enableBatching(reducer) {
|
||||
return function batchingReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'BATCH_ACTIONS':
|
||||
return action.actions.filter(isNotFunction).reduce(batchingReducer, state);
|
||||
default:
|
||||
return reducer(state, action);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const router = AppNavigator.router;
|
||||
const navAction = router.getActionForPathAndParams('FirstRun');
|
||||
const initialNavState = router.getStateForAction(navAction);
|
||||
const navigatorReducer = (state = initialNavState, action) => {
|
||||
const nextState = AppNavigator.router.getStateForAction(action, state);
|
||||
return nextState || state;
|
||||
};
|
||||
|
||||
const reducers = combineReducers({
|
||||
claims: claimsReducer,
|
||||
costInfo: costInfoReducer,
|
||||
fileInfo: fileInfoReducer,
|
||||
notifications: notificationsReducer,
|
||||
search: searchReducer,
|
||||
wallet: walletReducer,
|
||||
nav: navigatorReducer,
|
||||
settings: settingsReducer
|
||||
});
|
||||
|
||||
const bulkThunk = createBulkThunkMiddleware();
|
||||
const middleware = [thunk, bulkThunk, reactNavigationMiddleware];
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const composeEnhancers = compose;
|
||||
|
||||
const store = createStore(
|
||||
enableBatching(reducers),
|
||||
{}, // initial state,
|
||||
composeEnhancers(
|
||||
autoRehydrate(),
|
||||
applyMiddleware(...middleware)
|
||||
)
|
||||
);
|
||||
|
||||
const compressor = createCompressor();
|
||||
const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
|
||||
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
|
||||
const settingsFilter = createFilter('settings', ['clientSettings']);
|
||||
const walletFilter = createFilter('wallet', ['receiveAddress']);
|
||||
|
||||
const persistOptions = {
|
||||
whitelist: ['claims', 'subscriptions', 'settings', 'wallet'],
|
||||
// Order is important. Needs to be compressed last or other transforms can't
|
||||
// read the data
|
||||
transforms: [saveClaimsFilter, subscriptionsFilter, settingsFilter, walletFilter, compressor],
|
||||
debounce: 10000,
|
||||
storage: AsyncStorage
|
||||
};
|
||||
|
||||
persistStore(store, persistOptions, err => {
|
||||
if (err) {
|
||||
console.log('Unable to load saved SETTINGS');
|
||||
}
|
||||
});
|
||||
|
||||
class LBRYApp extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<AppWithNavigationState />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppRegistry.registerComponent('LBRYApp', () => LBRYApp);
|
||||
|
||||
export default LBRYApp;
|
6
app/src/page/about/index.js
Normal file
6
app/src/page/about/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import AboutPage from './view';
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(null, perform)(AboutPage);
|
86
app/src/page/about/view.js
Normal file
86
app/src/page/about/view.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import { NativeModules, Text, View, ScrollView } from 'react-native';
|
||||
import Link from '../../component/link';
|
||||
import PageHeader from '../../component/pageHeader';
|
||||
import aboutStyle from '../../styles/about';
|
||||
|
||||
class AboutPage extends React.PureComponent {
|
||||
state = {
|
||||
appVersion: null,
|
||||
lbryId: null,
|
||||
versionInfo: null
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (NativeModules.VersionInfo) {
|
||||
NativeModules.VersionInfo.getAppVersion().then(version => {
|
||||
this.setState({appVersion: version});
|
||||
});
|
||||
}
|
||||
Lbry.version().then(info => {
|
||||
this.setState({
|
||||
versionInfo: info,
|
||||
});
|
||||
});
|
||||
Lbry.status({ session_status: true }).then(info => {
|
||||
this.setState({
|
||||
lbryId: info.lbry_id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const loading = 'Loading...';
|
||||
const ver = this.state.versionInfo ? this.state.versionInfo : null;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<PageHeader title={"About LBRY"}
|
||||
onBackPressed={() => { this.props.navigation.goBack(); }} />
|
||||
<ScrollView style={aboutStyle.scrollContainer}>
|
||||
<Text style={aboutStyle.title}>Content Freedom</Text>
|
||||
<Text style={aboutStyle.paragraph}>
|
||||
LBRY is a free, open, and community-run digital marketplace. It is a decentralized peer-to-peer
|
||||
content distribution platform for creators to upload and share content, and earn LBRY credits
|
||||
for their effort. Users will be able to find a wide selection of videos, music, ebooks and other
|
||||
digital content they are interested in.
|
||||
</Text>
|
||||
<View style={aboutStyle.links}>
|
||||
<Link style={aboutStyle.link} href="https://lbry.io/faq/what-is-lbry" text="What is LBRY?" />
|
||||
<Link style={aboutStyle.link} href="https://lbry.io/faq" text="Frequently Asked Questions" />
|
||||
</View>
|
||||
<Text style={aboutStyle.releaseInfoTitle}>Release information</Text>
|
||||
<View style={aboutStyle.row}>
|
||||
<View style={aboutStyle.col}><Text style={aboutStyle.text}>App version</Text></View>
|
||||
<View style={aboutStyle.col}><Text selectable={true} style={aboutStyle.valueText}>{this.state.appVersion}</Text></View>
|
||||
</View>
|
||||
|
||||
<View style={aboutStyle.row}>
|
||||
<View style={aboutStyle.col}><Text style={aboutStyle.text}>Daemon (lbrynet)</Text></View>
|
||||
<View style={aboutStyle.col}><Text selectable={true} style={aboutStyle.valueText}>{ver ? ver.lbrynet_version : loading }</Text></View>
|
||||
</View>
|
||||
|
||||
<View style={aboutStyle.row}>
|
||||
<View style={aboutStyle.col}><Text style={aboutStyle.text}>Wallet (lbryum)</Text></View>
|
||||
<View style={aboutStyle.col}><Text selectable={true} style={aboutStyle.valueText}>{ver ? ver.lbryum_version : loading }</Text></View>
|
||||
</View>
|
||||
|
||||
<View style={aboutStyle.row}>
|
||||
<View style={aboutStyle.col}><Text style={aboutStyle.text}>Platform</Text></View>
|
||||
<View style={aboutStyle.col}><Text selectable={true} style={aboutStyle.valueText}>{ver ? ver.platform : loading }</Text></View>
|
||||
</View>
|
||||
|
||||
<View style={aboutStyle.row}>
|
||||
<View style={aboutStyle.col}>
|
||||
<Text style={aboutStyle.text}>Installation ID</Text>
|
||||
<Text selectable={true} style={aboutStyle.lineValueText}>{this.state.lbryId ? this.state.lbryId : loading}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AboutPage;
|
22
app/src/page/channel/index.js
Normal file
22
app/src/page/channel/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchClaimsByChannel,
|
||||
doFetchClaimCountByChannel,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimsInChannelForPage,
|
||||
makeSelectFetchingChannelClaims,
|
||||
} from 'lbry-redux';
|
||||
import ChannelPage from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimsInChannel: makeSelectClaimsInChannelForPage(props.uri, props.page || 1)(state),
|
||||
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)),
|
||||
fetchClaimCount: uri => dispatch(doFetchClaimCountByChannel(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ChannelPage);
|
58
app/src/page/channel/view.js
Normal file
58
app/src/page/channel/view.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, Text, View } from 'react-native';
|
||||
import Colors from '../../styles/colors';
|
||||
import FileList from '../../component/fileList';
|
||||
import PageHeader from '../../component/pageHeader';
|
||||
import UriBar from '../../component/uriBar';
|
||||
import channelPageStyle from '../../styles/channelPage';
|
||||
|
||||
class ChannelPage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { uri, page, claimsInChannel, fetchClaims, fetchClaimCount } = this.props;
|
||||
|
||||
if (!claimsInChannel || !claimsInChannel.length) {
|
||||
fetchClaims(uri, page || 1);
|
||||
fetchClaimCount(uri);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fetching, claimsInChannel, claim, navigation, uri } = this.props;
|
||||
const { name, permanent_url: permanentUrl } = claim;
|
||||
|
||||
let contentList;
|
||||
if (fetching) {
|
||||
contentList = (
|
||||
<View style={channelPageStyle.busyContainer}>
|
||||
<ActivityIndicator size="large" color={Colors.LbryGreen} />
|
||||
<Text style={channelPageStyle.infoText}>Fetching content...</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
contentList =
|
||||
claimsInChannel && claimsInChannel.length ? (
|
||||
<FileList sortByHeight
|
||||
hideFilter
|
||||
fileInfos={claimsInChannel}
|
||||
navigation={navigation}
|
||||
style={channelPageStyle.fileList} />
|
||||
) : (
|
||||
<View style={channelPageStyle.busyContainer}>
|
||||
<Text style={channelPageStyle.infoText}>No content found.</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<View style={channelPageStyle.container}>
|
||||
<PageHeader title={name} onBackPressed={() => { this.props.navigation.goBack(); }} />
|
||||
{contentList}
|
||||
<UriBar value={uri} navigation={navigation} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelPage;
|
14
app/src/page/discover/index.js
Normal file
14
app/src/page/discover/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doFetchFeaturedUris, selectFeaturedUris, selectFetchingFeaturedUris } from 'lbry-redux';
|
||||
import DiscoverPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
featuredUris: selectFeaturedUris(state),
|
||||
fetchingFeaturedUris: selectFetchingFeaturedUris(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchFeaturedUris: () => dispatch(doFetchFeaturedUris()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(DiscoverPage);
|
82
app/src/page/discover/view.js
Normal file
82
app/src/page/discover/view.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import React from 'react';
|
||||
import NavigationActions from 'react-navigation';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AsyncStorage,
|
||||
NativeModules,
|
||||
SectionList,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { normalizeURI } from 'lbry-redux';
|
||||
import moment from 'moment';
|
||||
import FileItem from '../../component/fileItem';
|
||||
import discoverStyle from '../../styles/discover';
|
||||
import Colors from '../../styles/colors';
|
||||
import UriBar from '../../component/uriBar';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
|
||||
class DiscoverPage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
// Track the total time taken if this is the first launch
|
||||
AsyncStorage.getItem('firstLaunchTime').then(startTime => {
|
||||
if (startTime !== null && !isNaN(parseInt(startTime, 10))) {
|
||||
// We don't need this value anymore once we've retrieved it
|
||||
AsyncStorage.removeItem('firstLaunchTime');
|
||||
|
||||
// We know this is the first app launch because firstLaunchTime is set and it's a valid number
|
||||
const start = parseInt(startTime, 10);
|
||||
const now = moment().unix();
|
||||
const delta = now - start;
|
||||
AsyncStorage.getItem('firstLaunchSuspended').then(suspended => {
|
||||
AsyncStorage.removeItem('firstLaunchSuspended');
|
||||
const appSuspended = (suspended === 'true');
|
||||
if (NativeModules.Mixpanel) {
|
||||
NativeModules.Mixpanel.track('First Run Time', {
|
||||
'Total Seconds': delta, 'App Suspended': appSuspended
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.props.fetchFeaturedUris();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { featuredUris, fetchingFeaturedUris, navigation } = this.props;
|
||||
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length,
|
||||
failedToLoad = !fetchingFeaturedUris && !hasContent;
|
||||
|
||||
return (
|
||||
<View style={discoverStyle.container}>
|
||||
{!hasContent && fetchingFeaturedUris && (
|
||||
<View style={discoverStyle.busyContainer}>
|
||||
<ActivityIndicator size="large" color={Colors.LbryGreen} />
|
||||
<Text style={discoverStyle.title}>Fetching content...</Text>
|
||||
</View>
|
||||
)}
|
||||
{hasContent &&
|
||||
<SectionList style={discoverStyle.scrollContainer}
|
||||
renderItem={ ({item, index, section}) => (
|
||||
<FileItem
|
||||
style={discoverStyle.fileItem}
|
||||
key={item}
|
||||
uri={normalizeURI(item)}
|
||||
navigation={navigation} />
|
||||
)
|
||||
}
|
||||
renderSectionHeader={
|
||||
({section: {title}}) => (<Text style={discoverStyle.categoryName}>{title}</Text>)
|
||||
}
|
||||
sections={Object.keys(featuredUris).map(category => ({ title: category, data: featuredUris[category] }))}
|
||||
keyExtractor={(item, index) => item}
|
||||
/>
|
||||
}
|
||||
<UriBar navigation={navigation} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DiscoverPage;
|
44
app/src/page/file/index.js
Normal file
44
app/src/page/file/index.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchFileInfo,
|
||||
doResolveUri,
|
||||
doFetchCostInfoForUri,
|
||||
makeSelectIsUriResolving,
|
||||
makeSelectCostInfoForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectContentTypeForUri,
|
||||
makeSelectMetadataForUri,
|
||||
selectRewardContentClaimIds,
|
||||
selectBlackListedOutpoints,
|
||||
} from 'lbry-redux';
|
||||
import { doDeleteFile, doStopDownloadingFile } from '../../redux/actions/file';
|
||||
import FilePage from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const selectProps = { uri: props.navigation.state.params.uri };
|
||||
return {
|
||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||
claim: makeSelectClaimForUri(selectProps.uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(selectProps.uri)(state),
|
||||
contentType: makeSelectContentTypeForUri(selectProps.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(selectProps.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(selectProps.uri)(state),
|
||||
//obscureNsfw: !selectShowNsfw(state),
|
||||
//tab: makeSelectCurrentParam('tab')(state),
|
||||
fileInfo: makeSelectFileInfoForUri(selectProps.uri)(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, selectProps),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
stopDownload: (uri, fileInfo) => dispatch(doStopDownloadingFile(uri, fileInfo)),
|
||||
deleteFile: (fileInfo, deleteFromDevice, abandonClaim) => {
|
||||
dispatch(doDeleteFile(fileInfo, deleteFromDevice, abandonClaim));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FilePage);
|
353
app/src/page/file/view.js
Normal file
353
app/src/page/file/view.js
Normal file
|
@ -0,0 +1,353 @@
|
|||
import React from 'react';
|
||||
import { Lbry, normalizeURI } from 'lbry-redux';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Button,
|
||||
Dimensions,
|
||||
NativeModules,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
WebView
|
||||
} from 'react-native';
|
||||
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||
import Colors from '../../styles/colors';
|
||||
import ChannelPage from '../channel';
|
||||
import FileDownloadButton from '../../component/fileDownloadButton';
|
||||
import FileItemMedia from '../../component/fileItemMedia';
|
||||
import FilePrice from '../../component/filePrice';
|
||||
import Link from '../../component/link';
|
||||
import MediaPlayer from '../../component/mediaPlayer';
|
||||
import UriBar from '../../component/uriBar';
|
||||
import Video from 'react-native-video';
|
||||
import filePageStyle from '../../styles/filePage';
|
||||
import uriBarStyle from '../../styles/uriBar';
|
||||
|
||||
class FilePage extends React.PureComponent {
|
||||
static navigationOptions = {
|
||||
title: ''
|
||||
};
|
||||
|
||||
playerBackground = null;
|
||||
|
||||
player = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
mediaLoaded: false,
|
||||
autoplayMedia: false,
|
||||
fullscreenMode: false,
|
||||
showImageViewer: false,
|
||||
showWebView: false,
|
||||
imageUrls: null,
|
||||
playerBgHeight: 0,
|
||||
playerHeight: 0,
|
||||
isLandscape: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
StatusBar.setHidden(false);
|
||||
|
||||
const { isResolvingUri, resolveUri, navigation } = this.props;
|
||||
const { uri } = navigation.state.params;
|
||||
if (!isResolvingUri) resolveUri(uri);
|
||||
|
||||
this.fetchFileInfo(this.props);
|
||||
this.fetchCostInfo(this.props);
|
||||
|
||||
if (NativeModules.Mixpanel) {
|
||||
NativeModules.Mixpanel.track('Open File Page', { Uri: uri });
|
||||
}
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.keepAwakeOn();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.fetchFileInfo(this.props);
|
||||
const { isResolvingUri, resolveUri, claim, navigation } = this.props;
|
||||
const { uri } = navigation.state.params;
|
||||
|
||||
if (!isResolvingUri && claim === undefined && uri) {
|
||||
resolveUri(uri);
|
||||
}
|
||||
}
|
||||
|
||||
fetchFileInfo(props) {
|
||||
if (props.fileInfo === undefined) {
|
||||
props.fetchFileInfo(props.navigation.state.params.uri);
|
||||
}
|
||||
}
|
||||
|
||||
fetchCostInfo(props) {
|
||||
if (props.costInfo === undefined) {
|
||||
props.fetchCostInfo(props.navigation.state.params.uri);
|
||||
}
|
||||
}
|
||||
|
||||
handleFullscreenToggle = (mode) => {
|
||||
this.setState({ fullscreenMode: mode });
|
||||
StatusBar.setHidden(mode);
|
||||
if (NativeModules.ScreenOrientation) {
|
||||
if (mode) {
|
||||
// fullscreen, so change orientation to landscape mode
|
||||
NativeModules.ScreenOrientation.lockOrientationLandscape();
|
||||
} else {
|
||||
// Switch back to portrait mode when the media is not fullscreen
|
||||
NativeModules.ScreenOrientation.lockOrientationPortrait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDeletePressed = () => {
|
||||
const { deleteFile, fileInfo } = this.props;
|
||||
|
||||
Alert.alert(
|
||||
'Delete file',
|
||||
'Are you sure you want to remove this file from your device?',
|
||||
[
|
||||
{ text: 'No' },
|
||||
{ text: 'Yes', onPress: () => { deleteFile(fileInfo.outpoint, true); } }
|
||||
],
|
||||
{ cancelable: true }
|
||||
);
|
||||
}
|
||||
|
||||
onStopDownloadPressed = () => {
|
||||
const { deleteFile, stopDownload, fileInfo, navigation } = this.props;
|
||||
|
||||
Alert.alert(
|
||||
'Stop download',
|
||||
'Are you sure you want to stop downloading this file?',
|
||||
[
|
||||
{ text: 'No' },
|
||||
{ text: 'Yes', onPress: () => { stopDownload(navigation.state.params.uri, fileInfo); } }
|
||||
],
|
||||
{ cancelable: true }
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
StatusBar.setHidden(false);
|
||||
if (NativeModules.ScreenOrientation) {
|
||||
NativeModules.ScreenOrientation.unlockOrientation();
|
||||
}
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.keepAwakeOff();
|
||||
}
|
||||
}
|
||||
|
||||
localUriForFileInfo = (fileInfo) => {
|
||||
if (!fileInfo) {
|
||||
return null;
|
||||
}
|
||||
return 'file:///' + fileInfo.download_path;
|
||||
}
|
||||
|
||||
linkify = (text) => {
|
||||
let linkifiedContent = [];
|
||||
let lines = text.split(/\n/g);
|
||||
linkifiedContent = lines.map((line, i) => {
|
||||
let tokens = line.split(/\s/g);
|
||||
let lineContent = tokens.length === 0 ? '' : tokens.map((token, j) => {
|
||||
let hasSpace = j !== (tokens.length - 1);
|
||||
let space = hasSpace ? ' ' : '';
|
||||
|
||||
if (token.match(/^(lbry|https?):\/\//g)) {
|
||||
return (
|
||||
<Link key={j}
|
||||
style={filePageStyle.link}
|
||||
href={token}
|
||||
text={token}
|
||||
effectOnTap={filePageStyle.linkTapped} />
|
||||
);
|
||||
} else {
|
||||
return token + space;
|
||||
}
|
||||
});
|
||||
|
||||
lineContent.push("\n");
|
||||
return (<Text key={i}>{lineContent}</Text>);
|
||||
});
|
||||
|
||||
return linkifiedContent;
|
||||
}
|
||||
|
||||
checkOrientation = () => {
|
||||
if (this.state.fullscreenMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const screenDimension = Dimensions.get('window');
|
||||
const screenWidth = screenDimension.width;
|
||||
const screenHeight = screenDimension.height;
|
||||
const isLandscape = screenWidth > screenHeight;
|
||||
this.setState({ isLandscape });
|
||||
|
||||
if (isLandscape) {
|
||||
this.playerBackground.setNativeProps({ height: screenHeight - StyleSheet.flatten(uriBarStyle.uriContainer).height });
|
||||
} else if (this.state.playerBgHeight > 0) {
|
||||
this.playerBackground.setNativeProps({ height: this.state.playerBgHeight });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
fileInfo,
|
||||
metadata,
|
||||
contentType,
|
||||
tab,
|
||||
rewardedContentClaimIds,
|
||||
isResolvingUri,
|
||||
blackListedOutpoints,
|
||||
navigation
|
||||
} = this.props;
|
||||
const { uri } = navigation.state.params;
|
||||
|
||||
let innerContent = null;
|
||||
if ((isResolvingUri && !claim) || !claim) {
|
||||
innerContent = (
|
||||
<View style={filePageStyle.container}>
|
||||
{isResolvingUri &&
|
||||
<View style={filePageStyle.busyContainer}>
|
||||
<ActivityIndicator size="large" color={Colors.LbryGreen} />
|
||||
<Text style={filePageStyle.infoText}>Loading decentralized data...</Text>
|
||||
</View>}
|
||||
{claim === null && !isResolvingUri &&
|
||||
<View style={filePageStyle.container}>
|
||||
<Text style={filePageStyle.emptyClaimText}>There's nothing at this location.</Text>
|
||||
</View>
|
||||
}
|
||||
<UriBar value={uri} navigation={navigation} />
|
||||
</View>
|
||||
);
|
||||
} else if (claim && claim.name.length && claim.name[0] === '@') {
|
||||
innerContent = (
|
||||
<ChannelPage uri={uri} navigation={navigation} />
|
||||
);
|
||||
} else if (claim) {
|
||||
const completed = fileInfo && fileInfo.completed;
|
||||
const title = metadata.title;
|
||||
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
|
||||
const description = metadata.description ? metadata.description : null;
|
||||
const mediaType = Lbry.getMediaType(contentType);
|
||||
const isPlayable = mediaType === 'video' || mediaType === 'audio';
|
||||
const { height, channel_name: channelName, value } = claim;
|
||||
const showActions = !this.state.fullscreenMode && !this.state.showImageViewer && !this.state.showWebView &&
|
||||
(completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes));
|
||||
const channelClaimId =
|
||||
value && value.publisherSignature && value.publisherSignature.certificateId;
|
||||
|
||||
const playerStyle = [filePageStyle.player,
|
||||
this.state.isLandscape ? filePageStyle.containedPlayerLandscape :
|
||||
(this.state.fullscreenMode ? filePageStyle.fullscreenPlayer : filePageStyle.containedPlayer)];
|
||||
const playerBgStyle = [filePageStyle.playerBackground, this.state.fullscreenMode ?
|
||||
filePageStyle.fullscreenPlayerBackground : filePageStyle.containedPlayerBackground];
|
||||
// at least 2MB (or the full download) before media can be loaded
|
||||
const canLoadMedia = fileInfo &&
|
||||
(fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes == fileInfo.total_bytes); // 2MB = 1024*1024*2
|
||||
const canOpen = (mediaType === 'image' || mediaType === 'text') && completed;
|
||||
const isWebViewable = mediaType === 'text';
|
||||
const localFileUri = this.localUriForFileInfo(fileInfo);
|
||||
|
||||
const openFile = () => {
|
||||
if (mediaType === 'image') {
|
||||
// use image viewer
|
||||
this.setState({
|
||||
imageUrls: [{
|
||||
url: localFileUri
|
||||
}],
|
||||
showImageViewer: true
|
||||
});
|
||||
}
|
||||
if (isWebViewable) {
|
||||
// show webview
|
||||
this.setState({
|
||||
showWebView: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
innerContent = (
|
||||
<View style={filePageStyle.pageContainer}>
|
||||
{this.state.showWebView && isWebViewable && <WebView source={{ uri: localFileUri }}
|
||||
style={filePageStyle.viewer} />}
|
||||
|
||||
{this.state.showImageViewer && <ImageViewer style={StyleSheet.flatten(filePageStyle.viewer)}
|
||||
imageUrls={this.state.imageUrls}
|
||||
renderIndicator={() => null} />}
|
||||
|
||||
{!this.state.showWebView && (
|
||||
<View style={this.state.fullscreenMode ? filePageStyle.innerPageContainerFsMode : filePageStyle.innerPageContainer}
|
||||
onLayout={this.checkOrientation}>
|
||||
<View style={filePageStyle.mediaContainer}>
|
||||
{(canOpen || (!fileInfo || (isPlayable && !canLoadMedia))) &&
|
||||
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />}
|
||||
{(canOpen || (isPlayable && !this.state.mediaLoaded)) && <ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />}
|
||||
{((isPlayable && !completed && !canLoadMedia) || !completed || canOpen) &&
|
||||
<FileDownloadButton uri={uri}
|
||||
style={filePageStyle.downloadButton}
|
||||
openFile={openFile}
|
||||
isPlayable={isPlayable}
|
||||
onPlay={() => this.setState({ autoPlayMedia: true })} />}
|
||||
{!fileInfo && <FilePrice uri={uri} style={filePageStyle.filePriceContainer} textStyle={filePageStyle.filePriceText} />}
|
||||
</View>
|
||||
{canLoadMedia && <View style={playerBgStyle} ref={(ref) => { this.playerBackground = ref; }}
|
||||
onLayout={(evt) => {
|
||||
if (!this.state.playerBgHeight) {
|
||||
this.setState({ playerBgHeight: evt.nativeEvent.layout.height });
|
||||
}
|
||||
}} />}
|
||||
{canLoadMedia && <MediaPlayer fileInfo={fileInfo}
|
||||
ref={(ref) => { this.player = ref; }}
|
||||
uri={uri}
|
||||
style={playerStyle}
|
||||
autoPlay={this.state.autoPlayMedia}
|
||||
onFullscreenToggled={this.handleFullscreenToggle}
|
||||
onMediaLoaded={() => { this.setState({ mediaLoaded: true }); }}
|
||||
onLayout={(evt) => {
|
||||
if (!this.state.playerHeight) {
|
||||
this.setState({ playerHeight: evt.nativeEvent.layout.height });
|
||||
}
|
||||
}}
|
||||
/>}
|
||||
|
||||
{ showActions &&
|
||||
<View style={filePageStyle.actions}>
|
||||
{completed && <Button color="red" title="Delete" onPress={this.onDeletePressed} />}
|
||||
{!completed && fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes &&
|
||||
<Button color="red" title="Stop Download" onPress={this.onStopDownloadPressed} />
|
||||
}
|
||||
</View>}
|
||||
<ScrollView style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer}>
|
||||
<Text style={filePageStyle.title} selectable={true}>{title}</Text>
|
||||
{channelName && <Link style={filePageStyle.channelName}
|
||||
selectable={true}
|
||||
text={channelName}
|
||||
onPress={() => {
|
||||
const channelUri = normalizeURI(channelName);
|
||||
navigation.navigate({ routeName: 'File', key: channelUri, params: { uri: channelUri }});
|
||||
}} />}
|
||||
{description && <Text style={filePageStyle.description} selectable={true}>{this.linkify(description)}</Text>}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!this.state.fullscreenMode && <UriBar value={uri} navigation={navigation} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return innerContent;
|
||||
}
|
||||
}
|
||||
|
||||
export default FilePage;
|
6
app/src/page/firstRun/index.js
Normal file
6
app/src/page/firstRun/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FirstRun from './view';
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(null, perform)(FirstRun);
|
21
app/src/page/firstRun/internal/welcome-page.js
Normal file
21
app/src/page/firstRun/internal/welcome-page.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import { View, Text, Linking } from 'react-native';
|
||||
import Colors from '../../../styles/colors';
|
||||
import firstRunStyle from '../../../styles/firstRun';
|
||||
|
||||
class WelcomePage extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<View style={firstRunStyle.container}>
|
||||
<Text style={firstRunStyle.title}>Welcome to LBRY.</Text>
|
||||
<Text style={firstRunStyle.paragraph}>LBRY is a decentralized peer-to-peer content sharing platform where
|
||||
you can upload and download videos, music, ebooks and other forms of digital content.</Text>
|
||||
<Text style={firstRunStyle.paragraph}>We make use of a blockchain which needs to be synchronized before
|
||||
you can use the app. Synchronization may take a while because this is the first app launch.</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WelcomePage;
|
96
app/src/page/firstRun/view.js
Normal file
96
app/src/page/firstRun/view.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
Linking,
|
||||
NativeModules,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { NavigationActions } from 'react-navigation';
|
||||
import Colors from '../../styles/colors';
|
||||
import WelcomePage from './internal/welcome-page';
|
||||
import firstRunStyle from '../../styles/firstRun';
|
||||
|
||||
class FirstRunScreen extends React.PureComponent {
|
||||
static pages = ['welcome'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
currentPage: null,
|
||||
launchUrl: null,
|
||||
isFirstRun: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) {
|
||||
this.setState({ launchUrl: url });
|
||||
}
|
||||
});
|
||||
|
||||
if (NativeModules.FirstRun) {
|
||||
NativeModules.FirstRun.isFirstRun().then(firstRun => {
|
||||
this.setState({ isFirstRun: firstRun });
|
||||
if (firstRun) {
|
||||
this.setState({ currentPage: FirstRunScreen.pages[0] });
|
||||
} else {
|
||||
// Not the first run. Navigate to the splash screen right away
|
||||
this.launchSplashScreen();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// The first run module was not detected. Go straight to the splash screen.
|
||||
this.launchSplashScreen();
|
||||
}
|
||||
}
|
||||
|
||||
launchSplashScreen() {
|
||||
const { navigation } = this.props;
|
||||
const resetAction = NavigationActions.reset({
|
||||
index: 0,
|
||||
actions: [
|
||||
NavigationActions.navigate({ routeName: 'Splash', params: { launchUri: this.state.launchUri } })
|
||||
]
|
||||
});
|
||||
navigation.dispatch(resetAction);
|
||||
}
|
||||
|
||||
handleContinuePressed = () => {
|
||||
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
|
||||
if (pageIndex === (FirstRunScreen.pages.length - 1)) {
|
||||
// Final page. Let the app know that first run experience is completed.
|
||||
if (NativeModules.FirstRun) {
|
||||
NativeModules.FirstRun.firstRunCompleted();
|
||||
}
|
||||
|
||||
// Navigate to the splash screen
|
||||
this.launchSplashScreen();
|
||||
} else {
|
||||
// TODO: Page transition animation?
|
||||
this.state.currentPage = FirstRunScreen.pages[pageIndex + 1];
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let page = null;
|
||||
if (this.state.currentPage === 'welcome') {
|
||||
// show welcome page
|
||||
page = (<WelcomePage />);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={firstRunStyle.screenContainer}>
|
||||
{page}
|
||||
{this.state.currentPage &&
|
||||
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
|
||||
<Text style={firstRunStyle.buttonText}>Continue</Text>
|
||||
</TouchableOpacity>}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FirstRunScreen;
|
20
app/src/page/search/index.js
Normal file
20
app/src/page/search/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doSearch,
|
||||
makeSelectSearchUris,
|
||||
selectIsSearching,
|
||||
selectSearchValue
|
||||
} from 'lbry-redux';
|
||||
import SearchPage from './view';
|
||||
|
||||
const select = (state) => ({
|
||||
isSearching: selectIsSearching(state),
|
||||
query: selectSearchValue(state),
|
||||
uris: makeSelectSearchUris(selectSearchValue(state))(state)
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
search: (query) => dispatch(doSearch(query))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SearchPage);
|
58
app/src/page/search/view.js
Normal file
58
app/src/page/search/view.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Button,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
ScrollView
|
||||
} from 'react-native';
|
||||
import Colors from '../../styles/colors';
|
||||
import PageHeader from '../../component/pageHeader';
|
||||
import SearchResultItem from '../../component/searchResultItem';
|
||||
import UriBar from '../../component/uriBar';
|
||||
import searchStyle from '../../styles/search';
|
||||
|
||||
class SearchPage extends React.PureComponent {
|
||||
static navigationOptions = {
|
||||
title: 'Search Results'
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { navigation, search } = this.props;
|
||||
const { searchQuery } = navigation.state.params;
|
||||
if (searchQuery && searchQuery.trim().length > 0) {
|
||||
search(searchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isSearching, navigation, uris, query } = this.props;
|
||||
const { searchQuery } = navigation.state.params;
|
||||
|
||||
return (
|
||||
<View style={searchStyle.container}>
|
||||
{!isSearching && (!uris || uris.length === 0) &&
|
||||
<Text style={searchStyle.noResultsText}>No results to display.</Text>}
|
||||
<ScrollView style={searchStyle.scrollContainer} contentContainerStyle={searchStyle.scrollPadding}>
|
||||
{!isSearching && uris && uris.length ? (
|
||||
uris.map(uri => <SearchResultItem key={uri}
|
||||
uri={uri}
|
||||
style={searchStyle.resultItem}
|
||||
navigation={navigation}
|
||||
onPress={() => navigation.navigate({
|
||||
routeName: 'File',
|
||||
key: 'filePage',
|
||||
params: { uri }})
|
||||
}/>)
|
||||
) : null }
|
||||
</ScrollView>
|
||||
{isSearching && <ActivityIndicator size="large" color={Colors.LbryGreen} style={searchStyle.loading} /> }
|
||||
<UriBar value={searchQuery} navigation={navigation} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchPage;
|
17
app/src/page/settings/index.js
Normal file
17
app/src/page/settings/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { SETTINGS } from 'lbry-redux';
|
||||
import { doSetClientSetting } from '../../redux/actions/settings';
|
||||
import { makeSelectClientSetting } from '../../redux/selectors/settings';
|
||||
import SettingsPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
|
||||
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
|
||||
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SettingsPage);
|
62
app/src/page/settings/view.js
Normal file
62
app/src/page/settings/view.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React from 'react';
|
||||
import { SETTINGS } from 'lbry-redux';
|
||||
import { Text, View, ScrollView, Switch } from 'react-native';
|
||||
import PageHeader from '../../component/pageHeader';
|
||||
import settingsStyle from '../../styles/settings';
|
||||
|
||||
class SettingsPage extends React.PureComponent {
|
||||
static navigationOptions = {
|
||||
title: 'Settings'
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
backgroundPlayEnabled,
|
||||
keepDaemonRunning,
|
||||
showNsfw,
|
||||
setClientSetting
|
||||
} = this.props;
|
||||
|
||||
// If no true / false value set, default to true
|
||||
const actualKeepDaemonRunning = (keepDaemonRunning === undefined || keepDaemonRunning === null) ? true : keepDaemonRunning;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<PageHeader title={"Settings"}
|
||||
onBackPressed={() => { this.props.navigation.goBack(); }} />
|
||||
<ScrollView style={settingsStyle.scrollContainer}>
|
||||
<View style={settingsStyle.row}>
|
||||
<View style={settingsStyle.switchText}>
|
||||
<Text style={settingsStyle.label}>Enable background media playback</Text>
|
||||
<Text style={settingsStyle.description}>Enable this option to play audio or video in the background when the app is suspended.</Text>
|
||||
</View>
|
||||
<View style={settingsStyle.switchContainer}>
|
||||
<Switch value={backgroundPlayEnabled} onValueChange={(value) => setClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED, value)} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={settingsStyle.row}>
|
||||
<View style={settingsStyle.switchText}>
|
||||
<Text style={settingsStyle.label}>Show NSFW content</Text>
|
||||
</View>
|
||||
<View style={settingsStyle.switchContainer}>
|
||||
<Switch value={showNsfw} onValueChange={(value) => setClientSetting(SETTINGS.SHOW_NSFW, value)} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={settingsStyle.row}>
|
||||
<View style={settingsStyle.switchText}>
|
||||
<Text style={settingsStyle.label}>Keep the daemon background service running when the app is suspended.</Text>
|
||||
<Text style={settingsStyle.description}>Enable this option for quicker app launch and to keep the synchronisation with the blockchain up to date.</Text>
|
||||
</View>
|
||||
<View style={settingsStyle.switchContainer}>
|
||||
<Switch value={actualKeepDaemonRunning} onValueChange={(value) => setClientSetting(SETTINGS.KEEP_DAEMON_RUNNING, value)} />
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsPage;
|
9
app/src/page/splash/index.js
Normal file
9
app/src/page/splash/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doBalanceSubscribe } from 'lbry-redux';
|
||||
import SplashScreen from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
balanceSubscribe: () => dispatch(doBalanceSubscribe())
|
||||
});
|
||||
|
||||
export default connect(null, perform)(SplashScreen);
|
175
app/src/page/splash/view.js
Normal file
175
app/src/page/splash/view.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
NativeModules,
|
||||
Platform,
|
||||
ProgressBarAndroid,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { NavigationActions } from 'react-navigation';
|
||||
import PropTypes from 'prop-types';
|
||||
import Colors from '../../styles/colors';
|
||||
import splashStyle from '../../styles/splash';
|
||||
|
||||
class SplashScreen extends React.PureComponent {
|
||||
static navigationOptions = {
|
||||
title: 'Splash'
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
details: 'Starting daemon',
|
||||
message: 'Connecting',
|
||||
isRunning: false,
|
||||
isLagging: false,
|
||||
launchUrl: null,
|
||||
didDownloadHeaders: false,
|
||||
isDownloadingHeaders: false,
|
||||
headersDownloadProgress: 0
|
||||
});
|
||||
|
||||
if (NativeModules.DaemonServiceControl) {
|
||||
NativeModules.DaemonServiceControl.startService();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Start measuring the first launch time from the splash screen (time from daemon start to user interaction)
|
||||
AsyncStorage.getItem('hasLaunched').then(value => {
|
||||
if (value == null || value !== 'true') {
|
||||
AsyncStorage.setItem('hasLaunched', 'true');
|
||||
// only set firstLaunchTime since we've determined that this is the first app launch ever
|
||||
AsyncStorage.setItem('firstLaunchTime', String(moment().unix()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus() {
|
||||
Lbry.status().then(status => {
|
||||
this._updateStatusCallback(status);
|
||||
});
|
||||
}
|
||||
|
||||
_updateStatusCallback(status) {
|
||||
const startupStatus = status.startup_status;
|
||||
if (startupStatus.code == 'started') {
|
||||
// Wait until we are able to resolve a name before declaring
|
||||
// that we are done.
|
||||
// TODO: This is a hack, and the logic should live in the daemon
|
||||
// to give us a better sense of when we are actually started
|
||||
this.setState({
|
||||
message: 'Testing Network',
|
||||
details: 'Waiting for name resolution',
|
||||
isLagging: false,
|
||||
isRunning: true,
|
||||
});
|
||||
|
||||
Lbry.resolve({ uri: 'lbry://one' }).then(() => {
|
||||
// Leave the splash screen
|
||||
const { balanceSubscribe, navigation } = this.props;
|
||||
balanceSubscribe();
|
||||
|
||||
const resetAction = NavigationActions.reset({
|
||||
index: 0,
|
||||
actions: [
|
||||
NavigationActions.navigate({ routeName: 'Main'})
|
||||
]
|
||||
});
|
||||
navigation.dispatch(resetAction);
|
||||
|
||||
const launchUrl = navigation.state.params.launchUrl || this.state.launchUrl;
|
||||
if (launchUrl) {
|
||||
navigation.navigate({ routeName: 'File', key: launchUrl, params: { uri: launchUrl } });
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const blockchainStatus = status.blockchain_status;
|
||||
if (blockchainStatus) {
|
||||
this.setState({
|
||||
isDownloadingHeaders: blockchainStatus.is_downloading_headers,
|
||||
headersDownloadProgress: blockchainStatus.headers_download_progress
|
||||
});
|
||||
}
|
||||
|
||||
if (blockchainStatus && (blockchainStatus.is_downloading_headers ||
|
||||
(this.state.didDownloadHeaders && 'loading_wallet' === startupStatus.code))) {
|
||||
if (!this.state.didDownloadHeaders) {
|
||||
this.setState({ didDownloadHeaders: true });
|
||||
}
|
||||
this.setState({
|
||||
message: 'Blockchain Sync',
|
||||
details: `Catching up with the blockchain (${blockchainStatus.headers_download_progress}%)`,
|
||||
isLagging: startupStatus.is_lagging
|
||||
});
|
||||
} else if (blockchainStatus && blockchainStatus.blocks_behind > 0) {
|
||||
const behind = blockchainStatus.blocks_behind;
|
||||
const behindText = behind + ' block' + (behind == 1 ? '' : 's') + ' behind';
|
||||
this.setState({
|
||||
message: 'Blockchain Sync',
|
||||
details: behindText,
|
||||
isLagging: startupStatus.is_lagging,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
message: 'Network Loading',
|
||||
details: startupStatus.message + (startupStatus.is_lagging ? '' : '...'),
|
||||
isLagging: startupStatus.is_lagging,
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.updateStatus();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (NativeModules.Mixpanel) {
|
||||
NativeModules.Mixpanel.track('App Launch', null);
|
||||
}
|
||||
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) {
|
||||
this.setState({ launchUrl: url });
|
||||
}
|
||||
});
|
||||
|
||||
Lbry
|
||||
.connect()
|
||||
.then(() => {
|
||||
this.updateStatus();
|
||||
})
|
||||
.catch((e) => {
|
||||
this.setState({
|
||||
isLagging: true,
|
||||
message: 'Connection Failure',
|
||||
details:
|
||||
'We could not establish a connection to the daemon. Your data connection may be preventing LBRY from connecting. Contact hello@lbry.io if you think this is a software bug.'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { message, details, isLagging, isRunning } = this.state;
|
||||
|
||||
return (
|
||||
<View style={splashStyle.container}>
|
||||
<Text style={splashStyle.title}>LBRY</Text>
|
||||
{'android' === Platform.OS && this.state.isDownloadingHeaders &&
|
||||
<ProgressBarAndroid color={Colors.White}
|
||||
indeterminate={false}
|
||||
styleAttr={"Horizontal"}
|
||||
style={splashStyle.progress}
|
||||
progress={this.state.headersDownloadProgress/100.0} />}
|
||||
{!this.state.isDownloadingHeaders && <ActivityIndicator color={Colors.White} style={splashStyle.loading} size={"small"} />}
|
||||
<Text style={splashStyle.message}>{message}</Text>
|
||||
<Text style={splashStyle.details}>{details}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SplashScreen;
|
18
app/src/page/transactionHistory/index.js
Normal file
18
app/src/page/transactionHistory/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchTransactions,
|
||||
selectTransactionItems,
|
||||
selectIsFetchingTransactions,
|
||||
} from 'lbry-redux';
|
||||
import TransactionHistoryPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
fetchingTransactions: selectIsFetchingTransactions(state),
|
||||
transactions: selectTransactionItems(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchTransactions: () => dispatch(doFetchTransactions()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(TransactionHistoryPage);
|
32
app/src/page/transactionHistory/view.js
Normal file
32
app/src/page/transactionHistory/view.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import { View, ScrollView, Text } from 'react-native';
|
||||
import TransactionList from '../../component/transactionList';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
class TransactionHistoryPage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.fetchTransactions();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fetchingTransactions, transactions, navigation } = this.props;
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={walletStyle.historyList}>
|
||||
{fetchingTransactions && !transactions.length && (
|
||||
<Text style={walletStyle.infoText}>Loading transactions...</Text>
|
||||
)}
|
||||
{!fetchingTransactions && transactions.length === 0 && (
|
||||
<Text style={walletStyle.infoText}>No transactions to list.</Text>
|
||||
)}
|
||||
{!fetchingTransactions && transactions && (transactions.length > 0) && (
|
||||
<TransactionList navigation={navigation} transactions={transactions} />
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionHistoryPage;
|
14
app/src/page/trending/index.js
Normal file
14
app/src/page/trending/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doFetchTrendingUris, selectTrendingUris, selectFetchingTrendingUris } from 'lbry-redux';
|
||||
import TrendingPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
trendingUris: selectTrendingUris(state),
|
||||
fetchingTrendingUris: selectFetchingTrendingUris(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchTrendingUris: () => dispatch(doFetchTrendingUris()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(TrendingPage);
|
57
app/src/page/trending/view.js
Normal file
57
app/src/page/trending/view.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
import NavigationActions from 'react-navigation';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AsyncStorage,
|
||||
NativeModules,
|
||||
FlatList,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { normalizeURI } from 'lbry-redux';
|
||||
import moment from 'moment';
|
||||
import FileItem from '../../component/fileItem';
|
||||
import discoverStyle from '../../styles/discover';
|
||||
import Colors from '../../styles/colors';
|
||||
import UriBar from '../../component/uriBar';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
|
||||
class TrendingPage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.fetchTrendingUris();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { trendingUris, fetchingTrendingUris, navigation } = this.props;
|
||||
const hasContent = typeof trendingUris === 'object' && trendingUris.length,
|
||||
failedToLoad = !fetchingTrendingUris && !hasContent;
|
||||
|
||||
return (
|
||||
<View style={discoverStyle.container}>
|
||||
{!hasContent && fetchingTrendingUris && (
|
||||
<View style={discoverStyle.busyContainer}>
|
||||
<ActivityIndicator size="large" color={Colors.LbryGreen} />
|
||||
<Text style={discoverStyle.title}>Fetching content...</Text>
|
||||
</View>
|
||||
)}
|
||||
{hasContent &&
|
||||
<FlatList style={discoverStyle.trendingContainer}
|
||||
renderItem={ ({item}) => (
|
||||
<FileItem
|
||||
style={discoverStyle.fileItem}
|
||||
key={item}
|
||||
uri={normalizeURI(item)}
|
||||
navigation={navigation} />
|
||||
)
|
||||
}
|
||||
data={trendingUris.map(uri => uri.url)}
|
||||
keyExtractor={(item, index) => item}
|
||||
/>
|
||||
}
|
||||
<UriBar navigation={navigation} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TrendingPage;
|
15
app/src/page/wallet/index.js
Normal file
15
app/src/page/wallet/index.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doSetClientSetting } from '../../redux/actions/settings';
|
||||
import { makeSelectClientSetting } from '../../redux/selectors/settings';
|
||||
import Constants from '../../constants';
|
||||
import WalletPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
understandsRisks: makeSelectClientSetting(Constants.SETTING_ALPHA_UNDERSTANDS_RISKS)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(WalletPage);
|
40
app/src/page/wallet/view.js
Normal file
40
app/src/page/wallet/view.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import { ScrollView, Text, View } from 'react-native';
|
||||
import TransactionListRecent from '../../component/transactionListRecent';
|
||||
import WalletAddress from '../../component/walletAddress';
|
||||
import WalletBalance from '../../component/walletBalance';
|
||||
import WalletSend from '../../component/walletSend';
|
||||
import Button from '../../component/button';
|
||||
import Constants from '../../constants';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
class WalletPage extends React.PureComponent {
|
||||
render() {
|
||||
const { understandsRisks, setClientSetting } = this.props;
|
||||
|
||||
if (!understandsRisks) {
|
||||
return (
|
||||
<View>
|
||||
<View style={walletStyle.warning}>
|
||||
<Text style={walletStyle.warningText}>
|
||||
This is alpha software. You may lose any LBC that you send to your wallet due to uninstallation, software bugs, deleted files, or malicious third-party software. You should not use this wallet as your primary wallet. If you understand the risks and you wish to continue, please click the button below.
|
||||
</Text>
|
||||
</View>
|
||||
<Button text={'I understand the risks'} style={[walletStyle.button, walletStyle.understand]}
|
||||
onPress={() => setClientSetting(Constants.SETTING_ALPHA_UNDERSTANDS_RISKS, true)}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<WalletBalance />
|
||||
<WalletAddress />
|
||||
<WalletSend />
|
||||
<TransactionListRecent navigation={this.props.navigation} />
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletPage;
|
303
app/src/redux/actions/file.js
Normal file
303
app/src/redux/actions/file.js
Normal file
|
@ -0,0 +1,303 @@
|
|||
import {
|
||||
ACTIONS,
|
||||
Lbry,
|
||||
doNotify,
|
||||
formatCredits,
|
||||
selectBalance,
|
||||
makeSelectCostInfoForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectMetadataForUri,
|
||||
selectDownloadingByOutpoint,
|
||||
} from 'lbry-redux';
|
||||
import { Alert, NativeModules } from 'react-native';
|
||||
|
||||
const DOWNLOAD_POLL_INTERVAL = 250;
|
||||
|
||||
export function doUpdateLoadStatus(uri, outpoint) {
|
||||
return (dispatch, getState) => {
|
||||
Lbry.file_list({
|
||||
outpoint,
|
||||
full_status: true,
|
||||
}).then(([fileInfo]) => {
|
||||
if (!fileInfo || fileInfo.written_bytes === 0) {
|
||||
// download hasn't started yet
|
||||
setTimeout(() => {
|
||||
dispatch(doUpdateLoadStatus(uri, outpoint));
|
||||
}, DOWNLOAD_POLL_INTERVAL);
|
||||
} else if (fileInfo.completed) {
|
||||
// TODO this isn't going to get called if they reload the client before
|
||||
// the download finished
|
||||
const { total_bytes: totalBytes, written_bytes: writtenBytes } = fileInfo;
|
||||
dispatch({
|
||||
type: ACTIONS.DOWNLOADING_COMPLETED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint,
|
||||
fileInfo,
|
||||
},
|
||||
});
|
||||
|
||||
if (NativeModules.LbryDownloadManager) {
|
||||
NativeModules.LbryDownloadManager.updateDownload(uri, fileInfo.file_name, 100, writtenBytes, totalBytes);
|
||||
}
|
||||
|
||||
/*const notif = new window.Notification('LBRY Download Complete', {
|
||||
body: fileInfo.metadata.stream.metadata.title,
|
||||
silent: false,
|
||||
});
|
||||
notif.onclick = () => {
|
||||
ipcRenderer.send('focusWindow', 'main');
|
||||
};*/
|
||||
} else {
|
||||
// ready to play
|
||||
const { total_bytes: totalBytes, written_bytes: writtenBytes } = fileInfo;
|
||||
const progress = writtenBytes / totalBytes * 100;
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.DOWNLOADING_PROGRESSED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint,
|
||||
fileInfo,
|
||||
progress,
|
||||
},
|
||||
});
|
||||
|
||||
if (NativeModules.LbryDownloadManager) {
|
||||
NativeModules.LbryDownloadManager.updateDownload(uri, fileInfo.file_name, progress, writtenBytes, totalBytes);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(doUpdateLoadStatus(uri, outpoint));
|
||||
}, DOWNLOAD_POLL_INTERVAL);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doStartDownload(uri, outpoint) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
if (!outpoint) {
|
||||
throw new Error('outpoint is required to begin a download');
|
||||
}
|
||||
|
||||
const { downloadingByOutpoint = {} } = state.fileInfo;
|
||||
|
||||
if (downloadingByOutpoint[outpoint]) return;
|
||||
|
||||
Lbry.file_list({ outpoint, full_status: true }).then(([fileInfo]) => {
|
||||
dispatch({
|
||||
type: ACTIONS.DOWNLOADING_STARTED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint,
|
||||
fileInfo,
|
||||
},
|
||||
});
|
||||
|
||||
if (NativeModules.LbryDownloadManager) {
|
||||
NativeModules.LbryDownloadManager.startDownload(uri, fileInfo.file_name);
|
||||
}
|
||||
|
||||
dispatch(doUpdateLoadStatus(uri, outpoint));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doStopDownloadingFile(uri, fileInfo) {
|
||||
return dispatch => {
|
||||
let params = { status: 'stop' };
|
||||
if (fileInfo.sd_hash) {
|
||||
params.sd_hash = fileInfo.sd_hash;
|
||||
}
|
||||
if (fileInfo.stream_hash) {
|
||||
params.stream_hash = fileInfo.stream_hash;
|
||||
}
|
||||
|
||||
Lbry.file_set_status(params).then(() => {
|
||||
dispatch({
|
||||
type: ACTIONS.DOWNLOADING_CANCELED,
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
|
||||
if (NativeModules.LbryDownloadManager) {
|
||||
NativeModules.LbryDownloadManager.stopDownload(uri, fileInfo.file_name);
|
||||
}
|
||||
|
||||
// Should also delete the file after the user stops downloading
|
||||
dispatch(doDeleteFile(fileInfo.outpoint, uri));
|
||||
};
|
||||
}
|
||||
|
||||
export function doDownloadFile(uri, streamInfo) {
|
||||
return dispatch => {
|
||||
dispatch(doStartDownload(uri, streamInfo.outpoint));
|
||||
|
||||
//analytics.apiLog(uri, streamInfo.output, streamInfo.claim_id);
|
||||
|
||||
//dispatch(doClaimEligiblePurchaseRewards());
|
||||
};
|
||||
}
|
||||
|
||||
export function doSetPlayingUri(uri) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ACTIONS.SET_PLAYING_URI,
|
||||
data: { uri },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doLoadVideo(uri) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ACTIONS.LOADING_VIDEO_STARTED,
|
||||
data: {
|
||||
uri,
|
||||
},
|
||||
});
|
||||
|
||||
Lbry.get({ uri })
|
||||
.then(streamInfo => {
|
||||
const timeout =
|
||||
streamInfo === null || typeof streamInfo !== 'object' || streamInfo.error === 'Timeout';
|
||||
|
||||
if (timeout) {
|
||||
dispatch(doSetPlayingUri(null));
|
||||
dispatch({
|
||||
type: ACTIONS.LOADING_VIDEO_FAILED,
|
||||
data: { uri },
|
||||
});
|
||||
|
||||
dispatch(doNotify({
|
||||
message: `File timeout for uri ${uri}`,
|
||||
displayType: ['toast']
|
||||
}));
|
||||
} else {
|
||||
dispatch(doDownloadFile(uri, streamInfo));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(doSetPlayingUri(null));
|
||||
dispatch({
|
||||
type: ACTIONS.LOADING_VIDEO_FAILED,
|
||||
data: { uri },
|
||||
});
|
||||
|
||||
dispatch(doNotify({
|
||||
message: `Failed to download ${uri}, please try again. If this problem persists, visit https://lbry.io/faq/support for support.`,
|
||||
displayType: ['toast']
|
||||
}));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doPurchaseUri(uri, specificCostInfo) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const balance = selectBalance(state);
|
||||
const fileInfo = makeSelectFileInfoForUri(uri)(state);
|
||||
const metadata = makeSelectMetadataForUri(uri)(state);
|
||||
const title = metadata ? metadata.title : uri;
|
||||
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
|
||||
const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
|
||||
|
||||
function attemptPlay(cost, instantPurchaseMax = null) {
|
||||
if (cost > 0 && (!instantPurchaseMax || cost > instantPurchaseMax)) {
|
||||
// display alert
|
||||
const formattedCost = formatCredits(cost, 2);
|
||||
const unit = cost === 1 ? 'credit' : 'credits';
|
||||
Alert.alert('Confirm purchase',
|
||||
`This will purchase "${title}" for ${formattedCost} ${unit}`,
|
||||
[
|
||||
{ text: 'OK', onPress: () => dispatch(doLoadVideo(uri)) },
|
||||
{ text: 'Cancel', style: 'cancel' }
|
||||
],
|
||||
{ cancelable: true });
|
||||
} else {
|
||||
dispatch(doLoadVideo(uri));
|
||||
}
|
||||
}
|
||||
|
||||
// we already fully downloaded the file.
|
||||
if (fileInfo && fileInfo.completed) {
|
||||
// If written_bytes is false that means the user has deleted/moved the
|
||||
// file manually on their file system, so we need to dispatch a
|
||||
// doLoadVideo action to reconstruct the file from the blobs
|
||||
if (!fileInfo.written_bytes) dispatch(doLoadVideo(uri));
|
||||
|
||||
Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// we are already downloading the file
|
||||
if (alreadyDownloading) {
|
||||
Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const costInfo = makeSelectCostInfoForUri(uri)(state) || specificCostInfo;
|
||||
const { cost } = costInfo;
|
||||
|
||||
if (cost > balance) {
|
||||
dispatch(doSetPlayingUri(null));
|
||||
dispatch(doNotify({
|
||||
message: 'Insufficient credits',
|
||||
displayType: ['toast']
|
||||
}));
|
||||
Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
attemptPlay(cost);
|
||||
/*if (cost === 0 || !makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state)) {
|
||||
attemptPlay(cost);
|
||||
} else {
|
||||
const instantPurchaseMax = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state);
|
||||
if (instantPurchaseMax.currency === 'LBC') {
|
||||
attemptPlay(cost, instantPurchaseMax.amount);
|
||||
} else {
|
||||
// Need to convert currency of instant purchase maximum before trying to play
|
||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => {
|
||||
attemptPlay(cost, instantPurchaseMax.amount / LBC_USD);
|
||||
});
|
||||
}
|
||||
}*/
|
||||
};
|
||||
}
|
||||
|
||||
export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
|
||||
return (dispatch, getState) => {
|
||||
Lbry.file_delete({
|
||||
outpoint,
|
||||
delete_from_download_dir: deleteFromComputer,
|
||||
});
|
||||
|
||||
// If the file is for a claim we published then also abandon the claim
|
||||
/*const myClaimsOutpoints = selectMyClaimsOutpoints(state);
|
||||
if (abandonClaim && myClaimsOutpoints.indexOf(outpoint) !== -1) {
|
||||
const byOutpoint = selectFileInfosByOutpoint(state);
|
||||
const fileInfo = byOutpoint[outpoint];
|
||||
|
||||
if (fileInfo) {
|
||||
const txid = fileInfo.outpoint.slice(0, -2);
|
||||
const nout = Number(fileInfo.outpoint.slice(-1));
|
||||
|
||||
dispatch(doAbandonClaim(txid, nout));
|
||||
}
|
||||
}*/
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.FILE_DELETE,
|
||||
data: {
|
||||
outpoint,
|
||||
},
|
||||
});
|
||||
|
||||
//const totalProgress = selectTotalDownloadProgress(getState());
|
||||
//setProgressBar(totalProgress);
|
||||
};
|
||||
}
|
11
app/src/redux/actions/settings.js
Normal file
11
app/src/redux/actions/settings.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { ACTIONS } from 'lbry-redux';
|
||||
|
||||
export function doSetClientSetting(key, value) {
|
||||
return {
|
||||
type: ACTIONS.CLIENT_SETTING_CHANGED,
|
||||
data: {
|
||||
key,
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
23
app/src/redux/reducers/settings.js
Normal file
23
app/src/redux/reducers/settings.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ACTIONS } from 'lbry-redux';
|
||||
|
||||
const reducers = {};
|
||||
const defaultState = {
|
||||
clientSettings: {}
|
||||
};
|
||||
|
||||
reducers[ACTIONS.CLIENT_SETTING_CHANGED] = (state, action) => {
|
||||
const { key, value } = action.data;
|
||||
const clientSettings = Object.assign({}, state.clientSettings);
|
||||
|
||||
clientSettings[key] = value;
|
||||
|
||||
return Object.assign({}, state, {
|
||||
clientSettings,
|
||||
});
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action) {
|
||||
const handler = reducers[action.type];
|
||||
if (handler) return handler(state, action);
|
||||
return state;
|
||||
}
|
19
app/src/redux/selectors/settings.js
Normal file
19
app/src/redux/selectors/settings.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { SETTINGS } from 'lbry-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const selectState = state => state.settings || {};
|
||||
|
||||
export const selectDaemonSettings = createSelector(selectState, state => state.daemonSettings);
|
||||
|
||||
export const selectClientSettings = createSelector(
|
||||
selectState,
|
||||
state => state.clientSettings || {}
|
||||
);
|
||||
|
||||
export const makeSelectClientSetting = setting =>
|
||||
createSelector(selectClientSettings, settings => (settings ? settings[setting] : undefined));
|
||||
|
||||
// refactor me
|
||||
export const selectShowNsfw = makeSelectClientSetting(SETTINGS.SHOW_NSFW);
|
||||
|
||||
export const selectKeepDaemonRunning = makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING);
|
69
app/src/styles/about.js
Normal file
69
app/src/styles/about.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const aboutStyle = StyleSheet.create({
|
||||
scrollContainer: {
|
||||
paddingTop: 16,
|
||||
paddingBottom: 16
|
||||
},
|
||||
row: {
|
||||
marginBottom: 1,
|
||||
backgroundColor: '#f9f9f9',
|
||||
padding: 16,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
title: {
|
||||
color: Colors.LbryGreen,
|
||||
fontSize: 24,
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
marginBottom: 8
|
||||
},
|
||||
paragraph: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
marginBottom: 24
|
||||
},
|
||||
links: {
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
marginBottom: 12
|
||||
},
|
||||
link: {
|
||||
color: Colors.LbryGreen,
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 16,
|
||||
marginBottom: 24
|
||||
},
|
||||
col: {
|
||||
alignSelf: 'stretch'
|
||||
},
|
||||
releaseInfoTitle: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
marginBottom: 12,
|
||||
fontSize: 20
|
||||
},
|
||||
text: {
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
fontSize: 15
|
||||
},
|
||||
valueText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
textAlign: 'right',
|
||||
fontSize: 15
|
||||
},
|
||||
lineValueText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 15
|
||||
}
|
||||
});
|
||||
|
||||
export default aboutStyle;
|
30
app/src/styles/button.js
Normal file
30
app/src/styles/button.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
const buttonStyle = StyleSheet.create({
|
||||
button: {
|
||||
borderRadius: 24,
|
||||
padding: 8,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12
|
||||
},
|
||||
disabled: {
|
||||
backgroundColor: '#999999'
|
||||
},
|
||||
row: {
|
||||
alignSelf: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
icon: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
text: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14
|
||||
},
|
||||
textWithIcon: {
|
||||
marginLeft: 8
|
||||
}
|
||||
});
|
||||
|
||||
export default buttonStyle;
|
37
app/src/styles/channelPage.js
Normal file
37
app/src/styles/channelPage.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const channelPageStyle = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
content: {
|
||||
flex: 1
|
||||
},
|
||||
fileList: {
|
||||
paddingTop: 30,
|
||||
flex: 1,
|
||||
marginBottom: 60
|
||||
},
|
||||
title: {
|
||||
color: Colors.LbryGreen,
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
fontSize: 30,
|
||||
margin: 16
|
||||
},
|
||||
busyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
infoText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 20,
|
||||
textAlign: 'center',
|
||||
marginLeft: 10
|
||||
}
|
||||
});
|
||||
|
||||
export default channelPageStyle;
|
13
app/src/styles/colors.js
Normal file
13
app/src/styles/colors.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
const Colors = {
|
||||
Black: '#000000',
|
||||
ChannelGrey: '#9b9b9b',
|
||||
DescriptionGrey: '#999999',
|
||||
LbryGreen: '#40b89a',
|
||||
LightGrey: '#cccccc',
|
||||
Orange: '#ffbb00',
|
||||
Red: '#ff0000',
|
||||
VeryLightGrey: '#f1f1f1',
|
||||
White: '#ffffff'
|
||||
};
|
||||
|
||||
export default Colors;
|
97
app/src/styles/discover.js
Normal file
97
app/src/styles/discover.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const discoverStyle = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
scrollContainer: {
|
||||
flex: 1,
|
||||
marginBottom: 60,
|
||||
paddingTop: 12
|
||||
},
|
||||
trendingContainer: {
|
||||
flex: 1,
|
||||
marginBottom: 60,
|
||||
paddingTop: 30
|
||||
},
|
||||
busyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 20,
|
||||
textAlign: 'center',
|
||||
marginLeft: 10
|
||||
},
|
||||
categoryName: {
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
fontSize: 20,
|
||||
marginLeft: 24,
|
||||
marginTop: 16,
|
||||
marginBottom: 16,
|
||||
color: Colors.Black
|
||||
},
|
||||
fileItem: {
|
||||
marginLeft: 24,
|
||||
marginRight: 24,
|
||||
marginBottom: 48
|
||||
},
|
||||
fileItemName: {
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
marginTop: 8,
|
||||
fontSize: 18
|
||||
},
|
||||
channelName: {
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
fontSize: 16,
|
||||
marginTop: 4,
|
||||
color: Colors.LbryGreen
|
||||
},
|
||||
filePriceContainer: {
|
||||
backgroundColor: '#61fcd8',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
top: 16,
|
||||
width: 56,
|
||||
height: 24,
|
||||
borderRadius: 4
|
||||
},
|
||||
filePriceText: {
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
color: '#0c604b'
|
||||
},
|
||||
drawerHamburger: {
|
||||
marginLeft: 16
|
||||
},
|
||||
rightHeaderIcon: {
|
||||
marginRight: 16
|
||||
},
|
||||
overlay: {
|
||||
flex: 1,
|
||||
opacity: 1,
|
||||
backgroundColor: '#222222',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 32,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
overlayText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
fontFamily: 'Metropolis-Regular'
|
||||
}
|
||||
});
|
||||
|
||||
export default discoverStyle;
|
19
app/src/styles/fileDownloadButton.js
Normal file
19
app/src/styles/fileDownloadButton.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
const fileDownloadButtonStyle = StyleSheet.create({
|
||||
container: {
|
||||
width: 160,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#40c0a9',
|
||||
},
|
||||
text: {
|
||||
fontFamily: 'Metropolis-Medium',
|
||||
color: '#ffffff',
|
||||
fontSize: 14,
|
||||
textAlign: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
export default fileDownloadButtonStyle;
|
71
app/src/styles/fileItemMedia.js
Normal file
71
app/src/styles/fileItemMedia.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { StyleSheet, Dimensions } from 'react-native';
|
||||
|
||||
const screenDimension = Dimensions.get('window');
|
||||
const width = screenDimension.width - 48; // screen width minus combined left and right margins
|
||||
|
||||
const fileItemMediaStyle = StyleSheet.create({
|
||||
autothumb: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
height: 200,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
autothumbText: {
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
textAlign: 'center',
|
||||
color: '#ffffff',
|
||||
fontSize: 40
|
||||
},
|
||||
autothumbPurple: {
|
||||
backgroundColor: '#9c27b0'
|
||||
},
|
||||
autothumbRed: {
|
||||
backgroundColor: '#e53935'
|
||||
},
|
||||
autothumbPink: {
|
||||
backgroundColor: '#e91e63'
|
||||
},
|
||||
autothumbIndigo: {
|
||||
backgroundColor: '#3f51b5'
|
||||
},
|
||||
autothumbBlue: {
|
||||
backgroundColor: '#2196f3'
|
||||
},
|
||||
autothumbLightBlue: {
|
||||
backgroundColor: '#039be5'
|
||||
},
|
||||
autothumbCyan: {
|
||||
backgroundColor: '#00acc1'
|
||||
},
|
||||
autothumbTeal: {
|
||||
backgroundColor: '#009688'
|
||||
},
|
||||
autothumbGreen: {
|
||||
backgroundColor: '#43a047'
|
||||
},
|
||||
autothumbYellow: {
|
||||
backgroundColor: '#ffeb3b'
|
||||
},
|
||||
autothumbOrange: {
|
||||
backgroundColor: '#ffa726'
|
||||
},
|
||||
resolving: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
text: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 16,
|
||||
marginTop: 8
|
||||
},
|
||||
thumbnail: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
height: 200,
|
||||
shadowColor: 'transparent'
|
||||
}
|
||||
});
|
||||
|
||||
export default fileItemMediaStyle;
|
179
app/src/styles/filePage.js
Normal file
179
app/src/styles/filePage.js
Normal file
|
@ -0,0 +1,179 @@
|
|||
import { StyleSheet, Dimensions } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const screenDimension = Dimensions.get('window');
|
||||
const screenWidth = screenDimension.width;
|
||||
|
||||
const filePageStyle = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
pageContainer: {
|
||||
flex: 1
|
||||
},
|
||||
innerPageContainer: {
|
||||
flex: 1,
|
||||
marginBottom: 60
|
||||
},
|
||||
innerPageContainerFsMode: {
|
||||
flex: 1,
|
||||
marginBottom: 0
|
||||
},
|
||||
mediaContainer: {
|
||||
alignItems: 'center',
|
||||
width: screenWidth,
|
||||
height: 220
|
||||
},
|
||||
emptyClaimText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
textAlign: 'center',
|
||||
fontSize: 20,
|
||||
marginLeft: 16,
|
||||
marginRight: 16
|
||||
},
|
||||
scrollContainer: {
|
||||
flex: 1,
|
||||
marginTop: -16,
|
||||
marginBottom: -4,
|
||||
paddingTop: 10
|
||||
},
|
||||
scrollContainerActions: {
|
||||
flex: 1
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 24,
|
||||
marginTop: 12,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
marginBottom: 12
|
||||
},
|
||||
channelName: {
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
fontSize: 20,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
marginBottom: 20,
|
||||
color: Colors.LbryGreen
|
||||
},
|
||||
description: {
|
||||
color: Colors.DescriptionGrey,
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 16,
|
||||
lineHeight: 20,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
marginBottom: 40
|
||||
},
|
||||
thumbnail: {
|
||||
width: screenWidth,
|
||||
height: 204,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
downloadButton: {
|
||||
position: 'absolute',
|
||||
top: '40%'
|
||||
},
|
||||
player: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 301,
|
||||
elevation: 21
|
||||
},
|
||||
containedPlayer: {
|
||||
width: '100%',
|
||||
height: 220,
|
||||
},
|
||||
containedPlayerLandscape: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
fullscreenPlayer: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
right: 0,
|
||||
bottom: 0
|
||||
},
|
||||
playerBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 300,
|
||||
elevation: 20,
|
||||
backgroundColor: Colors.Black
|
||||
},
|
||||
containedPlayerBackground: {
|
||||
width: '100%',
|
||||
height: 206,
|
||||
},
|
||||
fullscreenPlayerBackground: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
right: 0,
|
||||
bottom: 0
|
||||
},
|
||||
filePriceContainer: {
|
||||
backgroundColor: '#61fcd8',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
top: 16,
|
||||
width: 56,
|
||||
height: 24,
|
||||
borderRadius: 4
|
||||
},
|
||||
filePriceText: {
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
color: '#0c604b'
|
||||
},
|
||||
actions: {
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 8,
|
||||
marginTop: -14,
|
||||
width: '50%',
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: Colors.Red,
|
||||
width: 80
|
||||
},
|
||||
loading: {
|
||||
position: 'absolute',
|
||||
top: '40%'
|
||||
},
|
||||
busyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
infoText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 20,
|
||||
textAlign: 'center',
|
||||
marginLeft: 10
|
||||
},
|
||||
viewer: {
|
||||
position: 'absolute',
|
||||
flex: 1,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 60,
|
||||
zIndex: 100
|
||||
},
|
||||
link: {
|
||||
color: Colors.LbryGreen
|
||||
},
|
||||
linkTapped: {
|
||||
color: "rgba(64, 184, 154, .2)"
|
||||
}
|
||||
});
|
||||
|
||||
export default filePageStyle;
|
44
app/src/styles/firstRun.js
Normal file
44
app/src/styles/firstRun.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const firstRunStyle = StyleSheet.create({
|
||||
screenContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.LbryGreen
|
||||
},
|
||||
container: {
|
||||
flex: 9,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: Colors.LbryGreen
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
fontSize: 40,
|
||||
marginLeft: 32,
|
||||
marginRight: 32,
|
||||
marginBottom: 32,
|
||||
color: Colors.White
|
||||
},
|
||||
paragraph: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 18,
|
||||
lineHeight: 24,
|
||||
marginLeft: 32,
|
||||
marginRight: 32,
|
||||
marginBottom: 20,
|
||||
color: Colors.White
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
alignSelf: 'flex-end',
|
||||
marginLeft: 32,
|
||||
marginRight: 32
|
||||
},
|
||||
buttonText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 28,
|
||||
color: Colors.White
|
||||
}
|
||||
});
|
||||
|
||||
export default firstRunStyle;
|
122
app/src/styles/mediaPlayer.js
Normal file
122
app/src/styles/mediaPlayer.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const mediaPlayerStyle = StyleSheet.create({
|
||||
player: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: 16
|
||||
},
|
||||
fullscreenContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
progress: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: 3
|
||||
},
|
||||
innerProgressCompleted: {
|
||||
height: 4,
|
||||
backgroundColor: Colors.LbryGreen,
|
||||
},
|
||||
innerProgressRemaining: {
|
||||
height: 4,
|
||||
backgroundColor: '#2c2c2c',
|
||||
},
|
||||
trackingControls: {
|
||||
height: 3,
|
||||
position: 'absolute',
|
||||
bottom: 14
|
||||
},
|
||||
containedTrackingControls: {
|
||||
left: 0,
|
||||
width: '100%'
|
||||
},
|
||||
fullscreenTrackingControls: {
|
||||
alignSelf: 'center',
|
||||
width: '70%'
|
||||
},
|
||||
playerControls: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
playerControlsContainer: {
|
||||
backgroundColor: 'transparent',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
playPauseButton: {
|
||||
position: 'absolute',
|
||||
width: 64,
|
||||
height: 64,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
toggleFullscreenButton: {
|
||||
position: 'absolute',
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
right: 0,
|
||||
bottom: 14,
|
||||
},
|
||||
elapsedDuration: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
position: 'absolute',
|
||||
left: 8,
|
||||
bottom: 24,
|
||||
fontSize: 12,
|
||||
color: '#ffffff'
|
||||
},
|
||||
totalDuration: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
position: 'absolute',
|
||||
right: 40,
|
||||
bottom: 24,
|
||||
fontSize: 12,
|
||||
color: '#ffffff'
|
||||
},
|
||||
seekerCircle: {
|
||||
borderRadius: 12,
|
||||
position: 'relative',
|
||||
top: 14,
|
||||
left: 15,
|
||||
height: 12,
|
||||
width: 12,
|
||||
backgroundColor: '#40c0a9'
|
||||
},
|
||||
seekerHandle: {
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
height: 36,
|
||||
width: 48,
|
||||
marginLeft: -18
|
||||
},
|
||||
seekerHandleContained: {
|
||||
bottom: -17
|
||||
},
|
||||
seekerHandleFs: {
|
||||
bottom: 0
|
||||
},
|
||||
bigSeekerCircle: {
|
||||
borderRadius: 24,
|
||||
position: 'relative',
|
||||
top: 8,
|
||||
left: 15,
|
||||
height: 24,
|
||||
width: 24,
|
||||
backgroundColor: '#40c0a9'
|
||||
}
|
||||
});
|
||||
|
||||
export default mediaPlayerStyle;
|
81
app/src/styles/pageHeader.js
Normal file
81
app/src/styles/pageHeader.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { Platform, StyleSheet } from 'react-native';
|
||||
|
||||
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
|
||||
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
|
||||
|
||||
let platformContainerStyles;
|
||||
if (Platform.OS === 'ios') {
|
||||
platformContainerStyles = {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#A7A7AA',
|
||||
};
|
||||
} else {
|
||||
platformContainerStyles = {
|
||||
shadowColor: 'black',
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: StyleSheet.hairlineWidth,
|
||||
shadowOffset: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
},
|
||||
elevation: 4,
|
||||
};
|
||||
}
|
||||
|
||||
const pageHeaderStyle = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
|
||||
...platformContainerStyles,
|
||||
},
|
||||
transparentContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
...platformContainerStyles,
|
||||
},
|
||||
backIcon: {
|
||||
marginLeft: 16
|
||||
},
|
||||
header: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
titleText: {
|
||||
fontSize: Platform.OS === 'ios' ? 17 : 20,
|
||||
fontWeight: Platform.OS === 'ios' ? '700' : '500',
|
||||
color: 'rgba(0, 0, 0, .9)',
|
||||
textAlign: Platform.OS === 'ios' ? 'center' : 'left',
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
title: {
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
left: TITLE_OFFSET,
|
||||
right: TITLE_OFFSET,
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
|
||||
},
|
||||
left: {
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
right: {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
position: 'absolute',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
flexOne: {
|
||||
flex: 1,
|
||||
}
|
||||
});
|
||||
|
||||
export default pageHeaderStyle;
|
67
app/src/styles/search.js
Normal file
67
app/src/styles/search.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
const searchStyle = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
scrollContainer: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 16,
|
||||
marginBottom: 60
|
||||
},
|
||||
scrollPadding: {
|
||||
paddingBottom: 16
|
||||
},
|
||||
resultItem: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16
|
||||
},
|
||||
thumbnail: {
|
||||
width: '100%',
|
||||
height: 80
|
||||
},
|
||||
thumbnailContainer: {
|
||||
width: '25%'
|
||||
},
|
||||
detailsContainer: {
|
||||
width: '70%'
|
||||
},
|
||||
searchInput: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 16
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
fontSize: 16
|
||||
},
|
||||
uri: {
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
fontSize: 14,
|
||||
marginBottom: 8
|
||||
},
|
||||
publisher: {
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
color: '#c0c0c0'
|
||||
},
|
||||
noResultsText: {
|
||||
textAlign: 'center',
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14,
|
||||
position: 'absolute'
|
||||
},
|
||||
loading: {
|
||||
position: 'absolute'
|
||||
}
|
||||
});
|
||||
|
||||
export default searchStyle;
|
37
app/src/styles/settings.js
Normal file
37
app/src/styles/settings.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
const settingsStyle = StyleSheet.create({
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
margin: 16
|
||||
},
|
||||
scrollContainer: {
|
||||
padding: 16
|
||||
},
|
||||
row: {
|
||||
marginBottom: 24,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
switchText: {
|
||||
width: '70%',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
switchContainer: {
|
||||
width: '25%',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontFamily: 'Metropolis-Regular'
|
||||
},
|
||||
description: {
|
||||
fontSize: 12,
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
color: '#aaaaaa'
|
||||
}
|
||||
});
|
||||
|
||||
export default settingsStyle;
|
44
app/src/styles/splash.js
Normal file
44
app/src/styles/splash.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const splashStyle = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: Colors.LbryGreen
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 64,
|
||||
textAlign: 'center',
|
||||
marginBottom: 48,
|
||||
color: Colors.White
|
||||
},
|
||||
loading: {
|
||||
marginBottom: 36
|
||||
},
|
||||
progress: {
|
||||
alignSelf: 'center',
|
||||
marginBottom: 36,
|
||||
width: '50%'
|
||||
},
|
||||
details: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14,
|
||||
marginLeft: 16,
|
||||
marginRight: 16,
|
||||
color: Colors.White,
|
||||
textAlign: 'center'
|
||||
},
|
||||
message: {
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 18,
|
||||
color: Colors.White,
|
||||
marginLeft: 16,
|
||||
marginRight: 16,
|
||||
marginBottom: 4,
|
||||
textAlign: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
export default splashStyle;
|
53
app/src/styles/transactionList.js
Normal file
53
app/src/styles/transactionList.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const transactionListStyle = StyleSheet.create({
|
||||
listItem: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eeeeee',
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
topRow: {
|
||||
marginBottom: 4
|
||||
},
|
||||
col: {
|
||||
alignSelf: 'stretch'
|
||||
},
|
||||
text: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14
|
||||
},
|
||||
link: {
|
||||
color: Colors.LbryGreen,
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14
|
||||
},
|
||||
amount: {
|
||||
textAlign: 'right'
|
||||
},
|
||||
smallText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 12,
|
||||
color: '#aaaaaa'
|
||||
},
|
||||
smallLink: {
|
||||
color: Colors.LbryGreen,
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 12
|
||||
},
|
||||
noTransactions: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
textAlign: 'center',
|
||||
padding: 16,
|
||||
color: '#aaaaaa'
|
||||
}
|
||||
});
|
||||
|
||||
export default transactionListStyle;
|
57
app/src/styles/uriBar.js
Normal file
57
app/src/styles/uriBar.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const uriBarStyle = StyleSheet.create({
|
||||
uriContainer: {
|
||||
backgroundColor: Colors.White,
|
||||
padding: 8,
|
||||
alignSelf: 'flex-end',
|
||||
height: 60,
|
||||
width: '100%',
|
||||
shadowColor: Colors.Black,
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: StyleSheet.hairlineWidth,
|
||||
shadowOffset: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
},
|
||||
elevation: 4
|
||||
},
|
||||
uriText: {
|
||||
backgroundColor: Colors.White,
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.LightGrey,
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 16,
|
||||
width: '100%'
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'transparent',
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
zIndex: 200,
|
||||
elevation: 16
|
||||
},
|
||||
inFocus: {
|
||||
height: '100%'
|
||||
},
|
||||
suggestions: {
|
||||
backgroundColor: 'white',
|
||||
flex: 1
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
padding: 12
|
||||
},
|
||||
itemText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 16,
|
||||
marginLeft: 12,
|
||||
marginRight: 12
|
||||
}
|
||||
});
|
||||
|
||||
export default uriBarStyle;
|
156
app/src/styles/wallet.js
Normal file
156
app/src/styles/wallet.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const walletStyle = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
},
|
||||
amountRow: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
address: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'dashed',
|
||||
borderColor: '#cccccc',
|
||||
backgroundColor: '#f9f9f9',
|
||||
padding: 8,
|
||||
width: '85%'
|
||||
},
|
||||
button: {
|
||||
backgroundColor: Colors.LbryGreen
|
||||
},
|
||||
historyList: {
|
||||
backgroundColor: '#ffffff'
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
marginTop: 16,
|
||||
marginLeft: 16,
|
||||
marginRight: 16,
|
||||
padding: 16
|
||||
},
|
||||
transactionsCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
margin: 16
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 20,
|
||||
marginBottom: 24
|
||||
},
|
||||
transactionsTitle: {
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 20
|
||||
},
|
||||
transactionsHeader: {
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eeeeee'
|
||||
},
|
||||
text: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14
|
||||
},
|
||||
link: {
|
||||
color: Colors.LbryGreen,
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14
|
||||
},
|
||||
smallText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 12
|
||||
},
|
||||
balanceCard: {
|
||||
marginTop: 16,
|
||||
marginLeft: 16,
|
||||
marginRight: 16
|
||||
},
|
||||
balanceBackground: {
|
||||
position: 'absolute',
|
||||
alignSelf: 'stretch',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
balanceTitle: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 18,
|
||||
marginLeft: 16,
|
||||
marginTop: 16
|
||||
},
|
||||
balanceCaption: {
|
||||
color: '#caedB9',
|
||||
fontFamily: 'Metropolis-Medium',
|
||||
fontSize: 14,
|
||||
marginLeft: 16,
|
||||
marginTop: 8,
|
||||
marginBottom: 96
|
||||
},
|
||||
balance: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 36,
|
||||
marginLeft: 16,
|
||||
marginBottom: 16
|
||||
},
|
||||
infoText: {
|
||||
color: '#aaaaaa',
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14,
|
||||
padding: 16,
|
||||
textAlign: 'center'
|
||||
},
|
||||
input: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14
|
||||
},
|
||||
amountInput: {
|
||||
alignSelf: 'flex-start',
|
||||
width: 100,
|
||||
fontSize: 16,
|
||||
letterSpacing: 1
|
||||
},
|
||||
addressInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
letterSpacing: 1.5
|
||||
},
|
||||
warning: {
|
||||
backgroundColor: Colors.Orange,
|
||||
padding: 16,
|
||||
margin: 16
|
||||
},
|
||||
warningText: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 16,
|
||||
lineHeight: 30
|
||||
},
|
||||
understand: {
|
||||
marginLeft: 16
|
||||
},
|
||||
currency: {
|
||||
alignSelf: 'flex-start',
|
||||
marginTop: 17
|
||||
},
|
||||
sendButton: {
|
||||
marginTop: 8
|
||||
},
|
||||
bottomMarginSmall: {
|
||||
marginBottom: 8
|
||||
},
|
||||
bottomMarginMedium: {
|
||||
marginBottom: 16
|
||||
},
|
||||
bottomMarginLarge: {
|
||||
marginBottom: 24
|
||||
}
|
||||
});
|
||||
|
||||
export default walletStyle;
|
15
app/src/utils/redux.js
Normal file
15
app/src/utils/redux.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {
|
||||
createReactNavigationReduxMiddleware,
|
||||
createReduxBoundAddListener,
|
||||
} from 'react-navigation-redux-helpers';
|
||||
|
||||
const reactNavigationMiddleware = createReactNavigationReduxMiddleware(
|
||||
"root",
|
||||
state => state.nav,
|
||||
);
|
||||
const addListener = createReduxBoundAddListener("root");
|
||||
|
||||
export {
|
||||
reactNavigationMiddleware,
|
||||
addListener,
|
||||
};
|
3
lbry-ios/.gitignore
vendored
3
lbry-ios/.gitignore
vendored
|
@ -1,3 +1,6 @@
|
|||
lbry.xcodeproj/*.backup
|
||||
lbry.xcodeproj/project.xcworkspace/
|
||||
lbry.xcodeproj/xcuserdata/
|
||||
lbry.xcworkspace/xcuserdata/
|
||||
index.ios.jsbundle
|
||||
index.ios.jsbundle.meta
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// Created by Akinwale Ariwodola on 01/06/2018.
|
||||
//
|
||||
|
||||
#import <React/RCTRootView.h>
|
||||
#import "AppDelegate.h"
|
||||
#import "MainViewController.h"
|
||||
#include "../../kivy-ios/dist/include/common/sdl2/SDL_main.h"
|
||||
|
@ -43,8 +44,16 @@
|
|||
SDL_main(0, nil);
|
||||
SDL_iPhoneSetEventPump(SDL_FALSE);
|
||||
|
||||
// initialize React Native root view
|
||||
NSURL *bundleUrl = [[NSBundle mainBundle] URLForResource:@"index.ios" withExtension:@"jsbundle"];
|
||||
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:bundleUrl
|
||||
moduleName:@"LBRYApp" initialProperties:nil launchOptions:nil];
|
||||
|
||||
// show main view
|
||||
MainViewController *viewController = [[MainViewController alloc] initWithNibName:@"MainView" bundle:nil];
|
||||
//MainViewController *viewController = [[MainViewController alloc] initWithNibName:@"MainView" bundle:nil];
|
||||
MainViewController *viewController = [[MainViewController alloc] init];
|
||||
viewController.view = rootView;
|
||||
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
self.window.rootViewController = viewController;
|
||||
[viewController release];
|
||||
|
|
28
lbry-ios/Podfile
Normal file
28
lbry-ios/Podfile
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Uncomment the next line to define a global platform for your project
|
||||
platform :ios, '8.1'
|
||||
|
||||
target 'lbry' do
|
||||
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
|
||||
# use_frameworks!
|
||||
|
||||
# Pods for lbry
|
||||
pod 'React', :path => '../app/node_modules/react-native', :subspecs => [
|
||||
'Core',
|
||||
'CxxBridge',
|
||||
'DevSupport',
|
||||
'RCTAnimation',
|
||||
'RCTImage',
|
||||
'RCTLinkingIOS',
|
||||
'RCTNetwork',
|
||||
'RCTWebSocket',
|
||||
'RCTText'
|
||||
]
|
||||
pod 'yoga', :path => '../app/node_modules/react-native/ReactCommon/yoga'
|
||||
|
||||
# Third party deps
|
||||
pod 'DoubleConversion', :podspec => '../app/node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
|
||||
pod 'glog', :podspec => '../app/node_modules/react-native/third-party-podspecs/glog.podspec'
|
||||
pod 'Folly', :podspec => '../app/node_modules/react-native/third-party-podspecs/Folly.podspec'
|
||||
|
||||
pod 'react-native-video', :podspec => '../app/node_modules/react-native-video/react-native-video.podspec'
|
||||
end
|
26
lbry-ios/Pods/DoubleConversion/LICENSE
generated
Normal file
26
lbry-ios/Pods/DoubleConversion/LICENSE
generated
Normal file
|
@ -0,0 +1,26 @@
|
|||
Copyright 2006-2011, the V8 project authors. All rights reserved.
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
11
lbry-ios/Pods/DoubleConversion/README
generated
Normal file
11
lbry-ios/Pods/DoubleConversion/README
generated
Normal file
|
@ -0,0 +1,11 @@
|
|||
http://code.google.com/p/double-conversion
|
||||
|
||||
This project (double-conversion) provides binary-decimal and decimal-binary
|
||||
routines for IEEE doubles.
|
||||
|
||||
The library consists of efficient conversion routines that have been extracted
|
||||
from the V8 JavaScript engine. The code has been refactored and improved so that
|
||||
it can be used more easily in other projects.
|
||||
|
||||
There is extensive documentation in src/double-conversion.h. Other examples can
|
||||
be found in test/cctest/test-conversions.cc.
|
641
lbry-ios/Pods/DoubleConversion/double-conversion/bignum-dtoa.cc
generated
Normal file
641
lbry-ios/Pods/DoubleConversion/double-conversion/bignum-dtoa.cc
generated
Normal file
|
@ -0,0 +1,641 @@
|
|||
// Copyright 2010 the V8 project authors. All rights reserved.
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following
|
||||
// disclaimer in the documentation and/or other materials provided
|
||||
// with the distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived
|
||||
// from this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#include "bignum-dtoa.h"
|
||||
|
||||
#include "bignum.h"
|
||||
#include "ieee.h"
|
||||
|
||||
namespace double_conversion {
|
||||
|
||||
static int NormalizedExponent(uint64_t significand, int exponent) {
|
||||
ASSERT(significand != 0);
|
||||
while ((significand & Double::kHiddenBit) == 0) {
|
||||
significand = significand << 1;
|
||||
exponent = exponent - 1;
|
||||
}
|
||||
return exponent;
|
||||
}
|
||||
|
||||
|
||||
// Forward declarations:
|
||||
// Returns an estimation of k such that 10^(k-1) <= v < 10^k.
|
||||
static int EstimatePower(int exponent);
|
||||
// Computes v / 10^estimated_power exactly, as a ratio of two bignums, numerator
|
||||
// and denominator.
|
||||
static void InitialScaledStartValues(uint64_t significand,
|
||||
int exponent,
|
||||
bool lower_boundary_is_closer,
|
||||
int estimated_power,
|
||||
bool need_boundary_deltas,
|
||||
Bignum* numerator,
|
||||
Bignum* denominator,
|
||||
Bignum* delta_minus,
|
||||
Bignum* delta_plus);
|
||||
// Multiplies numerator/denominator so that its values lies in the range 1-10.
|
||||
// Returns decimal_point s.t.
|
||||
// v = numerator'/denominator' * 10^(decimal_point-1)
|
||||
// where numerator' and denominator' are the values of numerator and
|
||||
// denominator after the call to this function.
|
||||
static void FixupMultiply10(int estimated_power, bool is_even,
|
||||
int* decimal_point,
|
||||
Bignum* numerator, Bignum* denominator,
|
||||
Bignum* delta_minus, Bignum* delta_plus);
|
||||
// Generates digits from the left to the right and stops when the generated
|
||||
// digits yield the shortest decimal representation of v.
|
||||
static void GenerateShortestDigits(Bignum* numerator, Bignum* denominator,
|
||||
Bignum* delta_minus, Bignum* delta_plus,
|
||||
bool is_even,
|
||||
Vector<char> buffer, int* length);
|
||||
// Generates 'requested_digits' after the decimal point.
|
||||
static void BignumToFixed(int requested_digits, int* decimal_point,
|
||||
Bignum* numerator, Bignum* denominator,
|
||||
Vector<char>(buffer), int* length);
|
||||
// Generates 'count' digits of numerator/denominator.
|
||||
// Once 'count' digits have been produced rounds the result depending on the
|
||||
// remainder (remainders of exactly .5 round upwards). Might update the
|
||||
// decimal_point when rounding up (for example for 0.9999).
|
||||
static void GenerateCountedDigits(int count, int* decimal_point,
|
||||
Bignum* numerator, Bignum* denominator,
|
||||
Vector<char>(buffer), int* length);
|
||||
|
||||
|
||||
void BignumDtoa(double v, BignumDtoaMode mode, int requested_digits,
|
||||
Vector<char> buffer, int* length, int* decimal_point) {
|
||||
ASSERT(v > 0);
|
||||
ASSERT(!Double(v).IsSpecial());
|
||||
uint64_t significand;
|
||||
int exponent;
|
||||
bool lower_boundary_is_closer;
|
||||
if (mode == BIGNUM_DTOA_SHORTEST_SINGLE) {
|
||||
float f = static_cast<float>(v);
|
||||
ASSERT(f == v);
|
||||
significand = Single(f).Significand();
|
||||
exponent = Single(f).Exponent();
|
||||
lower_boundary_is_closer = Single(f).LowerBoundaryIsCloser();
|
||||
} else {
|
||||
significand = Double(v).Significand();
|
||||
exponent = Double(v).Exponent();
|
||||
lower_boundary_is_closer = Double(v).LowerBoundaryIsCloser();
|
||||
}
|
||||
bool need_boundary_deltas =
|
||||
(mode == BIGNUM_DTOA_SHORTEST || mode == BIGNUM_DTOA_SHORTEST_SINGLE);
|
||||
|
||||
bool is_even = (significand & 1) == 0;
|
||||
int normalized_exponent = NormalizedExponent(significand, exponent);
|
||||
// estimated_power might be too low by 1.
|
||||
int estimated_power = EstimatePower(normalized_exponent);
|
||||
|
||||
// Shortcut for Fixed.
|
||||
// The requested digits correspond to the digits after the point. If the
|
||||
// number is much too small, then there is no need in trying to get any
|
||||
// digits.
|
||||
if (mode == BIGNUM_DTOA_FIXED && -estimated_power - 1 > requested_digits) {
|
||||
buffer[0] = '\0';
|
||||
*length = 0;
|
||||
// Set decimal-point to -requested_digits. This is what Gay does.
|
||||
// Note that it should not have any effect anyways since the string is
|
||||
// empty.
|
||||
*decimal_point = -requested_digits;
|
||||
return;
|
||||
}
|
||||
|
||||
Bignum numerator;
|
||||
Bignum denominator;
|
||||
Bignum delta_minus;
|
||||
Bignum delta_plus;
|
||||
// Make sure the bignum can grow large enough. The smallest double equals
|
||||
// 4e-324. In this case the denominator needs fewer than 324*4 binary digits.
|
||||
// The maximum double is 1.7976931348623157e308 which needs fewer than
|
||||
// 308*4 binary digits.
|
||||
ASSERT(Bignum::kMaxSignificantBits >= 324*4);
|
||||
InitialScaledStartValues(significand, exponent, lower_boundary_is_closer,
|
||||
estimated_power, need_boundary_deltas,
|
||||
&numerator, &denominator,
|
||||
&delta_minus, &delta_plus);
|
||||
// We now have v = (numerator / denominator) * 10^estimated_power.
|
||||
FixupMultiply10(estimated_power, is_even, decimal_point,
|
||||
&numerator, &denominator,
|
||||
&delta_minus, &delta_plus);
|
||||
// We now have v = (numerator / denominator) * 10^(decimal_point-1), and
|
||||
// 1 <= (numerator + delta_plus) / denominator < 10
|
||||
switch (mode) {
|
||||
case BIGNUM_DTOA_SHORTEST:
|
||||
case BIGNUM_DTOA_SHORTEST_SINGLE:
|
||||
GenerateShortestDigits(&numerator, &denominator,
|
||||
&delta_minus, &delta_plus,
|
||||
is_even, buffer, length);
|
||||
break;
|
||||
case BIGNUM_DTOA_FIXED:
|
||||
BignumToFixed(requested_digits, decimal_point,
|
||||
&numerator, &denominator,
|
||||
buffer, length);
|
||||
break;
|
||||
case BIGNUM_DTOA_PRECISION:
|
||||
GenerateCountedDigits(requested_digits, decimal_point,
|
||||
&numerator, &denominator,
|
||||
buffer, length);
|
||||
break;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
buffer[*length] = '\0';
|
||||
}
|
||||
|
||||
|
||||
// The procedure starts generating digits from the left to the right and stops
|
||||
// when the generated digits yield the shortest decimal representation of v. A
|
||||
// decimal representation of v is a number lying closer to v than to any other
|
||||
// double, so it converts to v when read.
|
||||
//
|
||||
// This is true if d, the decimal representation, is between m- and m+, the
|
||||
// upper and lower boundaries. d must be strictly between them if !is_even.
|
||||
// m- := (numerator - delta_minus) / denominator
|
||||
// m+ := (numerator + delta_plus) / denominator
|
||||
//
|
||||
// Precondition: 0 <= (numerator+delta_plus) / denominator < 10.
|
||||
// If 1 <= (numerator+delta_plus) / denominator < 10 then no leading 0 digit
|
||||
// will be produced. This should be the standard precondition.
|
||||
static void GenerateShortestDigits(Bignum* numerator, Bignum* denominator,
|
||||
Bignum* delta_minus, Bignum* delta_plus,
|
||||
bool is_even,
|
||||
Vector<char> buffer, int* length) {
|
||||
// Small optimization: if delta_minus and delta_plus are the same just reuse
|
||||
// one of the two bignums.
|
||||
if (Bignum::Equal(*delta_minus, *delta_plus)) {
|
||||
delta_plus = delta_minus;
|
||||
}
|
||||
*length = 0;
|
||||
for (;;) {
|
||||
uint16_t digit;
|
||||
digit = numerator->DivideModuloIntBignum(*denominator);
|
||||
ASSERT(digit <= 9); // digit is a uint16_t and therefore always positive.
|
||||
// digit = numerator / denominator (integer division).
|
||||
// numerator = numerator % denominator.
|
||||
buffer[(*length)++] = static_cast<char>(digit + '0');
|
||||
|
||||
// Can we stop already?
|
||||
// If the remainder of the division is less than the distance to the lower
|
||||
// boundary we can stop. In this case we simply round down (discarding the
|
||||
// remainder).
|
||||
// Similarly we test if we can round up (using the upper boundary).
|
||||
bool in_delta_room_minus;
|
||||
bool in_delta_room_plus;
|
||||
if (is_even) {
|
||||
in_delta_room_minus = Bignum::LessEqual(*numerator, *delta_minus);
|
||||
} else {
|
||||
in_delta_room_minus = Bignum::Less(*numerator, *delta_minus);
|
||||
}
|
||||
if (is_even) {
|
||||
in_delta_room_plus =
|
||||
Bignum::PlusCompare(*numerator, *delta_plus, *denominator) >= 0;
|
||||
} else {
|
||||
in_delta_room_plus =
|
||||
Bignum::PlusCompare(*numerator, *delta_plus, *denominator) > 0;
|
||||
}
|
||||
if (!in_delta_room_minus && !in_delta_room_plus) {
|
||||
// Prepare for next iteration.
|
||||
numerator->Times10();
|
||||
delta_minus->Times10();
|
||||
// We optimized delta_plus to be equal to delta_minus (if they share the
|
||||
// same value). So don't multiply delta_plus if they point to the same
|
||||
// object.
|
||||
if (delta_minus != delta_plus) {
|
||||
delta_plus->Times10();
|
||||
}
|
||||
} else if (in_delta_room_minus && in_delta_room_plus) {
|
||||
// Let's see if 2*numerator < denominator.
|
||||
// If yes, then the next digit would be < 5 and we can round down.
|
||||
int compare = Bignum::PlusCompare(*numerator, *numerator, *denominator);
|
||||
if (compare < 0) {
|
||||
// Remaining digits are less than .5. -> Round down (== do nothing).
|
||||
} else if (compare > 0) {
|
||||
// Remaining digits are more than .5 of denominator. -> Round up.
|
||||
// Note that the last digit could not be a '9' as otherwise the whole
|
||||
// loop would have stopped earlier.
|
||||
// We still have an assert here in case the preconditions were not
|
||||
// satisfied.
|
||||
ASSERT(buffer[(*length) - 1] != '9');
|
||||
buffer[(*length) - 1]++;
|
||||
} else {
|
||||
// Halfway case.
|
||||
// TODO(floitsch): need a way to solve half-way cases.
|
||||
// For now let's round towards even (since this is what Gay seems to
|
||||
// do).
|
||||
|
||||
if ((buffer[(*length) - 1] - '0') % 2 == 0) {
|
||||
// Round down => Do nothing.
|
||||
} else {
|
||||
ASSERT(buffer[(*length) - 1] != '9');
|
||||
buffer[(*length) - 1]++;
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else if (in_delta_room_minus) {
|
||||
// Round down (== do nothing).
|
||||
return;
|
||||
} else { // in_delta_room_plus
|
||||
// Round up.
|
||||
// Note again that the last digit could not be '9' since this would have
|
||||
// stopped the loop earlier.
|
||||
// We still have an ASSERT here, in case the preconditions were not
|
||||
// satisfied.
|
||||
ASSERT(buffer[(*length) -1] != '9');
|
||||
buffer[(*length) - 1]++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Let v = numerator / denominator < 10.
|
||||
// Then we generate 'count' digits of d = x.xxxxx... (without the decimal point)
|
||||
// from left to right. Once 'count' digits have been produced we decide wether
|
||||
// to round up or down. Remainders of exactly .5 round upwards. Numbers such
|
||||
// as 9.999999 propagate a carry all the way, and change the
|
||||
// exponent (decimal_point), when rounding upwards.
|
||||
static void GenerateCountedDigits(int count, int* decimal_point,
|
||||
Bignum* numerator, Bignum* denominator,
|
||||
Vector<char> buffer, int* length) {
|
||||
ASSERT(count >= 0);
|
||||
for (int i = 0; i < count - 1; ++i) {
|
||||
uint16_t digit;
|
||||
digit = numerator->DivideModuloIntBignum(*denominator);
|
||||
ASSERT(digit <= 9); // digit is a uint16_t and therefore always positive.
|
||||
// digit = numerator / denominator (integer division).
|
||||
// numerator = numerator % denominator.
|
||||
buffer[i] = static_cast<char>(digit + '0');
|
||||
// Prepare for next iteration.
|
||||
numerator->Times10();
|
||||
}
|
||||
// Generate the last digit.
|
||||
uint16_t digit;
|
||||
digit = numerator->DivideModuloIntBignum(*denominator);
|
||||
if (Bignum::PlusCompare(*numerator, *numerator, *denominator) >= 0) {
|
||||
digit++;
|
||||
}
|
||||
ASSERT(digit <= 10);
|
||||
buffer[count - 1] = static_cast<char>(digit + '0');
|
||||
// Correct bad digits (in case we had a sequence of '9's). Propagate the
|
||||
// carry until we hat a non-'9' or til we reach the first digit.
|
||||
for (int i = count - 1; i > 0; --i) {
|
||||
if (buffer[i] != '0' + 10) break;
|
||||
buffer[i] = '0';
|
||||
buffer[i - 1]++;
|
||||
}
|
||||
if (buffer[0] == '0' + 10) {
|
||||
// Propagate a carry past the top place.
|
||||
buffer[0] = '1';
|
||||
(*decimal_point)++;
|
||||
}
|
||||
*length = count;
|
||||
}
|
||||
|
||||
|
||||
// Generates 'requested_digits' after the decimal point. It might omit
|
||||
// trailing '0's. If the input number is too small then no digits at all are
|
||||
// generated (ex.: 2 fixed digits for 0.00001).
|
||||
//
|
||||
// Input verifies: 1 <= (numerator + delta) / denominator < 10.
|
||||
static void BignumToFixed(int requested_digits, int* decimal_point,
|
||||
Bignum* numerator, Bignum* denominator,
|
||||
Vector<char>(buffer), int* length) {
|
||||
// Note that we have to look at more than just the requested_digits, since
|
||||
// a number could be rounded up. Example: v=0.5 with requested_digits=0.
|
||||
// Even though the power of v equals 0 we can't just stop here.
|
||||
if (-(*decimal_point) > requested_digits) {
|
||||
// The number is definitively too small.
|
||||
// Ex: 0.001 with requested_digits == 1.
|
||||
// Set decimal-point to -requested_digits. This is what Gay does.
|
||||
// Note that it should not have any effect anyways since the string is
|
||||
// empty.
|
||||
*decimal_point = -requested_digits;
|
||||
*length = 0;
|
||||
return;
|
||||
} else if (-(*decimal_point) == requested_digits) {
|
||||
// We only need to verify if the number rounds down or up.
|
||||
// Ex: 0.04 and 0.06 with requested_digits == 1.
|
||||
ASSERT(*decimal_point == -requested_digits);
|
||||
// Initially the fraction lies in range (1, 10]. Multiply the denominator
|
||||
// by 10 so that we can compare more easily.
|
||||
denominator->Times10();
|
||||
if (Bignum::PlusCompare(*numerator, *numerator, *denominator) >= 0) {
|
||||
// If the fraction is >= 0.5 then we have to include the rounded
|
||||
// digit.
|
||||
buffer[0] = '1';
|
||||
*length = 1;
|
||||
(*decimal_point)++;
|
||||
} else {
|
||||
// Note that we caught most of similar cases earlier.
|
||||
*length = 0;
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// The requested digits correspond to the digits after the point.
|
||||
// The variable 'needed_digits' includes the digits before the point.
|
||||
int needed_digits = (*decimal_point) + requested_digits;
|
||||
GenerateCountedDigits(needed_digits, decimal_point,
|
||||
numerator, denominator,
|
||||
buffer, length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Returns an estimation of k such that 10^(k-1) <= v < 10^k where
|
||||
// v = f * 2^exponent and 2^52 <= f < 2^53.
|
||||
// v is hence a normalized double with the given exponent. The output is an
|
||||
// approximation for the exponent of the decimal approimation .digits * 10^k.
|
||||
//
|
||||
// The result might undershoot by 1 in which case 10^k <= v < 10^k+1.
|
||||
// Note: this property holds for v's upper boundary m+ too.
|
||||
// 10^k <= m+ < 10^k+1.
|
||||
// (see explanation below).
|
||||
//
|
||||
// Examples:
|
||||
// EstimatePower(0) => 16
|
||||
// EstimatePower(-52) => 0
|
||||
//
|
||||
// Note: e >= 0 => EstimatedPower(e) > 0. No similar claim can be made for e<0.
|
||||
static int EstimatePower(int exponent) {
|
||||
// This function estimates log10 of v where v = f*2^e (with e == exponent).
|
||||
// Note that 10^floor(log10(v)) <= v, but v <= 10^ceil(log10(v)).
|
||||
// Note that f is bounded by its container size. Let p = 53 (the double's
|
||||
// significand size). Then 2^(p-1) <= f < 2^p.
|
||||
//
|
||||
// Given that log10(v) == log2(v)/log2(10) and e+(len(f)-1) is quite close
|
||||
// to log2(v) the function is simplified to (e+(len(f)-1)/log2(10)).
|
||||
// The computed number undershoots by less than 0.631 (when we compute log3
|
||||
// and not log10).
|
||||
//
|
||||
// Optimization: since we only need an approximated result this computation
|
||||
// can be performed on 64 bit integers. On x86/x64 architecture the speedup is
|
||||
// not really measurable, though.
|
||||
//
|
||||
// Since we want to avoid overshooting we decrement by 1e10 so that
|
||||
// floating-point imprecisions don't affect us.
|
||||
//
|
||||
// Explanation for v's boundary m+: the computation takes advantage of
|
||||
// the fact that 2^(p-1) <= f < 2^p. Boundaries still satisfy this requirement
|
||||
// (even for denormals where the delta can be much more important).
|
||||
|
||||
const double k1Log10 = 0.30102999566398114; // 1/lg(10)
|
||||
|
||||
// For doubles len(f) == 53 (don't forget the hidden bit).
|
||||
const int kSignificandSize = Double::kSignificandSize;
|
||||
double estimate = ceil((exponent + kSignificandSize - 1) * k1Log10 - 1e-10);
|
||||
return static_cast<int>(estimate);
|
||||
}
|
||||
|
||||
|
||||
// See comments for InitialScaledStartValues.
|
||||
static void InitialScaledStartValuesPositiveExponent(
|
||||
uint64_t significand, int exponent,
|
||||
int estimated_power, bool need_boundary_deltas,
|
||||
Bignum* numerator, Bignum* denominator,
|
||||
Bignum* delta_minus, Bignum* delta_plus) {
|
||||
// A positive exponent implies a positive power.
|
||||
ASSERT(estimated_power >= 0);
|
||||
// Since the estimated_power is positive we simply multiply the denominator
|
||||
// by 10^estimated_power.
|
||||
|
||||
// numerator = v.
|
||||
numerator->AssignUInt64(significand);
|
||||
numerator->ShiftLeft(exponent);
|
||||
// denominator = 10^estimated_power.
|
||||
denominator->AssignPowerUInt16(10, estimated_power);
|
||||
|
||||
if (need_boundary_deltas) {
|
||||
// Introduce a common denominator so that the deltas to the boundaries are
|
||||
// integers.
|
||||
denominator->ShiftLeft(1);
|
||||
numerator->ShiftLeft(1);
|
||||
// Let v = f * 2^e, then m+ - v = 1/2 * 2^e; With the common
|
||||
// denominator (of 2) delta_plus equals 2^e.
|
||||
delta_plus->AssignUInt16(1);
|
||||
delta_plus->ShiftLeft(exponent);
|
||||
// Same for delta_minus. The adjustments if f == 2^p-1 are done later.
|
||||
delta_minus->AssignUInt16(1);
|
||||
delta_minus->ShiftLeft(exponent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// See comments for InitialScaledStartValues
|
||||
static void InitialScaledStartValuesNegativeExponentPositivePower(
|
||||
uint64_t significand, int exponent,
|
||||
int estimated_power, bool need_boundary_deltas,
|
||||
Bignum* numerator, Bignum* denominator,
|
||||
Bignum* delta_minus, Bignum* delta_plus) {
|
||||
// v = f * 2^e with e < 0, and with estimated_power >= 0.
|
||||
// This means that e is close to 0 (have a look at how estimated_power is
|
||||
// computed).
|
||||
|
||||
// numerator = significand
|
||||
// since v = significand * 2^exponent this is equivalent to
|
||||
// numerator = v * / 2^-exponent
|
||||
numerator->AssignUInt64(significand);
|
||||
// denominator = 10^estimated_power * 2^-exponent (with exponent < 0)
|
||||
denominator->AssignPowerUInt16(10, estimated_power);
|
||||
denominator->ShiftLeft(-exponent);
|
||||
|
||||
if (need_boundary_deltas) {
|
||||
// Introduce a common denominator so that the deltas to the boundaries are
|
||||
// integers.
|
||||
denominator->ShiftLeft(1);
|
||||
numerator->ShiftLeft(1);
|
||||
// Let v = f * 2^e, then m+ - v = 1/2 * 2^e; With the common
|
||||
// denominator (of 2) delta_plus equals 2^e.
|
||||
// Given that the denominator already includes v's exponent the distance
|
||||
// to the boundaries is simply 1.
|
||||
delta_plus->AssignUInt16(1);
|
||||
// Same for delta_minus. The adjustments if f == 2^p-1 are done later.
|
||||
delta_minus->AssignUInt16(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// See comments for InitialScaledStartValues
|
||||
static void InitialScaledStartValuesNegativeExponentNegativePower(
|
||||
uint64_t significand, int exponent,
|
||||
int estimated_power, bool need_boundary_deltas,
|
||||
Bignum* numerator, Bignum* denominator,
|
||||
Bignum* delta_minus, Bignum* delta_plus) {
|
||||
// Instead of multiplying the denominator with 10^estimated_power we
|
||||
// multiply all values (numerator and deltas) by 10^-estimated_power.
|
||||
|
||||
// Use numerator as temporary container for power_ten.
|
||||
Bignum* power_ten = numerator;
|
||||
power_ten->AssignPowerUInt16(10, -estimated_power);
|
||||
|
||||
if (need_boundary_deltas) {
|
||||
// Since power_ten == numerator we must make a copy of 10^estimated_power
|
||||
// before we complete the computation of the numerator.
|
||||
// delta_plus = delta_minus = 10^estimated_power
|
||||
delta_plus->AssignBignum(*power_ten);
|
||||
delta_minus->AssignBignum(*power_ten);
|
||||
}
|
||||
|
||||
// numerator = significand * 2 * 10^-estimated_power
|
||||
// since v = significand * 2^exponent this is equivalent to
|
||||
// numerator = v * 10^-estimated_power * 2 * 2^-exponent.
|
||||
// Remember: numerator has been abused as power_ten. So no need to assign it
|
||||
// to itself.
|
||||
ASSERT(numerator == power_ten);
|
||||
numerator->MultiplyByUInt64(significand);
|
||||
|
||||
// denominator = 2 * 2^-exponent with exponent < 0.
|
||||
denominator->AssignUInt16(1);
|
||||
denominator->ShiftLeft(-exponent);
|
||||
|
||||
if (need_boundary_deltas) {
|
||||
// Introduce a common denominator so that the deltas to the boundaries are
|
||||
// integers.
|
||||
numerator->ShiftLeft(1);
|
||||
denominator->ShiftLeft(1);
|
||||
// With this shift the boundaries have their correct value, since
|
||||
// delta_plus = 10^-estimated_power, and
|
||||
// delta_minus = 10^-estimated_power.
|
||||
// These assignments have been done earlier.
|
||||
// The adjustments if f == 2^p-1 (lower boundary is closer) are done later.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Let v = significand * 2^exponent.
|
||||
// Computes v / 10^estimated_power exactly, as a ratio of two bignums, numerator
|
||||
// and denominator. The functions GenerateShortestDigits and
|
||||
// GenerateCountedDigits will then convert this ratio to its decimal
|
||||
// representation d, with the required accuracy.
|
||||
// Then d * 10^estimated_power is the representation of v.
|
||||
// (Note: the fraction and the estimated_power might get adjusted before
|
||||
// generating the decimal representation.)
|
||||
//
|
||||
// The initial start values consist of:
|
||||
// - a scaled numerator: s.t. numerator/denominator == v / 10^estimated_power.
|
||||
// - a scaled (common) denominator.
|
||||
// optionally (used by GenerateShortestDigits to decide if it has the shortest
|
||||
// decimal converting back to v):
|
||||
// - v - m-: the distance to the lower boundary.
|
||||
// - m+ - v: the distance to the upper boundary.
|
||||
//
|
||||
// v, m+, m-, and therefore v - m- and m+ - v all share the same denominator.
|
||||
//
|
||||
// Let ep == estimated_power, then the returned values will satisfy:
|
||||
// v / 10^ep = numerator / denominator.
|
||||
// v's boundarys m- and m+:
|
||||
// m- / 10^ep == v / 10^ep - delta_minus / denominator
|
||||
// m+ / 10^ep == v / 10^ep + delta_plus / denominator
|
||||
// Or in other words:
|
||||
// m- == v - delta_minus * 10^ep / denominator;
|
||||
// m+ == v + delta_plus * 10^ep / denominator;
|
||||
//
|
||||
// Since 10^(k-1) <= v < 10^k (with k == estimated_power)
|
||||
// or 10^k <= v < 10^(k+1)
|
||||
// we then have 0.1 <= numerator/denominator < 1
|
||||
// or 1 <= numerator/denominator < 10
|
||||
//
|
||||
// It is then easy to kickstart the digit-generation routine.
|
||||
//
|
||||
// The boundary-deltas are only filled if the mode equals BIGNUM_DTOA_SHORTEST
|
||||
// or BIGNUM_DTOA_SHORTEST_SINGLE.
|
||||
|
||||
static void InitialScaledStartValues(uint64_t significand,
|
||||
int exponent,
|
||||
bool lower_boundary_is_closer,
|
||||
int estimated_power,
|
||||
bool need_boundary_deltas,
|
||||
Bignum* numerator,
|
||||
Bignum* denominator,
|
||||
Bignum* delta_minus,
|
||||
Bignum* delta_plus) {
|
||||
if (exponent >= 0) {
|
||||
InitialScaledStartValuesPositiveExponent(
|
||||
significand, exponent, estimated_power, need_boundary_deltas,
|
||||
numerator, denominator, delta_minus, delta_plus);
|
||||
} else if (estimated_power >= 0) {
|
||||
InitialScaledStartValuesNegativeExponentPositivePower(
|
||||
significand, exponent, estimated_power, need_boundary_deltas,
|
||||
numerator, denominator, delta_minus, delta_plus);
|
||||
} else {
|
||||
InitialScaledStartValuesNegativeExponentNegativePower(
|
||||
significand, exponent, estimated_power, need_boundary_deltas,
|
||||
numerator, denominator, delta_minus, delta_plus);
|
||||
}
|
||||
|
||||
if (need_boundary_deltas && lower_boundary_is_closer) {
|
||||
// The lower boundary is closer at half the distance of "normal" numbers.
|
||||
// Increase the common denominator and adapt all but the delta_minus.
|
||||
denominator->ShiftLeft(1); // *2
|
||||
numerator->ShiftLeft(1); // *2
|
||||
delta_plus->ShiftLeft(1); // *2
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This routine multiplies numerator/denominator so that its values lies in the
|
||||
// range 1-10. That is after a call to this function we have:
|
||||
// 1 <= (numerator + delta_plus) /denominator < 10.
|
||||
// Let numerator the input before modification and numerator' the argument
|
||||
// after modification, then the output-parameter decimal_point is such that
|
||||
// numerator / denominator * 10^estimated_power ==
|
||||
// numerator' / denominator' * 10^(decimal_point - 1)
|
||||
// In some cases estimated_power was too low, and this is already the case. We
|
||||
// then simply adjust the power so that 10^(k-1) <= v < 10^k (with k ==
|
||||
// estimated_power) but do not touch the numerator or denominator.
|
||||
// Otherwise the routine multiplies the numerator and the deltas by 10.
|
||||
static void FixupMultiply10(int estimated_power, bool is_even,
|
||||
int* decimal_point,
|
||||
Bignum* numerator, Bignum* denominator,
|
||||
Bignum* delta_minus, Bignum* delta_plus) {
|
||||
bool in_range;
|
||||
if (is_even) {
|
||||
// For IEEE doubles half-way cases (in decimal system numbers ending with 5)
|
||||
// are rounded to the closest floating-point number with even significand.
|
||||
in_range = Bignum::PlusCompare(*numerator, *delta_plus, *denominator) >= 0;
|
||||
} else {
|
||||
in_range = Bignum::PlusCompare(*numerator, *delta_plus, *denominator) > 0;
|
||||
}
|
||||
if (in_range) {
|
||||
// Since numerator + delta_plus >= denominator we already have
|
||||
// 1 <= numerator/denominator < 10. Simply update the estimated_power.
|
||||
*decimal_point = estimated_power + 1;
|
||||
} else {
|
||||
*decimal_point = estimated_power;
|
||||
numerator->Times10();
|
||||
if (Bignum::Equal(*delta_minus, *delta_plus)) {
|
||||
delta_minus->Times10();
|
||||
delta_plus->AssignBignum(*delta_minus);
|
||||
} else {
|
||||
delta_minus->Times10();
|
||||
delta_plus->Times10();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace double_conversion
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue