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