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
- rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz
- 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"
- 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",
"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": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
@ -5553,8 +5558,8 @@
}
},
"lbry-redux": {
"version": "github:lbryio/lbry-redux#03998a2acf1a9e6c1b0818821612d137b31ebea3",
"from": "github:lbryio/lbry-redux#03998a2acf1a9e6c1b0818821612d137b31ebea3",
"version": "github:lbryio/lbry-redux#9a676ee311d573b84d11f402d918aeee77be76e1",
"from": "github:lbryio/lbry-redux",
"requires": {
"proxy-polyfill": "0.1.6",
"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": {
"version": "0.6.2",
"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": {
"version": "2.9.0",
"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",
"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": {
"version": "1.2.1",
"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"
}
},
"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": {
"version": "1.4.1",
"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",
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View file

@ -10,7 +10,8 @@
"dependencies": {
"base-64": "^0.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",
"lodash": ">=4.17.11",
"merge": ">=1.2.1",
@ -18,13 +19,17 @@
"react": "16.8.6",
"react-native": "0.59.3",
"@react-native-community/async-storage": "^1.2.2",
"react-native-camera": "^2.11.0",
"react-native-country-picker-modal": "^0.6.2",
"react-native-document-picker": "^2.3.0",
"react-native-exception-handler": "2.9.0",
"react-native-fast-image": "^5.0.3",
"react-native-fs": "^2.13.3",
"react-native-gesture-handler": "^1.1.0",
"react-native-image-zoom-viewer": "^2.2.5",
"react-native-password-strength-meter": "^0.0.2",
"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-video": "lbryio/react-native-video#exoplayer-lbry-android",
"react-navigation": "^3.11.0",

View file

@ -5,6 +5,7 @@ import DownloadsPage from 'page/downloads';
import DrawerContent from 'component/drawerContent';
import FilePage from 'page/file';
import FirstRunScreen from 'page/firstRun';
import PublishPage from 'page/publish';
import RewardsPage from 'page/rewards';
import TrendingPage from 'page/trending';
import SearchPage from 'page/search';
@ -148,6 +149,12 @@ const drawer = createDrawerNavigator(
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: {
screen: RewardsPage,
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 obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
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 =
claim && claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
const fullChannelUri = channelClaimId ? `${channelName}#${channelClaimId}` : channelName;

View file

@ -97,6 +97,7 @@ class FileItemMedia extends React.PureComponent {
{!isResolvingUri && (
<Text style={fileItemMediaStyle.autothumbText}>
{title &&
title.trim().length > 0 &&
title
.replace(/\s+/g, '')
.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;
if (claim) {
name = claim.name;
channel = claim.channel_name;
signingChannel = claim.signing_channel;
channel = signingChannel ? signingChannel.name : null;
height = claim.height;
channelClaimId = claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
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_VERIFICATION: 'verification',
PHASE_SELECTOR: 'selector',
PHASE_DETAILS: 'details',
PHASE_PUBLISH: 'publish',
CONTENT_TAB: 'content',
ABOUT_TAB: 'about',
@ -48,6 +52,7 @@ const Constants = {
DRAWER_ROUTE_TRENDING: 'Trending',
DRAWER_ROUTE_SUBSCRIPTIONS: 'Subscriptions',
DRAWER_ROUTE_MY_LBRY: 'Downloads',
DRAWER_ROUTE_PUBLISH: 'Publish',
DRAWER_ROUTE_REWARDS: 'Rewards',
DRAWER_ROUTE_WALLET: 'Wallet',
DRAWER_ROUTE_SETTINGS: 'Settings',
@ -63,6 +68,9 @@ const Constants = {
ROUTE_FILE: 'File',
ITEM_CREATE_A_CHANNEL: 'Create a channel...',
ITEM_ANONYMOUS: 'Publish anonymously',
SUBSCRIPTIONS_VIEW_ALL: 'view_all',
SUBSCRIPTIONS_VIEW_LATEST_FIRST: 'view_latest_first',

View file

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

View file

@ -22,6 +22,7 @@ import { navigateBack, navigateToUri } from 'utils/helper';
import Icon from 'react-native-vector-icons/FontAwesome5';
import ImageViewer from 'react-native-image-zoom-viewer';
import Button from 'component/button';
import Tag from 'component/tag';
import ChannelPage from 'page/channel';
import Colors from 'styles/colors';
import Constants from 'constants';
@ -502,11 +503,7 @@ class FilePage extends React.PureComponent {
};
renderTags = tags => {
return tags.map((tag, i) => (
<Text style={filePageStyle.tagItem} key={`${tag}-${i}`}>
{tag}
</Text>
));
return tags.map((tag, i) => <Tag style={filePageStyle.tagItem} key={`${tag}-${i}`} name={tag} />);
};
onFileDownloadButtonPlayed = () => {
@ -618,7 +615,8 @@ class FilePage extends React.PureComponent {
const description = metadata.description ? metadata.description : null;
const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio';
const { height, channel_name: channelName, value } = claim;
const { height, signing_channel: signingChannel, value } = claim;
const channelName = signingChannel && signingChannel.name;
const showActions =
fileInfo &&
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',
ChannelGrey: '#9b9b9b',
DarkerGrey: '#222222',
DarkGrey: '#555555',
DescriptionGrey: '#999999',
LbryGreen: '#2f9176',
BrighterLbryGreen: '#40b887',
NextLbryGreen: '#38d9a9',
TagGreen: '#e3f6f1',
LightGrey: '#cccccc',
LighterGrey: '#e5e5e5',
Orange: '#ffbb00',

View file

@ -322,14 +322,16 @@ const filePageStyle = StyleSheet.create({
tagTitle: {
fontFamily: 'Inter-UI-SemiBold',
flex: 0.2,
marginTop: 4,
},
tagList: {
fontFamily: 'Inter-UI-Regular',
flex: 0.8,
flexDirection: 'row',
flexWrap: 'wrap',
},
tagItem: {
marginRight: 16,
marginRight: 4,
},
rewardDriverCard: {
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
# (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
android.api = 28
@ -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: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
#p4a.branch = stable

View file

@ -86,7 +86,7 @@ fullscreen = 0
#android.presplash_color = #FFFFFF
# (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
android.api = 28
@ -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: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
#p4a.branch = stable

View file

@ -86,7 +86,7 @@ fullscreen = 0
#android.presplash_color = #FFFFFF
# (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
android.api = 28
@ -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: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
#p4a.branch = stable
@ -272,3 +272,4 @@ warn_on_root = 1
# Then, invoke the command line with the "demo" profile:
#
#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 {
google()
jcenter()
maven { url "https://jitpack.io" }
}
dependencies {
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 }}
versionCode {{ args.numeric_version }}
versionName '{{ args.version }}'
missingDimensionStrategy 'react-native-camera', 'general'
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "x86"
@ -85,6 +88,8 @@ ext {
minSdkVersion = {{ args.min_sdk_version }}
targetSdkVersion = {{ android_api }}
supportLibVersion = '27.1.1'
googlePlayServicesVersion = '16.1.0'
googlePlayServicesVisionVersion = '17.0.2'
}
subprojects {
@ -100,8 +105,11 @@ subprojects {
dependencies {
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-fast-image')
compile project(':react-native-fs')
compile project(':react-native-gesture-handler')
compile project(':react-native-video')
compile project(':rn-fetch-blob')

View file

@ -1,10 +1,16 @@
rootProject.name = 'browser'
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')
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'
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-exception-handler/android')
include ':react-native-fast-image'
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'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-gesture-handler/android')
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;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Bundle;
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.WritableMap;
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.ReactRootView;
import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
import com.rnfs.RNFSPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
@ -54,8 +59,9 @@ import java.util.Random;
import org.json.JSONObject;
import org.json.JSONArray;
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;
@ -99,6 +105,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
private boolean receivedStopService;
private PermissionListener permissionListener;
protected String getMainComponentName() {
return "LBRYApp";
}
@ -139,8 +147,11 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
.addPackage(new MainReactPackage())
.addPackage(new AsyncStoragePackage())
.addPackage(new FastImageViewPackage())
.addPackage(new ReactNativeDocumentPicker())
.addPackage(new ReactVideoPackage())
.addPackage(new RNCameraPackage())
.addPackage(new RNFetchBlobPackage())
.addPackage(new RNFSPackage())
.addPackage(new RNGestureHandlerPackage())
.addPackage(new LbryReactPackage())
.setUseDeveloperSupport(true)
@ -352,6 +363,10 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
}
break;
}
if (permissionListener != null) {
permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
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
public void onNewIntent(Intent intent) {
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.FirstRunModule;
import io.lbry.browser.reactmodules.FirebaseModule;
import io.lbry.browser.reactmodules.GalleryModule;
import io.lbry.browser.reactmodules.ScreenOrientationModule;
import io.lbry.browser.reactmodules.VersionInfoModule;
import io.lbry.browser.reactmodules.UtilityModule;;
@ -31,6 +32,7 @@ public class LbryReactPackage implements ReactPackage {
modules.add(new DaemonServiceControlModule(reactContext));
modules.add(new FirstRunModule(reactContext));
modules.add(new FirebaseModule(reactContext));
modules.add(new GalleryModule(reactContext));
modules.add(new ScreenOrientationModule(reactContext));
modules.add(new UtilityModule(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.Daemon import Daemon
from lbry.extras.daemon.loggly_handler import get_loggly_handler
from lbry.utils import check_connection
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)