Publishing (#577)

* create gallery module for retrieving media from device
* gallery and ui flow for publishing
* publishing. add channel selector component.
* enable record and take photo buttons
* create thumbnails for camera media
* upload thumbnails. check publish success status.
* update to sdk 0.38.0. add tags / tag selection to publish.
This commit is contained in:
Akinwale Ariwodola 2019-07-09 01:43:30 +01:00 committed by GitHub
parent 60836ec5ec
commit 8459d10dc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 2252 additions and 22 deletions

View file

@ -25,7 +25,7 @@ build apk:
- cp -f $CI_PROJECT_DIR/scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h - cp -f $CI_PROJECT_DIR/scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
- rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz - rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz
- git secret reveal - git secret reveal
- mv buildozer.spec.travis buildozer.spec - mv buildozer.spec.ci buildozer.spec
- "./release.sh | grep -Fv -e 'working:' -e 'copy' -e 'Compiling' --line-buffered" - "./release.sh | grep -Fv -e 'working:' -e 'copy' -e 'Compiling' --line-buffered"
- cp $CI_PROJECT_DIR/bin/browser-$BUILD_VERSION-release.apk /dev/null - cp $CI_PROJECT_DIR/bin/browser-$BUILD_VERSION-release.apk /dev/null

44
app/package-lock.json generated
View file

@ -4881,6 +4881,11 @@
"resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
"integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg="
}, },
"gfycat-style-urls": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/gfycat-style-urls/-/gfycat-style-urls-1.0.3.tgz",
"integrity": "sha512-HirQ+dsQWChjnfwZXB07ytzh3eZQFVlOdO2ML1YvpHBOXplumtzGIAejVu91wmj4Cw7t4760M2fKtgI+NAi/2w=="
},
"glob": { "glob": {
"version": "7.1.4", "version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
@ -5553,8 +5558,8 @@
} }
}, },
"lbry-redux": { "lbry-redux": {
"version": "github:lbryio/lbry-redux#03998a2acf1a9e6c1b0818821612d137b31ebea3", "version": "github:lbryio/lbry-redux#9a676ee311d573b84d11f402d918aeee77be76e1",
"from": "github:lbryio/lbry-redux#03998a2acf1a9e6c1b0818821612d137b31ebea3", "from": "github:lbryio/lbry-redux",
"requires": { "requires": {
"proxy-polyfill": "0.1.6", "proxy-polyfill": "0.1.6",
"reselect": "^3.0.0", "reselect": "^3.0.0",
@ -7617,6 +7622,14 @@
} }
} }
}, },
"react-native-camera": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/react-native-camera/-/react-native-camera-2.11.0.tgz",
"integrity": "sha512-ay8te4nvL5mGzRjb2QMTOyJX+JfaIW/9oFjFVIkXOB9DzFipfeVTPMdwNx9GMpdmQ0muSXkuF16pa7K/1QLHlQ==",
"requires": {
"prop-types": "^15.6.2"
}
},
"react-native-country-picker-modal": { "react-native-country-picker-modal": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-native-country-picker-modal/-/react-native-country-picker-modal-0.6.2.tgz", "resolved": "https://registry.npmjs.org/react-native-country-picker-modal/-/react-native-country-picker-modal-0.6.2.tgz",
@ -7666,6 +7679,11 @@
} }
} }
}, },
"react-native-document-picker": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-2.3.0.tgz",
"integrity": "sha512-bHMyAOzFl+II0ZdfzobKsZKvTErmXfmQGalpxpGbeN8+/uhfhUcdp4WuIMecZhFyX6rbj3h3XXLdA12hVlGgmw=="
},
"react-native-exception-handler": { "react-native-exception-handler": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-native-exception-handler/-/react-native-exception-handler-2.9.0.tgz", "resolved": "https://registry.npmjs.org/react-native-exception-handler/-/react-native-exception-handler-2.9.0.tgz",
@ -7676,6 +7694,15 @@
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-5.4.2.tgz", "resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-5.4.2.tgz",
"integrity": "sha512-S4E96Lwmx6z6QD3MaAuP7cNcXRLfgEUYU2GB694TbGEoOjk/FO1OnfbxfFp0vUs/klr4HJwACcwihPPxrFTt8w==" "integrity": "sha512-S4E96Lwmx6z6QD3MaAuP7cNcXRLfgEUYU2GB694TbGEoOjk/FO1OnfbxfFp0vUs/klr4HJwACcwihPPxrFTt8w=="
}, },
"react-native-fs": {
"version": "2.13.3",
"resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.13.3.tgz",
"integrity": "sha512-B62LSSAEYQGItg7KVTzTVVCxezOYFBYp4DMVFbdoZUd1mZVFdqR2sy1HY1mye1VI/Lf3IbxSyZEQ0GmrrdwLjg==",
"requires": {
"base-64": "^0.1.0",
"utf8": "^2.1.1"
}
},
"react-native-gesture-handler": { "react-native-gesture-handler": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.2.1.tgz", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.2.1.tgz",
@ -7738,6 +7765,14 @@
"prop-types": "^15.6.2" "prop-types": "^15.6.2"
} }
}, },
"react-native-super-grid": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-native-super-grid/-/react-native-super-grid-3.0.4.tgz",
"integrity": "sha512-aoK71FGP5sFcLujuODYkAqyFDAZZRpvTeEwwaoXsc0JENhExEG7rGg65T5ELqyykiDOLBihuCLKasK5gLb0WtQ==",
"requires": {
"prop-types": "^15.6.0"
}
},
"react-native-tab-view": { "react-native-tab-view": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.4.1.tgz", "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.4.1.tgz",
@ -9534,6 +9569,11 @@
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
}, },
"utf8": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz",
"integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY="
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View file

