fetch and display rewards, add styles for the reward card

This commit is contained in:
Akinwale Ariwodola 2018-08-16 12:17:07 +01:00
commit c8b6c1c21c
66 changed files with 2784 additions and 1741 deletions

2
.gitignore vendored
View file

@ -6,4 +6,6 @@ build.log
recipes/**/*.pyc
src/main/assets/index.android.bundle
src/main/assets/index.android.bundle.meta
*.log
.vagrant

View file

@ -28,13 +28,13 @@ install:
- mkdir -p cd ~/.buildozer/android/platform/
- wget -q 'https://dl.google.com/android/repository/android-ndk-r13b-linux-x86_64.zip' -P ~/.buildozer/android/platform/
- wget -q 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P ~/.buildozer/android/platform/
- wget -q 'https://dl.google.com/android/repository/platform-26_r02.zip' -P ~/.buildozer/android/platform/
- wget -q 'https://dl.google.com/android/repository/platform-27_r01.zip' -P ~/.buildozer/android/platform/
- wget -q 'https://dl.google.com/android/repository/build-tools_r26.0.1-linux.zip' -P ~/.buildozer/android/platform/
- unzip -qq ~/.buildozer/android/platform/android-ndk-r13b-linux-x86_64.zip -d ~/.buildozer/android/platform/
- tar -xf ~/.buildozer/android/platform/android-sdk_r23-linux.tgz -C ~/.buildozer/android/platform/
- mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk-23
- unzip -qq ~/.buildozer/android/platform/platform-26_r02.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms
- mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-8.0.0 ~/.buildozer/android/platform/android-sdk-23/platforms/android-26
- unzip -qq ~/.buildozer/android/platform/platform-27_r01.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms
- mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 ~/.buildozer/android/platform/android-sdk-23/platforms/android-27
- mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools
- unzip -qq ~/.buildozer/android/platform/build-tools_r26.0.1-linux.zip -d ~/.buildozer/android/platform/android-sdk-23/build-tools
- mv ~/.buildozer/android/platform/android-sdk-23/build-tools/android-8.0.0 ~/.buildozer/android/platform/android-sdk-23/build-tools/26.0.1

97
Vagrantfile vendored Normal file
View file

@ -0,0 +1,97 @@
echoed=false
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64"
#config.disksize.size = "20GB"
config.vm.provider "virtualbox" do |v|
host = RbConfig::CONFIG['host_os']
# Give VM 1/4 system memory & access to all cpu cores on the host
if host =~ /darwin/
cpus = `sysctl -n hw.ncpu`.to_i
# sysctl returns Bytes and we need to convert to MB
mem = `sysctl -n hw.memsize`.to_i / 1024 / 1024 / 4
elsif host =~ /linux/
cpus = `nproc`.to_i
# meminfo shows KB and we need to convert to MB
mem = `grep 'MemTotal' /proc/meminfo | sed -e 's/MemTotal://' -e 's/ kB//'`.to_i / 1024 / 4
else
cpus = `wmic cpu get NumberOfCores`.split("\n")[2].to_i
mem = `wmic OS get TotalVisibleMemorySize`.split("\n")[2].to_i / 1024 /4
end
mem = mem / 1024 / 4
mem = [mem, 2048].max # Minimum 2048
if echoed === false
echoed=true
puts("Memory", mem)
puts("CPUs", cpus)
end
#v.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/home_vagrant_lbry-android", "1"]
#v.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/vagrant", "1"]
v.customize ["modifyvm", :id, "--memory", mem]
v.customize ["modifyvm", :id, "--cpus", cpus]
end
config.vm.synced_folder "./", "/home/vagrant/lbry-android"
config.vm.provision "shell", inline: <<-SHELL
dpkg --add-architecture i386
apt-get update
apt-get install -y libssl-dev
apt-get install -y autoconf libffi-dev pkg-config libtool build-essential ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386 python-pip
pip install -f --upgrade setuptools pyopenssl
git clone https://github.com/lbryio/buildozer.git
cd buildozer
python2.7 setup.py install
cd ../
rm -rf ./buildozer
# Install additonal buildozer dependencies
sudo apt-get install cython
# Install node
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get install -y nodejs
export HOME=/home/vagrant
cp $HOME/lbry-android/buildozer.spec.vagrant $HOME/lbry-android/buildozer.spec
mkdir -p cd $HOME/.buildozer/android/platform/
wget -q 'https://dl.google.com/android/repository/android-ndk-r13b-linux-x86_64.zip' -P $HOME/.buildozer/android/platform/
wget -q 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P $HOME/.buildozer/android/platform/
wget -q 'https://dl.google.com/android/repository/platform-27_r01.zip' -P $HOME/.buildozer/android/platform/
wget -q 'https://dl.google.com/android/repository/build-tools_r26.0.1-linux.zip' -P $HOME/.buildozer/android/platform/
unzip -qq $HOME/.buildozer/android/platform/android-ndk-r13b-linux-x86_64.zip -d $HOME/.buildozer/android/platform/
rm $HOME/.buildozer/android/platform/android-ndk-r13b-linux-x86_64.zip
tar -xf $HOME/.buildozer/android/platform/android-sdk_r23-linux.tgz -C $HOME/.buildozer/android/platform/
rm $HOME/.buildozer/android/platform/android-sdk_r23-linux.tgz
mv $HOME/.buildozer/android/platform/android-sdk-linux $HOME/.buildozer/android/platform/android-sdk-23
unzip -qq $HOME/.buildozer/android/platform/android-23_r02.zip -d $HOME/.buildozer/android/platform/android-sdk-23/platforms
rm $HOME/.buildozer/android/platform/platform-27_r01.zip
mv $HOME/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 $HOME/.buildozer/android/platform/android-sdk-23/platforms/android-27
mkdir -p $HOME/.buildozer/android/platform/android-sdk-23/build-tools
unzip -qq $HOME/.buildozer/android/platform/build-tools_r26.0.1-linux.zip -d $HOME/.buildozer/android/platform/android-sdk-23/build-tools
rm $HOME/.buildozer/android/platform/build-tools_r26.0.1-linux.zip
mv $HOME/.buildozer/android/platform/android-sdk-23/build-tools/android-8.0.0 $HOME/.buildozer/android/platform/android-sdk-23/build-tools/26.0.1
mkdir -p $HOME/.buildozer/android/platform/android-sdk-23/licenses
rm -rf $HOME/.buildozer/android/platform/android-sdk-23/tools
# https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
wget -q https://dl.google.com/android/repository/tools_r25.2.5-linux.zip
unzip -o -q ./tools_r25.2.5-linux.zip -d $HOME/.buildozer/android/platform/android-sdk-23/
rm sdk-tools-linux-3859397.zip
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > $HOME/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
sudo chown -r vagrant $HOME
echo "Installing React Native via NPM..."
sudo npm install -g react-native-cli
SHELL
end

