diff --git a/app/src/component/channelSelector/view.js b/app/src/component/channelSelector/view.js index 33c4f085..3031227c 100644 --- a/app/src/component/channelSelector/view.js +++ b/app/src/component/channelSelector/view.js @@ -170,7 +170,6 @@ export default class ChannelSelector extends React.PureComponent { render() { const channel = this.state.addingChannel ? 'new' : this.props.channel; const { fetchingChannels, channels = [] } = this.props; - console.log(channels); const pickerItems = [{ name: Constants.ITEM_ANONYMOUS }, { name: Constants.ITEM_CREATE_A_CHANNEL }].concat( channels ); diff --git a/app/src/page/publish/view.js b/app/src/page/publish/view.js index 9a49637d..1b8dddcd 100644 --- a/app/src/page/publish/view.js +++ b/app/src/page/publish/view.js @@ -34,17 +34,25 @@ class PublishPage extends React.PureComponent { camera = null; state = { + // gallery videos + videos: null, + + // camera cameraType: RNCamera.Constants.Type.back, videoRecordingMode: false, recordingVideo: false, showCameraOverlay: false, + + // paths and media + uploadsPath: null, thumbnailPath: null, - videos: null, currentMedia: null, + currentThumbnailUri: null, + updatingThumbnailUri: false, currentPhase: Constants.PHASE_SELECTOR, - advancedMode: false, // publish + advancedMode: false, anonymous: true, channelName: CLAIM_VALUES.CHANNEL_ANONYMOUS, priceSet: false, @@ -145,6 +153,7 @@ class PublishPage extends React.PureComponent { pushDrawerStack(); setPlayerVisible(); + NativeModules.Gallery.getThumbnailPath().then(thumbnailPath => { if (thumbnailPath != null) { this.setState({ thumbnailPath }); @@ -184,6 +193,7 @@ class PublishPage extends React.PureComponent { showSelector() { this.setState({ currentMedia: null, + currentThumbnailUri: null, currentPhase: Constants.PHASE_SELECTOR, // publish @@ -218,6 +228,10 @@ class PublishPage extends React.PureComponent { 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) { @@ -231,13 +245,15 @@ class PublishPage extends React.PureComponent { this.setState({ recordingVideo: false }); const currentMedia = { id: -1, - filePath: data.uri, + 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, @@ -250,13 +266,19 @@ class PublishPage extends React.PureComponent { this.camera.takePictureAsync(options).then(data => { const currentMedia = { id: -1, - filePath: data.uri, + 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, showCameraOverlay: false, videoRecordingMode: false }); + this.setState({ + currentPhase: Constants.PHASE_DETAILS, + currentThumbnailUri: null, + updatingThumbnailUri: false, + showCameraOverlay: false, + videoRecordingMode: false + }); }); } } @@ -283,6 +305,12 @@ class PublishPage extends React.PureComponent { ); }; + 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(); }; @@ -311,18 +339,35 @@ class PublishPage extends React.PureComponent { this.setState({ channelName: channel }); }; - getThumbnailUriForMedia = (media) => { - const { thumbnailPath } = this.state; - if (media.type) { - const mediaType = media.type.substring(0, 5); - if ('video' === mediaType && media.id > -1) { - return `file://${thumbnailPath}/${media.id}.png` - } else if ('image' === mediaType) { - return media.filePath; - } + updateThumbnailUriForMedia = (media) => { + if (this.state.updatingThumbnailUri) { + return; } - return null; + const { notify } = 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 }); + } else if ('image' === mediaType) { + // photo taken or file selected + NativeModules.Gallery.createImageThumbnail(tempId, media.filePath). + then(path => this.setState({ currentThumbnailUri: `file://${path}`, updatingThumbnailUri: false })). + catch(err => { notify({ message: err }); this.setState({ updatingThumbnailUri: false }); }); + } else if ('video' === mediaType) { + // recorded video + NativeModules.Gallery.createVideoThumbnail(tempId, media.filePath). + then(path => this.setState({ currentThumbnailUri: `file://${path}`, updatingThumbnailUri: false })). + catch(err => { notify({ message: err }); this.setState({ updatingThumbnailUri: false }); }); + } + } } handleTitleChange = title => { @@ -406,16 +451,18 @@ class PublishPage extends React.PureComponent { ); } else if (Constants.PHASE_DETAILS === this.state.currentPhase && this.state.currentMedia) { - const { currentMedia } = this.state; - const thumbnailUri = this.getThumbnailUriForMedia(currentMedia); + const { currentMedia, currentThumbnailUri } = this.state; + if (!currentThumbnailUri) { + this.updateThumbnailUriForMedia(currentMedia); + } content = ( - {thumbnailUri && thumbnailUri.trim().length > 0 && + {currentThumbnailUri && currentThumbnailUri.trim().length > 0 && } @@ -589,7 +636,8 @@ class PublishPage extends React.PureComponent { buttonNegative: 'Cancel', }} /> - + diff --git a/src/main/java/io/lbry/browser/reactmodules/GalleryModule.java b/src/main/java/io/lbry/browser/reactmodules/GalleryModule.java index 192c144f..f16639a6 100644 --- a/src/main/java/io/lbry/browser/reactmodules/GalleryModule.java +++ b/src/main/java/io/lbry/browser/reactmodules/GalleryModule.java @@ -5,6 +5,7 @@ import android.content.ContentResolver; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.media.ThumbnailUtils; import android.os.AsyncTask; import android.os.Bundle; import android.provider.MediaStore; @@ -17,7 +18,10 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +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; @@ -59,6 +63,119 @@ public class GalleryModule extends ReactContextBaseJavaModule { 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() { + 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() { + protected String doInBackground(Void... param) { + String thumbnailPath = null; + FileOutputStream os = null; + try { + Bitmap source = BitmapFactory.decodeFile(filePath); + // MINI_KIND dimensions + Bitmap thumbnail = BitmapFactory.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 loadVideos() { String[] projection = { MediaStore.MediaColumns._ID,