@ -10,7 +10,8 @@
"dependencies": { "dependencies": {
"base-64": "^0.1.0", "base-64": "^0.1.0",
"@expo/vector-icons": "^8.1.0", "@expo/vector-icons": "^8.1.0",
"lbry-redux": "lbryio/lbry-redux#03998a2acf1a9e6c1b0818821612d137b31ebea3", "gfycat-style-urls": "^1.0.3",
"lbry-redux": "lbryio/lbry-redux",
"lbryinc": "lbryio/lbryinc", "lbryinc": "lbryio/lbryinc",
"lodash": ">=4.17.11", "lodash": ">=4.17.11",
"merge": ">=1.2.1", "merge": ">=1.2.1",
@ -18,13 +19,17 @@
"react": "16.8.6", "react": "16.8.6",
"react-native": "0.59.3", "react-native": "0.59.3",
"@react-native-community/async-storage": "^1.2.2", "@react-native-community/async-storage": "^1.2.2",
"react-native-camera": "^2.11.0",
"react-native-country-picker-modal": "^0.6.2", "react-native-country-picker-modal": "^0.6.2",
"react-native-document-picker": "^2.3.0",
"react-native-exception-handler": "2.9.0", "react-native-exception-handler": "2.9.0",
"react-native-fast-image": "^5.0.3", "react-native-fast-image": "^5.0.3",
"react-native-fs": "^2.13.3",
"react-native-gesture-handler": "^1.1.0", "react-native-gesture-handler": "^1.1.0",
"react-native-image-zoom-viewer": "^2.2.5", "react-native-image-zoom-viewer": "^2.2.5",
"react-native-password-strength-meter": "^0.0.2", "react-native-password-strength-meter": "^0.0.2",
"react-native-phone-input": "lbryio/react-native-phone-input", "react-native-phone-input": "lbryio/react-native-phone-input",
"react-native-super-grid": "^3.0.4",
"react-native-vector-icons": "^6.4.2", "react-native-vector-icons": "^6.4.2",
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android", "react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
"react-navigation": "^3.11.0", "react-navigation": "^3.11.0",

View file

@ -5,6 +5,7 @@ import DownloadsPage from 'page/downloads';
import DrawerContent from 'component/drawerContent'; import DrawerContent from 'component/drawerContent';
import FilePage from 'page/file'; import FilePage from 'page/file';
import FirstRunScreen from 'page/firstRun'; import FirstRunScreen from 'page/firstRun';
import PublishPage from 'page/publish';
import RewardsPage from 'page/rewards'; import RewardsPage from 'page/rewards';
import TrendingPage from 'page/trending'; import TrendingPage from 'page/trending';
import SearchPage from 'page/search'; import SearchPage from 'page/search';
@ -148,6 +149,12 @@ const drawer = createDrawerNavigator(
drawerIcon: ({ tintColor }) => <Icon name="wallet" size={20} style={{ color: tintColor }} />, drawerIcon: ({ tintColor }) => <Icon name="wallet" size={20} style={{ color: tintColor }} />,
}, },
}, },
Publish: {
screen: PublishPage,
navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="upload" size={20} style={{ color: tintColor }} />,
},
},
Rewards: { Rewards: {
screen: RewardsPage, screen: RewardsPage,
navigationOptions: { navigationOptions: {

View file

@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import {
selectBalance,
selectMyChannelClaims,
selectFetchingMyChannels,
doFetchChannelListMine,
doCreateChannel,
doToast,
} from 'lbry-redux';
import ChannelSelector from './view';
const select = state => ({
channels: selectMyChannelClaims(state),
fetchingChannels: selectFetchingMyChannels(state),
balance: selectBalance(state),
});
const perform = dispatch => ({
notify: data => dispatch(doToast(data)),
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
});
export default connect(
select,
perform
)(ChannelSelector);

View file

@ -0,0 +1,241 @@
import React from 'react';
import { CLAIM_VALUES, isNameValid } from 'lbry-redux';
import { ActivityIndicator, Picker, Text, TextInput, TouchableOpacity, View } from 'react-native';
import Button from 'component/button';
import Colors from 'styles/colors';
import Constants from 'constants';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
import channelSelectorStyle from 'styles/channelSelector';
export default class ChannelSelector extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
currentSelectedValue: Constants.ITEM_ANONYMOUS,
newChannelName: '',
newChannelBid: 0.1,
addingChannel: false,
creatingChannel: false,
newChannelNameError: '',
newChannelBidError: '',
createChannelError: undefined,
showCreateChannel: false,
};
}
componentDidMount() {
const { channels, fetchChannelListMine, fetchingChannels } = this.props;
if (!channels.length && !fetchingChannels) {
fetchChannelListMine();
}
}
handleCreateCancel = () => {
this.setState({ showCreateChannel: false, newChannelName: '', newChannelBid: 0.1 });
};
handlePickerValueChange = (itemValue, itemIndex) => {
if (Constants.ITEM_CREATE_A_CHANNEL === itemValue) {
this.setState({ showCreateChannel: true });
} else {
this.handleCreateCancel();
this.handleChannelChange(Constants.ITEM_ANONYMOUS === itemValue ? CLAIM_VALUES.CHANNEL_ANONYMOUS : itemValue);
}
this.setState({ currentSelectedValue: itemValue });
};
handleChannelChange = value => {
const { onChannelChange } = this.props;
const { newChannelBid } = this.state;
const channel = value;
if (channel === CLAIM_VALUES.CHANNEL_NEW) {
this.setState({ addingChannel: true });
if (onChannelChange) {
onChannelChange(channel);
}
this.handleNewChannelBidChange(newChannelBid);
} else {
this.setState({ addingChannel: false });
if (onChannelChange) {
onChannelChange(channel);
}
}
};
handleNewChannelNameChange = value => {
const { notify } = this.props;
let newChannelName = value;
if (newChannelName.startsWith('@')) {
newChannelName = newChannelName.slice(1);
}
this.setState({
newChannelName,
});
};
handleNewChannelBidChange = newChannelBid => {
const { balance, notify } = this.props;
let newChannelBidError;
if (newChannelBid <= 0) {
newChannelBidError = __('Please enter a deposit above 0');
} else if (newChannelBid === balance) {
newChannelBidError = __('Please decrease your deposit to account for transaction fees');
} else if (newChannelBid > balance) {
newChannelBidError = __('Deposit cannot be higher than your balance');
}
notify({ message: newChannelBidError });
this.setState({
newChannelBid,
newChannelBidError,
});
};
handleCreateChannelClick = () => {
const { balance, createChannel, onChannelChange, notify } = this.props;
const { newChannelBid, newChannelName } = this.state;
if (newChannelName.trim().length === 0 || !isNameValid(newChannelName.substr(1), false)) {
notify({ message: 'Your channel name contains invalid characters.' });
return;
}
if (this.channelExists(newChannelName)) {
notify({ message: 'You have already created a channel with the same name.' });
return;
}
if (newChannelBid > balance) {
notify({ message: 'Deposit cannot be higher than your balance' });
return;
}
const channelName = `@${newChannelName}`;
this.setState({
creatingChannel: true,
createChannelError: undefined,
});
const success = () => {
this.setState({
creatingChannel: false,
addingChannel: false,
currentSelectedValue: channelName,
showCreateChannel: false,
});
if (onChannelChange) {
onChannelChange(channelName);
}
};
const failure = () => {
notify({ message: 'Unable to create channel due to an internal error.' });
this.setState({
creatingChannel: false,
});
};
createChannel(channelName, newChannelBid).then(success, failure);
};
channelExists = name => {
const { channels = [] } = this.props;
for (let channel of channels) {
if (
name.toLowerCase() === channel.name.toLowerCase() ||
`@${name}`.toLowerCase() === channel.name.toLowerCase()
) {
return true;
}
}
return false;
};
render() {
const channel = this.state.addingChannel ? 'new' : this.props.channel;
const { fetchingChannels, channels = [] } = this.props;
const pickerItems = [{ name: Constants.ITEM_ANONYMOUS }, { name: Constants.ITEM_CREATE_A_CHANNEL }].concat(
channels
);
const {
newChannelName,
newChannelNameError,
newChannelBid,
newChannelBidError,
creatingChannel,
createChannelError,
addingChannel,
} = this.state;
return (
<View style={channelSelectorStyle.container}>
<Picker
selectedValue={this.state.currentSelectedValue}
style={channelSelectorStyle.channelPicker}
itemStyle={channelSelectorStyle.channelPickerItem}
onValueChange={this.handlePickerValueChange}
>
{pickerItems.map(item => (
<Picker.Item label={item.name} value={item.name} key={item.name} />
))}
</Picker>
{this.state.showCreateChannel && (
<View style={channelSelectorStyle.createChannelContainer}>
<View style={channelSelectorStyle.channelInputContainer}>
<Text style={channelSelectorStyle.channelAt}>@</Text>
<TextInput
style={channelSelectorStyle.channelNameInput}
value={this.state.newChannelName}
onChangeText={this.handleNewChannelNameChange}
placeholder={'Channel name'}
underlineColorAndroid={Colors.NextLbryGreen}
/>
</View>
<View style={channelSelectorStyle.bidRow}>
<Text style={channelSelectorStyle.label}>Deposit</Text>
<TextInput
style={channelSelectorStyle.bidAmountInput}
value={String(this.state.newChannelBid)}
onChangeText={this.handleNewChannelBidChange}
placeholder={'0.00'}
keyboardType={'number-pad'}
underlineColorAndroid={Colors.NextLbryGreen}
/>
<Text style={channelSelectorStyle.currency}>LBC</Text>
</View>
<Text style={channelSelectorStyle.helpText}>
This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.
</Text>
<View style={channelSelectorStyle.buttonContainer}>
{creatingChannel && <ActivityIndicator size={'small'} color={Colors.LbryGreen} />}
{!creatingChannel && (
<View style={channelSelectorStyle.buttons}>
<Link style={channelSelectorStyle.cancelLink} text="Cancel" onPress={this.handleCreateCancel} />
<Button
style={channelSelectorStyle.createButton}
disabled={!(this.state.newChannelName.trim().length > 0 && this.state.newChannelBid > 0)}
text="Create"
onPress={this.handleCreateChannelClick}
/>
</View>
)}
</View>
</View>
)}
</View>
);
}
}

View file

@ -62,7 +62,8 @@ class FileItem extends React.PureComponent {
const uri = normalizeURI(this.props.uri); const uri = normalizeURI(this.props.uri);
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
const channelName = claim ? claim.channel_name : null; const signingChannel = claim ? claim.signing_channel : null;
const channelName = signingChannel ? signingChannel.name : null;
const channelClaimId = const channelClaimId =
claim && claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId; claim && claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
const fullChannelUri = channelClaimId ? `${channelName}#${channelClaimId}` : channelName; const fullChannelUri = channelClaimId ? `${channelName}#${channelClaimId}` : channelName;

View file

@ -97,6 +97,7 @@ class FileItemMedia extends React.PureComponent {
{!isResolvingUri && ( {!isResolvingUri && (
<Text style={fileItemMediaStyle.autothumbText}> <Text style={fileItemMediaStyle.autothumbText}>
{title && {title &&
title.trim().length > 0 &&
title title
.replace(/\s+/g, '') .replace(/\s+/g, '')
.substring(0, Math.min(title.replace(' ', '').length, 5)) .substring(0, Math.min(title.replace(' ', '').length, 5))

View file

@ -63,7 +63,8 @@ class FileListItem extends React.PureComponent {
let name, channel, height, channelClaimId, fullChannelUri; let name, channel, height, channelClaimId, fullChannelUri;
if (claim) { if (claim) {
name = claim.name; name = claim.name;
channel = claim.channel_name; signingChannel = claim.signing_channel;
channel = signingChannel ? signingChannel.name : null;
height = claim.height; height = claim.height;
channelClaimId = claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId; channelClaimId = claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
fullChannelUri = channelClaimId ? `${channel}#${channelClaimId}` : channel; fullChannelUri = channelClaimId ? `${channel}#${channelClaimId}` : channel;

View file

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

View file

@ -0,0 +1,20 @@
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
import publishStyle from 'styles/publish';
class PublishRewadsDriver extends React.PureComponent<Props> {
render() {
const { navigation } = this.props;
return (
<TouchableOpacity style={publishStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
<Icon name="award" size={16} style={publishStyle.rewardIcon} />
<Text style={publishStyle.rewardDriverText}>Earn some credits to be able to publish your content.</Text>
</TouchableOpacity>
);
}
}
export default PublishRewadsDriver;

View file

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

View file

@ -0,0 +1,55 @@
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import tagStyle from 'styles/tag';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
export default class Tag extends React.PureComponent {
onPressDefault = () => {
const { name, navigation, type, onAddPress, onRemovePress } = this.props;
if ('add' === type) {
if (onAddPress) {
onAddPress(name);
}
return;
}
if ('remove' === type) {
if (onRemovePress) {
onRemovePress(name);
}
return;
}
if (navigation) {
// navigate to tag page
}
};
render() {
const { name, onPress, style, type } = this.props;
let styles = [];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
styles.push({
backgroundColor: Colors.TagGreen,
borderRadius: 8,
marginBottom: 4,
});
return (
<TouchableOpacity style={styles} onPress={onPress || this.onPressDefault}>
<View style={tagStyle.content}>
<Text style={tagStyle.text}>{name}</Text>
{type && <Icon style={tagStyle.icon} name={type === 'add' ? 'plus' : 'times'} size={8} />}
</View>
</TouchableOpacity>
);
}
}

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { selectUnfollowedTags } from 'lbry-redux';
import TagSearch from './view';
const select = state => ({
unfollowedTags: selectUnfollowedTags(state),
});
export default connect(
select,
null
)(TagSearch);

View file

@ -0,0 +1,81 @@
import React from 'react';
import { Text, TextInput, TouchableOpacity, View } from 'react-native';
import Tag from 'component/tag';
import tagStyle from 'styles/tag';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
export default class TagSearch extends React.PureComponent {
state = {
tag: null,
tagResults: [],
};
componentDidMount() {
const { selectedTags = [] } = this.props;
this.updateTagResults(this.state.tag, selectedTags);
}
componentWillReceiveProps(nextProps) {
const { selectedTags: prevSelectedTags = [] } = this.props;
const { selectedTags = [] } = nextProps;
if (selectedTags.length !== prevSelectedTags.length) {
this.updateTagResults(this.state.tag, selectedTags);
}
}
onAddTagPress = tag => {
const { handleAddTag } = this.props;
if (handleAddTag) {
handleAddTag(tag);
}
};
handleTagChange = tag => {
const { selectedTags = [] } = this.props;
this.setState({ tag });
this.updateTagResults(tag, selectedTags);
};
updateTagResults = (tag, selectedTags = []) => {
const { unfollowedTags } = this.props;
// the search term should always be the first result
let results = [];
const tagNotSelected = name => selectedTags.indexOf(name.toLowerCase()) === -1;
const suggestedTagsSet = new Set(unfollowedTags.map(tag => tag.name));
const suggestedTags = Array.from(suggestedTagsSet).filter(tagNotSelected);
if (tag && tag.trim().length > 0) {
results.push(tag.toLowerCase());
const doesTagMatch = name => name.toLowerCase().includes(tag.toLowerCase());
results = results.concat(suggestedTags.filter(doesTagMatch).slice(0, 5));
} else {
results = results.concat(suggestedTags.slice(0, 5));
}
this.setState({ tagResults: results });
};
render() {
const { name, style, type, selectedTags = [] } = this.props;
return (
<View>
<TextInput
style={tagStyle.searchInput}
placeholder={'Search for more tags'}
underlineColorAndroid={Colors.NextLbryGreen}
value={this.state.tag}
numberOfLines={1}
onChangeText={this.handleTagChange}
/>
<View style={tagStyle.tagResultsList}>
{this.state.tagResults.map(tag => (
<Tag key={tag} name={tag} style={tagStyle.tag} type="add" onAddPress={name => this.onAddTagPress(name)} />
))}
</View>
</View>
);
}
}

View file

@ -11,6 +11,10 @@ const Constants = {
PHASE_COLLECTION: 'collection', PHASE_COLLECTION: 'collection',
PHASE_VERIFICATION: 'verification', PHASE_VERIFICATION: 'verification',
PHASE_SELECTOR: 'selector',
PHASE_DETAILS: 'details',
PHASE_PUBLISH: 'publish',
CONTENT_TAB: 'content', CONTENT_TAB: 'content',
ABOUT_TAB: 'about', ABOUT_TAB: 'about',
@ -48,6 +52,7 @@ const Constants = {
DRAWER_ROUTE_TRENDING: 'Trending', DRAWER_ROUTE_TRENDING: 'Trending',
DRAWER_ROUTE_SUBSCRIPTIONS: 'Subscriptions', DRAWER_ROUTE_SUBSCRIPTIONS: 'Subscriptions',
DRAWER_ROUTE_MY_LBRY: 'Downloads', DRAWER_ROUTE_MY_LBRY: 'Downloads',
DRAWER_ROUTE_PUBLISH: 'Publish',
DRAWER_ROUTE_REWARDS: 'Rewards', DRAWER_ROUTE_REWARDS: 'Rewards',
DRAWER_ROUTE_WALLET: 'Wallet', DRAWER_ROUTE_WALLET: 'Wallet',
DRAWER_ROUTE_SETTINGS: 'Settings', DRAWER_ROUTE_SETTINGS: 'Settings',
@ -63,6 +68,9 @@ const Constants = {
ROUTE_FILE: 'File', ROUTE_FILE: 'File',
ITEM_CREATE_A_CHANNEL: 'Create a channel...',
ITEM_ANONYMOUS: 'Publish anonymously',
SUBSCRIPTIONS_VIEW_ALL: 'view_all', SUBSCRIPTIONS_VIEW_ALL: 'view_all',
SUBSCRIPTIONS_VIEW_LATEST_FIRST: 'view_latest_first', SUBSCRIPTIONS_VIEW_LATEST_FIRST: 'view_latest_first',

View file

@ -9,7 +9,9 @@ import {
fileReducer, fileReducer,
fileInfoReducer, fileInfoReducer,
notificationsReducer, notificationsReducer,
publishReducer,
searchReducer, searchReducer,
tagsReducer,
walletReducer, walletReducer,
} from 'lbry-redux'; } from 'lbry-redux';
import { import {
@ -90,11 +92,13 @@ const reducers = combineReducers({
homepage: homepageReducer, homepage: homepageReducer,
nav: navigatorReducer, nav: navigatorReducer,
notifications: notificationsReducer, notifications: notificationsReducer,
publish: publishReducer,
rewards: rewardsReducer, rewards: rewardsReducer,
settings: settingsReducer, settings: settingsReducer,
search: searchReducer, search: searchReducer,
subscriptions: subscriptionsReducer, subscriptions: subscriptionsReducer,
sync: syncReducer, sync: syncReducer,
tags: tagsReducer,
user: userReducer, user: userReducer,
wallet: walletReducer, wallet: walletReducer,
}); });

View file

@ -22,6 +22,7 @@ import { navigateBack, navigateToUri } from 'utils/helper';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
import ImageViewer from 'react-native-image-zoom-viewer'; import ImageViewer from 'react-native-image-zoom-viewer';
import Button from 'component/button'; import Button from 'component/button';
import Tag from 'component/tag';
import ChannelPage from 'page/channel'; import ChannelPage from 'page/channel';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import Constants from 'constants'; import Constants from 'constants';
@ -502,11 +503,7 @@ class FilePage extends React.PureComponent {
}; };
renderTags = tags => { renderTags = tags => {
return tags.map((tag, i) => ( return tags.map((tag, i) => <Tag style={filePageStyle.tagItem} key={`${tag}-${i}`} name={tag} />);
<Text style={filePageStyle.tagItem} key={`${tag}-${i}`}>
{tag}
</Text>
));
}; };
onFileDownloadButtonPlayed = () => { onFileDownloadButtonPlayed = () => {
@ -618,7 +615,8 @@ class FilePage extends React.PureComponent {
const description = metadata.description ? metadata.description : null; const description = metadata.description ? metadata.description : null;
const mediaType = Lbry.getMediaType(contentType); const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio'; const isPlayable = mediaType === 'video' || mediaType === 'audio';
const { height, channel_name: channelName, value } = claim; const { height, signing_channel: signingChannel, value } = claim;
const channelName = signingChannel && signingChannel.name;
const showActions = const showActions =
fileInfo && fileInfo &&
fileInfo.download_path && fileInfo.download_path &&

View file

@ -0,0 +1,31 @@
import { connect } from 'react-redux';
import {
doPublish,
doResolveUri,
doToast,
doUploadThumbnail,
selectBalance,
selectPublishFormValues,
} from 'lbry-redux';
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import Constants from 'constants';
import PublishPage from './view';
const select = state => ({
balance: selectBalance(state),
publishFormValues: selectPublishFormValues(state),
});
const perform = dispatch => ({
notify: data => dispatch(doToast(data)),
uploadThumbnail: (filePath, fsAdapter) => dispatch(doUploadThumbnail(filePath, null, fsAdapter)),
publish: params => dispatch(doPublish(params)),
resolveUri: uri => dispatch(doResolveUri(uri)),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_PUBLISH)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false)),
});
export default connect(
select,
perform
)(PublishPage);

View file

@ -0,0 +1,890 @@
import React from 'react';
import {
ActivityIndicator,
Clipboard,
Image,
NativeModules,
Picker,
ScrollView,
Switch,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';
import { FlatGrid } from 'react-native-super-grid';
import { isNameValid, buildURI, regexInvalidURI, CLAIM_VALUES, LICENSES, THUMBNAIL_STATUSES } from 'lbry-redux';
import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker';
import { RNCamera } from 'react-native-camera';
import { generateCombination } from 'gfycat-style-urls';
import RNFS from 'react-native-fs';
import Button from 'component/button';
import ChannelSelector from 'component/channelSelector';
import Colors from 'styles/colors';
import Constants from 'constants';
import FastImage from 'react-native-fast-image';
import FloatingWalletBalance from 'component/floatingWalletBalance';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Feather from 'react-native-vector-icons/Feather';
import Link from 'component/link';
import PublishRewardsDriver from 'component/publishRewardsDriver';
import Tag from 'component/tag';
import TagSearch from 'component/tagSearch';
import UriBar from 'component/uriBar';
import publishStyle from 'styles/publish';
const languages = {
en: 'English',
zh: 'Chinese',
fr: 'French',
de: 'German',
jp: 'Japanese',
ru: 'Russian',
es: 'Spanish',
id: 'Indonesian',
it: 'Italian',
nl: 'Dutch',
tr: 'Turkish',
pl: 'Polish',
ms: 'Malay',
pt: 'Portuguese',
vi: 'Vietnamese',
th: 'Thai',
ar: 'Arabic',
cs: 'Czech',
hr: 'Croatian',
km: 'Cambodian',
ko: 'Korean',
no: 'Norwegian',
ro: 'Romanian',
hi: 'Hindi',
el: 'Greek',
};
class PublishPage extends React.PureComponent {
camera = null;
state = {
canUseCamera: false,
titleFocused: false,
descriptionFocused: false,
// gallery videos
videos: null,
// camera
cameraType: RNCamera.Constants.Type.back,
videoRecordingMode: false,
recordingVideo: false,
showCameraOverlay: false,
// paths and media
uploadsPath: null,
thumbnailPath: null,
currentMedia: null,
currentThumbnailUri: null,
updatingThumbnailUri: false,
currentPhase: Constants.PHASE_SELECTOR,
// publish
advancedMode: false,
anonymous: true,
channelName: CLAIM_VALUES.CHANNEL_ANONYMOUS,
priceSet: false,
// input data
bid: 0.1,
description: null,
title: null,
language: 'en',
license: LICENSES.NONE,
mature: false,
name: null,
price: 0,
uri: null,
tags: [],
uploadedThumbnailUri: null,
// other
publishStarted: false,
};
didFocusListener;
componentWillMount() {
const { navigation } = this.props;
this.didFocusListener = navigation.addListener('didFocus', this.onComponentFocused);
}
componentWillUnmount() {
if (this.didFocusListener) {
this.didFocusListener.remove();
}
}
getNewUri(name, channel) {
const { resolveUri } = this.props;
// If they are midway through a channel creation, treat it as anonymous until it completes
const channelName =
channel === CLAIM_VALUES.CHANNEL_ANONYMOUS || channel === CLAIM_VALUES.CHANNEL_NEW ? '' : channel;
// We are only going to store the full uri, but we need to resolve the uri with and without the channel name
let uri;
try {
uri = buildURI({ contentName: name, channelName });
} catch (e) {
// something wrong with channel or name
}
if (uri) {
if (channelName) {
// resolve without the channel name so we know the winning bid for it
const uriLessChannel = buildURI({ contentName: name });
resolveUri(uriLessChannel);
}
resolveUri(uri);
return uri;
}
return '';
}
handleModePressed = () => {
this.setState({ advancedMode: !this.state.advancedMode });
};
handlePublishPressed = () => {
const { notify, publish } = this.props;
const {
bid,
channelName,
currentMedia,
description,
language,
license,
mature,
name,
price,
priceSet,
tags,
title,
uploadedThumbnailUri: thumbnail,
uri,
} = this.state;
if (!title || title.trim().length === 0) {
notify({ message: 'Please provide a title' });
return;
}
if (!name) {
notify({ message: 'Please specify an address where people can find your content.' });
return;
}
const publishTags = tags.slice();
if (mature) {
publishTags.push('nsfw');
}
const publishParams = {
filePath: currentMedia.filePath,
bid: bid || 0.1,
tags: publishTags,
title: title || '',
thumbnail: thumbnail,
description: description || '',
language,
nsfw: mature,
license,
licenseUrl: '',
otherLicenseDescription: '',
name: name || undefined,
contentIsFree: !priceSet,
fee: { currency: 'LBC', price },
uri: uri || undefined,
channel_name: CLAIM_VALUES.CHANNEL_ANONYMOUS === channelName ? undefined : channelName,
isStillEditing: false,
claim: null,
};
this.setState({ publishStarted: true }, () => publish(publishParams));
};
onComponentFocused = () => {
const { pushDrawerStack, setPlayerVisible } = this.props;
pushDrawerStack();
setPlayerVisible();
NativeModules.Gallery.canUseCamera().then(canUseCamera => this.setState({ canUseCamera }));
NativeModules.Gallery.getThumbnailPath().then(thumbnailPath => this.setState({ thumbnailPath }));
NativeModules.Gallery.getVideos().then(videos => this.setState({ videos }));
};
componentDidMount() {
this.onComponentFocused();
}
componentWillReceiveProps(nextProps) {
const { currentRoute, publishFormValues } = nextProps;
const { currentRoute: prevRoute } = this.props;
if (Constants.DRAWER_ROUTE_PUBLISH === currentRoute && currentRoute !== prevRoute) {
this.onComponentFocused();
}
if (publishFormValues) {
if (publishFormValues.thumbnail && !this.state.uploadedThumbnailUri) {
this.setState({ uploadedThumbnailUri: publishFormValues.thumbnail });
}
if (this.state.publishStarted) {
if (publishFormValues.publishSuccess) {
this.setState({ publishStarted: false, currentPhase: Constants.PHASE_PUBLISH });
} else if (publishFormValues.publishError) {
// TODO: Display error if any
}
if (!publishFormValues.publishing && this.state.publishStarted) {
this.setState({ publishStarted: false });
}
}
}
}
setCurrentMedia(media) {
this.setState(
{
currentMedia: media,
title: media.name,
name: this.formatNameForTitle(media.name),
currentPhase: Constants.PHASE_DETAILS,
},
() => this.handleNameChange(this.state.name)
);
}
formatNameForTitle = title => {
return title.replace(regexInvalidURI, '-').toLowerCase();
};
showSelector() {
this.setState({
publishStarted: false,
currentMedia: null,
currentThumbnailUri: null,
currentPhase: Constants.PHASE_SELECTOR,
// publish
advancedMode: false,
anonymous: true,
channelName: CLAIM_VALUES.CHANNEL_ANONYMOUS,
priceSet: false,
// input data
bid: 0.1,
description: null,
title: null,
language: 'en',
license: LICENSES.NONE,
name: null,
price: 0,
uri: null,
tags: [],
uploadedThumbnailUri: null,
});
}
handleRecordVideoPressed = () => {
if (!this.state.showCameraOverlay) {
this.setState({ canUseCamera: true, showCameraOverlay: true, videoRecordingMode: true });
}
};
handleTakePhotoPressed = () => {
if (!this.state.showCameraOverlay) {
this.setState({ canUseCamera: true, showCameraOverlay: true, videoRecordingMode: false });
}
};
handleCloseCameraPressed = () => {
this.setState({ showCameraOverlay: false, videoRecordingMode: false });
};
getFilePathFromUri = uri => {
return uri.substring('file://'.length);
};
handleCameraActionPressed = () => {
// check if it's video or photo mode
if (this.state.videoRecordingMode) {
if (this.state.recordingVideo) {
this.camera.stopRecording();
} else {
this.setState({ recordingVideo: true });
const options = { quality: RNCamera.Constants.VideoQuality['1080p'] };
this.camera.recordAsync(options).then(data => {
this.setState({ recordingVideo: false });
const currentMedia = {
id: -1,
filePath: this.getFilePathFromUri(data.uri),
name: generateCombination(2, ' ', true),
type: 'video/mp4', // always MP4
duration: 0,
};
this.setCurrentMedia(currentMedia);
this.setState({
currentThumbnailUri: null,
updatingThumbnailUri: false,
currentPhase: Constants.PHASE_DETAILS,
showCameraOverlay: false,
videoRecordingMode: false,
recordingVideo: false,
});
});
}
} else {
const options = { quality: 0.7 };
this.camera.takePictureAsync(options).then(data => {
const currentMedia = {
id: -1,
filePath: this.getFilePathFromUri(data.uri),
name: generateCombination(2, ' ', true),
type: 'image/jpg', // always JPEG
duration: 0,
};
this.setCurrentMedia(currentMedia);
this.setState({
currentPhase: Constants.PHASE_DETAILS,
currentThumbnailUri: null,
updatingThumbnailUri: false,
showCameraOverlay: false,
videoRecordingMode: false,
});
});
}
};
handleSwitchCameraPressed = () => {
const { cameraType } = this.state;
this.setState({
cameraType:
cameraType === RNCamera.Constants.Type.back ? RNCamera.Constants.Type.front : RNCamera.Constants.Type.back,
});
};
handleUploadPressed = () => {
DocumentPicker.show(
{
filetype: [DocumentPickerUtil.allFiles()],
},
(error, res) => {
if (!error) {
//console.log(res);
}
}
);
};
getRandomFileId = () => {
// generate a random id for a photo or recorded video between 1 and 20 (for creating thumbnails)
const id = Math.floor(Math.random() * (20 - 2)) + 1;
return '_' + id;
};
handlePublishAgainPressed = () => {
this.showSelector();
};
handleBidChange = bid => {
this.setState({ bid });
};
handlePriceChange = price => {
this.setState({ price });
};
handleNameChange = name => {
const { notify } = this.props;
this.setState({ name });
if (!isNameValid(name, false)) {
notify({ message: 'Your content address contains invalid characters' });
return;
}
const uri = this.getNewUri(name, this.state.channelName);
this.setState({ uri });
};
handleChannelChanged = channel => {
this.setState({ channelName: channel });
const uri = this.getNewUri(name, this.state.channelName);
this.setState({ uri });
};
handleAddTag = tag => {
if (!tag) {
return;
}
const { notify } = this.props;
const { tags } = this.state;
const index = tags.indexOf(tag.toLowerCase());
if (index === -1) {
const newTags = tags.slice();
newTags.push(tag);
this.setState({ tags: newTags });
} else {
notify({ message: `You already added the "${tag}" tag.` });
}
};
handleRemoveTag = tag => {
if (!tag) {
return;
}
const newTags = this.state.tags.slice();
const index = newTags.indexOf(tag.toLowerCase());
if (index > -1) {
newTags.splice(index, 1);
this.setState({ tags: newTags });
}
};
updateThumbnailUriForMedia = media => {
if (this.state.updatingThumbnailUri) {
return;
}
const { notify, uploadThumbnail } = this.props;
const { thumbnailPath } = this.state;
this.setState({ updatingThumbnailUri: true });
if (media.type) {
const mediaType = media.type.substring(0, 5);
const tempId = this.getRandomFileId();
if ('video' === mediaType && media.id > -1) {
const uri = `file://${thumbnailPath}/${media.id}.png`;
this.setState({ currentThumbnailUri: uri, updatingThumbnailUri: false });
// upload the thumbnail
if (!this.state.uploadedThumbnailUri) {
this.setState({ uploadThumbnailStarted: true }, () => uploadThumbnail(this.getFilePathFromUri(uri), RNFS));
}
} else if ('image' === mediaType || 'video' === mediaType) {
const create =
'image' === mediaType
? NativeModules.Gallery.createImageThumbnail
: NativeModules.Gallery.createVideoThumbnail;
create(tempId, media.filePath)
.then(path => {
this.setState({ currentThumbnailUri: `file://${path}`, updatingThumbnailUri: false });
if (!this.state.uploadedThumbnailUri) {
this.setState({ uploadThumbnailStarted: true }, () => uploadThumbnail(path, RNFS));
}
})
.catch(err => {
notify({ message: err });
this.setState({ updatingThumbnailUri: false });
});
}
}
};
handleTitleChange = title => {
this.setState(
{
title,
name: this.formatNameForTitle(title),
},
() => {
this.handleNameChange(this.state.name);
}
);
};
render() {
const { balance, navigation, notify, publishFormValues } = this.props;
const { thumbnailPath } = this.state;
let content;
if (Constants.PHASE_SELECTOR === this.state.currentPhase) {
content = (
<View style={publishStyle.gallerySelector}>
<View style={publishStyle.actionsView}>
{this.state.canUseCamera && (
<RNCamera style={publishStyle.cameraPreview} type={RNCamera.Constants.Type.back} />
)}
<View style={publishStyle.actionsSubView}>
<TouchableOpacity style={publishStyle.record} onPress={this.handleRecordVideoPressed}>
<Icon name="video" size={48} color={Colors.White} />
<Text style={publishStyle.actionText}>Record</Text>
</TouchableOpacity>
<View style={publishStyle.subActions}>
<TouchableOpacity style={publishStyle.photo} onPress={this.handleTakePhotoPressed}>
<Icon name="camera" size={48} color={Colors.White} />
<Text style={publishStyle.actionText}>Take a photo</Text>
</TouchableOpacity>
{false && (
<TouchableOpacity style={publishStyle.upload} onPress={this.handleUploadPressed}>
<Icon name="file-upload" size={48} color={Colors.White} />
<Text style={publishStyle.actionText}>Upload a file</Text>
</TouchableOpacity>
)}
</View>
</View>
</View>
{(!this.state.videos || !thumbnailPath) && (
<View style={publishStyle.loadingView}>
<ActivityIndicator size="large" color={Colors.LbryGreen} />
</View>
)}
{this.state.videos && thumbnailPath && (
<FlatGrid
style={publishStyle.galleryGrid}
itemDimension={134}
spacing={2}
items={this.state.videos}
renderItem={({ item, index }) => {
return (
<TouchableOpacity key={index} onPress={() => this.setCurrentMedia(item)}>
<FastImage
style={publishStyle.galleryGridImage}
resizeMode={FastImage.resizeMode.cover}
source={{ uri: `file://${thumbnailPath}/${item.id}.png` }}
/>
</TouchableOpacity>
);
}}
/>
)}
</View>
);
} else if (Constants.PHASE_DETAILS === this.state.currentPhase && this.state.currentMedia) {
const { currentMedia, currentThumbnailUri } = this.state;
if (!currentThumbnailUri) {
this.updateThumbnailUriForMedia(currentMedia);
}
content = (
<ScrollView style={publishStyle.publishDetails}>
{currentThumbnailUri && currentThumbnailUri.trim().length > 0 && (
<View style={publishStyle.mainThumbnailContainer}>
<FastImage
style={publishStyle.mainThumbnail}
resizeMode={FastImage.resizeMode.contain}
source={{ uri: currentThumbnailUri }}
/>
</View>
)}
{balance < 0.1 && <PublishRewardsDriver navigation={navigation} />}
{this.state.uploadThumbnailStarted && !this.state.uploadedThumbnailUri && (
<View style={publishStyle.thumbnailUploadContainer}>
<ActivityIndicator size={'small'} color={Colors.LbryGreen} />
<Text style={publishStyle.thumbnailUploadText}>Uploading thumbnail...</Text>
</View>
)}
<View style={publishStyle.card}>
<View style={publishStyle.textInputLayout}>
{(this.state.titleFocused || (this.state.title != null && this.state.title.trim().length > 0)) && (
<Text style={publishStyle.textInputTitle}>Title</Text>
)}
<TextInput
placeholder={this.state.titleFocused ? '' : 'Title'}
style={publishStyle.inputText}
value={this.state.title}
numberOfLines={1}
underlineColorAndroid={Colors.NextLbryGreen}
onChangeText={this.handleTitleChange}
onFocus={() => this.setState({ titleFocused: true })}
onBlur={() => this.setState({ titleFocused: false })}
/>
</View>
<View style={publishStyle.textInputLayout}>
{(this.state.descriptionFocused ||
(this.state.description != null && this.state.description.trim().length > 0)) && (
<Text style={publishStyle.textInputTitle}>Description</Text>
)}
<TextInput
placeholder={this.state.descriptionFocused ? '' : 'Description'}
style={publishStyle.inputText}
value={this.state.description}
underlineColorAndroid={Colors.NextLbryGreen}
onChangeText={this.handleDescriptionChange}
onFocus={() => this.setState({ descriptionFocused: true })}
onBlur={() => this.setState({ descriptionFocused: false })}
/>
</View>
</View>
<View style={publishStyle.card}>
<Text style={publishStyle.cardTitle}>Tags</Text>
<View style={publishStyle.tagList}>
{this.state.tags &&
this.state.tags.map(tag => (
<Tag
key={tag}
name={tag}
type={'remove'}
style={publishStyle.tag}
onRemovePress={this.handleRemoveTag}
/>
))}
</View>
<TagSearch handleAddTag={this.handleAddTag} selectedTags={this.state.tags} />
</View>
<View style={publishStyle.card}>
<Text style={publishStyle.cardTitle}>Channel</Text>
<ChannelSelector onChannelChange={this.handleChannelChange} />
</View>
<View style={publishStyle.card}>
<View style={publishStyle.titleRow}>
<Text style={publishStyle.cardTitle}>Price</Text>
<View style={publishStyle.switchTitleRow}>
<Switch value={this.state.priceSet} onValueChange={value => this.setState({ priceSet: value })} />
</View>
</View>
{!this.state.priceSet && (
<Text style={publishStyle.cardText}>Your content will be free. Press the toggle to set a price.</Text>
)}
{this.state.priceSet && (
<View style={[publishStyle.inputRow, publishStyle.priceInputRow]}>
<TextInput
placeholder={'0.00'}
keyboardType={'number-pad'}
style={publishStyle.priceInput}
underlineColorAndroid={Colors.NextLbryGreen}
numberOfLines={1}
value={String(this.state.price)}
onChangeText={this.handlePriceChange}
/>
<Text style={publishStyle.currency}>LBC</Text>
</View>
)}
</View>
{this.state.advancedMode && (
<View style={publishStyle.card}>
<Text style={publishStyle.cardTitle}>Content Address</Text>
<Text style={publishStyle.helpText}>
The address where people can find your content (ex. lbry://myvideo)
</Text>
<TextInput
placeholder={'lbry://'}
style={publishStyle.inputText}
underlineColorAndroid={Colors.NextLbryGreen}
numberOfLines={1}
value={this.state.name}
onChangeText={this.handleNameChange}
/>
<View style={publishStyle.inputRow}>
<TextInput
placeholder={'0.00'}
style={publishStyle.priceInput}
underlineColorAndroid={Colors.NextLbryGreen}
numberOfLines={1}
keyboardType={'numeric'}
value={String(this.state.bid)}
onChangeText={this.handleBidChange}
/>
<Text style={publishStyle.currency}>LBC</Text>
</View>
<Text style={publishStyle.helpText}>
This LBC remains yours and the deposit can be undone at any time.
</Text>
</View>
)}
{this.state.advancedMode && (
<View style={publishStyle.card}>
<Text style={publishStyle.cardTitle}>Additional Options</Text>
<View style={publishStyle.toggleField}>
<Switch value={this.state.mature} onValueChange={value => this.setState({ mature: value })} />
<Text style={publishStyle.toggleText}>Mature content</Text>
</View>
<View>
<Text style={publishStyle.cardText}>Language</Text>
<Picker
selectedValue={this.state.language}
style={publishStyle.picker}
itemStyle={publishStyle.pickerItem}
onValueChange={this.handleLanguageValueChange}
>
{Object.keys(languages).map(lang => (
<Picker.Item label={languages[lang]} value={lang} key={lang} />
))}
</Picker>
</View>
<View>
<Text style={publishStyle.cardText}>License</Text>
<Picker
selectedValue={this.state.license}
style={publishStyle.picker}
itemStyle={publishStyle.pickerItem}
onValueChange={this.handleLicenseValueChange}
>
<Picker.Item label={'None'} value={LICENSES.NONE} key={LICENSES.NONE} />
<Picker.Item label={'Public Domain'} value={LICENSES.PUBLIC_DOMAIN} key={LICENSES.PUBLIC_DOMAIN} />
{LICENSES.CC_LICENSES.map(({ value, url }) => (
<Picker.Item label={value} value={value} key={value} />
))}
<Picker.Item label={'Copyrighted...'} value={LICENSES.COPYRIGHT} key={LICENSES.COPYRIGHT} />
<Picker.Item label={'Other...'} value={LICENSES.OTHER} key={LICENSES.OTHER} />
</Picker>
</View>
</View>
)}
<View style={publishStyle.toggleContainer}>
<Link
text={this.state.advancedMode ? 'Hide extra fields' : 'Show extra fields'}
onPress={this.handleModePressed}
style={publishStyle.modeLink}
/>
</View>
<View style={publishStyle.actionButtons}>
{(this.state.publishStarted || publishFormValues.publishing) && (
<View style={publishStyle.progress}>
<ActivityIndicator size={'small'} color={Colors.LbryGreen} />
</View>
)}
{!publishFormValues.publishing && !this.state.publishStarted && (
<Link
style={publishStyle.cancelLink}
text="Cancel"
onPress={() => this.setState({ currentPhase: Constants.PHASE_SELECTOR })}
/>
)}
{!publishFormValues.publishing && !this.state.publishStarted && (
<View style={publishStyle.rightActionButtons}>
<Button
style={publishStyle.publishButton}
disabled={balance < 0.1 || !this.state.uploadedThumbnailUri}
text="Publish"
onPress={this.handlePublishPressed}
/>
</View>
)}
</View>
</ScrollView>
);
} else if (Constants.PHASE_PUBLISH === this.state.currentPhase) {
content = (
<ScrollView style={publishStyle.publishDetails}>
<View style={publishStyle.successContainer}>
<Text style={publishStyle.successTitle}>Success!</Text>
<Text style={publishStyle.successText}>Congratulations! Your content was successfully uploaded.</Text>
<View style={publishStyle.successRow}>
<Link style={publishStyle.successUrl} text={this.state.uri} href={this.state.uri} />
<TouchableOpacity
onPress={() => {
Clipboard.setString(this.state.uri);
notify({ message: 'Copied.' });
}}
>
<Icon name="clipboard" size={24} color={Colors.LbryGreen} />
</TouchableOpacity>
</View>
<Text style={publishStyle.successText}>
Your content will be live in a few minutes. In the mean time, feel free to publish more content or explore
the app.
</Text>
</View>
<View style={publishStyle.actionButtons}>
<Button style={publishStyle.publishButton} text="Publish again" onPress={this.handlePublishAgainPressed} />
</View>
</ScrollView>
);
}
return (
<View style={publishStyle.container}>
<UriBar navigation={navigation} />
{content}
{false && Constants.PHASE_SELECTOR !== this.state.currentPhase && (
<FloatingWalletBalance navigation={navigation} />
)}
{this.state.canUseCamera && this.state.showCameraOverlay && (
<View style={publishStyle.cameraOverlay}>
<RNCamera
style={publishStyle.fullCamera}
ref={ref => {
this.camera = ref;
}}
type={this.state.cameraType}
flashMode={RNCamera.Constants.FlashMode.off}
androidCameraPermissionOptions={{
title: 'Camera',
message: 'Please grant access to make use of your camera',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}}
androidRecordAudioPermissionOptions={{
title: 'Audio',
message: 'Please grant access to record audio',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}}
/>
<View
style={[
publishStyle.cameraControls,
this.state.videoRecordingMode ? publishStyle.transparentControls : publishStyle.opaqueControls,
]}
>
<View style={publishStyle.controlsRow}>
<TouchableOpacity onPress={this.handleCloseCameraPressed}>
<Icon name="arrow-left" size={28} color={Colors.White} />
</TouchableOpacity>
<View style={publishStyle.mainControlsRow}>
<TouchableOpacity style={publishStyle.switchCameraToggle} onPress={this.handleSwitchCameraPressed}>
<Feather name="rotate-cw" size={36} color={Colors.White} />
</TouchableOpacity>
<TouchableOpacity onPress={this.handleCameraActionPressed}>
<View style={publishStyle.cameraAction}>
<Feather style={publishStyle.cameraActionIcon} name="circle" size={72} color={Colors.White} />
{this.state.recordingVideo && (
<Icon
style={publishStyle.recordingIcon}
name="circle"
solid={true}
size={44}
color={Colors.Red}
/>
)}
</View>
</TouchableOpacity>
</View>
</View>
</View>
</View>
)}
</View>
);
}
}
export default PublishPage;

View file

@ -0,0 +1,70 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const channelSelectorStyle = StyleSheet.create({
container: {
flex: 1,
},
channelPicker: {
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
height: 52,
width: '100%',
},
bidRow: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
label: {
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
},
channelNameInput: {
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
paddingLeft: 20,
},
bidAmountInput: {
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
marginLeft: 16,
textAlign: 'right',
width: 80,
},
helpText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 12,
},
createChannelContainer: {
flex: 1,
marginLeft: 8,
marginRight: 8,
},
channelAt: {
position: 'absolute',
left: 4,
top: 13,
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
},
buttonContainer: {
flex: 1,
marginTop: 16,
justifyContent: 'flex-end',
},
buttons: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
},
cancelLink: {
marginRight: 16,
},
createButton: {
backgroundColor: Colors.NextLbryGreen,
},
});
export default channelSelectorStyle;

View file

@ -3,11 +3,13 @@ const Colors = {
Black: '#000000', Black: '#000000',
ChannelGrey: '#9b9b9b', ChannelGrey: '#9b9b9b',
DarkerGrey: '#222222',
DarkGrey: '#555555', DarkGrey: '#555555',
DescriptionGrey: '#999999', DescriptionGrey: '#999999',
LbryGreen: '#2f9176', LbryGreen: '#2f9176',
BrighterLbryGreen: '#40b887', BrighterLbryGreen: '#40b887',
NextLbryGreen: '#38d9a9', NextLbryGreen: '#38d9a9',
TagGreen: '#e3f6f1',
LightGrey: '#cccccc', LightGrey: '#cccccc',
LighterGrey: '#e5e5e5', LighterGrey: '#e5e5e5',
Orange: '#ffbb00', Orange: '#ffbb00',

View file

@ -322,14 +322,16 @@ const filePageStyle = StyleSheet.create({
tagTitle: { tagTitle: {
fontFamily: 'Inter-UI-SemiBold', fontFamily: 'Inter-UI-SemiBold',
flex: 0.2, flex: 0.2,
marginTop: 4,
}, },
tagList: { tagList: {
fontFamily: 'Inter-UI-Regular', fontFamily: 'Inter-UI-Regular',
flex: 0.8, flex: 0.8,
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap',
}, },
tagItem: { tagItem: {
marginRight: 16, marginRight: 4,
}, },
rewardDriverCard: { rewardDriverCard: {
alignItems: 'center', alignItems: 'center',

331
app/src/styles/publish.js Normal file
View file

@ -0,0 +1,331 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const publishStyle = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.PageBackground,
},
gallerySelector: {
flex: 1,
marginTop: 62,
paddingTop: 2,
backgroundColor: Colors.DarkGrey,
},
galleryGrid: {
flex: 1,
},
galleryGridImage: {
width: 134,
height: 90,
},
inputText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
},
card: {
backgroundColor: Colors.White,
marginTop: 16,
marginLeft: 16,
marginRight: 16,
padding: 16,
},
actionButtons: {
marginLeft: 16,
marginRight: 16,
marginBottom: 16,
marginTop: 24,
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
rightActionButtons: {
flexDirection: 'row',
alignItems: 'center',
},
modeLink: {
color: Colors.LbryGreen,
alignSelf: 'flex-end',
marginRight: 16,
},
publishButton: {
backgroundColor: Colors.LbryGreen,
alignSelf: 'flex-end',
},
cardTitle: {
fontFamily: 'Inter-UI-Regular',
fontSize: 20,
marginBottom: 8,
},
actionsView: {
width: '100%',
height: 240,
overflow: 'hidden',
},
record: {
backgroundColor: 'transparent',
flex: 0.5,
justifyContent: 'center',
alignItems: 'center',
},
subActions: {
flex: 0.5,
borderLeftWidth: 2,
borderLeftColor: Colors.DarkerGrey,
},
actionText: {
color: Colors.White,
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
marginTop: 8,
},
photo: {
backgroundColor: 'transparent',
height: 240,
justifyContent: 'center',
alignItems: 'center',
},
upload: {
backgroundColor: Colors.Black,
height: 120,
borderTopWidth: 2,
borderTopColor: Colors.DarkerGrey,
justifyContent: 'center',
alignItems: 'center',
},
publishDetails: {
marginTop: 60,
},
mainThumbnailContainer: {
backgroundColor: Colors.Black,
width: '100%',
height: 240,
},
mainThumbnail: {
height: 240,
},
inputRow: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
priceInput: {
width: 80,
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
},
currency: {
fontFamily: 'Inter-UI-Regular',
},
cardRow: {
flex: 1,
flexDirection: 'row',
alignItems: 'flex-start',
},
switchRow: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginLeft: 24,
},
switchTitleRow: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 24,
marginTop: -10,
},
switchText: {
marginRight: 4,
fontSize: 16,
},
loadingView: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
titleRow: {
flexDirection: 'row',
justifyContent: 'space-between',
},
cardText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
},
cameraPreview: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
height: 240,
},
actionsSubView: {
flex: 1,
flexDirection: 'row',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
successContainer: {
padding: 16,
},
successTitle: {
fontFamily: 'Inter-UI-Regular',
fontSize: 28,
marginBottom: 16,
},
successText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
marginBottom: 16,
lineHeight: 20,
},
successRow: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
successUrl: {
flex: 0.9,
fontSize: 32,
fontFamily: 'Inter-UI-Regular',
color: Colors.NextLbryGreen,
marginRight: 16,
},
cameraOverlay: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
backgroundColor: Colors.Black,
elevation: 24,
},
fullCamera: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
zIndex: 100,
},
cameraControls: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 120,
zIndex: 200,
alignItems: 'center',
},
controlsRow: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
mainControlsRow: {
flex: 0.8,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
switchCameraToggle: {
marginRight: 48,
},
cameraAction: {
width: 72,
height: 72,
alignItems: 'center',
justifyContent: 'center',
},
cameraActionIcon: {
position: 'absolute',
},
recordingIcon: {
position: 'absolute',
},
transparentControls: {
backgroundColor: '#00000022',
},
opaqueControls: {
backgroundColor: Colors.Black,
},
progress: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 16,
},
toggleContainer: {
marginTop: 24,
alignItems: 'center',
justifyContent: 'flex-end',
},
rewardDriverCard: {
alignItems: 'center',
backgroundColor: Colors.BrighterLbryGreen,
flexDirection: 'row',
paddingLeft: 16,
paddingRight: 16,
paddingTop: 12,
paddingBottom: 12,
},
rewardDriverText: {
fontFamily: 'Inter-UI-Regular',
color: Colors.White,
fontSize: 14,
},
rewardIcon: {
color: Colors.White,
marginRight: 8,
},
tag: {
marginRight: 4,
marginBottom: 4,
},
tagList: {
flexDirection: 'row',
flexWrap: 'wrap',
},
textInputLayout: {
marginBottom: 4,
},
textInputTitle: {
fontFamily: 'Inter-UI-Regular',
fontSize: 12,
marginBottom: -10,
marginLeft: 4,
},
thumbnailUploadContainer: {
marginTop: 16,
marginLeft: 16,
marginRight: 16,
paddingLeft: 2,
paddingRight: 2,
flexDirection: 'row',
alignItems: 'center',
},
thumbnailUploadText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
marginLeft: 8,
},
toggleField: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 4,
marginBottom: 16,
},
toggleText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
marginLeft: 8,
},
});
export default publishStyle;

35
app/src/styles/tag.js Normal file
View file

@ -0,0 +1,35 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const tagStyle = StyleSheet.create({
tag: {
marginRight: 4,
marginBottom: 4,
},
tagSearchInput: {
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
},
content: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingLeft: 8,
paddingTop: 4,
paddingBottom: 4,
},
icon: {
marginRight: 8,
},
text: {
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
marginRight: 8,
},
tagResultsList: {
flexDirection: 'row',
flexWrap: 'wrap',
},
});
export default tagStyle;

View file

@ -86,7 +86,7 @@ fullscreen = 0
#android.presplash_color = #FFFFFF #android.presplash_color = #FFFFFF
# (list) Permissions # (list) Permissions
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE android.permissions = CAMERA,INTERNET,READ_EXTERNAL_STORAGE,RECORD_AUDIO,WRITE_EXTERNAL_STORAGE
# (int) Android API to use # (int) Android API to use
android.api = 28 android.api = 28
@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # bootstrap)
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, 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, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1 android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, 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, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1, com.google.android.gms:play-services-base:16.1.0, com.android.support:exifinterface:27.1.1
# (str) python-for-android branch to use, defaults to master # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -86,7 +86,7 @@ fullscreen = 0
#android.presplash_color = #FFFFFF #android.presplash_color = #FFFFFF
# (list) Permissions # (list) Permissions
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE android.permissions = CAMERA,INTERNET,READ_EXTERNAL_STORAGE,RECORD_AUDIO,WRITE_EXTERNAL_STORAGE
# (int) Android API to use # (int) Android API to use
android.api = 28 android.api = 28
@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # bootstrap)
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, 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, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1 android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, 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, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1, com.google.android.gms:play-services-base:16.1.0, com.android.support:exifinterface:27.1.1
# (str) python-for-android branch to use, defaults to master # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -86,7 +86,7 @@ fullscreen = 0
#android.presplash_color = #FFFFFF #android.presplash_color = #FFFFFF
# (list) Permissions # (list) Permissions
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE android.permissions = CAMERA,INTERNET,READ_EXTERNAL_STORAGE,RECORD_AUDIO,WRITE_EXTERNAL_STORAGE
# (int) Android API to use # (int) Android API to use
android.api = 28 android.api = 28
@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # bootstrap)
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, 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, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1 android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, 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, com.squareup.picasso:picasso:2.71828, com.google.firebase:firebase-core:16.0.1, com.google.android.gms:play-services-base:16.1.0, com.android.support:exifinterface:27.1.1
# (str) python-for-android branch to use, defaults to master # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable
@ -272,3 +272,4 @@ warn_on_root = 1
# Then, invoke the command line with the "demo" profile: # Then, invoke the command line with the "demo" profile:
# #
#buildozer --profile demo android debug #buildozer --profile demo android debug
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/aioupnp.git@a404269d91cff5358bcffb8067b0fd1d9c6842d3#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir, "git+https://github.com/lbryio/lbry-sdk@v0.38.0#egg=lbry&subdirectory=lbry", "git+https://github.com/lbryio/lbry-sdk@v0.38.0#egg=torba&subdirectory=torba"

View file

@ -3,10 +3,11 @@ buildscript {
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url "https://jitpack.io" }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.0.0' classpath 'com.android.tools.build:gradle:3.0.0'
classpath 'com.google.gms:google-services:4.0.1' classpath 'com.google.gms:google-services:4.2.0'
} }
} }
@ -36,6 +37,8 @@ android {
targetSdkVersion {{ android_api }} targetSdkVersion {{ android_api }}
versionCode {{ args.numeric_version }} versionCode {{ args.numeric_version }}
versionName '{{ args.version }}' versionName '{{ args.version }}'
missingDimensionStrategy 'react-native-camera', 'general'
multiDexEnabled true
ndk { ndk {
abiFilters "armeabi-v7a", "x86" abiFilters "armeabi-v7a", "x86"
@ -85,6 +88,8 @@ ext {
minSdkVersion = {{ args.min_sdk_version }} minSdkVersion = {{ args.min_sdk_version }}
targetSdkVersion = {{ android_api }} targetSdkVersion = {{ android_api }}
supportLibVersion = '27.1.1' supportLibVersion = '27.1.1'
googlePlayServicesVersion = '16.1.0'
googlePlayServicesVisionVersion = '17.0.2'
} }
subprojects { subprojects {
@ -100,8 +105,11 @@ subprojects {
dependencies { dependencies {
compile project(':@react-native-community_async-storage') compile project(':@react-native-community_async-storage')
compile project(':react-native-camera')
compile project(':react-native-document-picker')
compile project(':react-native-exception-handler') compile project(':react-native-exception-handler')
compile project(':react-native-fast-image') compile project(':react-native-fast-image')
compile project(':react-native-fs')
compile project(':react-native-gesture-handler') compile project(':react-native-gesture-handler')
compile project(':react-native-video') compile project(':react-native-video')
compile project(':rn-fetch-blob') compile project(':rn-fetch-blob')

View file

@ -1,10 +1,16 @@
rootProject.name = 'browser' rootProject.name = 'browser'
include ':@react-native-community_async-storage' include ':@react-native-community_async-storage'
project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, './react/node_modules/@react-native-community/async-storage/android') project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, './react/node_modules/@react-native-community/async-storage/android')
include ':react-native-camera'
project(':react-native-camera').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-camera/android')
include ':react-native-document-picker'
project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-document-picker/android')
include ':react-native-exception-handler' include ':react-native-exception-handler'
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-exception-handler/android') project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-exception-handler/android')
include ':react-native-fast-image' include ':react-native-fast-image'
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-fast-image/android') project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-fast-image/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(settingsDir, './react/node_modules/react-native-fs/android')
include ':react-native-gesture-handler' include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-gesture-handler/android') project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-gesture-handler/android')
include ':react-native-video' include ':react-native-video'

3
package-lock.json generated Normal file
View file

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

View file

@ -1,5 +1,6 @@
package io.lbry.browser; package io.lbry.browser;
import android.annotation.TargetApi;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.app.Activity; import android.app.Activity;
@ -31,9 +32,13 @@ import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.shell.MainReactPackage; import com.facebook.react.shell.MainReactPackage;
import com.facebook.react.ReactRootView; import com.facebook.react.ReactRootView;
import com.reactnativecommunity.asyncstorage.AsyncStoragePackage; import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
import com.rnfs.RNFSPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.RNFetchBlob.RNFetchBlobPackage; import com.RNFetchBlob.RNFetchBlobPackage;
@ -54,8 +59,9 @@ import java.util.Random;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.reactnative.camera.RNCameraPackage;
public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler { public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
private static Activity currentActivity = null; private static Activity currentActivity = null;
@ -99,6 +105,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
private boolean receivedStopService; private boolean receivedStopService;
private PermissionListener permissionListener;
protected String getMainComponentName() { protected String getMainComponentName() {
return "LBRYApp"; return "LBRYApp";
} }
@ -139,8 +147,11 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
.addPackage(new MainReactPackage()) .addPackage(new MainReactPackage())
.addPackage(new AsyncStoragePackage()) .addPackage(new AsyncStoragePackage())
.addPackage(new FastImageViewPackage()) .addPackage(new FastImageViewPackage())
.addPackage(new ReactNativeDocumentPicker())
.addPackage(new ReactVideoPackage()) .addPackage(new ReactVideoPackage())
.addPackage(new RNCameraPackage())
.addPackage(new RNFetchBlobPackage()) .addPackage(new RNFetchBlobPackage())
.addPackage(new RNFSPackage())
.addPackage(new RNGestureHandlerPackage()) .addPackage(new RNGestureHandlerPackage())
.addPackage(new LbryReactPackage()) .addPackage(new LbryReactPackage())
.setUseDeveloperSupport(true) .setUseDeveloperSupport(true)
@ -352,6 +363,10 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
} }
break; break;
} }
if (permissionListener != null) {
permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
} }
public static String acquireDeviceId(Context context) { public static String acquireDeviceId(Context context) {
@ -473,6 +488,12 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
} }
} }
@TargetApi(Build.VERSION_CODES.M)
public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {
permissionListener = listener;
ActivityCompat.requestPermissions(this, permissions, requestCode);
}
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
if (mReactInstanceManager != null) { if (mReactInstanceManager != null) {

View file

@ -0,0 +1,320 @@
package io.lbry.browser.reactmodules;
import android.content.Context;
import android.content.ContentResolver;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.Manifest;
import android.media.ThumbnailUtils;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.MediaStore;
import com.facebook.react.bridge.Arguments;
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 com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import io.lbry.browser.MainActivity;
import io.lbry.browser.Utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
public class GalleryModule extends ReactContextBaseJavaModule {
private Context context;
public GalleryModule(ReactApplicationContext reactContext) {
super(reactContext);
this.context = reactContext;
}
@Override
public String getName() {
return "Gallery";
}
@ReactMethod
public void getVideos(Promise promise) {
WritableArray items = Arguments.createArray();
List<GalleryItem> videos = loadVideos();
for (int i = 0; i < videos.size(); i++) {
items.pushMap(videos.get(i).toMap());
}
promise.resolve(items);
}
@ReactMethod
public void getThumbnailPath(Promise promise) {
if (context != null) {
File cacheDir = context.getExternalCacheDir();
String thumbnailPath = String.format("%s/thumbnails", cacheDir.getAbsolutePath());
promise.resolve(thumbnailPath);
return;
}
promise.resolve(null);
}
/*
@ReactMethod
public void copyImage(String sourcePath, String destinationPath, Promise promise) {
try {
File source = new File(sourcePath);
File destination = new File(destinationPath);
if (source.exists()) {
FileChannel src = new FileInputStream(source).getChannel();
FileChannel dst = new FileOutputStream(destination).getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
promise.resolve(true);
} else {
promise.reject("The source image could not be found. Please try again.");
}
} catch (Exception ex) {
promise.reject("The image could not be saved. Please try again.");
}
}*/
@ReactMethod
public void getUploadsPath(Promise promise) {
if (context != null) {
String baseFolder = Utils.getExternalStorageDir(context);
String uploadsPath = String.format("%s/LBRY/Uploads", baseFolder);
File uploadsDir = new File(uploadsPath);
if (!uploadsDir.isDirectory()) {
uploadsDir.mkdirs();
}
promise.resolve(uploadsPath);
}
promise.reject("The content could not be saved to the device. Please check your storage permissions.");
}
@ReactMethod
public void createVideoThumbnail(String targetId, String filePath, Promise promise) {
(new AsyncTask<Void, Void, String>() {
protected String doInBackground(Void... param) {
String thumbnailPath = null;
if (context != null) {
Bitmap thumbnail = ThumbnailUtils.createVideoThumbnail(filePath, MediaStore.Video.Thumbnails.MINI_KIND);
File cacheDir = context.getExternalCacheDir();
thumbnailPath = String.format("%s/thumbnails/%s.png", cacheDir.getAbsolutePath(), targetId);
File file = new File(thumbnailPath);
try (FileOutputStream os = new FileOutputStream(thumbnailPath)) {
thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os);
os.close();
} catch (IOException ex) {
promise.reject("Could not create a thumbnail for the video");
return null;
}
}
return thumbnailPath;
}
public void onPostExecute(String thumbnailPath) {
if (thumbnailPath != null && thumbnailPath.trim().length() > 0) {
promise.resolve(thumbnailPath);
}
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@ReactMethod
public void createImageThumbnail(String targetId, String filePath, Promise promise) {
(new AsyncTask<Void, Void, String>() {
protected String doInBackground(Void... param) {
String thumbnailPath = null;
FileOutputStream os = null;
try {
Bitmap source = BitmapFactory.decodeFile(filePath);
// MINI_KIND dimensions
Bitmap thumbnail = Bitmap.createScaledBitmap(source, 512, 384, false);
if (context != null) {
File cacheDir = context.getExternalCacheDir();
thumbnailPath = String.format("%s/thumbnails/%s.png", cacheDir.getAbsolutePath(), targetId);
os = new FileOutputStream(thumbnailPath);
if (thumbnail != null) {
thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os);
}
os.close();
}
} catch (IOException ex) {
promise.reject("Could not create a thumbnail for the image");
return null;
} finally {
if (os != null) {
try {
os.close();
} catch (IOException ex) {
// ignoe
}
}
}
return thumbnailPath;
}
public void onPostExecute(String thumbnailPath) {
if (thumbnailPath != null && thumbnailPath.trim().length() > 0) {
promise.resolve(thumbnailPath);
}
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private List<GalleryItem> loadVideos() {
String[] projection = {
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.Video.Media.DURATION
};
List<String> ids = new ArrayList<String>();
List<GalleryItem> items = new ArrayList<GalleryItem>();
Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projection, null, null,
String.format("%s DESC", MediaStore.MediaColumns.DATE_MODIFIED));
while (cursor.moveToNext()) {
int idColumn = cursor.getColumnIndex(MediaStore.MediaColumns._ID);
int nameColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
int typeColumn = cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE);
int pathColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
int durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION);
String id = cursor.getString(idColumn);
GalleryItem item = new GalleryItem();
item.setId(id);
item.setName(cursor.getString(nameColumn));
item.setType(cursor.getString(typeColumn));
item.setFilePath(cursor.getString(pathColumn));
items.add(item);
ids.add(id);
}
checkThumbnails(ids);
return items;
}
private void checkThumbnails(final List<String> ids) {
(new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void... param) {
if (context != null) {
ContentResolver resolver = context.getContentResolver();
for (int i = 0; i < ids.size(); i++) {
String id = ids.get(i);
File cacheDir = context.getExternalCacheDir();
File thumbnailsDir = new File(String.format("%s/thumbnails", cacheDir.getAbsolutePath()));
if (!thumbnailsDir.isDirectory()) {
thumbnailsDir.mkdirs();
}
String thumbnailPath = String.format("%s/%s.png", thumbnailsDir.getAbsolutePath(), id);
File file = new File(thumbnailPath);
if (!file.exists()) {
// save the thumbnail to the path
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 1;
Bitmap thumbnail = MediaStore.Video.Thumbnails.getThumbnail(
resolver, Long.parseLong(id), MediaStore.Video.Thumbnails.MINI_KIND, options);
if (thumbnail != null) {
try (FileOutputStream os = new FileOutputStream(thumbnailPath)) {
thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os);
} catch (IOException ex) {
// skip
}
}
}
}
}
return null;
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private static class GalleryItem {
private String id;
private int duration;
private String filePath;
private String name;
private String type;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = duration;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public WritableMap toMap() {
WritableMap map = Arguments.createMap();
map.putString("id", id);
map.putString("name", name);
map.putString("filePath", filePath);
map.putString("type", type);
map.putInt("duration", duration);
return map;
}
}
@ReactMethod
public void canUseCamera(final Promise promise) {
promise.resolve(MainActivity.hasPermission(Manifest.permission.CAMERA, MainActivity.getActivity()));
}
}

View file

@ -9,6 +9,7 @@ import io.lbry.browser.reactmodules.BackgroundMediaModule;
import io.lbry.browser.reactmodules.DaemonServiceControlModule; import io.lbry.browser.reactmodules.DaemonServiceControlModule;
import io.lbry.browser.reactmodules.FirstRunModule; import io.lbry.browser.reactmodules.FirstRunModule;
import io.lbry.browser.reactmodules.FirebaseModule; import io.lbry.browser.reactmodules.FirebaseModule;
import io.lbry.browser.reactmodules.GalleryModule;
import io.lbry.browser.reactmodules.ScreenOrientationModule; import io.lbry.browser.reactmodules.ScreenOrientationModule;
import io.lbry.browser.reactmodules.VersionInfoModule; import io.lbry.browser.reactmodules.VersionInfoModule;
import io.lbry.browser.reactmodules.UtilityModule;; import io.lbry.browser.reactmodules.UtilityModule;;
@ -31,6 +32,7 @@ public class LbryReactPackage implements ReactPackage {
modules.add(new DaemonServiceControlModule(reactContext)); modules.add(new DaemonServiceControlModule(reactContext));
modules.add(new FirstRunModule(reactContext)); modules.add(new FirstRunModule(reactContext));
modules.add(new FirebaseModule(reactContext)); modules.add(new FirebaseModule(reactContext));
modules.add(new GalleryModule(reactContext));
modules.add(new ScreenOrientationModule(reactContext)); modules.add(new ScreenOrientationModule(reactContext));
modules.add(new UtilityModule(reactContext)); modules.add(new UtilityModule(reactContext));
modules.add(new VersionInfoModule(reactContext)); modules.add(new VersionInfoModule(reactContext));

View file

@ -12,7 +12,6 @@ from lbry.extras.daemon.loggly_handler import get_loggly_handler
from lbry.extras.daemon.Components import DHT_COMPONENT, HASH_ANNOUNCER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT from lbry.extras.daemon.Components import DHT_COMPONENT, HASH_ANNOUNCER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT
from lbry.extras.daemon.Daemon import Daemon from lbry.extras.daemon.Daemon import Daemon
from lbry.extras.daemon.loggly_handler import get_loggly_handler from lbry.extras.daemon.loggly_handler import get_loggly_handler
from lbry.utils import check_connection
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG) log.setLevel(logging.DEBUG)