2675
app/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,20 +6,23 @@
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"dependencies": {
"base-64": "^0.1.0",
"lbry-redux": "lbryio/lbry-redux",
"lbryinc": "lbryio/lbryinc",
"lbryinc": "lbryio/lbryinc#authentication-flow",
"moment": "^2.22.1",
"react": "16.2.0",
"react-native": "0.55.3",
"react-native-fetch-blob": "^0.10.8",
"react-native-image-zoom-viewer": "^2.2.5",
"react-native-vector-icons": "^4.5.0",
"react-native-video": "2.0.0",
"react-native-vector-icons": "^5.0.0",
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
"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-filesystem-storage": "^1.2.0",
"redux-persist-transform-compress": "^4.2.0",
"redux-persist-transform-filter": "0.0.10",
"redux-thunk": "^2.2.0"

View file

@ -28,19 +28,29 @@ import {
TextInput,
ToastAndroid
} from 'react-native';
import { SETTINGS, doHideNotification, selectNotification } from 'lbry-redux';
import { SETTINGS, doHideNotification, doNotify, selectNotification } from 'lbry-redux';
import {
doUserEmailVerify,
doUserEmailVerifyFailure,
selectEmailToVerify,
selectEmailVerifyIsPending,
selectEmailVerifyErrorMessage,
selectUser
} from 'lbryinc';
import { makeSelectClientSetting } from '../redux/selectors/settings';
import Feather from 'react-native-vector-icons/Feather';
import { decode as atob } from 'base-64';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Constants from '../constants';
import discoverStyle from '../styles/discover';
import searchStyle from '../styles/search';
import SearchRightHeaderIcon from "../component/searchRightHeaderIcon";
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')} />,
headerLeft: <Icon name="bars" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
})
},
File: {
@ -65,7 +75,7 @@ const trendingStack = StackNavigator({
screen: TrendingPage,
navigationOptions: ({ navigation }) => ({
title: 'Trending',
headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
headerLeft: <Icon name="bars" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
})
}
});
@ -75,7 +85,7 @@ const walletStack = StackNavigator({
screen: WalletPage,
navigationOptions: ({ navigation }) => ({
title: 'Wallet',
headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
headerLeft: <Icon name="bars" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
})
},
TransactionHistory: {
@ -94,7 +104,7 @@ const rewardsStack = StackNavigator({
screen: RewardsPage,
navigationOptions: ({ navigation }) => ({
title: 'Rewards',
headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
headerLeft: <Icon name="bars" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
})
}
});
@ -134,18 +144,27 @@ export const AppNavigator = new StackNavigator({
class AppWithNavigationState extends React.Component {
static supportedDisplayTypes = ['toast'];
constructor() {
super();
this.state = {
emailVerifyDone: false
};
}
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].routes && nav.routes[0].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) {
@ -170,7 +189,14 @@ class AppWithNavigationState extends React.Component {
componentWillUpdate(nextProps) {
const { dispatch } = this.props;
const { notification } = nextProps;
const {
notification,
emailToVerify,
emailVerifyPending,
emailVerifyErrorMessage,
user
} = nextProps;
if (notification) {
const { displayType, message } = notification;
let currentDisplayType;
@ -192,6 +218,20 @@ class AppWithNavigationState extends React.Component {
dispatch(doHideNotification());
}
if (user &&
!emailVerifyPending &&
!this.state.emailVerifyDone &&
(emailToVerify || emailVerifyErrorMessage)) {
AsyncStorage.getItem(Constants.KEY_SHOULD_VERIFY_EMAIL).then(shouldVerify => {
if ('true' === shouldVerify) {
this.setState({ emailVerifyDone: true });
const message = emailVerifyErrorMessage ?
String(emailVerifyErrorMessage) : 'Your email address was successfully verified.';
dispatch(doNotify({ message, displayType: ['toast'] }));
}
});
}
}
_handleAppStateChange = (nextAppState) => {
@ -210,12 +250,38 @@ class AppWithNavigationState extends React.Component {
_handleUrl = (evt) => {
const { dispatch } = this.props;
if (evt.url) {
const navigateAction = NavigationActions.navigate({
routeName: 'File',
key: evt.url,
params: { uri: evt.url }
});
dispatch(navigateAction);
if (evt.url.startsWith('lbry://?verify=')) {
this.setState({ emailVerifyDone: false });
let verification = {};
try {
verification = JSON.parse(atob(evt.url.substring(15)));
} catch (error) {
console.log(error);
}
if (verification.token && verification.recaptcha) {
AsyncStorage.setItem(Constants.KEY_SHOULD_VERIFY_EMAIL, 'true');
try {
dispatch(doUserEmailVerify(verification.token, verification.recaptcha));
} catch (error) {
const message = 'Invalid Verification Token';
dispatch(doUserEmailVerifyFailure(message));
dispatch(doNotify({ message, displayType: ['toast'] }));
}
} else {
dispatch(doNotify({
message: 'Invalid Verification URI',
displayType: ['toast'],
}));
}
} else {
const navigateAction = NavigationActions.navigate({
routeName: 'File',
key: evt.url,
params: { uri: evt.url }
});
dispatch(navigateAction);
}
}
}
@ -234,10 +300,14 @@ class AppWithNavigationState extends React.Component {
}
const mapStateToProps = state => ({
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
nav: state.nav,
notification: selectNotification(state),
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state)
emailToVerify: selectEmailToVerify(state),
emailVerifyPending: selectEmailVerifyIsPending(state),
emailVerifyErrorMessage: selectEmailVerifyErrorMessage(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state),
user: selectUser(state),
});
export default connect(mapStateToProps)(AppWithNavigationState);

View file

@ -1,7 +1,8 @@
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
import buttonStyle from '../../styles/button';
import Icon from 'react-native-vector-icons/FontAwesome';
import Colors from '../../styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
export default class Button extends React.PureComponent {
render() {
@ -10,9 +11,10 @@ export default class Button extends React.PureComponent {
style,
text,
icon,
theme,
onPress
} = this.props;
let styles = [buttonStyle.button, buttonStyle.row];
if (style) {
if (style.length) {
@ -21,19 +23,26 @@ export default class Button extends React.PureComponent {
styles.push(style);
}
}
if (disabled) {
styles.push(buttonStyle.disabled);
}
const textStyles = [buttonStyle.text];
if (icon && icon.trim().length > 0) {
textStyles.push(buttonStyle.textWithIcon);
}
if (theme === 'light') {
textStyles.push(buttonStyle.textDark);
} else {
// Dark background, default
textStyles.push(buttonStyle.textLight);
}
return (
<TouchableOpacity disabled={disabled} style={styles} onPress={onPress}>
{icon && <Icon name={icon} size={18} color='#ffffff' class={buttonStyle.icon} /> }
{icon && <Icon name={icon} size={18} color={'light' === theme ? Colors.DarkGrey : Colors.White} />}
{text && (text.trim().length > 0) && <Text style={textStyles}>{text}</Text>}
</TouchableOpacity>
);

View file

@ -1,5 +1,6 @@
import React from 'react';
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
import Button from '../button';
import fileDownloadButtonStyle from '../../styles/fileDownloadButton';
class FileDownloadButton extends React.PureComponent {
@ -64,17 +65,17 @@ class FileDownloadButton extends React.PureComponent {
);
}
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>
<Button icon={isPlayable ? 'play' : null}
text={isPlayable ? 'Play' : 'Download'}
style={[style, fileDownloadButtonStyle.container]} onPress={() => {
if (NativeModules.Mixpanel) {
NativeModules.Mixpanel.track('Purchase Uri', { Uri: uri });
}
purchaseUri(uri);
if (isPlayable && onPlay) {
this.props.onPlay();
}
}} />
);
} else if (fileInfo && fileInfo.download_path) {
return (

View file

@ -9,7 +9,7 @@ import {
TouchableOpacity
} from 'react-native';
import Video from 'react-native-video';
import Icon from 'react-native-vector-icons/FontAwesome';
import Icon from 'react-native-vector-icons/FontAwesome5';
import FileItemMedia from '../fileItemMedia';
import mediaPlayerStyle from '../../styles/mediaPlayer';
@ -152,11 +152,10 @@ class MediaPlayer extends React.PureComponent {
}
checkSeekerPosition(val = 0) {
const offset = this.getTrackingOffset();
if (val < offset) {
val = offset;
} else if (val >= (offset + this.seekerWidth)) {
return offset + this.seekerWidth;
if (val < 0) {
val = 0;
} else if (val >= this.seekerWidth) {
return this.seekerWidth;
}
return val;
@ -211,9 +210,6 @@ class MediaPlayer extends React.PureComponent {
}
calculateSeekerPosition() {
if (this.state.fullscreenMode) {
return this.getTrackingOffset() + (this.seekerWidth * this.getCurrentTimePercentage());
}
return this.seekerWidth * this.getCurrentTimePercentage();
}
@ -229,7 +225,7 @@ class MediaPlayer extends React.PureComponent {
}
componentDidMount() {
this.setSeekerPosition(this.calculateSeekerPosition());
}
componentWillUnmount() {
@ -265,10 +261,16 @@ class MediaPlayer extends React.PureComponent {
return null;
}
getEncodedDownloadPath = (fileInfo) => {
const { file_name: fileName } = fileInfo;
const encodedFileName = encodeURIComponent(fileName).replace(/!/g, '%21');
return fileInfo.download_path.replace(new RegExp(fileName, 'g'), encodedFileName);
}
render() {
const { backgroundPlayEnabled, fileInfo, thumbnail, onLayout, style } = this.props;
const flexCompleted = this.getCurrentTimePercentage() * 100;
const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100;
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) {
@ -283,7 +285,7 @@ class MediaPlayer extends React.PureComponent {
return (
<View style={styles} onLayout={onLayout}>
<Video source={{ uri: 'file:///' + fileInfo.download_path }}
<Video source={{ uri: 'file:///' + this.getEncodedDownloadPath(fileInfo) }}
ref={(ref: Video) => { this.video = ref }}
resizeMode={this.state.resizeMode}
playInBackground={backgroundPlayEnabled}
@ -306,14 +308,18 @@ class MediaPlayer extends React.PureComponent {
this.seekerWidth = evt.nativeEvent.layout.width;
}}>
<View style={mediaPlayerStyle.progress}>
<View style={[mediaPlayerStyle.innerProgressCompleted, { flex: flexCompleted }]} />
<View style={[mediaPlayerStyle.innerProgressRemaining, { flex: flexRemaining }]} />
<View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} />
<View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} />
</View>
</View>}
{this.state.areControlsVisible &&
<View style={[mediaPlayerStyle.seekerHandle, { left: this.state.seekerPosition }]} { ...this.seekResponder.panHandlers }>
<View style={this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle} />
<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>
);

View file

@ -8,7 +8,7 @@ import {
TouchableOpacity,
View
} from 'react-native';
import Feather from 'react-native-vector-icons/Feather';
import Icon from 'react-native-vector-icons/FontAwesome5';
import pageHeaderStyle from '../../styles/pageHeader';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
@ -21,7 +21,7 @@ class PageHeader extends React.PureComponent {
pageHeaderStyle.container,
{ height: APPBAR_HEIGHT }
];
return (
<View style={containerStyles}>
<View style={pageHeaderStyle.flexOne}>
@ -35,7 +35,7 @@ class PageHeader extends React.PureComponent {
</AnimatedText>
</View>
<TouchableOpacity style={pageHeaderStyle.left}>
<Feather name="arrow-left" size={24} onPress={onBackPressed} style={pageHeaderStyle.backIcon} />
<Icon name="arrow-left" size={24} onPress={onBackPressed} style={pageHeaderStyle.backIcon} />
</TouchableOpacity>
</View>
</View>

View file

@ -1,6 +1,7 @@
// @flow
import React from 'react';
import { Text, View } from 'react-native';
import { Text, TouchableOpacity, View } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome5';
import rewardStyle from '../../styles/reward';
type Props = {
@ -17,20 +18,25 @@ type Props = {
class RewardCard extends React.PureComponent<Props> {
render() {
const { reward } = props;
const { canClaim, onClaimPress, reward } = this.props;
const claimed = !!reward.transaction_id;
return (
<View style={[rewardStyle.card, rewardStyle.row]}>
<View style={rewardStyle.leftCol}>
<TouchableOpacity onPress={() => { if (!claimed && onClaimPress) { onClaimPress(); } }}>
<Icon name={claimed ? "check-circle" : "circle"}
style={claimed ? rewardStyle.claimed : (canClaim ? rewardStyle.unclaimed : rewardStyle.disabled)}
size={20} />
</TouchableOpacity>
</View>
<View style={rewardStyle.midCol}>
<View style={rewardStyle.rewardTitle}>{reward.reward_title}</View>
<View style={rewardStyle.rewardDescription}>{reward.reward_description}</View>
<Text style={rewardStyle.rewardTitle}>{reward.reward_title}</Text>
<Text style={rewardStyle.rewardDescription}>{reward.reward_description}</Text>
</View>
<View style={rewardStyle.rightCol}>
<View style={rewardStyle.rewardAmount}>{reward.reward_amount}</View>
<Text style={rewardStyle.rewardAmount}>{reward.reward_amount}</Text>
<Text style={rewardStyle.rewardCurrency}>LBC</Text>
</View>
</View>
);

View file

@ -2,37 +2,37 @@
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 Icon from 'react-native-vector-icons/FontAwesome5';
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} />
icon = <Icon name="at" size={18} />
break;
case SEARCH_TYPES.SEARCH:
icon = <Feather name="search" size={18} />
icon = <Icon name="search" size={18} />
break;
case SEARCH_TYPES.FILE:
default:
icon = <Feather name="file" size={18} />
icon = <Icon name="file" size={18} />
break;
}
return (
<TouchableOpacity style={uriBarStyle.item} onPress={onPress}>
{icon}
<Text style={uriBarStyle.itemText} numberOfLines={1}>{shorthand || value} - {type === 'search' ? 'Search' : value}</Text>
</TouchableOpacity>
)
}
}
}
export default UriBarItem;

View file

@ -24,14 +24,14 @@ class WalletAddress extends React.PureComponent<Props> {
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'}
icon={'sync'}
text={'Get New Address'}
onPress={getNewAddress}
disabled={gettingNewAddress}

View file

@ -17,21 +17,21 @@ type Props = {
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;
@ -42,7 +42,7 @@ class WalletSend extends React.PureComponent<Props> {
});
return;
}
if (amount > balance) {
notify({
message: 'Insufficient credits',
@ -50,13 +50,13 @@ class WalletSend extends React.PureComponent<Props> {
});
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;
@ -66,7 +66,7 @@ class WalletSend extends React.PureComponent<Props> {
});
}
}
handleAddressInputSubmit = () => {
if (this.amountInput) {
this.amountInput.focus();

View file

@ -1,5 +1,10 @@
const Constants = {
SETTING_ALPHA_UNDERSTANDS_RISKS: "ALPHA_UNDERSTANDS_RISKS"
KEY_FIRST_RUN_EMAIL: "firstRunEmail",
KEY_SHOULD_VERIFY_EMAIL: "shouldVerifyEmail",
SETTING_ALPHA_UNDERSTANDS_RISKS: "alphaUnderstandRisks",
ACTION_FIRST_RUN_PAGE_CHANGED: "FIRST_RUN_PAGE_CHANGED",
};
export default Constants;

View file

@ -1,6 +1,5 @@
import React from 'react';
import { Provider, connect } from 'react-redux';
import DiscoverPage from './page/discover';
import {
AppRegistry,
AppState,
@ -9,16 +8,6 @@ import {
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,
@ -28,9 +17,20 @@ import {
searchReducer,
walletReducer
} from 'lbry-redux';
import settingsReducer from './redux/reducers/settings';
import moment from 'moment';
import { authReducer, rewardsReducer, userReducer } from 'lbryinc';
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import { createLogger } from 'redux-logger';
import { StackNavigator, addNavigationHelpers } from 'react-navigation';
import { AppNavigator } from './component/AppNavigator';
import { persistStore, autoRehydrate } from 'redux-persist';
import { reactNavigationMiddleware } from './utils/redux';
import AppWithNavigationState from './component/AppNavigator';
import FilesystemStorage from 'redux-persist-filesystem-storage';
import createCompressor from 'redux-persist-transform-compress';
import createFilter from 'redux-persist-transform-filter';
import moment from 'moment';
import settingsReducer from './redux/reducers/settings';
import thunk from 'redux-thunk';
function isFunction(object) {
return typeof object === 'function';
@ -69,17 +69,21 @@ const navigatorReducer = (state = initialNavState, action) => {
};
const reducers = combineReducers({
auth: authReducer,
claims: claimsReducer,
costInfo: costInfoReducer,
fileInfo: fileInfoReducer,
notifications: notificationsReducer,
search: searchReducer,
wallet: walletReducer,
nav: navigatorReducer,
settings: settingsReducer
notifications: notificationsReducer,
rewards: rewardsReducer,
settings: settingsReducer,
search: searchReducer,
user: userReducer,
wallet: walletReducer
});
const bulkThunk = createBulkThunkMiddleware();
const logger = createLogger({ collapsed: true });
const middleware = [thunk, bulkThunk, reactNavigationMiddleware];
// eslint-disable-next-line no-underscore-dangle
@ -93,20 +97,22 @@ const store = createStore(
applyMiddleware(...middleware)
)
);
window.store = store;
const compressor = createCompressor();
const authFilter = createFilter('auth', ['authToken']);
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'],
whitelist: ['auth', '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],
transforms: [authFilter, saveClaimsFilter, subscriptionsFilter, settingsFilter, walletFilter, compressor],
debounce: 10000,
storage: AsyncStorage
storage: FilesystemStorage
};
persistStore(store, persistOptions, err => {

View file

@ -11,11 +11,11 @@ class AboutPage extends React.PureComponent {
lbryId: null,
versionInfo: null
};
componentDidMount() {
if (NativeModules.VersionInfo) {
NativeModules.VersionInfo.getAppVersion().then(version => {
this.setState({appVersion: version});
this.setState({appVersion: version});
});
}
Lbry.version().then(info => {
@ -23,19 +23,19 @@ class AboutPage extends React.PureComponent {
versionInfo: info,
});
});
Lbry.status({ session_status: true }).then(info => {
Lbry.status().then(info => {
this.setState({
lbryId: info.lbry_id,
lbryId: info.installation_id,
});
});
}
render() {
const loading = 'Loading...';
const ver = this.state.versionInfo ? this.state.versionInfo : null;
return (
<View>
<View style={aboutStyle.container}>
<PageHeader title={"About LBRY"}
onBackPressed={() => { this.props.navigation.goBack(); }} />
<ScrollView style={aboutStyle.scrollContainer}>
@ -50,27 +50,38 @@ class AboutPage extends React.PureComponent {
<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.socialTitle}>Get Social</Text>
<Text style={aboutStyle.paragraph}>
You can interact with the LBRY team and members of the community on Discord, Facebook, Instagram, Twitter or Reddit.
</Text>
<View style={aboutStyle.links}>
<Link style={aboutStyle.link} href="https://discordapp.com/invite/Z3bERWA" text="Discord" />
<Link style={aboutStyle.link} href="https://www.facebook.com/LBRYio" text="Facebook" />
<Link style={aboutStyle.link} href="https://www.instagram.com/LBRYio/" text="Instagram" />
<Link style={aboutStyle.link} href="https://twitter.com/LBRYio" text="Twitter" />
<Link style={aboutStyle.link} href="https://reddit.com/r/lbry" text="Reddit" />
</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>

View file

@ -14,7 +14,6 @@ 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() {

View file

@ -3,7 +3,6 @@ import { Lbry, normalizeURI } from 'lbry-redux';
import {
ActivityIndicator,
Alert,
Button,
Dimensions,
NativeModules,
ScrollView,
@ -16,6 +15,7 @@ import {
WebView
} from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
import Button from '../../component/button';
import Colors from '../../styles/colors';
import ChannelPage from '../channel';
import FileDownloadButton from '../../component/fileDownloadButton';
@ -99,9 +99,17 @@ class FilePage extends React.PureComponent {
if (mode) {
// fullscreen, so change orientation to landscape mode
NativeModules.ScreenOrientation.lockOrientationLandscape();
if (NativeModules.UtilityModule) {
// hide the navigation bar (on devices that use have soft navigation bar)
NativeModules.UtilityModule.hideNavigationBar();
}
} else {
// Switch back to portrait mode when the media is not fullscreen
NativeModules.ScreenOrientation.lockOrientationPortrait();
if (NativeModules.UtilityModule) {
// hide the navigation bar (on devices that use have soft navigation bar)
NativeModules.UtilityModule.showNavigationBar();
}
}
}
}
@ -138,9 +146,11 @@ class FilePage extends React.PureComponent {
StatusBar.setHidden(false);
if (NativeModules.ScreenOrientation) {
NativeModules.ScreenOrientation.unlockOrientation();
}
}
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.keepAwakeOff();
const utility = NativeModules.UtilityModule;
utility.keepAwakeOff();
utility.showNavigationBar();
}
}
@ -322,9 +332,16 @@ class FilePage extends React.PureComponent {
{ showActions &&
<View style={filePageStyle.actions}>
{completed && <Button color="red" title="Delete" onPress={this.onDeletePressed} />}
{completed && <Button style={filePageStyle.actionButton}
theme={"light"}
icon={"trash"}
text={"Delete"}
onPress={this.onDeletePressed} />}
{!completed && fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes &&
<Button color="red" title="Stop Download" onPress={this.onStopDownloadPressed} />
<Button style={filePageStyle.actionButton}
theme={"light"}
text={"Stop Download"}
onPress={this.onStopDownloadPressed} />
}
</View>}
<ScrollView style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer}>
@ -336,6 +353,9 @@ class FilePage extends React.PureComponent {
const channelUri = normalizeURI(channelName);
navigation.navigate({ routeName: 'File', key: channelUri, params: { uri: channelUri }});
}} />}
{description && description.length > 0 && <View style={filePageStyle.divider} />}
{description && <Text style={filePageStyle.description} selectable={true}>{this.linkify(description)}</Text>}
</ScrollView>
</View>

View file

@ -1,6 +1,28 @@
import { connect } from 'react-redux';
import { doNotify } from 'lbry-redux';
import {
doGenerateAuthToken,
doUserEmailNew,
selectAuthToken,
selectEmailNewErrorMessage,
selectEmailNewIsPending,
selectEmailToVerify,
selectIsAuthenticating
} from 'lbryinc';
import FirstRun from './view';
const perform = dispatch => ({});
const select = (state) => ({
authenticating: selectIsAuthenticating(state),
authToken: selectAuthToken(state),
emailToVerify: selectEmailToVerify(state),
emailNewErrorMessage: selectEmailNewErrorMessage(state),
emailNewPending: selectEmailNewIsPending(state),
});
export default connect(null, perform)(FirstRun);
const perform = dispatch => ({
addUserEmail: email => dispatch(doUserEmailNew(email)),
generateAuthToken: installationId => dispatch(doGenerateAuthToken(installationId)),
notify: data => dispatch(doNotify(data))
});
export default connect(select, perform)(FirstRun);

View file

@ -0,0 +1,113 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
ActivityIndicator,
AsyncStorage,
Linking,
Text,
TextInput,
View
} from 'react-native';
import Colors from '../../../styles/colors';
import Constants from '../../../constants';
import firstRunStyle from '../../../styles/firstRun';
class EmailCollectPage extends React.PureComponent {
static MAX_STATUS_TRIES = 15;
state = {
email: null,
authenticationStarted: false,
authenticationFailed: false,
statusTries: 0
};
componentWillReceiveProps(nextProps) {
const { authenticating, authToken } = this.props;
if (this.state.authenticationStarted && !authenticating && authToken === null) {
this.setState({ authenticationFailed: true, authenticationStarted: false });
}
}
componentDidMount() {
// call user/new
const { generateAuthToken, authenticating, authToken } = this.props;
if (!authToken && !authenticating) {
this.startAuthenticating();
}
AsyncStorage.getItem('firstRunEmail').then(email => {
if (email) {
this.setState({ email });
}
});
}
startAuthenticating = () => {
const { generateAuthToken } = this.props;
this.setState({ authenticationStarted: true, authenticationFailed: false });
Lbry.status().then(info => {
generateAuthToken(info.installation_id)
}).catch(error => {
if (this.state.statusTries >= EmailCollectPage.MAX_STATUS_TRIES) {
this.setState({ authenticationFailed: true });
} else {
setTimeout(() => {
this.startAuthenticating();
this.setState({ statusTries: this.state.statusTries + 1 });
}, 1000); // Retry every second for a maximum of MAX_STATUS_TRIES tries (15 seconds)
}
});
}
handleChangeText = (text) => {
// save the value to the state email
this.setState({ email: text });
AsyncStorage.setItem(Constants.KEY_FIRST_RUN_EMAIL, text);
}
render() {
const { authenticating, authToken, onEmailViewLayout, emailToVerify } = this.props;
let content;
if (this.state.authenticationFailed) {
// Ask the user to try again
content = (
<View>
<Text style={firstRunStyle.paragraph}>The LBRY servers were unreachable at this time. Please check your Internet connection and then restart the app to try again.</Text>
</View>
);
} else if (!authToken || authenticating) {
content = (
<View>
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
<Text style={firstRunStyle.paragraph}>Please wait while we get some things ready...</Text>
</View>
);
} else {
content = (
<View onLayout={onEmailViewLayout}>
<Text style={firstRunStyle.title}>Rewards.</Text>
<Text style={firstRunStyle.paragraph}>You can earn LBRY Credits (LBC) rewards by completing various tasks in the app.</Text>
<Text style={firstRunStyle.paragraph}>Please provide a valid email address below to be able to claim your rewards.</Text>
<TextInput style={firstRunStyle.emailInput}
placeholder="you@example.com"
underlineColorAndroid="transparent"
value={this.state.email}
onChangeText={text => this.handleChangeText(text)}
/>
<Text style={firstRunStyle.infoParagraph}>This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards and may be used to sync usage data across devices.</Text>
</View>
);
}
return (
<View style={firstRunStyle.container}>
{content}
</View>
);
}
}
export default EmailCollectPage;

View file

@ -1,6 +1,8 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
ActivityIndicator,
AsyncStorage,
Linking,
NativeModules,
Text,
@ -9,20 +11,24 @@ import {
} from 'react-native';
import { NavigationActions } from 'react-navigation';
import Colors from '../../styles/colors';
import Constants from '../../constants';
import WelcomePage from './internal/welcome-page';
import EmailCollectPage from './internal/email-collect-page';
import firstRunStyle from '../../styles/firstRun';
class FirstRunScreen extends React.PureComponent {
static pages = ['welcome'];
static pages = [
'welcome',
'email-collect'
];
constructor() {
super();
this.state = {
currentPage: null,
launchUrl: null,
isFirstRun: false
}
}
state = {
currentPage: null,
emailSubmitted: false,
isFirstRun: false,
launchUrl: null,
showBottomContainer: true
};
componentDidMount() {
Linking.getInitialURL().then((url) => {
@ -47,6 +53,21 @@ class FirstRunScreen extends React.PureComponent {
}
}
componentWillReceiveProps(nextProps) {
const { emailNewErrorMessage, emailNewPending } = nextProps;
const { notify } = this.props;
if (this.state.emailSubmitted && !emailNewPending) {
this.setState({ emailSubmitted: false });
if (emailNewErrorMessage) {
notify ({ message: String(emailNewErrorMessage), displayType: ['toast']});
} else {
// Request successful. Navigate to discover.
this.closeFinalPage();
}
}
}
launchSplashScreen() {
const { navigation } = this.props;
const resetAction = NavigationActions.reset({
@ -60,36 +81,92 @@ class FirstRunScreen extends React.PureComponent {
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();
if (this.state.currentPage !== 'email-collect' &&
pageIndex === (FirstRunScreen.pages.length - 1)) {
this.closeFinalPage();
} else {
// TODO: Page transition animation?
this.state.currentPage = FirstRunScreen.pages[pageIndex + 1];
// TODO: Actions and page verification for specific pages
if (this.state.currentPage === 'email-collect') {
// handle email collect
this.handleEmailCollectPageContinue();
} else {
this.showNextPage();
}
}
}
handleEmailCollectPageContinue() {
const { notify, addUserEmail } = this.props;
// validate the email
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
if (!email || email.trim().length === 0 || email.indexOf('@') === -1) {
return notify({
message: 'Please provide a valid email address to continue.',
displayType: ['toast'],
});
}
addUserEmail(email);
this.setState({ emailSubmitted: true });
});
}
showNextPage() {
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
const nextPage = FirstRunScreen.pages[pageIndex + 1];
this.setState({ currentPage: nextPage });
if (nextPage === 'email-collect') {
// do not show the buttons (because we're waiting to get things ready)
this.setState({ showBottomContainer: false });
}
}
closeFinalPage() {
// 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();
}
render() {
const {
authenticating,
authToken,
generateAuthToken,
emailNewErrorMessage,
emailNewPending,
emailToVerify
} = this.props;
let page = null;
if (this.state.currentPage === 'welcome') {
// show welcome page
page = (<WelcomePage />);
} else if (this.state.currentPage === 'email-collect') {
page = (<EmailCollectPage authenticating={authenticating}
authToken={authToken}
generateAuthToken={generateAuthToken}
onEmailViewLayout={() => this.setState({ showBottomContainer: true })} />);
}
return (
<View style={firstRunStyle.screenContainer}>
{page}
{this.state.currentPage &&
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
<Text style={firstRunStyle.buttonText}>Continue</Text>
</TouchableOpacity>}
{this.state.currentPage && this.state.showBottomContainer &&
<View style={firstRunStyle.bottomContainer}>
{emailNewPending &&
<ActivityIndicator size="small" color={Colors.White} style={firstRunStyle.pageWaiting} />}
{!emailNewPending &&
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
<Text style={firstRunStyle.buttonText}>Continue</Text>
</TouchableOpacity>}
</View>}
</View>
)
);
}
}

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import { NativeModules, Text, View, ScrollView } from 'react-native';
import { ActivityIndicator, NativeModules, ScrollView, Text, View } from 'react-native';
import Colors from '../../styles/colors';
import Link from '../../component/link';
import PageHeader from '../../component/pageHeader';
@ -17,7 +17,7 @@ class RewardsPage extends React.PureComponent {
if (user && !user.is_reward_approved) {
if (!user.primary_email || !user.has_verified_email || !user.is_identity_verified) {
return (
<View style={rewardStyle.card}>
<View style={[rewardStyle.card, rewardStyle.verification]}>
<Text style={rewardStyle.title}>Humans Only</Text>
<Text style={rewardStyle.text}>Rewards are for human beings only. You'll have to prove you're one of us before you can claim any rewards.</Text>
</View>
@ -25,12 +25,11 @@ class RewardsPage extends React.PureComponent {
}
return (
<View>
<View style={[rewardStyle.card, rewardStyle.verification]}>
<Text style={rewardStyle.text}>This account must undergo review.</Text>
</View>
);
}
console.log(user);
return null;
}
@ -38,9 +37,6 @@ class RewardsPage extends React.PureComponent {
renderUnclaimedRewards() {
const { fetching, rewards, user, claimed } = this.props;
console.log(fetching);
console.log(rewards);
if (fetching) {
return (
<View style={rewardStyle.busyContainer}>
@ -68,19 +64,19 @@ class RewardsPage extends React.PureComponent {
const isNotEligible =
!user || !user.primary_email || !user.has_verified_email || !user.is_reward_approved;
return (
<View>
<ScrollView contentContainerStyle={rewardStyle.scrollContentContainer}>
{rewards.map(reward => <RewardCard key={reward.reward_type} reward={reward} />)}
</View>
</ScrollView>
);
}
render() {
return (
<View>
<ScrollView style={rewardStyle.scrollContainer}>
{this.renderVerification()}
<View style={rewardStyle.container}>
{this.renderVerification()}
<View style={rewardStyle.rewardsContainer}>
{this.renderUnclaimedRewards()}
</ScrollView>
</View>
</View>
);
}

View file

@ -1,6 +1,6 @@
import React from 'react';
import { SETTINGS } from 'lbry-redux';
import { Text, View, ScrollView, Switch } from 'react-native';
import { Text, View, ScrollView, Switch, NativeModules } from 'react-native';
import PageHeader from '../../component/pageHeader';
import settingsStyle from '../../styles/settings';
@ -8,7 +8,7 @@ class SettingsPage extends React.PureComponent {
static navigationOptions = {
title: 'Settings'
}
render() {
const {
backgroundPlayEnabled,
@ -16,10 +16,10 @@ class SettingsPage extends React.PureComponent {
showNsfw,
setClientSetting
} = this.props;
// If no true / false value set, default to true
const actualKeepDaemonRunning = (keepDaemonRunning === undefined || keepDaemonRunning === null) ? true : keepDaemonRunning;
// default to true if the setting is null or undefined
const actualKeepDaemonRunning = (keepDaemonRunning === null || keepDaemonRunning === undefined) ? true : keepDaemonRunning;
return (
<View>
<PageHeader title={"Settings"}
@ -34,7 +34,7 @@ class SettingsPage extends React.PureComponent {
<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>
@ -43,14 +43,19 @@ class SettingsPage extends React.PureComponent {
<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)} />
<Switch value={actualKeepDaemonRunning} onValueChange={(value) => {
setClientSetting(SETTINGS.KEEP_DAEMON_RUNNING, value);
if (NativeModules.DaemonServiceControl) {
NativeModules.DaemonServiceControl.setKeepDaemonRunning(value);
}
}} />
</View>
</View>
</ScrollView>

View file

@ -1,9 +1,18 @@
import { connect } from 'react-redux';
import { doBalanceSubscribe } from 'lbry-redux';
import { doBalanceSubscribe, doNotify } from 'lbry-redux';
import { doAuthenticate, doUserEmailVerify, doUserEmailVerifyFailure, selectUser } from 'lbryinc';
import SplashScreen from './view';
const perform = dispatch => ({
balanceSubscribe: () => dispatch(doBalanceSubscribe())
const select = state => ({
user: selectUser(state),
});
export default connect(null, perform)(SplashScreen);
const perform = dispatch => ({
authenticate: (appVersion, deviceId) => dispatch(doAuthenticate(appVersion, deviceId)),
balanceSubscribe: () => dispatch(doBalanceSubscribe()),
notify: data => dispatch(doNotify(data)),
verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)),
verifyUserEmailFailure: error => dispatch(doUserEmailVerifyFailure(error)),
});
export default connect(select, perform)(SplashScreen);

View file

@ -2,6 +2,7 @@ import React from 'react';
import { Lbry } from 'lbry-redux';
import {
ActivityIndicator,
AsyncStorage,
Linking,
NativeModules,
Platform,
@ -10,8 +11,10 @@ import {
View
} from 'react-native';
import { NavigationActions } from 'react-navigation';
import { decode as atob } from 'base-64';
import PropTypes from 'prop-types';
import Colors from '../../styles/colors';
import Constants from '../../constants';
import splashStyle from '../../styles/splash';
class SplashScreen extends React.PureComponent {
@ -21,14 +24,15 @@ class SplashScreen extends React.PureComponent {
componentWillMount() {
this.setState({
daemonReady: false,
details: 'Starting daemon',
message: 'Connecting',
isRunning: false,
isLagging: false,
launchUrl: null,
didDownloadHeaders: false,
isDownloadingHeaders: false,
headersDownloadProgress: 0
headersDownloadProgress: 0,
shouldAuthenticate: false
});
if (NativeModules.DaemonServiceControl) {
@ -53,14 +57,61 @@ class SplashScreen extends React.PureComponent {
});
}
componentWillUpdate(nextProps) {
const { navigation, verifyUserEmail, verifyUserEmailFailure } = this.props;
const { user } = nextProps;
if (this.state.daemonReady && this.state.shouldAuthenticate && user && user.id) {
// user is authenticated, navigate to the main view
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) {
if (launchUrl.startsWith('lbry://?verify=')) {
let verification = {};
try {
verification = JSON.parse(atob(launchUrl.substring(15)));
} catch (error) {
console.log(error);
}
if (verification.token && verification.recaptcha) {
AsyncStorage.setItem(Constants.KEY_SHOULD_VERIFY_EMAIL, 'true');
try {
verifyUserEmail(verification.token, verification.recaptcha);
} catch (error) {
const message = 'Invalid Verification Token';
verifyUserEmailFailure(message);
notify({ message, displayType: ['toast'] });
}
} else {
notify({
message: 'Invalid Verification URI',
displayType: ['toast'],
});
}
} else {
navigation.navigate({ routeName: 'File', key: launchUrl, params: { uri: launchUrl } });
}
}
}
}
_updateStatusCallback(status) {
const startupStatus = status.startup_status;
if (startupStatus.code == 'started') {
// At the minimum, wallet should be started and blocks_behind equal to 0 before calling resolve
const hasStarted = startupStatus.wallet && status.wallet.blocks_behind <= 0;
if (hasStarted) {
// 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({
daemonReady: true,
message: 'Testing Network',
details: 'Waiting for name resolution',
isLagging: false,
@ -69,56 +120,61 @@ class SplashScreen extends React.PureComponent {
Lbry.resolve({ uri: 'lbry://one' }).then(() => {
// Leave the splash screen
const { balanceSubscribe, navigation } = this.props;
const {
authenticate,
balanceSubscribe,
navigation,
notify
} = this.props;
balanceSubscribe();
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'Main'})
]
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
if (NativeModules.UtilityModule) {
// authenticate with the device ID if the method is available
NativeModules.UtilityModule.getDeviceId().then(deviceId => {
authenticate(`android-${appVersion}`, deviceId);
});
} else {
authenticate(appVersion);
}
this.setState({ shouldAuthenticate: true });
});
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) {
const blockchainHeaders = status.blockchain_headers;
const walletStatus = status.wallet;
if (blockchainHeaders) {
this.setState({
isDownloadingHeaders: blockchainStatus.is_downloading_headers,
headersDownloadProgress: blockchainStatus.headers_download_progress
isDownloadingHeaders: blockchainHeaders.downloading_headers,
headersDownloadProgress: blockchainHeaders.download_progress
});
} else {
// set downloading flag to false if blockchain_headers isn't in the status response
this.setState({
isDownloadingHeaders: false,
});
}
if (blockchainStatus && (blockchainStatus.is_downloading_headers ||
(this.state.didDownloadHeaders && 'loading_wallet' === startupStatus.code))) {
if (!this.state.didDownloadHeaders) {
this.setState({ didDownloadHeaders: true });
}
if (blockchainHeaders && blockchainHeaders.downloading_headers) {
const downloadProgress = blockchainHeaders.download_progress ? blockchainHeaders.download_progress : 0;
this.setState({
message: 'Blockchain Sync',
details: `Catching up with the blockchain (${blockchainStatus.headers_download_progress}%)`,
isLagging: startupStatus.is_lagging
details: `Catching up with the blockchain (${downloadProgress}%)`,
});
} else if (blockchainStatus && blockchainStatus.blocks_behind > 0) {
const behind = blockchainStatus.blocks_behind;
} else if (walletStatus && walletStatus.blocks_behind > 0) {
const behind = walletStatus.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,
details: 'Initializing LBRY service...'
});
}
setTimeout(() => {

View file

@ -14,7 +14,6 @@ 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() {

View file

@ -9,3 +9,4 @@ export function doSetClientSetting(key, value) {
},
};
}

View file

@ -16,6 +16,7 @@ reducers[ACTIONS.CLIENT_SETTING_CHANGED] = (state, action) => {
});
};
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

View file

@ -2,9 +2,11 @@ import { StyleSheet } from 'react-native';
import Colors from './colors';
const aboutStyle = StyleSheet.create({
container: {
flex: 1
},
scrollContainer: {
paddingTop: 16,
paddingBottom: 16
flex: 1
},
row: {
marginBottom: 1,
@ -18,6 +20,7 @@ const aboutStyle = StyleSheet.create({
color: Colors.LbryGreen,
fontSize: 24,
fontFamily: 'Metropolis-SemiBold',
marginTop: 16,
marginLeft: 12,
marginRight: 12,
marginBottom: 8
@ -33,7 +36,7 @@ const aboutStyle = StyleSheet.create({
links: {
marginLeft: 12,
marginRight: 12,
marginBottom: 12
marginBottom: 18
},
link: {
color: Colors.LbryGreen,
@ -44,6 +47,13 @@ const aboutStyle = StyleSheet.create({
col: {
alignSelf: 'stretch'
},
socialTitle: {
fontFamily: 'Metropolis-Regular',
marginLeft: 12,
marginRight: 12,
marginBottom: 8,
fontSize: 20
},
releaseInfoTitle: {
fontFamily: 'Metropolis-Regular',
marginLeft: 12,

View file

@ -1,24 +1,34 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const buttonStyle = StyleSheet.create({
button: {
borderRadius: 24,
padding: 8,
paddingLeft: 12,
paddingRight: 12
paddingRight: 12,
alignItems: 'center',
justifyContent: 'center'
},
disabled: {
backgroundColor: '#999999'
},
row: {
alignSelf: 'flex-start',
flexDirection: 'row',
flexDirection: 'row'
},
icon: {
color: '#ffffff',
iconLight: {
color: Colors.White,
},
iconDark: {
color: Colors.DarkGrey,
},
textLight: {
color: Colors.White,
},
textDark: {
color: Colors.DarkGrey
},
text: {
color: '#ffffff',
fontFamily: 'Metropolis-Regular',
fontSize: 14
},

View file

@ -1,9 +1,11 @@
const Colors = {
Black: '#000000',
ChannelGrey: '#9b9b9b',
DarkGrey: '#555555',
DescriptionGrey: '#999999',
LbryGreen: '#40b89a',
LightGrey: '#cccccc',
LighterGrey: '#e5e5e5',
Orange: '#ffbb00',
Red: '#ff0000',
VeryLightGrey: '#f1f1f1',

View file

@ -12,6 +12,13 @@ const filePageStyle = StyleSheet.create({
pageContainer: {
flex: 1
},
divider: {
backgroundColor: Colors.LighterGrey,
width: '100%',
height: 1,
marginTop: 4,
marginBottom: 20
},
innerPageContainer: {
flex: 1,
marginBottom: 60
@ -23,7 +30,7 @@ const filePageStyle = StyleSheet.create({
mediaContainer: {
alignItems: 'center',
width: screenWidth,
height: 220
height: 240
},
emptyClaimText: {
fontFamily: 'Metropolis-Regular',
@ -68,7 +75,7 @@ const filePageStyle = StyleSheet.create({
},
thumbnail: {
width: screenWidth,
height: 204,
height: 224,
justifyContent: 'center',
alignItems: 'center'
},
@ -85,7 +92,7 @@ const filePageStyle = StyleSheet.create({
},
containedPlayer: {
width: '100%',
height: 220,
height: 240,
},
containedPlayerLandscape: {
width: '100%',
@ -139,9 +146,9 @@ const filePageStyle = StyleSheet.create({
marginTop: -14,
width: '50%',
},
deleteButton: {
backgroundColor: Colors.Red,
width: 80
actionButton: {
backgroundColor: Colors.White,
width: 160
},
loading: {
position: 'absolute',

View file

@ -28,16 +28,56 @@ const firstRunStyle = StyleSheet.create({
marginBottom: 20,
color: Colors.White
},
button: {
infoParagraph: {
fontFamily: 'Metropolis-Regular',
fontSize: 14,
lineHeight: 20,
marginLeft: 32,
marginRight: 32,
marginBottom: 20,
color: Colors.White
},
emailInput: {
fontFamily: 'Metropolis-Regular',
fontSize: 24,
lineHeight: 24,
marginLeft: 32,
marginRight: 32,
marginBottom: 20,
textAlign: 'center'
},
leftButton: {
flex: 1,
alignSelf: 'flex-start',
marginLeft: 32,
marginRight: 32
},
bottomContainer: {
flex: 1
},
actionButton: {
backgroundColor: Colors.White,
alignSelf: 'center',
fontFamily: 'Metropolis-Regular',
fontSize: 12,
paddingLeft: 16,
paddingRight: 16
},
button: {
alignSelf: 'flex-end',
marginLeft: 32,
marginRight: 32
},
buttonText: {
fontFamily: 'Metropolis-Regular',
fontSize: 28,
fontSize: 24,
color: Colors.White
},
waiting: {
marginBottom: 24
},
pageWaiting: {
alignSelf: 'center'
}
});

View file

@ -18,7 +18,6 @@ const mediaPlayerStyle = StyleSheet.create({
progress: {
flex: 1,
flexDirection: 'row',
width: '100%',
height: 3
},
innerProgressCompleted: {
@ -101,8 +100,13 @@ const mediaPlayerStyle = StyleSheet.create({
position: 'absolute',
height: 36,
width: 48,
bottom: 0,
marginLeft: -16
marginLeft: -18
},
seekerHandleContained: {
bottom: -17
},
seekerHandleFs: {
bottom: 0
},
bigSeekerCircle: {
borderRadius: 24,

View file

@ -1,8 +1,8 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const walletStyle = StyleSheet.create({
scrollContainer: {
const rewardStyle = StyleSheet.create({
container: {
flex: 1
},
row: {
@ -16,9 +16,11 @@ const walletStyle = StyleSheet.create({
alignItems: 'center',
flexDirection: 'row'
},
infoText: {
fontFamily: 'Metropolis-Regular',
fontSize: 18
rewardsContainer: {
flex: 1
},
scrollContentContainer: {
paddingBottom: 16
},
card: {
backgroundColor: Colors.White,
@ -27,10 +29,21 @@ const walletStyle = StyleSheet.create({
marginRight: 16,
padding: 16
},
text: {
fontFamily: 'Metropolis-Regular',
fontSize: 16,
lineHeight: 24
},
infoText: {
fontFamily: 'Metropolis-Regular',
fontSize: 18,
marginLeft: 12
},
title: {
fontFamily: 'Metropolis-Bold',
fontFamily: 'Metropolis-SemiBold',
fontSize: 20,
marginBottom: 24
marginBottom: 8,
color: Colors.LbryGreen
},
bottomMarginSmall: {
marginBottom: 8
@ -40,7 +53,43 @@ const walletStyle = StyleSheet.create({
},
bottomMarginLarge: {
marginBottom: 24
},
leftCol: {
width: '10%'
},
midCol: {
width: '65%'
},
rightCol: {
width: '15%',
alignItems: 'center',
justifyContent: 'center'
},
rewardAmount: {
fontFamily: 'Metropolis-Regular',
fontSize: 24,
textAlign: 'center'
},
rewardCurrency: {
fontFamily: 'Metropolis-Regular'
},
rewardTitle: {
fontFamily: 'Metropolis-Regular',
fontSize: 16,
color: Colors.LbryGreen,
marginBottom: 4,
},
rewardDescription: {
fontFamily: 'Metropolis-Regular',
fontSize: 14,
lineHeight: 18
},
claimed: {
color: Colors.LbryGreen,
},
disabled: {
color: Colors.LightGrey
}
});
export default walletStyle;
export default rewardStyle;

View file

@ -10,7 +10,8 @@ const searchStyle = StyleSheet.create({
flex: 1,
width: '100%',
height: '100%',
padding: 16,
paddingLeft: 16,
paddingRight: 16,
marginBottom: 60
},
scrollPadding: {
@ -20,14 +21,14 @@ const searchStyle = StyleSheet.create({
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16
marginTop: 16
},
thumbnail: {
width: '100%',
height: 80
},
thumbnailContainer: {
width: '25%'
width: '25%'
},
detailsContainer: {
width: '70%'

View file

@ -20,7 +20,8 @@ const walletStyle = StyleSheet.create({
width: '85%'
},
button: {
backgroundColor: Colors.LbryGreen
backgroundColor: Colors.LbryGreen,
alignSelf: 'flex-start'
},
historyList: {
backgroundColor: '#ffffff'

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy
requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, git+https://github.com/lbryio/lbryschema.git@v0.0.15#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, netifaces, txupnp==0.0.1a10, git+https://github.com/lbryio/lbryschema.git@v0.0.16#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
@ -89,7 +89,7 @@ fullscreen = 0
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,READ_PHONE_STATE,WRITE_EXTERNAL_STORAGE
# (int) Android API to use
android.api = 26
android.api = 27
# (int) Minimum API required
android.minapi = 21
@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
android.gradle_dependencies = com.android.support:appcompat-v7:23.4.0, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+
android.gradle_dependencies = com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0
# (str) python-for-android branch to use, defaults to master
#p4a.branch = stable

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy
requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, git+https://github.com/lbryio/lbryschema.git@v0.0.15#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, netifaces, txupnp==0.0.1a10, git+https://github.com/lbryio/lbryschema.git@v0.0.16#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
@ -89,7 +89,7 @@ fullscreen = 0
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,READ_PHONE_STATE,WRITE_EXTERNAL_STORAGE
# (int) Android API to use
android.api = 26
android.api = 27
# (int) Minimum API required
android.minapi = 21
@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
android.gradle_dependencies = com.android.support:appcompat-v7:23.4.0, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+
android.gradle_dependencies = com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0
# (str) python-for-android branch to use, defaults to master
#p4a.branch = stable

276
buildozer.spec.vagrant Normal file
View file

@ -0,0 +1,276 @@
[app]
# (str) Title of your application
title = LBRY Browser
# (str) Package name
package.name = browser
# (str) Package domain (needed for android/ios packaging)
package.domain = io.lbry
# (str) Source code where the main.py live
source.dir = ./src/main/python
# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,kv,atlas
# (list) List of inclusions using pattern matching
#source.include_patterns = assets/*, res/*
# (list) Source files to exclude (let empty to not exclude anything)
#source.exclude_exts = spec
# (list) List of directory to exclude (let empty to not exclude anything)
#source.exclude_dirs = tests, bin
# (list) List of exclusions using pattern matching
#source.exclude_patterns = license,images/*/*.jpg
# (str) Application versioning (method 1)
#version = 0.1
# (str) Application versioning (method 2)
version.regex = __version__ = ['"](.*)['"]
version.filename = %(source.dir)s/main.py
# (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy
requirements = openssl, sqlite3, hostpython2, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, functools32, miniupnpc==1.9, gmpy==1.17, twisted==16.6.0, appdirs==1.4.3, argparse==1.2.1, docopt==0.6.2, base58==0.2.2, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse==0.2.0, jsonrpc==1.2, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2==1.3, pyyaml==3.12, qrcode==5.2.2, requests==2.9.1, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, slowaes==0.1a1, txJSON-RPC==0.5, wsgiref==0.1.2, zope.interface==4.3.3, protobuf==3.2.0, keyring==10.4.0, netifaces, txupnp==0.0.1a10, git+https://github.com/lbryio/lbryschema.git@v0.0.16#egg=lbryschema, git+https://github.com/lbryio/lbryum.git#egg=lbryum, git+https://github.com/lbryio/lbry.git#egg=lbrynet, asn1crypto, cryptography==2.2.2, pyopenssl==17.4.0, treq==17.8.0, funcsigs, mock, pbr, unqlite
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy
# (list) Garden requirements
#garden_requirements =
# (str) Presplash of the application
#presplash.filename = %(source.dir)s/data/presplash.png
# (str) Icon of the application
#icon.filename = %(source.dir)s/data/icon.png
# (str) Supported orientation (one of landscape, portrait or all)
orientation = portrait
# (list) List of service to declare
#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY
services = lbrynetservice:./lbrynetservice.py
#
# OSX Specific
#
#
# author = © Copyright Info
# change the major version of python used by the app
osx.python_version = 3
# Kivy version to use
osx.kivy_version = 1.9.1
#
# Android specific
#
# (bool) Indicate if the application should be fullscreen or not
fullscreen = 0
# (string) Presplash background color (for new android toolchain)
# Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray,
# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy,
# olive, purple, silver, teal.
#android.presplash_color = #FFFFFF
# (list) Permissions
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,READ_PHONE_STATE,WRITE_EXTERNAL_STORAGE
# (int) Android API to use
android.api = 27
# (int) Minimum API required
android.minapi = 21
# (int) Android SDK version to use
android.sdk = 23
# (str) Android NDK version to use
android.ndk = 13b
# (bool) Use --private data storage (True) or --dir public storage (False)
#android.private_storage = True
# (str) Android NDK directory (if empty, it will be automatically downloaded.)
#android.ndk_path = ~/Dev/SDKs/android-ndk-r13b
# (str) Android SDK directory (if empty, it will be automatically downloaded.)
#android.sdk_path = ~/Dev/SDKs/android
# (str) ANT directory (if empty, it will be automatically downloaded.)
#android.ant_path =
# (bool) If True, then skip trying to update the Android sdk
# This can be useful to avoid excess Internet downloads or save time
# when an update is due and you just want to test/build your package
#android.skip_update = False
# (str) Android entry point, default is ok for Kivy-based app
#android.entrypoint = org.renpy.android.PythonActivity
# (list) Pattern to whitelist for the whole project
#android.whitelist =
# (str) Path to a custom whitelist file
#android.whitelist_src =
# (str) Path to a custom blacklist file
#android.blacklist_src =
# (list) List of Java .jar files to add to the libs so that pyjnius can access
# their classes. Don't add jars that you do not need, since extra jars can slow
# down the build process. Allows wildcards matching, for example:
# OUYA-ODK/libs/*.jar
#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar
# (list) List of Java files to add to the android project (can be java or a
# directory containing the files)
android.add_src = ./src/main
# (str) Path to the React files and node_modules for building
android.react_src = ./app
# (list) Android AAR archives to add (currently works only with sdl2_gradle
# bootstrap)
#android.add_aars =
# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
android.gradle_dependencies = com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0
# (str) python-for-android branch to use, defaults to master
#p4a.branch = stable
# (str) OUYA Console category. Should be one of GAME or APP
# If you leave this blank, OUYA support will not be enabled
#android.ouya.category = GAME
# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png
# (str) XML file to include as an intent filters in <activity> tag
#android.manifest.intent_filters =
# (list) Android additionnal libraries to copy into libs/armeabi
#android.add_libs_armeabi = libs/android-v7/*
#android.add_libs_armeabi_v7a = libs/android-v7/*
#android.add_libs_x86 = libs/android-x86/*.so
#android.add_libs_mips = libs/android-mips/*.so
# (bool) Indicate whether the screen should stay on
# Don't forget to add the WAKE_LOCK permission if you set this to True
#android.wakelock = False
# (list) Android application meta-data to set (key=value format)
#android.meta_data =
# (list) Android library project to add (will be added in the
# project.properties automatically.)
#android.library_references =
# (str) Android logcat filters to use
#android.logcat_filters = *:S python:D
# (bool) Copy library instead of making a libpymodules.so
#android.copy_libs = 1
# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86
android.arch = armeabi-v7a
#
# Python for android (p4a) specific
#
# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
p4a.source_dir = ./p4a
# (str) The directory in which python-for-android should look for your own build recipes (if any)
p4a.local_recipes = ./recipes
# (str) Filename to the hook for p4a
#p4a.hook =
# (str) Bootstrap to use for android builds
p4a.bootstrap = lbry
#
# iOS specific
#
# (str) Path to a custom kivy-ios folder
#ios.kivy_ios_dir = ../kivy-ios
# (str) Name of the certificate to use for signing the debug version
# Get a list of available identities: buildozer ios list_identities
#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"
# (str) Name of the certificate to use for signing the release version
#ios.codesign.release = %(ios.codesign.debug)s
[buildozer]
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2
# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
warn_on_root = 1
# (str) Path to build artifact storage, absolute or relative to spec file
# build_dir = ./.buildozer
# (str) Path to build output (i.e. .apk, .ipa) storage
# bin_dir = ./bin
# -----------------------------------------------------------------------------
# List as sections
#
# You can define all the "list" as [section:key].
# Each line will be considered as a option to the list.
# Let's take [app] / source.exclude_patterns.
# Instead of doing:
#
#[app]
#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
#
# This can be translated into:
#
#[app:source.exclude_patterns]
#license
#data/audio/*.wav
#data/images/original/*
#
# -----------------------------------------------------------------------------
# Profiles
#
# You can extend section / key with a profile
# For example, you want to deploy a demo version of your application without
# HD content. You could first change the title to add "(demo)" in the name
# and extend the excluded directories to remove the HD content.
#
#[app@demo]
#title = My Application (demo)
#
#[app:source.exclude_patterns@demo]
#images/hd/*
#
# Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug
builddir = /home/vagrant

View file

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
@ -11,6 +11,9 @@ buildscript {
allprojects {
repositories {
jcenter()
maven {
url 'https://maven.google.com'
}
maven {
// All of React Native (JS, Android binaries) is installed from npm
url "$rootDir/react/node_modules/react-native/android"
@ -26,14 +29,14 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion {{ android_api }}
buildToolsVersion '{{ build_tools_version }}'
defaultConfig {
defaultConfig {
minSdkVersion {{ args.min_sdk_version }}
targetSdkVersion {{ android_api }}
versionCode {{ args.numeric_version }}
versionName '{{ args.version }}'
ndk {
abiFilters "armeabi-v7a", "x86"
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
@ -60,14 +63,25 @@ android {
sourceSets {
main {
jniLibs.srcDir 'libs'
jniLibs.srcDir 'libs'
}
}
}
subprojects {
afterEvaluate {project ->
if (project.hasProperty("android")) {
android {
compileSdkVersion {{ android_api }}
buildToolsVersion '{{ build_tools_version }}'
}
}
}
}
dependencies {
compile project(':react-native-video')
compile project(':react-native-fetch-blob')
{%- for aar in aars %}
compile(name: '{{ aar }}', ext: 'aar')
{%- endfor -%}

View file

@ -1,3 +1,5 @@
rootProject.name = 'browser'
include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-video/android')
project(':react-native-video').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-video/android-exoplayer')
include ':react-native-fetch-blob'
project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-fetch-blob/android')

View file

@ -7,7 +7,7 @@ import sh
class LibGMPRecipe(Recipe):
version = '6.1.2'
url = 'https://ftp.gnu.org/pub/gnu/gmp/gmp-{version}.tar.bz2'
url = 'http://www.mirrorservice.org/pub/gnu/gmp/gmp-{version}.tar.bz2'
#url = 'https://gmplib.org/download/gmp/gmp-{version}.tar.bz2'
def should_build(self, arch):

View file

@ -0,0 +1,25 @@
import glob
from pythonforandroid.toolchain import CompiledComponentsPythonRecipe, Recipe
from os.path import join
import sh
class NetifacesRecipe(CompiledComponentsPythonRecipe):
version = '0.10.7'
url = 'https://files.pythonhosted.org/packages/81/39/4e9a026265ba944ddf1fea176dbb29e0fe50c43717ba4fcf3646d099fe38/netifaces-{version}.tar.gz'
depends = ['python2', 'setuptools']
call_hostpython_via_targetpython = False
def get_recipe_env(self, arch):
env = super(NetifacesRecipe, self).get_recipe_env(arch)
target_python = Recipe.get_recipe('python2', self.ctx).get_build_dir(arch.arch)
env['PYTHON_ROOT'] = join(target_python, 'python-install')
env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions'
env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7'
env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + ' -lpython2.7'
return env
recipe = NetifacesRecipe()

View file

@ -1,59 +0,0 @@
from pythonforandroid.toolchain import (
CompiledComponentsPythonRecipe,
Recipe,
current_directory,
info,
shprint,
)
from os.path import join
import sh
class PyCryptoRecipe(CompiledComponentsPythonRecipe):
version = '2.6.1'
url = 'https://pypi.python.org/packages/source/p/pycrypto/pycrypto-{version}.tar.gz'
depends = ['libgmp', 'openssl', 'python2']
site_packages_name = 'Crypto'
call_hostpython_via_targetpython = False
patches = ['add_length.patch', 'fix-fastmath-include-dirs.patch']
def get_recipe_env(self, arch=None):
env = super(PyCryptoRecipe, self).get_recipe_env(arch)
openssl_build_dir = Recipe.get_recipe('openssl', self.ctx).get_build_dir(arch.arch)
target_python = Recipe.get_recipe('python2', self.ctx).get_build_dir(arch.arch)
# include libgmp build dir for gmp.h
libgmp_build_dir = Recipe.get_recipe('libgmp', self.ctx).get_build_dir(arch.arch)
# set to prevent including hostpython headers to avoid
# LONG_BIT definition appears wrong for platform error when compiling for Androidd
env['PYTHONXCPREFIX'] = target_python
env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions'
env['PYTHON_ROOT'] = join(target_python, 'python-install')
env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7'
env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + ' -lpython2.7'
env['CFLAGS'] += ' -I%s' % (join(libgmp_build_dir, 'include'))
env['CC'] = '%s -I%s' % (env['CC'], join(openssl_build_dir, 'include'))
env['LDFLAGS'] = env['LDFLAGS'] + ' -L{}'.format(
self.ctx.get_libs_dir(arch.arch) +
'-L{}'.format(self.ctx.libs_dir)) + ' -L{}'.format(
openssl_build_dir)
env['EXTRA_CFLAGS'] = '--host linux-armv'
env['ac_cv_func_malloc_0_nonnull'] = 'yes'
return env
def build_compiled_components(self, arch):
info('Configuring compiled components in {}'.format(self.name))
env = self.get_recipe_env(arch)
with current_directory(self.get_build_dir(arch.arch)):
configure = sh.Command('./configure')
shprint(configure, '--host=arm-eabi',
'--prefix={}'.format(self.ctx.get_python_install_dir()),
'--enable-shared', _env=env)
super(PyCryptoRecipe, self).build_compiled_components(arch)
recipe = PyCryptoRecipe()

View file

@ -1,11 +0,0 @@
--- pycrypto-2.6.1/src/hash_SHA2_template.c.orig 2013-10-14 14:38:10.000000000 -0700
+++ pycrypto-2.6.1/src/hash_SHA2_template.c 2014-05-19 10:15:51.000000000 -0700
@@ -87,7 +87,7 @@
* return 1 on success
* return 0 if the length overflows
*/
-int add_length(hash_state *hs, sha2_word_t inc) {
+static int add_length(hash_state *hs, sha2_word_t inc) {
sha2_word_t overflow_detector;
overflow_detector = hs->length_lower;
hs->length_lower += inc;

View file

@ -1,11 +0,0 @@
--- a/setup.py 2013-10-14 22:38:10.000000000 +0100
+++ b/setup.py 2017-12-20 16:05:16.726389781 +0100
@@ -370,7 +370,7 @@
'ext_modules': plat_ext + [
# _fastmath (uses GNU mp library)
Extension("Crypto.PublicKey._fastmath",
- include_dirs=['src/','/usr/include/'],
+ include_dirs=['src/'],
libraries=['gmp'],
sources=["src/_fastmath.c"]),

View file

@ -0,0 +1,11 @@
LOCAL_PATH := $(call my-dir)/..
include $(CLEAR_VARS)
LOCAL_SRC_FILES := sqlite3.c
LOCAL_MODULE := sqlite3
LOCAL_CFLAGS := -DSQLITE_ENABLE_FTS4
include $(BUILD_SHARED_LIBRARY)

View file

@ -0,0 +1,32 @@
from pythonforandroid.toolchain import NDKRecipe, shprint, shutil, current_directory
from os.path import join, exists
import sh
class Sqlite3Recipe(NDKRecipe):
version = '3.24.0'
# Don't forget to change the URL when changing the version
url = 'https://www.sqlite.org/2018/sqlite-amalgamation-3240000.zip'
generated_libraries = ['sqlite3']
def should_build(self, arch):
return not self.has_libs(arch, 'libsqlite3.so')
def prebuild_arch(self, arch):
super(Sqlite3Recipe, self).prebuild_arch(arch)
# Copy the Android make file
sh.mkdir('-p', join(self.get_build_dir(arch.arch), 'jni'))
shutil.copyfile(join(self.get_recipe_dir(), 'Android.mk'),
join(self.get_build_dir(arch.arch), 'jni/Android.mk'))
def build_arch(self, arch, *extra_args):
super(Sqlite3Recipe, self).build_arch(arch)
# Copy the shared library
shutil.copyfile(join(self.get_build_dir(arch.arch), 'libs', arch.arch, 'libsqlite3.so'),
join(self.ctx.get_libs_dir(arch.arch), 'libsqlite3.so'))
def get_recipe_env(self, arch):
env = super(Sqlite3Recipe, self).get_recipe_env(arch)
env['NDK_PROJECT_PATH'] = self.get_build_dir(arch.arch)
return env
recipe = Sqlite3Recipe()

View file

@ -12,7 +12,7 @@ import sh
class TwistedRecipe(CythonRecipe):
version = '16.0.0'
version = '16.6.0'
url = 'https://github.com/twisted/twisted/archive/twisted-{version}.tar.gz'
depends = ['setuptools', 'zope_interface']

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -2,12 +2,16 @@ package io.lbry.browser;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Binder;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.io.File;
@ -29,16 +33,48 @@ import org.renpy.android.ResourceManager;
* @version 0.1
*/
public class LbrynetService extends PythonService {
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DAEMON_NOTIFICATION_CHANNEL";
public static String TAG = "LbrynetService";
public static LbrynetService serviceInstance;
@Override
public boolean canDisplayNotification() {
return false;
return true;
}
@Override
protected void doStartForeground(Bundle extras) {
String serviceTitle = extras.getString("serviceTitle");
String serviceDescription = "The LBRY service is running in the background.";
Context context = getApplicationContext();
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID, "LBRY Browser", NotificationManager.IMPORTANCE_LOW);
channel.setDescription("LBRY service notification channel");
channel.setShowBadge(false);
notificationManager.createNotificationChannel(channel);
}
Intent contextIntent = new Intent(context, MainActivity.class);
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
Notification notification = builder.setContentTitle(serviceTitle)
.setContentText(serviceDescription)
.setContentIntent(pIntent)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_lbry)
.setOngoing(true)
.build();
startForeground(1, notification);
}
@Override
public int startType() {
return START_STICKY;
@ -66,7 +102,7 @@ public class LbrynetService extends PythonService {
super.onDestroy();
serviceInstance = null;
}
public String getAppRoot() {
String app_root = getApplicationContext().getFilesDir().getAbsolutePath() + "/app";
return app_root;

View file

@ -23,6 +23,7 @@ import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.ReactRootView;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.shell.MainReactPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import io.lbry.browser.reactpackages.LbryReactPackage;
import io.lbry.browser.reactmodules.DownloadManagerModule;
@ -51,6 +52,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
public static final String DEVICE_ID_KEY = "deviceId";
public static final String SETTING_KEEP_DAEMON_RUNNING = "keepDaemonRunning";
/**
* Flag which indicates whether or not the service is running. Will be updated in the
* onResume method.
@ -86,6 +89,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
.setJSMainModulePath("index")
.addPackage(new MainReactPackage())
.addPackage(new ReactVideoPackage())
.addPackage(new RNFetchBlobPackage())
.addPackage(new LbryReactPackage())
.setUseDeveloperSupport(true)
.setInitialLifecycleState(LifecycleState.RESUMED)
@ -168,19 +172,10 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
Toast.makeText(context, "Rewards cannot be claimed because we could not identify your device.", Toast.LENGTH_LONG).show();
}
try {
MessageDigest md = MessageDigest.getInstance("SHA-384");
md.update(id.getBytes("UTF-8"));
String hash = new BigInteger(1, md.digest()).toString(16);
SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString(DEVICE_ID_KEY, hash);
editor.commit();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) {
// SHA-384 not found, UTF-8 encoding not supported
Toast.makeText(context, "Rewards cannot be claimed because we could not identify your device.", Toast.LENGTH_LONG).show();
}
SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString(DEVICE_ID_KEY, id);
editor.commit();
return id;
}
@ -218,7 +213,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
protected void onDestroy() {
// check service running setting and end it here
SharedPreferences sp = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
boolean shouldKeepDaemonRunning = sp.getBoolean("keepDaemonRunning", true);
boolean shouldKeepDaemonRunning = sp.getBoolean(SETTING_KEEP_DAEMON_RUNNING, true);
if (!shouldKeepDaemonRunning) {
serviceRunning = isServiceRunning(LbrynetService.class);
if (serviceRunning) {

View file

@ -3,12 +3,14 @@ package io.lbry.browser.reactmodules;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.SharedPreferences;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import io.lbry.browser.LbrynetService;
import io.lbry.browser.MainActivity;
import io.lbry.browser.ServiceHelper;
public class DaemonServiceControlModule extends ReactContextBaseJavaModule {
@ -33,4 +35,14 @@ public class DaemonServiceControlModule extends ReactContextBaseJavaModule {
public void stopService() {
ServiceHelper.stop(context, LbrynetService.class);
}
@ReactMethod
public void setKeepDaemonRunning(boolean value) {
if (context != null) {
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putBoolean(MainActivity.SETTING_KEEP_DAEMON_RUNNING, value);
editor.commit();
}
}
}

View file

@ -1,10 +1,13 @@
package io.lbry.browser.reactmodules;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
@ -28,18 +31,22 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
private HashMap<String, Integer> downloadIdNotificationIdMap = new HashMap<String, Integer>();
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#");
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#");
private static final int MAX_PROGRESS = 100;
private static final String GROUP_DOWNLOADS = "io.lbry.browser.GROUP_DOWNLOADS";
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DOWNLOADS_NOTIFICATION_CHANNEL";
private static boolean channelCreated = false;
public static final String NOTIFICATION_ID_KEY = "io.lbry.browser.notificationId";
public static final int GROUP_ID = 0;
public static boolean groupCreated = false;
public DownloadManagerModule(ReactApplicationContext reactContext) {
super(reactContext);
this.context = reactContext;
@ -53,28 +60,40 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
public String getName() {
return "LbryDownloadManager";
}
private void createNotificationChannel() {
// Only applies to Android 8.0 Oreo (API Level 26) or higher
if (!channelCreated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID, "LBRY Downloads", NotificationManager.IMPORTANCE_LOW);
channel.setDescription("LBRY file downloads");
notificationManager.createNotificationChannel(channel);
}
}
private void createNotificationGroup() {
if (!groupCreated) {
Intent intent = new Intent(context, NotificationDeletedReceiver.class);
intent.putExtra(NOTIFICATION_ID_KEY, GROUP_ID);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, GROUP_ID, intent, 0);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder.setContentTitle("Active downloads")
.setContentText("Active downloads")
.setSmallIcon(R.drawable.ic_file_download_black_24dp)
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setContentTitle("Active LBRY downloads")
.setContentText("Active LBRY downloads")
.setSmallIcon(android.R.drawable.stat_sys_download)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setGroup(GROUP_DOWNLOADS)
.setGroupSummary(true)
.setDeleteIntent(pendingIntent);
notificationManager.notify(GROUP_ID, builder.build());
groupCreated = true;
}
}
private PendingIntent getLaunchPendingIntent(String uri) {
Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
@ -84,17 +103,18 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
@ReactMethod
public void startDownload(String id, String fileName) {
createNotificationChannel();
createNotificationGroup();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
// The file URI is used as the unique ID
builder.setContentIntent(getLaunchPendingIntent(id))
.setContentTitle(String.format("Downloading %s...", fileName))
.setGroup(GROUP_DOWNLOADS)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setProgress(MAX_PROGRESS, 0, false)
.setSmallIcon(R.drawable.ic_file_download_black_24dp);
.setSmallIcon(android.R.drawable.stat_sys_download);
int notificationId = generateNotificationId();
downloadIdNotificationIdMap.put(id, notificationId);
@ -114,6 +134,7 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
return;
}
createNotificationChannel();
createNotificationGroup();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = builders.get(notificationId);
@ -128,12 +149,12 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
.setContentText(String.format("%s", formatBytes(totalBytes)))
.setProgress(0, 0, false);
notificationManager.notify(notificationId, builder.build());
downloadIdNotificationIdMap.remove(id);
builders.remove(notificationId);
}
}
@ReactMethod
public void stopDownload(String id, String filename) {
if (!downloadIdNotificationIdMap.containsKey(id)) {
@ -144,14 +165,14 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
if (!builders.containsKey(notificationId)) {
return;
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = builders.get(notificationId);
notificationManager.cancel(notificationId);
downloadIdNotificationIdMap.remove(id);
builders.remove(notificationId);
if (builders.values().size() == 0) {
notificationManager.cancel(GROUP_ID);
groupCreated = false;

View file

@ -2,12 +2,17 @@ package io.lbry.browser.reactmodules;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.view.View;
import android.view.WindowManager;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import io.lbry.browser.MainActivity;
public class UtilityModule extends ReactContextBaseJavaModule {
private Context context;
@ -23,29 +28,58 @@ public class UtilityModule extends ReactContextBaseJavaModule {
@ReactMethod
public void keepAwakeOn() {
final Activity activity = getCurrentActivity();
final Activity activity = getCurrentActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
}
@ReactMethod
public void keepAwakeOff() {
final Activity activity = getCurrentActivity();
final Activity activity = getCurrentActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
}
@ReactMethod
public void hideNavigationBar() {
if (context != null && context instanceof Activity) {
Activity activity = (Activity) context;
View decorView = activity.getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
}
@ReactMethod
public void showNavigationBar() {
if (context != null && context instanceof Activity) {
Activity activity = (Activity) context;
View decorView = activity.getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_VISIBLE);
}
}
@ReactMethod
public void getDeviceId(final Promise promise) {
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
String deviceId = sp.getString(MainActivity.DEVICE_ID_KEY, null);
promise.resolve(deviceId);
}
}

View file

@ -81,8 +81,8 @@ from jsonrpc.proxy import JSONRPCProxy
from lbrynet import analytics
from lbrynet import conf
from lbrynet.core import utils, system_info
from lbrynet.daemon.auth.client import LBRYAPIClient
from lbrynet.daemon.DaemonServer import DaemonServer
from lbrynet.daemon.Components import PEER_PROTOCOL_SERVER_COMPONENT, REFLECTOR_COMPONENT
from lbrynet.daemon.Daemon import Daemon
# https certificate verification
# TODO: this is bad. Need to find a way to properly verify https requests
@ -124,33 +124,22 @@ def start():
lbrynet_log = conf.settings.get_log_filename()
log_support.configure_logging(lbrynet_log, True, [])
log.debug('Final Settings: %s', conf.settings.get_current_settings_dict())
# TODO: specify components, initialise auth
conf.settings.update({
'components_to_skip': [PEER_PROTOCOL_SERVER_COMPONENT, REFLECTOR_COMPONENT],
'concurrent_announcers': 0
})
log.info('Final Settings: %s', conf.settings.get_current_settings_dict())
log.info("Starting lbrynet-daemon")
if test_internet_connection():
analytics_manager = analytics.Manager.new_instance()
start_server_and_listen(False, analytics_manager)
daemon = Daemon()
daemon.start_listening()
reactor.run()
else:
log.info("Not connected to internet, unable to start")
@defer.inlineCallbacks
def start_server_and_listen(use_auth, analytics_manager, max_tries=5):
"""The primary entry point for launching the daemon.
Args:
use_auth: set to true to enable http authentication
analytics_manager: to send analytics
"""
analytics_manager.send_server_startup()
daemon_server = DaemonServer(analytics_manager)
try:
yield daemon_server.start(use_auth)
analytics_manager.send_server_startup_success()
except Exception as e:
log.exception('Failed to startup')
yield daemon_server.stop()
analytics_manager.send_server_startup_error(str(e))
reactor.fireSystemEvent("shutdown")
log.info("Not connected to the Internet. Unable to start.")
if __name__ == '__main__':

View file

@ -1,4 +1,4 @@
__version__ = "0.1.8"
__version__ = "0.1.10"
class ServiceApp(App):
def build(self):

Binary file not shown.

After

(image error) Size: 134 KiB

Binary file not shown.

After

(image error) Size: 134 KiB

Binary file not shown.

After

(image error) Size: 134 KiB

Binary file not shown.

After

(image error) Size: 134 KiB

Binary file not shown.

After

(image error) Size: 134 KiB