Basic native mobile publishing

This commit is contained in:
Akinwale Ariwodola 2020-05-21 14:23:14 +01:00
parent 818fc80549
commit 4d2656f676
20 changed files with 1230 additions and 99 deletions

View file

@ -85,7 +85,7 @@ dependencies {
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.atlassian.commonmark:commonmark:0.14.0'
implementation 'com.arthenica:mobile-ffmpeg-full:4.3.1.LTS'
implementation 'com.arthenica:mobile-ffmpeg-full-gpl:4.3.1.LTS'
compileOnly 'org.projectlombok:lombok:1.18.10'
annotationProcessor 'org.projectlombok:lombok:1.18.10'

View file

@ -15,6 +15,7 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"

View file

@ -22,6 +22,7 @@ import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Base64;
@ -29,7 +30,6 @@ import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.Menu;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
@ -57,6 +57,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.GravityCompat;
@ -78,12 +79,14 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -101,6 +104,7 @@ import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.listener.CameraPermissionListener;
import io.lbry.browser.listener.DownloadActionListener;
import io.lbry.browser.listener.FetchChannelsListener;
import io.lbry.browser.listener.FilePickerListener;
import io.lbry.browser.listener.SdkStatusListener;
import io.lbry.browser.listener.StoragePermissionListener;
import io.lbry.browser.listener.WalletBalanceListener;
@ -173,9 +177,11 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
public static boolean startingShareActivity = false;
public static boolean startingPermissionRequest = false;
public static boolean startingSignInFlowActivity = false;
public static boolean startingCameraRequest = false;
private boolean enteringPIPMode = false;
private boolean fullSyncInProgress = false;
private int queuedSyncCount = 0;
private String cameraOutputFilename;
@Setter
private BackPressInterceptor backPressInterceptor;
@ -217,6 +223,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
public static final int REQUEST_REWARDS_VERIFY_SIGN_IN = 2003;
public static final int REQUEST_FILE_PICKER = 5001;
public static final int REQUEST_VIDEO_CAPTURE = 5002;
public static final int REQUEST_TAKE_PHOTO = 5003;
// broadcast action names
public static final String ACTION_SDK_READY = "io.lbry.browser.Broadcast.SdkReady";
@ -282,6 +290,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
private int selectedMenuItemId = -1;
private List<CameraPermissionListener> cameraPermissionListeners;
private List<DownloadActionListener> downloadActionListeners;
private List<FilePickerListener> filePickerListeners;
private List<SdkStatusListener> sdkStatusListeners;
private List<StoragePermissionListener> storagePermissionListeners;
private List<WalletBalanceListener> walletBalanceListeners;
@ -399,6 +408,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
openNavFragments = new HashMap<>();
cameraPermissionListeners = new ArrayList<>();
downloadActionListeners = new ArrayList<>();
filePickerListeners = new ArrayList<>();
sdkStatusListeners = new ArrayList<>();
storagePermissionListeners = new ArrayList<>();
walletBalanceListeners = new ArrayList<>();
@ -535,6 +545,16 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
downloadActionListeners.remove(listener);
}
public void addFilePickerListener(FilePickerListener listener) {
if (!filePickerListeners.contains(listener)) {
filePickerListeners.add(listener);
}
}
public void removeFilePickerListener(FilePickerListener listener) {
filePickerListeners.remove(listener);
}
public void addSdkStatusListener(SdkStatusListener listener) {
if (!sdkStatusListeners.contains(listener)) {
sdkStatusListeners.add(listener);
@ -641,6 +661,12 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
openFragment(ChannelFormFragment.class, true, NavMenuItem.ID_ITEM_CHANNELS, params);
}
public void openPublishesOnSuccessfulPublish() {
// close publish form
getSupportFragmentManager().popBackStack();
openFragment(PublishesFragment.class, true, NavMenuItem.ID_ITEM_PUBLISHES);
}
public void openPublishForm(Claim claim) {
Map<String, Object> params = new HashMap<>();
if (claim != null) {
@ -1355,7 +1381,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
syncWalletAndLoadPreferences();
scheduleWalletBalanceUpdate();
scheduleWalletSyncTask();
fetchChannels();
fetchOwnChannels();
fetchOwnClaims();
initFloatingWalletBalance();
}
@ -1740,6 +1767,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
drawer.closeDrawer(GravityCompat.START);
} else {
boolean handled = false;
// TODO: Refactor both forms as back press interceptors?
ChannelFormFragment channelFormFragment = null;
PublishFormFragment publishFormFragment = null;
for (Fragment fragment : openNavFragments.values()) {
@ -1756,7 +1784,13 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
handled = true;
return;
}
//if (publishFormFragment != null && )
if (publishFormFragment != null && (publishFormFragment.isSaveInProgress() || publishFormFragment.isTranscodeInProgress())) {
if (publishFormFragment.isTranscodeInProgress()) {
showMessage(R.string.transcode_in_progress);
}
handled = true;
return;
}
if (!handled) {
// check fragment and nav history
@ -1829,24 +1863,15 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_FILE_PICKER) {
startingFilePickerActivity = false;
ChannelFormFragment channelFormFragment = null;
//PublishFormFragment publishFormFragment = null;
for (Fragment fragment : openNavFragments.values()) {
if (fragment instanceof ChannelFormFragment) {
channelFormFragment = ((ChannelFormFragment) fragment);
break;
}
}
if (resultCode == RESULT_OK) {
Uri fileUri = data.getData();
String filePath = Helper.getRealPathFromURI_API19(this, fileUri);
if (channelFormFragment != null) {
channelFormFragment.onFilePicked(filePath);
for (FilePickerListener listener : filePickerListeners) {
listener.onFilePicked(filePath);
}
} else {
if (channelFormFragment != null) {
channelFormFragment.onFilePickerCancelled();
for (FilePickerListener listener : filePickerListeners) {
listener.onFilePickerCancelled();
}
}
} else if (requestCode == REQUEST_SIMPLE_SIGN_IN || requestCode == REQUEST_WALLET_SYNC_SIGN_IN) {
@ -1866,9 +1891,56 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
scheduleWalletSyncTask();
}
}
} else if (requestCode == REQUEST_VIDEO_CAPTURE || requestCode == REQUEST_TAKE_PHOTO) {
if (resultCode == RESULT_OK) {
Map<String, Object> params = new HashMap<>();
params.put("directFilePath", cameraOutputFilename);
openFragment(PublishFormFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH, params);
}
cameraOutputFilename = null;
}
}
public void requestVideoCapture() {
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
if (intent.resolveActivity(getPackageManager()) != null) {
String outputPath = String.format("%s/record", Utils.getAppInternalStorageDir(this));
File dir = new File(outputPath);
if (!dir.isDirectory()) {
dir.mkdirs();
}
cameraOutputFilename = String.format("%s/VID_%s.mp4", outputPath, Helper.FILESTAMP_FORMAT.format(new Date()));
Uri outputUri = FileProvider.getUriForFile(this, String.format("%s.fileprovider", getPackageName()), new File(cameraOutputFilename));
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
startActivityForResult(intent, REQUEST_VIDEO_CAPTURE);
return;
}
showError(getString(R.string.cannot_capture_video));
}
public void requestTakePhoto() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(getPackageManager()) != null) {
String outputPath = String.format("%s/photos", Utils.getAppInternalStorageDir(this));
File dir = new File(outputPath);
if (!dir.isDirectory()) {
dir.mkdirs();
}
cameraOutputFilename = String.format("%s/IMG_%s.jpg", outputPath, Helper.FILESTAMP_FORMAT.format(new Date()));
Uri outputUri = FileProvider.getUriForFile(this, String.format("%s.fileprovider", getPackageName()), new File(cameraOutputFilename));
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
startActivityForResult(intent, REQUEST_TAKE_PHOTO);
return;
}
showError(getString(R.string.cannot_take_photo));
}
private void applyNavbarSigninPadding() {
int statusBarHeight = getStatusBarHeight();
@ -2550,11 +2622,11 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
}
}
public void fetchChannels() {
public void fetchOwnChannels() {
ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, null, new ClaimListResultHandler() {
@Override
public void onSuccess(List<Claim> claims) {
Lbry.ownChannels = new ArrayList<>(claims);
Lbry.ownChannels = Helper.filterDeletedClaims(new ArrayList<>(claims));
for (FetchChannelsListener listener : fetchChannelsListeners) {
listener.onChannelsFetched(claims);
}
@ -2568,6 +2640,19 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void fetchOwnClaims() {
ClaimListTask task = new ClaimListTask(Arrays.asList(Claim.TYPE_STREAM, Claim.TYPE_REPOST), null, new ClaimListResultHandler() {
@Override
public void onSuccess(List<Claim> claims) {
Lbry.ownClaims = Helper.filterDeletedClaims(new ArrayList<>(claims));
}
@Override
public void onError(Exception error) { }
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void checkSyncedWallet() {
String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE);
// Just check if the current user has a synced wallet, no need to do anything else here

View file

@ -11,14 +11,11 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.listener.ChannelItemSelectionListener;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.GalleryItem;
import io.lbry.browser.utils.Helper;
import lombok.Setter;
@ -78,7 +75,7 @@ public class GalleryGridAdapter extends RecyclerView.Adapter<GalleryGridAdapter.
@Override
public void onBindViewHolder(GalleryGridAdapter.ViewHolder vh, int position) {
GalleryItem item = items.get(position);
String thumbnailUrl = item.getThumbnailUrl();
String thumbnailUrl = item.getThumbnailPath();
Glide.with(context.getApplicationContext()).load(thumbnailUrl).centerCrop().into(vh.thumbnailView);
vh.durationView.setVisibility(item.getDuration() > 0 ? View.VISIBLE : View.INVISIBLE);
vh.durationView.setText(item.getDuration() > 0 ? Helper.formatDuration(Double.valueOf(item.getDuration() / 1000.0).longValue()) : null);

View file

@ -0,0 +1,6 @@
package io.lbry.browser.listener;
public interface FilePickerListener {
void onFilePicked(String filePath);
void onFilePickerCancelled();
}

View file

@ -8,6 +8,6 @@ public class GalleryItem {
private String name;
private String filePath;
private String type;
private String thumbnailUrl;
private String thumbnailPath;
private long duration;
}

View file

@ -7,7 +7,6 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.Random;
import java.io.File;
@ -47,14 +46,14 @@ public class UploadImageTask extends AsyncTask<Void, Void, String> {
}
String fileType = String.format("image/%s", extension);
RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM).
addFormDataPart("name", makeid()).
addFormDataPart("name", Helper.makeid(24)).
addFormDataPart("file", fileName, RequestBody.create(file, MediaType.parse(fileType))).
build();
Request request = new Request.Builder().url("https://spee.ch/api/claim/publish").post(body).build();
OkHttpClient client = new OkHttpClient();
Response response = client.newCall(request).execute();
JSONObject json = new JSONObject(response.body().string());
if (json.has("success")) {
if (json.has("success") && Helper.getJSONBoolean("success", false, json)) {
JSONObject data = json.getJSONObject("data");
String url = Helper.getJSONString("url", null, data);
if (Helper.isNullOrEmpty(url)) {
@ -62,9 +61,15 @@ public class UploadImageTask extends AsyncTask<Void, Void, String> {
}
thumbnailUrl = String.format("%s.%s", url, extension);
} else if (json.has("error")) {
JSONObject error = json.getJSONObject("error");
String message = Helper.getJSONString("message", null, error);
} else if (json.has("error") || json.has("message")) {
JSONObject error = Helper.getJSONObject("error", json);
String message = null;
if (error != null) {
message = Helper.getJSONString("message", null, error);
}
if (Helper.isNullOrEmpty(message)) {
message = Helper.getJSONString("message", null, json);
}
throw new LbryResponseException(Helper.isNullOrEmpty(message) ? "The image failed to upload." : message);
}
} catch (IOException | JSONException | LbryResponseException ex) {
@ -84,16 +89,6 @@ public class UploadImageTask extends AsyncTask<Void, Void, String> {
}
}
private static String makeid() {
Random random = new Random();
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder id = new StringBuilder();
for (int i = 0; i < 24; i++) {
id.append(chars.charAt(random.nextInt(chars.length())));
}
return id.toString();
}
public interface UploadThumbnailHandler {
void onSuccess(String url);
void onError(Exception error);

View file

@ -1,4 +1,4 @@
package io.lbry.browser.tasks;
package io.lbry.browser.tasks.claim;
import android.os.AsyncTask;
import android.view.View;

View file

@ -0,0 +1,99 @@
package io.lbry.browser.tasks.claim;
import android.os.AsyncTask;
import android.view.View;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.model.Claim;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class PublishClaimTask extends AsyncTask<Void, Void, Claim> {
private Claim claim;
private String filePath;
private boolean update;
private View progressView;
private ClaimResultHandler handler;
private Exception error;
public PublishClaimTask(Claim claim, String filePath, boolean update, View progressView, ClaimResultHandler handler) {
this.claim = claim;
this.filePath = filePath;
this.update = update;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
if (handler != null) {
handler.beforeStart();
}
}
protected Claim doInBackground(Void... params) {
Claim.StreamMetadata metadata = (Claim.StreamMetadata) claim.getValue();
DecimalFormat amountFormat = new DecimalFormat(Helper.SDK_AMOUNT_FORMAT);
Map<String, Object> options = new HashMap<>();
options.put("blocking", true);
options.put("name", claim.getName());
options.put("bid", amountFormat.format(new BigDecimal(claim.getAmount()).doubleValue()));
options.put("title", Helper.isNullOrEmpty(claim.getTitle()) ? "" : claim.getTitle());
options.put("description", Helper.isNullOrEmpty(claim.getDescription()) ? "" : claim.getDescription());
options.put("thumbnail_url", Helper.isNullOrEmpty(claim.getThumbnailUrl()) ? "" : claim.getThumbnailUrl());
if (!Helper.isNullOrEmpty(filePath)) {
options.put("file_path", filePath);
}
if (claim.getTags() != null && claim.getTags().size() > 0) {
options.put("tags", new ArrayList<>(claim.getTags()));
}
if (metadata.getFee() != null) {
options.put("fee_currency", metadata.getFee().getCurrency());
options.put("fee_amount", amountFormat.format(new BigDecimal(metadata.getFee().getAmount()).doubleValue()));
}
if (claim.getSigningChannel() != null) {
options.put("channel_id", claim.getSigningChannel().getClaimId());
}
// TODO: license, license_url, languages
Claim claimResult = null;
try {
JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_PUBLISH, options);
if (result.has("outputs")) {
JSONArray outputs = result.getJSONArray("outputs");
for (int i = 0; i < outputs.length(); i++) {
JSONObject output = outputs.getJSONObject(i);
if (output.has("claim_id") && output.has("claim_op")) {
claimResult = Claim.claimFromOutput(output);
break;
}
}
}
} catch (ApiCallException | ClassCastException | JSONException ex) {
error = ex;
}
return claimResult;
}
protected void onPostExecute(Claim result) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (result != null) {
handler.onSuccess(result);
} else {
handler.onError(error);
}
}
}
}

View file

@ -105,7 +105,7 @@ public class LoadGalleryItemsTask extends AsyncTask<Void, GalleryItem, List<Gall
}
if (file.exists() && file.length() > 0) {
item.setThumbnailUrl(Uri.fromFile(file).toString());
item.setThumbnailPath(file.getAbsolutePath());
itemsWithThumbnails.add(item);
publishProgress(item);
}

View file

@ -38,6 +38,7 @@ import io.lbry.browser.BuildConfig;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.adapter.TagListAdapter;
import io.lbry.browser.listener.FilePickerListener;
import io.lbry.browser.listener.StoragePermissionListener;
import io.lbry.browser.listener.WalletBalanceListener;
import io.lbry.browser.model.Claim;
@ -46,7 +47,7 @@ import io.lbry.browser.model.Tag;
import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.tasks.UpdateSuggestedTagsTask;
import io.lbry.browser.tasks.UploadImageTask;
import io.lbry.browser.tasks.ChannelCreateUpdateTask;
import io.lbry.browser.tasks.claim.ChannelCreateUpdateTask;
import io.lbry.browser.tasks.claim.ClaimResultHandler;
import io.lbry.browser.tasks.lbryinc.LogPublishTask;
import io.lbry.browser.ui.BaseFragment;
@ -54,11 +55,10 @@ import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryAnalytics;
import io.lbry.browser.utils.LbryUri;
import io.lbry.browser.utils.Predefined;
import lombok.Getter;
public class ChannelFormFragment extends BaseFragment implements
StoragePermissionListener, TagListAdapter.TagClickListener, WalletBalanceListener {
FilePickerListener, StoragePermissionListener, TagListAdapter.TagClickListener, WalletBalanceListener {
private static final int SUGGESTED_LIMIT = 8;
@ -289,16 +289,16 @@ public class ChannelFormFragment extends BaseFragment implements
}
private void validateAndSaveClaim(Claim claim) {
String channelName = claim.getName().startsWith("@") ? claim.getName().substring(1) : claim.getName();
if (Helper.isNullOrEmpty(channelName)) {
showError(getString(R.string.please_enter_channel_name));
return;
}
if (!LbryUri.isNameValid(channelName)) {
showError(getString(R.string.channel_name_invalid_characters));
return;
}
if (!editMode) {
String channelName = claim.getName().startsWith("@") ? claim.getName().substring(1) : claim.getName();
if (Helper.isNullOrEmpty(channelName)) {
showError(getString(R.string.please_enter_channel_name));
return;
}
if (!LbryUri.isNameValid(channelName)) {
showError(getString(R.string.channel_name_invalid_characters));
return;
}
if (Helper.channelExists(channelName)) {
showError(getString(R.string.channel_name_already_created));
return;
@ -368,7 +368,7 @@ public class ChannelFormFragment extends BaseFragment implements
private void showError(String message) {
Context context = getContext();
if (context != null) {
Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).setBackgroundTint(Color.RED).show();
Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
}
}
@ -455,9 +455,9 @@ public class ChannelFormFragment extends BaseFragment implements
@Override
public void onError(Exception error) {
Snackbar.make(getView(), R.string.image_upload_failed, Snackbar.LENGTH_LONG).setBackgroundTint(
ContextCompat.getColor(context, R.color.red)
).show();
if (getContext() != null) {
showError(getString(R.string.image_upload_failed));
}
if (coverFilePickerActive) {
// cover selected
imageCover.setImageResource(R.drawable.default_channel_cover);
@ -498,6 +498,8 @@ public class ChannelFormFragment extends BaseFragment implements
activity.showNavigationBackIcon();
activity.lockDrawer();
activity.hideFloatingWalletBalance();
activity.addFilePickerListener(this);
activity.addWalletBalanceListener(this);
ActionBar actionBar = activity.getSupportActionBar();
@ -523,6 +525,7 @@ public class ChannelFormFragment extends BaseFragment implements
activity.restoreToggle();
activity.showFloatingWalletBalance();
if (!MainActivity.startingFilePickerActivity) {
activity.removeFilePickerListener(this);
activity.removeNavFragment(ChannelFormFragment.class, NavMenuItem.ID_ITEM_CHANNELS);
}
}

View file

@ -1249,7 +1249,7 @@ public class FileViewFragment extends BaseFragment implements
public void onClick(DialogInterface dialogInterface, int i) {
Bundle bundle = new Bundle();
bundle.putString("uri", currentUrl);
bundle.putBoolean("paid", true);
bundle.putString("paid", "true");
bundle.putDouble("amount", Helper.parseDouble(fee.getAmount(), 0));
bundle.putDouble("lbc_amount", cost);
bundle.putString("currency", fee.getCurrency());
@ -1333,6 +1333,7 @@ public class FileViewFragment extends BaseFragment implements
// paid is handled differently
Bundle bundle = new Bundle();
bundle.putString("uri", currentUrl);
bundle.putString("paid", "false");
LbryAnalytics.logEvent(LbryAnalytics.EVENT_PURCHASE_URI, bundle);
}

View file

@ -1,11 +1,17 @@
package io.lbry.browser.ui.publish;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
@ -14,22 +20,37 @@ import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.AdapterView;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.AppCompatSpinner;
import androidx.cardview.widget.CardView;
import androidx.core.content.ContextCompat;
import androidx.core.widget.NestedScrollView;
import androidx.recyclerview.widget.RecyclerView;
import com.arthenica.mobileffmpeg.Config;
import com.arthenica.mobileffmpeg.FFmpeg;
import com.arthenica.mobileffmpeg.FFprobe;
import com.arthenica.mobileffmpeg.Statistics;
import com.arthenica.mobileffmpeg.StatisticsCallback;
import com.bumptech.glide.Glide;
import com.google.android.flexbox.FlexboxLayoutManager;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.google.android.material.textfield.TextInputEditText;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@ -40,19 +61,23 @@ import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.adapter.InlineChannelSpinnerAdapter;
import io.lbry.browser.adapter.TagListAdapter;
import io.lbry.browser.listener.FilePickerListener;
import io.lbry.browser.listener.SdkStatusListener;
import io.lbry.browser.listener.StoragePermissionListener;
import io.lbry.browser.listener.WalletBalanceListener;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.Fee;
import io.lbry.browser.model.GalleryItem;
import io.lbry.browser.model.NavMenuItem;
import io.lbry.browser.model.Tag;
import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.tasks.ChannelCreateUpdateTask;
import io.lbry.browser.tasks.claim.ChannelCreateUpdateTask;
import io.lbry.browser.tasks.UpdateSuggestedTagsTask;
import io.lbry.browser.tasks.UploadImageTask;
import io.lbry.browser.tasks.claim.ClaimListResultHandler;
import io.lbry.browser.tasks.claim.ClaimListTask;
import io.lbry.browser.tasks.claim.ClaimResultHandler;
import io.lbry.browser.tasks.claim.PublishClaimTask;
import io.lbry.browser.tasks.lbryinc.LogPublishTask;
import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.utils.Helper;
@ -60,15 +85,30 @@ import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryAnalytics;
import io.lbry.browser.utils.LbryUri;
import io.lbry.browser.utils.Predefined;
import io.lbry.lbrysdk.Utils;
import lombok.Data;
import lombok.Getter;
public class PublishFormFragment extends BaseFragment implements
SdkStatusListener, StoragePermissionListener, TagListAdapter.TagClickListener, WalletBalanceListener {
FilePickerListener, SdkStatusListener, StoragePermissionListener, TagListAdapter.TagClickListener, WalletBalanceListener {
private static final String H264_CODEC = "h264";
private static final int MAX_VIDEO_DIMENSION = 1920;
private static final int MAX_BITRATE = 5000000; // 5mbps
private static final int SUGGESTED_LIMIT = 8;
private boolean editMode;
private boolean fetchingChannels;
@Getter
private boolean saveInProgress;
private String currentFilter;
private boolean publishFileChecked;
private boolean fetchingChannels;
private boolean launchPickerPending;
@Getter
private boolean transcodeInProgress;
private long transcodeStartTime;
private VideoTranscodeTask videoTranscodeTask;
private TextInputEditText inputTagFilter;
private RecyclerView addedTagsList;
@ -77,6 +117,7 @@ public class PublishFormFragment extends BaseFragment implements
private TagListAdapter addedTagsAdapter;
private TagListAdapter suggestedTagsAdapter;
private TagListAdapter matureTagsAdapter;
private ProgressBar progressPublish;
private ProgressBar progressLoadingChannels;
private View noTagsView;
private View noTagResultsView;
@ -93,6 +134,8 @@ public class PublishFormFragment extends BaseFragment implements
private View textNoPrice;
private View layoutPrice;
private SwitchMaterial switchPrice;
private ImageView imageThumbnail;
private TextView linkGenerateAddress;
private TextInputEditText inputTitle;
private TextInputEditText inputDescription;
@ -102,7 +145,7 @@ public class PublishFormFragment extends BaseFragment implements
private View inlineDepositBalanceContainer;
private TextView inlineDepositBalanceValue;
private View linkCancel;
private View linkPublishCancel;
private MaterialButton buttonPublish;
private View inlineChannelCreator;
@ -114,38 +157,67 @@ public class PublishFormFragment extends BaseFragment implements
private View inlineChannelCreatorProgress;
private MaterialButton inlineChannelCreatorCreateButton;
private boolean uploading;
private String lastSelectedThumbnailFile;
private String uploadedThumbnailUrl;
private boolean editFieldsLoaded;
private Claim currentClaim;
private GalleryItem currentGalleryItem;
private String currentFilePath;
private String transcodedFilePath;
private boolean fileLoaded;
private View mediaContainer;
private View uploadProgress;
private CardView cardVideoOptimization;
private ProgressBar optimizationRealProgress;
private ProgressBar optimizationProgress;
private TextView textOptimizationProgress;
private TextView textOptimizationStatus;
private TextView textOptimizationElapsed;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_publish_form, container, false);
scrollView = root.findViewById(R.id.publish_form_scroll_view);
progressLoadingChannels = root.findViewById(R.id.publish_form_loading_channels);
progressPublish = root.findViewById(R.id.publish_form_publishing);
channelSpinner = root.findViewById(R.id.publish_form_channel_spinner);
mediaContainer = root.findViewById(R.id.publish_form_media_container);
inputTagFilter = root.findViewById(R.id.form_tag_filter_input);
noTagsView = root.findViewById(R.id.form_no_added_tags);
noTagResultsView = root.findViewById(R.id.form_no_tag_results);
inlineDepositBalanceContainer = root.findViewById(R.id.publish_form_inline_balance_container);
inlineDepositBalanceValue = root.findViewById(R.id.publish_form_inline_balance_value);
cardVideoOptimization = root.findViewById(R.id.publish_form_video_opt_card);
optimizationProgress = root.findViewById(R.id.publish_form_video_opt_progress);
optimizationRealProgress = root.findViewById(R.id.publish_form_video_opt_real_progress);
textOptimizationProgress = root.findViewById(R.id.publish_form_video_opt_progress_text);
textOptimizationStatus = root.findViewById(R.id.publish_form_video_opt_status);
textOptimizationElapsed = root.findViewById(R.id.publish_form_video_opt_elapsed);
layoutExtraFields = root.findViewById(R.id.publish_form_extra_options_container);
linkShowExtraFields = root.findViewById(R.id.publish_form_toggle_extra);
layoutPrice = root.findViewById(R.id.publish_form_price_container);
textNoPrice = root.findViewById(R.id.publish_form_no_price);
switchPrice = root.findViewById(R.id.publish_form_price_switch);
uploadProgress = root.findViewById(R.id.publish_form_thumbnail_upload_progress);
imageThumbnail = root.findViewById(R.id.publish_form_thumbnail_preview);
linkGenerateAddress = root.findViewById(R.id.publish_form_generate_address);
inputTitle = root.findViewById(R.id.publish_form_input_title);
inputDeposit = root.findViewById(R.id.publish_form_input_description);
inputDescription = root.findViewById(R.id.publish_form_input_description);
inputPrice = root.findViewById(R.id.publish_form_input_price);
inputAddress = root.findViewById(R.id.publish_form_input_address);
inputDeposit = root.findViewById(R.id.publish_form_input_deposit);
priceCurrencySpinner = root.findViewById(R.id.publish_form_currency_spinner);
linkCancel = root.findViewById(R.id.publish_form_cancel);
linkPublishCancel = root.findViewById(R.id.publish_form_cancel);
buttonPublish = root.findViewById(R.id.publish_form_publish_button);
Context context = getContext();
@ -192,7 +264,12 @@ public class PublishFormFragment extends BaseFragment implements
private void initUi() {
inputAddress.setText(Helper.generateUrl());
linkGenerateAddress.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
inputAddress.setText(Helper.generateUrl());
}
});
switchPrice.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
@ -228,13 +305,10 @@ public class PublishFormFragment extends BaseFragment implements
}
});
linkCancel.setOnClickListener(new View.OnClickListener() {
mediaContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).onBackPressed();
}
checkStoragePermissionAndLaunchFilePicker();
}
});
@ -278,10 +352,64 @@ public class PublishFormFragment extends BaseFragment implements
}
});
linkPublishCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (transcodeInProgress) {
// show alert confirming the user is sure, and then cancel
FFmpeg.cancel();
transcodeInProgress = false;
}
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).onBackPressed();
}
}
});
buttonPublish.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (uploading) {
Snackbar.make(view, R.string.publish_no_thumbnail, Snackbar.LENGTH_LONG).show();
return;
} else if (Helper.isNullOrEmpty(uploadedThumbnailUrl)) {
showError(getString(R.string.publish_thumbnail_in_progress));
return;
}
// check minimum deposit
String depositString = Helper.getValue(inputDeposit.getText());
double depositAmount = 0;
try {
depositAmount = Double.valueOf(depositString);
} catch (NumberFormatException ex) {
// pass
showError(getString(R.string.please_enter_valid_deposit));
return;
}
if (depositAmount < Helper.MIN_DEPOSIT) {
String error = getResources().getQuantityString(R.plurals.min_deposit_required, depositAmount == 1 ? 1 : 2, String.valueOf(Helper.MIN_DEPOSIT));
showError(error);
return;
}
if (Lbry.walletBalance == null || Lbry.walletBalance.getAvailable().doubleValue() < depositAmount) {
showError(getString(R.string.deposit_more_than_balance));
return;
}
String priceString = Helper.getValue(inputPrice.getText());
double priceAmount = Helper.parseDouble(priceString, 0);
if (switchPrice.isChecked() && priceAmount == 0) {
showError(getString(R.string.price_amount_not_set));
return;
}
Claim claim = buildPublishClaim();
if (validatePublishClaim(claim)) {
publishClaim(claim);
}
}
});
@ -306,6 +434,8 @@ public class PublishFormFragment extends BaseFragment implements
activity.showNavigationBackIcon();
activity.lockDrawer();
activity.hideFloatingWalletBalance();
activity.addFilePickerListener(this);
activity.addWalletBalanceListener(this);
ActionBar actionBar = activity.getSupportActionBar();
@ -320,13 +450,18 @@ public class PublishFormFragment extends BaseFragment implements
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) getContext();
activity.removeWalletBalanceListener(this);
activity.restoreToggle();
activity.showFloatingWalletBalance();
if (!MainActivity.startingFilePickerActivity) {
activity.removeWalletBalanceListener(this);
activity.removeFilePickerListener(this);
activity.removeNavFragment(PublishFormFragment.class, NavMenuItem.ID_ITEM_NEW_PUBLISH);
if (transcodeInProgress) {
FFmpeg.cancel();
}
}
}
super.onStop();
}
@ -352,6 +487,33 @@ public class PublishFormFragment extends BaseFragment implements
}
}
private void checkStoragePermissionAndLaunchFilePicker() {
Context context = getContext();
if (MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, context)) {
launchPickerPending = false;
launchFilePicker();
} else {
launchPickerPending = true;
MainActivity.requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE,
MainActivity.REQUEST_STORAGE_PERMISSION,
getString(R.string.storage_permission_rationale_images),
context,
true);
}
}
private void launchFilePicker() {
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity.startingFilePickerActivity = true;
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("image/*");
((MainActivity) context).startActivityForResult(
Intent.createChooser(intent, getString(R.string.select_thumbnail)),
MainActivity.REQUEST_FILE_PICKER);
}
}
private void updateFieldsFromCurrentClaim() {
if (currentClaim != null && !editFieldsLoaded) {
@ -360,15 +522,20 @@ public class PublishFormFragment extends BaseFragment implements
}
private void checkPublishFile() {
if (publishFileChecked) {
return;
}
String filePath = "";
String thumbnailPath = null;
if (currentGalleryItem != null) {
// check gallery item type
filePath = currentGalleryItem.getFilePath();
thumbnailPath = currentGalleryItem.getThumbnailPath();
} else if (currentFilePath != null) {
filePath = currentFilePath;
}
android.util.Log.d("#HELP", "filePath=" + filePath);
File file = new File(filePath);
if (!file.exists()) {
// file doesn't exist. although this shouldn't happen
@ -376,19 +543,190 @@ public class PublishFormFragment extends BaseFragment implements
return;
}
// check content type
String type = null;
String extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(file).toString());
if (extension != null) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
android.util.Log.d("#HELP", "fileType=" + type);
boolean isVideo = false;
boolean isImage = !Helper.isNullOrEmpty(type) && type.startsWith("image");
if (!Helper.isNullOrEmpty(type) && type.startsWith("video")) {
// ffmpeg video handling
isVideo = true;
if (!transcodeInProgress) {
probeVideo(filePath);
}
}
if (isVideo || isImage) {
checkAndUploadThumbnail(filePath, thumbnailPath, isVideo ? "video" : "image");
}
Helper.setViewVisibility(cardVideoOptimization, isVideo ? View.VISIBLE : View.GONE);
publishFileChecked = true;
}
private void checkAndUploadThumbnail(String filePath, String thumbnailPath, String type) {
if (Helper.isNullOrEmpty(thumbnailPath)) {
createAndUploadThumbnail(filePath, type);
} else {
uploadThumbnail(thumbnailPath);
}
}
private void createAndUploadThumbnail(String filePath, String type) {
Context context = getContext();
CreateThumbnailTask task = new CreateThumbnailTask(filePath, type, context, new CreateThumbnailTask.CreateThumbnailHandler() {
@Override
public void onSuccess(String thumbnailPath) {
uploadThumbnail(thumbnailPath);
}
@Override
public void onError(Exception error) {
if (context != null) {
showError(getString(R.string.thumbnail_creation_failed));
}
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void uploadThumbnail(String thumbnailPath) {
if (uploading) {
Snackbar.make(getView(), R.string.wait_for_upload, Snackbar.LENGTH_LONG).show();
return;
}
Context context = getContext();
if (context != null) {
Glide.with(context.getApplicationContext()).load(thumbnailPath).centerCrop().into(imageThumbnail);
}
uploading = true;
uploadedThumbnailUrl = null;
UploadImageTask task = new UploadImageTask(thumbnailPath, uploadProgress, new UploadImageTask.UploadThumbnailHandler() {
@Override
public void onSuccess(String url) {
lastSelectedThumbnailFile = thumbnailPath;
uploadedThumbnailUrl = url;
uploading = false;
}
@Override
public void onError(Exception error) {
View view = getView();
if (context != null && view != null) {
showError(getString(R.string.image_upload_failed));
}
lastSelectedThumbnailFile = null;
imageThumbnail.setImageDrawable(null);
uploading = false;
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void probeVideo(String filePath) {
VideoProbeTask task = new VideoProbeTask(filePath, new VideoProbeTask.VideoProbeHandler() {
@Override
public void onVideoProbed(VideoInformation result) {
checkAndTranscodeVideo(filePath, result);
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void checkAndTranscodeVideo(String filePath, VideoInformation videoInformation) {
boolean transcodeRequired = (videoInformation == null ||
!H264_CODEC.equalsIgnoreCase(videoInformation.getCodecName()) ||
MAX_VIDEO_DIMENSION < videoInformation.getWidth() || MAX_VIDEO_DIMENSION < videoInformation.getHeight() ||
MAX_BITRATE < videoInformation.getBitrate());
String scalePart = "";
if (videoInformation != null) {
// check the max dimension that we need to scale
int videoWidth = videoInformation.getWidth();
int videoHeight = videoInformation.getHeight();
// get the highest dimension
int maxDimension = Math.max(videoWidth, videoHeight);
if (maxDimension > MAX_VIDEO_DIMENSION) {
scalePart = maxDimension == videoWidth ? String.format("-vf scale=%d:-2", MAX_VIDEO_DIMENSION) : String.format("-vf scale=-2:%d", MAX_VIDEO_DIMENSION);
}
}
Context context = getContext();
String outputPath = String.format("%s/videos", Utils.getAppInternalStorageDir(context));
File dir = new File(outputPath);
if (!dir.isDirectory()) {
dir.mkdirs();
}
boolean hasFullDuration = videoInformation != null && videoInformation.getDurationSeconds() > 0;
Helper.setViewVisibility(optimizationRealProgress, hasFullDuration ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(optimizationProgress, hasFullDuration ? View.GONE : View.VISIBLE);
File sourceFile = new File(filePath);
String filename = sourceFile.getName();
if (!filename.endsWith(".mp4")) {
int lastDotIndex = filename.lastIndexOf('.');
filename = String.format("%s.mp4", lastDotIndex > -1 ? filename.substring(0, lastDotIndex) : filename);
}
String videoFilePath = String.format("%s/%s", outputPath, filename);
File targetFile = new File(videoFilePath);
if (targetFile.exists()) {
targetFile.delete();
}
transcodeInProgress = true;
videoTranscodeTask = new VideoTranscodeTask(filePath, videoFilePath, scalePart, transcodeRequired, new VideoTranscodeTask.VideoTranscodeHandler() {
@Override
public void onProgress(int time) {
if (context != null) {
int currentDuration = Double.valueOf(time / 1000.0).intValue();
int fullDuration = videoInformation != null ? videoInformation.getDurationSeconds() : 0;
long elapsed = System.currentTimeMillis() - transcodeStartTime;
String completedDurationText = Helper.formatDuration(currentDuration);
if (fullDuration > 0) {
completedDurationText = String.format("%s / %s", completedDurationText, Helper.formatDuration(fullDuration));
int percentComplete = Double.valueOf(Math.ceil((double) currentDuration / (double) fullDuration * 100.0)).intValue();
optimizationRealProgress.setProgress(percentComplete);
}
String text = context.getString(R.string.completed_video_duration, completedDurationText);
Helper.setViewText(textOptimizationProgress, text);
Helper.setViewText(textOptimizationElapsed, Helper.formatDuration(Double.valueOf(elapsed / 1000.0).longValue()));
}
}
@Override
public void onSuccess(String outputFilePath) {
transcodedFilePath = outputFilePath;
transcodeInProgress = false;
Helper.setViewText(textOptimizationStatus, R.string.video_optimized);
Helper.setViewVisibility(optimizationRealProgress, View.GONE);
Helper.setViewVisibility(optimizationProgress, View.GONE);
Helper.setViewVisibility(textOptimizationProgress, View.GONE);
}
@Override
public void onErrorOrCancelled() {
transcodeInProgress = false;
Helper.setViewText(textOptimizationStatus, R.string.video_optimize_failed);
Helper.setViewVisibility(optimizationRealProgress, View.GONE);
Helper.setViewVisibility(optimizationProgress, View.GONE);
Helper.setViewVisibility(textOptimizationProgress, View.GONE);
}
});
transcodeStartTime = System.currentTimeMillis();
videoTranscodeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void cancelOnFatalCondition(String message) {
@ -415,7 +753,7 @@ public class PublishFormFragment extends BaseFragment implements
checkParams();
updateFieldsFromCurrentClaim();
if (currentClaim == null && (currentGalleryItem != null || (Helper.isNullOrEmpty(currentFilePath)))) {
if (currentClaim == null && (currentGalleryItem != null || !Helper.isNullOrEmpty(currentFilePath))) {
// load file information
checkPublishFile();
}
@ -533,11 +871,128 @@ public class PublishFormFragment extends BaseFragment implements
private Claim buildPublishClaim() {
Claim claim = new Claim();
claim.setName(Helper.getValue(inputAddress.getText()));
claim.setAmount(Helper.getValue(inputDeposit.getText()));
Claim.StreamMetadata metadata = new Claim.StreamMetadata();
metadata.setTitle(Helper.getValue(inputTitle.getText()));
metadata.setDescription(Helper.getValue(inputDescription.getText()));
metadata.setTags(Helper.getTagsForTagObjects(addedTagsAdapter.getTags()));
Claim selectedChannel = (Claim) channelSpinner.getSelectedItem();
if (selectedChannel != null && !selectedChannel.isPlaceholder() && !selectedChannel.isPlaceholderAnonymous()) {
claim.setSigningChannel(selectedChannel);
}
if (switchPrice.isChecked()) {
Fee fee = new Fee();
fee.setCurrency((String) priceCurrencySpinner.getSelectedItem());
fee.setAmount(Helper.getValue(inputPrice.getText()));
metadata.setFee(fee);
}
if (!Helper.isNullOrEmpty(uploadedThumbnailUrl)) {
Claim.Resource thumbnail = new Claim.Resource();
thumbnail.setUrl(uploadedThumbnailUrl);
metadata.setThumbnail(thumbnail);
}
// TODO: License, LicenseDescription, LicenseUrl, Language
claim.setValueType(Claim.TYPE_STREAM);
claim.setValue(metadata);
return claim;
}
private boolean validatePublishClaim() {
return false;
private boolean validatePublishClaim(Claim claim) {
if (Helper.isNullOrEmpty(claim.getTitle())) {
showError(getString(R.string.please_provide_title));
return false;
}
if (Helper.isNullOrEmpty(claim.getName())) {
showError(getString(R.string.please_specify_address));
return false;
}
if (!LbryUri.isNameValid(claim.getName())) {
showError(getString(R.string.address_invalid_characters));
return false;
}
if (Helper.claimNameExists(claim.getName())) {
showError(getString(R.string.address_already_used));
return false;
}
String publishFilePath = currentGalleryItem != null ? currentGalleryItem.getFilePath() : currentFilePath;
if (Helper.isNullOrEmpty(publishFilePath) && Helper.isNullOrEmpty(transcodedFilePath)) {
showError(getString(R.string.no_file_selected));
return false;
}
return true;
}
private void publishClaim(Claim claim) {
String finalFilePath = transcodedFilePath;
if (Helper.isNullOrEmpty(finalFilePath)) {
finalFilePath = currentGalleryItem != null ? currentGalleryItem.getFilePath() : currentFilePath;
}
saveInProgress = true;
PublishClaimTask task = new PublishClaimTask(claim, finalFilePath, editMode, progressPublish, new ClaimResultHandler() {
@Override
public void beforeStart() {
preSave();
}
@Override
public void onSuccess(Claim claimResult) {
postSave();
android.util.Log.d("#HELP", claimResult.toString());
// Run the logPublish task
if (!BuildConfig.DEBUG) {
LogPublishTask logPublish = new LogPublishTask(claimResult);
logPublish.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
// publish done
Bundle bundle = new Bundle();
bundle.putString("claim_id", claimResult.getClaimId());
bundle.putString("claim_name", claimResult.getName());
LbryAnalytics.logEvent(LbryAnalytics.EVENT_PUBLISH, bundle);
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
activity.showMessage(R.string.publish_successful);
activity.openPublishesOnSuccessfulPublish();
}
}
@Override
public void onError(Exception error) {
showError(error.getMessage());
postSave();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void preSave() {
saveInProgress = true;
// disable input views
Helper.setViewEnabled(linkShowExtraFields, false);
Helper.setViewEnabled(linkPublishCancel, false);
Helper.setViewEnabled(buttonPublish, false);
}
private void postSave() {
Helper.setViewEnabled(linkShowExtraFields, true);
Helper.setViewEnabled(linkPublishCancel, true);
Helper.setViewEnabled(buttonPublish, true);
saveInProgress = false;
}
@ -747,17 +1202,270 @@ public class PublishFormFragment extends BaseFragment implements
private void showError(String message) {
Context context = getContext();
if (context != null) {
Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).setBackgroundTint(Color.RED).show();
Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).
setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
}
}
private void checkUploadButton() {
}
@Override
public void onStoragePermissionGranted() {
if (launchPickerPending) {
launchPickerPending = false;
launchFilePicker();
}
}
@Override
public void onStoragePermissionRefused() {
showError(getString(R.string.storage_permission_rationale_images));
launchPickerPending = false;
}
@Override
public void onFilePicked(String filePath) {
if (Helper.isNullOrEmpty(filePath)) {
Snackbar.make(getView(), R.string.undetermined_image_filepath, Snackbar.LENGTH_LONG).setBackgroundTint(
ContextCompat.getColor(getContext(), R.color.red)).show();
return;
}
android.util.Log.d("#HELP", "FilePicked: " + filePath);
Context context = getContext();
if (context != null) {
if (filePath.equalsIgnoreCase(lastSelectedThumbnailFile)) {
// previous selected cover was uploaded successfully
android.util.Log.d("#HELP", "lastSelectedThumbnailFile the same");
return;
}
android.util.Log.d("#HELP", "PickedFilePath=" + filePath);
Uri fileUri = Uri.fromFile(new File(filePath));
Glide.with(context.getApplicationContext()).load(fileUri).centerCrop().into(imageThumbnail);
uploadThumbnail(filePath);
}
}
@Override
public void onFilePickerCancelled() {
// nothing to do here
// At some point in the future, allow file picking for publish file?
}
private static class VideoProbeTask extends AsyncTask<Void, Void, VideoInformation> {
private String filePath;
private VideoProbeHandler handler;
public VideoProbeTask(String filePath, VideoProbeHandler handler) {
this.filePath = filePath;
this.handler = handler;
}
protected VideoInformation doInBackground(Void... params) {
try {
int code = FFprobe.execute(String.format("-v quiet -show_streams -select_streams v -print_format json -i \"%s\"", filePath));
if (code == Config.RETURN_CODE_SUCCESS) {
String json = Config.getLastCommandOutput();
JSONObject result = new JSONObject(json);
if (result.has("streams")) {
JSONArray streams = result.getJSONArray("streams");
if (streams.length() > 0) {
JSONObject stream = streams.getJSONObject(0);
VideoInformation videoInformation = VideoInformation.fromJSONObject(stream);
return videoInformation;
}
}
}
} catch (JSONException ex) {
// pass
}
return null;
}
protected void onPostExecute(VideoInformation result) {
if (handler != null) {
handler.onVideoProbed(result);
}
}
public interface VideoProbeHandler {
void onVideoProbed(VideoInformation result);
}
}
private static class VideoTranscodeTask extends AsyncTask<Void, Integer, Boolean> {
private String filePath;
private String scaleFlag;
private String outputFilePath;
private boolean transcodeRequired;
private VideoTranscodeHandler handler;
public VideoTranscodeTask(String filePath, String outputFilePath, String scaleFlag, boolean transcodeRequired, VideoTranscodeHandler handler) {
this.handler = handler;
this.filePath = filePath;
this.outputFilePath = outputFilePath;
this.scaleFlag = scaleFlag;
this.transcodeRequired = transcodeRequired;
}
protected Boolean doInBackground(Void... params) {
String movFlagsCommand = String.format("-i \"%s\" -movflags +faststart \"%s\"", filePath, outputFilePath);
String command = transcodeRequired ? String.format(
"-i \"%s\" " +
"-c:v libx264 " +
"-c:a aac -b:a 128k " +
"%s " +
"-crf 27 -preset ultrafast " +
"-pix_fmt yuv420p " +
"-maxrate 5000K -bufsize 5000K " +
"-movflags +faststart \"%s\"", filePath, scaleFlag, outputFilePath) : movFlagsCommand;
android.util.Log.d("#HELP", command);
Config.enableStatisticsCallback(new StatisticsCallback() {
@Override
public void apply(Statistics statistics) {
publishProgress(statistics.getTime());
}
});
int code = FFmpeg.execute(command);
return code == Config.RETURN_CODE_SUCCESS;
}
protected void onProgressUpdate(Integer... times) {
if (handler != null) {
for (Integer time : times) {
handler.onProgress(time);
}
}
}
protected void onPostExecute(Boolean result) {
if (handler != null) {
if (result) {
handler.onSuccess(outputFilePath);
} else {
handler.onErrorOrCancelled();
}
}
}
public interface VideoTranscodeHandler {
void onProgress(int time);
void onSuccess(String outputFilePath);
void onErrorOrCancelled();
}
}
@Data
private static class VideoInformation {
private String codecName;
private int width;
private int height;
private int durationSeconds;
private long bitrate;
private static int tryParseDuration(JSONObject streamObject) {
String durationString = Helper.getJSONString("duration", "0", streamObject);
double parsedDuration = Helper.parseDouble(durationString, 0);
if (parsedDuration > 0) {
return Double.valueOf(parsedDuration).intValue();
}
try {
if (streamObject.has("tags") && !streamObject.isNull("tags")) {
JSONObject tags = streamObject.getJSONObject("tags");
String tagDurationString = Helper.getJSONString("DURATION", null, tags);
if (Helper.isNull(tagDurationString)) {
tagDurationString = Helper.getJSONString("duration", null, tags);
}
if (!Helper.isNullOrEmpty(tagDurationString) && tagDurationString.indexOf(':') > -1) {
String[] parts = tagDurationString.split(":");
if (parts.length == 3) {
int hours = Helper.parseInt(parts[0], 0);
int minutes = Helper.parseInt(parts[1], 0);
int seconds = Helper.parseDouble(parts[2], 0).intValue();
return (hours * 60 * 60) + (minutes * 60) + seconds;
}
}
}
} catch (JSONException ex) {
return 0;
}
return 0;
}
public static VideoInformation fromJSONObject(JSONObject streamObject) {
VideoInformation info = new VideoInformation();
info.setCodecName(Helper.getJSONString("codec_name", null, streamObject));
info.setWidth(Helper.getJSONInt("width", 0, streamObject));
info.setHeight(Helper.getJSONInt("height", 0, streamObject));
info.setBitrate(Helper.getJSONLong("bit_rate", 0, streamObject));
info.setDurationSeconds(tryParseDuration(streamObject));
return info;
}
}
private static class CreateThumbnailTask extends AsyncTask<Void, Void, String> {
private Context context;
private String filePath;
private String type;
private CreateThumbnailHandler handler;
private Exception error;
public CreateThumbnailTask(String filePath, String type, Context context, CreateThumbnailHandler handler) {
this.context = context;
this.type = type;
this.filePath = filePath;
this.handler = handler;
}
protected String doInBackground(Void... params) {
String thumbnailPath = null;
FileOutputStream os = null;
Bitmap thumbnail = null;
try {
File cacheDir = context.getExternalCacheDir();
File thumbnailsDir = new File(String.format("%s/thumbnails", cacheDir.getAbsolutePath()));
if (!thumbnailsDir.isDirectory()) {
thumbnailsDir.mkdirs();
}
// save the thumbnail to the path
thumbnailPath = String.format("%s/%s.png", thumbnailsDir.getAbsolutePath(), Helper.makeid(8));
if ("video".equals(type)) {
thumbnail = ThumbnailUtils.createVideoThumbnail(filePath, MediaStore.Video.Thumbnails.MINI_KIND);
} else {
Bitmap source = BitmapFactory.decodeFile(filePath);
// MINI_KIND dimensions
thumbnail = Bitmap.createScaledBitmap(source, 512, 384, false);
}
os = new FileOutputStream(thumbnailPath);
thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os);
} catch (Exception ex) {
error = ex;
return null;
} finally {
Helper.closeCloseable(os);
}
return thumbnailPath;
}
protected void onPostExecute(String thumbnailPath) {
if (handler != null) {
if (!Helper.isNullOrEmpty(thumbnailPath)) {
handler.onSuccess(thumbnailPath);
} else {
handler.onError(error);
}
}
}
public interface CreateThumbnailHandler {
void onSuccess(String thumbnailPath);
void onError(Exception error);
}
}
}

View file

@ -3,6 +3,7 @@ package io.lbry.browser.ui.publish;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.AsyncTask;
@ -38,6 +39,7 @@ import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.adapter.GalleryGridAdapter;
import io.lbry.browser.listener.CameraPermissionListener;
import io.lbry.browser.listener.FilePickerListener;
import io.lbry.browser.listener.StoragePermissionListener;
import io.lbry.browser.model.GalleryItem;
import io.lbry.browser.model.NavMenuItem;
@ -47,9 +49,9 @@ import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryAnalytics;
public class PublishFragment extends BaseFragment implements CameraPermissionListener, StoragePermissionListener {
public class PublishFragment extends BaseFragment implements
CameraPermissionListener, FilePickerListener, StoragePermissionListener {
private boolean loadGalleryItemsPending;
private PreviewView cameraPreview;
private RecyclerView galleryGrid;
private GalleryGridAdapter adapter;
@ -60,6 +62,8 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis
private View buttonTakePhoto;
private View buttonUpload;
private boolean loadGalleryItemsPending;
private boolean launchFilePickerPending;
private boolean recordPending;
private boolean takePhotoPending;
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
@ -95,6 +99,12 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis
checkCameraPermissionAndTakePhoto();
}
});
buttonUpload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
checkStoragePermissionAndLaunchFilePicker();
}
});
return root;
}
@ -137,6 +147,11 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis
}
private void checkCameraPermissionAndRecord() {
if (!Lbry.SDK_READY) {
Snackbar.make(getView(), R.string.sdk_initializing_functionality, Snackbar.LENGTH_LONG).show();
return;
}
Context context = getContext();
if (!MainActivity.hasPermission(Manifest.permission.CAMERA, context)) {
recordPending = true;
@ -147,11 +162,16 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis
context,
true);
} else {
// start video record intent
record();
}
}
private void checkCameraPermissionAndTakePhoto() {
if (!Lbry.SDK_READY) {
Snackbar.make(getView(), R.string.sdk_initializing_functionality, Snackbar.LENGTH_LONG).show();
return;
}
Context context = getContext();
if (!MainActivity.hasPermission(Manifest.permission.CAMERA, context)) {
takePhotoPending = true;
@ -161,8 +181,56 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis
getString(R.string.camera_permission_rationale_photo),
context,
true);
} else {
// start video record intent
} else {
takePhoto();
}
}
private void takePhoto() {
Context context = getContext();
if (context instanceof MainActivity) {
takePhotoPending = false;
((MainActivity) context).requestTakePhoto();
}
}
private void record() {
Context context = getContext();
if (context instanceof MainActivity) {
recordPending = false;
((MainActivity) context).requestVideoCapture();
}
}
private void checkStoragePermissionAndLaunchFilePicker() {
if (!Lbry.SDK_READY) {
Snackbar.make(getView(), R.string.sdk_initializing_functionality, Snackbar.LENGTH_LONG).show();
return;
}
Context context = getContext();
if (MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, context)) {
launchFilePickerPending = false;
launchFilePicker();
} else {
launchFilePickerPending = true;
MainActivity.requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE,
MainActivity.REQUEST_STORAGE_PERMISSION,
getString(R.string.storage_permission_rationale_images),
context,
true);
}
}
private void launchFilePicker() {
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity.startingFilePickerActivity = true;
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("*/*");
((MainActivity) context).startActivityForResult(
Intent.createChooser(intent, getString(R.string.upload_file)),
MainActivity.REQUEST_FILE_PICKER);
}
}
@ -173,6 +241,7 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis
MainActivity activity = (MainActivity) context;
LbryAnalytics.setCurrentScreen(activity, "Publish", "Publish");
activity.addCameraPermissionListener(this);
activity.addFilePickerListener(this);
activity.addStoragePermissionListener(this);
activity.hideFloatingWalletBalance();
@ -193,6 +262,9 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis
activity.removeCameraPermissionListener(this);
activity.removeStoragePermissionListener(this);
activity.showFloatingWalletBalance();
if (!MainActivity.startingFilePickerActivity) {
activity.removeFilePickerListener(this);
}
}
CameraX.unbindAll();
super.onStop();
@ -276,10 +348,10 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis
public void onCameraPermissionGranted() {
if (recordPending) {
// record video
recordPending = false;
record();
} else if (takePhotoPending) {
// take a photo
takePhotoPending = false;
takePhoto();
}
}
@ -313,6 +385,10 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis
loadGalleryItemsPending = false;
loadGalleryItems();
}
if (launchFilePickerPending) {
launchFilePickerPending = false;
launchFilePicker();
}
}
@Override
@ -331,4 +407,19 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis
public boolean shouldSuspendGlobalPlayer() {
return true;
}
@Override
public void onFilePicked(String filePath) {
Context context = getContext();
if (context instanceof MainActivity) {
Map<String, Object> params = new HashMap<>();
params.put("directFilePath", filePath);
((MainActivity) context).openFragment(PublishFormFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH, params);
}
}
@Override
public void onFilePickerCancelled() {
}
}

View file

@ -41,7 +41,7 @@ import io.lbry.browser.model.lbryinc.Invitee;
import io.lbry.browser.tasks.claim.ClaimListResultHandler;
import io.lbry.browser.tasks.claim.ClaimListTask;
import io.lbry.browser.tasks.GenericTaskHandler;
import io.lbry.browser.tasks.ChannelCreateUpdateTask;
import io.lbry.browser.tasks.claim.ChannelCreateUpdateTask;
import io.lbry.browser.tasks.claim.ClaimResultHandler;
import io.lbry.browser.tasks.lbryinc.LogPublishTask;
import io.lbry.browser.tasks.lbryinc.FetchInviteStatusTask;

View file

@ -37,6 +37,7 @@ import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@ -67,12 +68,13 @@ public final class Helper {
public static final MediaType FORM_MEDIA_TYPE = MediaType.parse("application/x-www-form-urlencoded");
public static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8");
public static final int CONTENT_PAGE_SIZE = 25;
public static final double MIN_DEPOSIT = 0.05;
public static final double MIN_DEPOSIT = 0.01;
public static final String LBC_CURRENCY_FORMAT_PATTERN = "#,###.##";
public static final String FILE_SIZE_FORMAT_PATTERN = "#,###.#";
public static final DecimalFormat LBC_CURRENCY_FORMAT = new DecimalFormat(LBC_CURRENCY_FORMAT_PATTERN);
public static final DecimalFormat FULL_LBC_CURRENCY_FORMAT = new DecimalFormat("#,###.########");
public static final DecimalFormat SIMPLE_CURRENCY_FORMAT = new DecimalFormat("#,##0.00");
public static final SimpleDateFormat FILESTAMP_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss");
public static final String EXPLORER_TX_PREFIX = "https://explorer.lbry.com/tx";
public static boolean isNull(String value) {
@ -438,6 +440,14 @@ public final class Helper {
}
return false;
}
public static boolean claimNameExists(String claimName) {
for (Claim claim : Lbry.ownClaims) {
if (claimName.equalsIgnoreCase(claim.getName())) {
return true;
}
}
return false;
}
public static String getRealPathFromURI_API19(final Context context, final Uri uri) {
return getRealPathFromURI_API19(context, uri, false);
@ -718,4 +728,14 @@ public final class Helper {
rv.setAdapter(adapter);
rv.scrollToPosition(prevScrollPosition > 0 ? prevScrollPosition : 0);
}
public static String makeid(int length) {
Random random = new Random();
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder id = new StringBuilder();
for (int i = 0; i < length; i++) {
id.append(chars.charAt(random.nextInt(chars.length())));
}
return id.toString();
}
}

View file

@ -65,6 +65,7 @@ public final class Lbry {
public static final String METHOD_FILE_LIST = "file_list";
public static final String METHOD_FILE_DELETE = "file_delete";
public static final String METHOD_GET = "get";
public static final String METHOD_PUBLISH = "publish";
public static final String METHOD_WALLET_BALANCE = "wallet_balance";
public static final String METHOD_WALLET_ENCRYPT = "wallet_encrypt";

View file

@ -117,6 +117,82 @@
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/publish_form_video_opt_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp">
<TextView
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter"
android:textSize="20sp"
android:text="@string/video_optimization" />
<TextView
android:id="@+id/publish_form_video_opt_elapsed"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:fontFamily="@font/inter"
android:textFontWeight="300"
android:textSize="16sp" />
</RelativeLayout>
<TextView
android:id="@+id/publish_form_video_opt_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter"
android:text="@string/video_being_optimized"
android:textFontWeight="300"
android:textSize="12sp" />
<ProgressBar
android:id="@+id/publish_form_video_opt_real_progress"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_centerVertical="true"
android:visibility="gone"
style="?android:progressBarStyleHorizontal"
/>
<RelativeLayout
android:id="@+id/publish_form_video_opt_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<ProgressBar
android:id="@+id/publish_form_video_opt_progress"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_centerVertical="true"
android:layout_marginRight="16dp"
android:visibility="gone" />
<TextView
android:id="@+id/publish_form_video_opt_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/publish_form_video_opt_progress"
android:fontFamily="@font/inter"
android:textFontWeight="300"
android:textSize="14sp" />
</RelativeLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -252,13 +328,31 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:fontFamily="@font/inter"
android:textSize="20sp"
android:text="@string/content_address" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:fontFamily="@font/inter"
android:textSize="20sp"
android:text="@string/content_address" />
<TextView
android:id="@+id/publish_form_generate_address"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:fontFamily="@font/inter"
android:textSize="14sp"
android:text="@string/randomize"
android:textFontWeight="300" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -449,9 +543,17 @@
android:text="@string/cancel"
android:textFontWeight="300" />
<ProgressBar
android:id="@+id/publish_form_publishing"
android:layout_centerVertical="true"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginRight="16dp"
android:layout_toLeftOf="@id/publish_form_publish_button"
android:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/publish_form_publish_button"
android:enabled="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"

View file

@ -107,6 +107,7 @@
<string name="price">Price</string>
<string name="free_publish">Your content will be free. Press the toggle to set a price.</string>
<string name="content_address">Content address</string>
<string name="randomize">Randomize</string>
<string name="address">Address</string>
<string name="content_address_desc">The address where people can find your content (ex. lbry://myvideo)</string>
<string name="license">License</string>
@ -115,7 +116,26 @@
<string name="show_extra_fields">Show extra fields</string>
<string name="hide_extra_fields">Hide extra fields</string>
<string name="no_file_found">No file found to publish.</string>
<string name="sdk_initializing_publish">You cannot publish content right now beceause the background service is still initializing.</string>
<string name="video_optimization">Video optimization</string>
<string name="thumbnail_creation_failed">A thumbnail could not be automatically created from your content file.</string>
<string name="video_being_optimized">Your video is being optimized for better support on a wide range of devices. You can fill out the remaining fields below while this is in progress.</string>
<string name="video_optimized">Your video was successfully optimized for better playback across as many devices as possible. Please proceed to publish your content.</string>
<string name="video_optimize_failed">Your video could not be optimized. The file will be uploaded with no changes.</string>
<string name="completed_video_duration">Completed Video Duration: %1$s</string>
<string name="sdk_initializing_publish">You cannot publish content right now because the background service is still initializing.</string>
<string name="publish_successful">Your content was successfully published. It may take a few moments to appear on the blockchain.</string>
<string name="transcode_in_progress">Video optimization is in progress. If you wish to cancel, press Cancel at the bottom of the page.</string>
<string name="cannot_capture_video">There is no camera app available to record videos on this device.</string>
<string name="cannot_take_photo">There is no camera app available to take photos on this device.</string>
<string name="please_provide_title">Please provide a title.</string>
<string name="please_specify_address">Please specify an address where people can find your content.</string>
<string name="address_invalid_characters">Your content address contains invalid characters.</string>
<string name="address_already_used">You have already published to the specified content address. Please enter a new address.</string>
<string name="no_file_selected">No file selected. Please choose a video or take a photo, or select a file before publishing.</string>
<string name="price_amount_not_set">Please enter a price or turn off the toggle to make your content free.</string>
<string name="publish_no_thumbnail">Please select a thumbnail to upload before publishing.</string>
<string name="publish_thumbnail_in_progress">Please wait for the thumbnail to finish uploading before publishing.</string>
<string name="language">Language</string>
<string name="english">English</string>

View file

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path path="lbrynet/" name="lbrynet" />
<external-files-path path="record/" name="record" />
<external-files-path path="photos/" name="photos" />
</paths>