added React Native code

This commit is contained in:
Akinwale Ariwodola 2018-07-30 03:26:48 +01:00
parent 03d218680e
commit 1bcb1ff228
13744 changed files with 2596354 additions and 6 deletions

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules/

2
app/bundle.sh Executable file
View 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
View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View 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);

View 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);

View 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>
);
}
}

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import Button from './view';
export default connect(null, null)(Button);

View 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>
);
}
};

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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>
);
}
};

View 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);

View 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;

View file

@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import NsfwOverlay from './view';
const perform = dispatch => ({});
export default connect(null, perform)(NsfwOverlay);

View 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;

View file

@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import PageHeader from './view';
const perform = dispatch => ({});
export default connect(null, perform)(PageHeader);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View file

@ -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;

View 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;

View 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);

View 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;

View 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);

View 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;

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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
View file

@ -0,0 +1,5 @@
const Constants = {
SETTING_ALPHA_UNDERSTANDS_RISKS: "ALPHA_UNDERSTANDS_RISKS"
};
export default Constants;

130
app/src/index.js Normal file
View 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;

View file

@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import AboutPage from './view';
const perform = dispatch => ({});
export default connect(null, perform)(AboutPage);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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
View 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;

View file

@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import FirstRun from './view';
const perform = dispatch => ({});
export default connect(null, perform)(FirstRun);

View 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;

View 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;

View 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);

View 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;

View 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);

View 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;

View 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
View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);
};
}

View file

@ -0,0 +1,11 @@
import { ACTIONS } from 'lbry-redux';
export function doSetClientSetting(key, value) {
return {
type: ACTIONS.CLIENT_SETTING_CHANGED,
data: {
key,
value,
},
};
}

View 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;
}

View 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
View 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
View 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;

View 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
View 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;

View 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;

View 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;

View 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
View 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;

View 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;

View 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;

View 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
View 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;

View 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
View 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;

View 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
View 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
View 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
View 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
View file

@ -1,3 +1,6 @@
lbry.xcodeproj/*.backup
lbry.xcodeproj/project.xcworkspace/
lbry.xcodeproj/xcuserdata/
lbry.xcworkspace/xcuserdata/
index.ios.jsbundle
index.ios.jsbundle.meta

View file

@ -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
View 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
View 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
View 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.

View 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